diff --git a/.github/values.yaml b/.github/values.yaml index 819df5066..bab85c47e 100644 --- a/.github/values.yaml +++ b/.github/values.yaml @@ -6,7 +6,7 @@ frontend: secrets.infisical.com/auto-reload: "true" replicaCount: 2 image: - repository: infisical/frontend + repository: infisical/staging_deployment_frontend tag: "latest" pullPolicy: Always kubeSecretRef: managed-secret-frontend @@ -25,7 +25,7 @@ backend: secrets.infisical.com/auto-reload: "true" replicaCount: 2 image: - repository: infisical/backend + repository: infisical/staging_deployment_backend tag: "latest" pullPolicy: Always kubeSecretRef: managed-backend-secret diff --git a/.github/workflows/build-docker-image-to-prod.yml b/.github/workflows/build-docker-image-to-prod.yml new file mode 100644 index 000000000..322a553c4 --- /dev/null +++ b/.github/workflows/build-docker-image-to-prod.yml @@ -0,0 +1,118 @@ +name: Release production images (frontend, backend) +on: + push: + tags: + - "infisical/v*.*.*" + +jobs: + backend-image: + name: Build backend image + runs-on: ubuntu-latest + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - name: ๐Ÿ“ฆ Install dependencies to test all dependencies + run: npm ci --only-production + working-directory: backend + - name: ๐Ÿงช Run tests + run: npm run test:ci + working-directory: backend + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: ๐Ÿ”ง Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: ๐Ÿ‹ Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: ๐Ÿ“ฆ Build backend and export to Docker + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + load: true + context: backend + tags: infisical/backend:test + - name: โป Spawn backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull + - name: ๐Ÿงช Test backend image + run: | + ./.github/resources/healthcheck.sh infisical-backend-test + - name: โป Shut down backend container and dependencies + run: | + docker compose -f .github/resources/docker-compose.be-test.yml down + - name: ๐Ÿ—๏ธ Build backend and push + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + push: true + context: backend + tags: | + infisical/backend:${{ steps.commit.outputs.short }} + infisical/backend:latest + infisical/backend:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + + frontend-image: + name: Build frontend image + runs-on: ubuntu-latest + steps: + - name: Extract version from tag + id: extract_version + run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" + - name: โ˜๏ธ Checkout source + uses: actions/checkout@v3 + - name: Save commit hashes for tag + id: commit + uses: pr-mpt/actions-commit-hash@v2 + - name: ๐Ÿ”ง Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + - name: ๐Ÿ‹ Login to Docker Hub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + - name: Set up Depot CLI + uses: depot/setup-action@v1 + - name: ๐Ÿ“ฆ Build frontend and export to Docker + uses: depot/build-push-action@v1 + with: + load: true + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + project: 64mmf0n610 + context: frontend + tags: infisical/frontend:test + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} + - name: โป Spawn frontend container + run: | + docker run -d --rm --name infisical-frontend-test infisical/frontend:test + - name: ๐Ÿงช Test frontend image + run: | + ./.github/resources/healthcheck.sh infisical-frontend-test + - name: โป Shut down frontend container + run: | + docker stop infisical-frontend-test + - name: ๐Ÿ—๏ธ Build frontend and push + uses: depot/build-push-action@v1 + with: + project: 64mmf0n610 + push: true + token: ${{ secrets.DEPOT_PROJECT_TOKEN }} + context: frontend + tags: | + infisical/frontend:${{ steps.commit.outputs.short }} + infisical/frontend:latest + infisical/frontend:${{ steps.extract_version.outputs.version }} + platforms: linux/amd64,linux/arm64 + build-args: | + POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} diff --git a/.github/workflows/docker-image.yml b/.github/workflows/build-staging-img.yml similarity index 86% rename from .github/workflows/docker-image.yml rename to .github/workflows/build-staging-img.yml index 87797c5f7..0e50d0906 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/build-staging-img.yml @@ -1,17 +1,11 @@ name: Build, Publish and Deploy to Gamma -on: - push: - tags: - - "infisical/v*.*.*" +on: [workflow_dispatch] jobs: backend-image: name: Build backend image runs-on: ubuntu-latest steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" - name: โ˜๏ธ Checkout source uses: actions/checkout@v3 - name: ๐Ÿ“ฆ Install dependencies to test all dependencies @@ -57,18 +51,14 @@ jobs: push: true context: backend tags: | - infisical/backend:${{ steps.commit.outputs.short }} - infisical/backend:latest - infisical/backend:${{ steps.extract_version.outputs.version }} + infisical/staging_deployment_backend:${{ steps.commit.outputs.short }} + infisical/staging_deployment_backend:latest platforms: linux/amd64,linux/arm64 frontend-image: name: Build frontend image runs-on: ubuntu-latest steps: - - name: Extract version from tag - id: extract_version - run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}" - name: โ˜๏ธ Checkout source uses: actions/checkout@v3 - name: Save commit hashes for tag @@ -90,12 +80,12 @@ jobs: token: ${{ secrets.DEPOT_PROJECT_TOKEN }} project: 64mmf0n610 context: frontend - tags: infisical/frontend:test + tags: infisical/staging_deployment_frontend:test build-args: | POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} - name: โป Spawn frontend container run: | - docker run -d --rm --name infisical-frontend-test infisical/frontend:test + docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test - name: ๐Ÿงช Test frontend image run: | ./.github/resources/healthcheck.sh infisical-frontend-test @@ -110,9 +100,8 @@ jobs: token: ${{ secrets.DEPOT_PROJECT_TOKEN }} context: frontend tags: | - infisical/frontend:${{ steps.commit.outputs.short }} - infisical/frontend:latest - infisical/frontend:${{ steps.extract_version.outputs.version }} + infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }} + infisical/staging_deployment_frontend:latest platforms: linux/amd64,linux/arm64 build-args: | POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} @@ -146,7 +135,7 @@ jobs: - name: Download helm values to file and upgrade gamma deploy run: | wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml - helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods + helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait if [[ $(helm status infisical) == *"FAILED"* ]]; then echo "Helm upgrade failed" exit 1 diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..1ffdeb337 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "workbench.editor.wrapTabs": true +} diff --git a/Dockerfile.standalone-infisical b/Dockerfile.standalone-infisical index 12325fedd..8770bb234 100644 --- a/Dockerfile.standalone-infisical +++ b/Dockerfile.standalone-infisical @@ -25,6 +25,8 @@ ARG POSTHOG_HOST ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST ARG POSTHOG_API_KEY ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY +ARG INTERCOM_ID +ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID # Build RUN npm run build @@ -42,6 +44,9 @@ VOLUME /app/.next/cache/images ARG POSTHOG_API_KEY ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY +ARG INTERCOM_ID +ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ + BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts COPY --from=frontend-builder /app/public ./public diff --git a/README.md b/README.md index c8e67d219..89a44050f 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ git commit activity - Cloudsmith downloads + Cloudsmith downloads Slack community channel @@ -127,7 +127,7 @@ Whether it's big or small, we love contributions. Check out our guide to see how Not sure where to get started? You can: -- [Book a free, non-pressure pairing sessions with one of our teammates](mailto:tony@infisical.com?subject=Pairing%20session&body=I'd%20like%20to%20do%20a%20pairing%20session!)! +- [Book a free, non-pressure pairing session / code walkthrough with one of our teammates](https://cal.com/tony-infisical/30-min-meeting-contributing)! - Join our Slack, and ask us any questions there. ## Resources diff --git a/backend/.eslintrc b/backend/.eslintrc index c1ca1a1eb..64b049ea7 100644 --- a/backend/.eslintrc +++ b/backend/.eslintrc @@ -1,12 +1,21 @@ { "parser": "@typescript-eslint/parser", - "plugins": ["@typescript-eslint"], + "plugins": ["@typescript-eslint", "unused-imports"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" ], "rules": { - "no-console": 2 + "no-console": 2, + "quotes": ["error", "double", { "avoidEscape": true }], + "comma-dangle": ["error", "only-multiline"], + "@typescript-eslint/no-unused-vars": "off", + "unused-imports/no-unused-imports": "error", + "unused-imports/no-unused-vars": [ + "warn", + { "vars": "all", "varsIgnorePattern": "^_", "args": "after-used", "argsIgnorePattern": "^_" } + ], + "sort-imports": ["error", { "ignoreDeclarationSort": true }] } } diff --git a/backend/.prettierrc b/backend/.prettierrc new file mode 100644 index 000000000..0b8ef54d2 --- /dev/null +++ b/backend/.prettierrc @@ -0,0 +1,7 @@ +{ + "singleQuote": false, + "printWidth": 100, + "trailingComma": "none", + "tabWidth": 2, + "semi": true +} diff --git a/backend/environment.d.ts b/backend/environment.d.ts index 3793552b1..9bbff7a24 100644 --- a/backend/environment.d.ts +++ b/backend/environment.d.ts @@ -14,7 +14,7 @@ declare global { JWT_SIGNUP_LIFETIME: string; JWT_SIGNUP_SECRET: string; MONGO_URL: string; - NODE_ENV: 'development' | 'staging' | 'testing' | 'production'; + NODE_ENV: "development" | "staging" | "testing" | "production"; VERBOSE_ERROR_OUTPUT: string; LOKI_HOST: string; CLIENT_ID_HEROKU: string; diff --git a/backend/jest.config.ts b/backend/jest.config.ts index 8a72ad84c..7c657505b 100644 --- a/backend/jest.config.ts +++ b/backend/jest.config.ts @@ -1,9 +1,9 @@ export default { - preset: 'ts-jest', - testEnvironment: 'node', - collectCoverageFrom: ['src/*.{js,ts}', '!**/node_modules/**'], - modulePaths: ['/src'], - testMatch: ['/tests/**/*.test.ts'], - setupFiles: ['/test-resources/env-vars.js'], - setupFilesAfterEnv: ['/tests/setupTests.ts'] + preset: "ts-jest", + testEnvironment: "node", + collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"], + modulePaths: ["/src"], + testMatch: ["/tests/**/*.test.ts"], + setupFiles: ["/test-resources/env-vars.js"], + setupFilesAfterEnv: ["/tests/setupTests.ts"], }; diff --git a/backend/package-lock.json b/backend/package-lock.json index 302ade10b..735f033b8 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -81,6 +81,7 @@ "@typescript-eslint/parser": "^5.40.1", "cross-env": "^7.0.3", "eslint": "^8.26.0", + "eslint-plugin-unused-imports": "^2.0.0", "install": "^0.13.0", "jest": "^29.3.1", "jest-junit": "^15.0.0", @@ -2959,6 +2960,7 @@ "version": "10.2.0", "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.2.0.tgz", "integrity": "sha512-OPwQlEdg40HAj5KNF8WW6q2KG4Z+cBCZb3m4ninfTZKaBmbIJodviQsDBoYMPHkOyJJMHnOJo5j2+LKDOhOACg==", + "deprecated": "Use version 10.1.0. Version 10.2.0 has potential breaking issues", "dev": true, "dependencies": { "@sinonjs/commons": "^3.0.0" @@ -4972,6 +4974,36 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "dependencies": { + "eslint-rule-composer": "^0.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0", + "eslint": "^8.0.0" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + } + } + }, + "node_modules/eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -16291,6 +16323,21 @@ } } }, + "eslint-plugin-unused-imports": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unused-imports/-/eslint-plugin-unused-imports-2.0.0.tgz", + "integrity": "sha512-3APeS/tQlTrFa167ThtP0Zm0vctjr4M44HMpeg1P4bK6wItarumq0Ma82xorMKdFsWpphQBlRPzw/pxiVELX1A==", + "dev": true, + "requires": { + "eslint-rule-composer": "^0.3.0" + } + }, + "eslint-rule-composer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/eslint-rule-composer/-/eslint-rule-composer-0.3.0.tgz", + "integrity": "sha512-bt+Sh8CtDmn2OajxvNO+BX7Wn4CIWMpTRm3MaiKPCQcnnlm0CS2mhui6QaoeQugs+3Kj2ESKEEGJUdVafwhiCg==", + "dev": true + }, "eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", diff --git a/backend/package.json b/backend/package.json index 6d855001e..b65fe13aa 100644 --- a/backend/package.json +++ b/backend/package.json @@ -99,6 +99,7 @@ "@typescript-eslint/parser": "^5.40.1", "cross-env": "^7.0.3", "eslint": "^8.26.0", + "eslint-plugin-unused-imports": "^2.0.0", "install": "^0.13.0", "jest": "^29.3.1", "jest-junit": "^15.0.0", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 4613a05c9..62638859a 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -1,93 +1,93 @@ -import InfisicalClient from 'infisical-node'; +import InfisicalClient from "infisical-node"; export const client = new InfisicalClient({ - token: process.env.INFISICAL_TOKEN! + token: process.env.INFISICAL_TOKEN!, }); -export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000; +export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000; export const getEncryptionKey = async () => { - const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue; - return secretValue === '' ? undefined : secretValue; + const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue; + return secretValue === "" ? undefined : secretValue; } export const getRootEncryptionKey = async () => { - const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue; - return secretValue === '' ? undefined : secretValue; + const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue; + return secretValue === "" ? undefined : secretValue; } -export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true' -export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10; -export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d'; -export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue; -export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m'; -export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m'; -export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d'; -export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue; -export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue; -export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m'; -export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue; -export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m'; -export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue; -export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue; -export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production'; -export const getVerboseErrorOutput = async () => (await client.getSecret('VERBOSE_ERROR_OUTPUT')).secretValue === 'true' && true; -export const getLokiHost = async () => (await client.getSecret('LOKI_HOST')).secretValue; -export const getClientIdAzure = async () => (await client.getSecret('CLIENT_ID_AZURE')).secretValue; -export const getClientIdHeroku = async () => (await client.getSecret('CLIENT_ID_HEROKU')).secretValue; -export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_VERCEL')).secretValue; -export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue; -export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue; -export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue; -export const getClientIdGoogle = async () => (await client.getSecret('CLIENT_ID_GOOGLE')).secretValue; -export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue; -export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue; -export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue; -export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue; -export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue; -export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue; -export const getClientSecretGoogle = async () => (await client.getSecret('CLIENT_SECRET_GOOGLE')).secretValue; -export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue; -export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com'; -export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; -export const getSentryDSN = async () => (await client.getSecret('SENTRY_DSN')).secretValue; -export const getSiteURL = async () => (await client.getSecret('SITE_URL')).secretValue; -export const getSmtpHost = async () => (await client.getSecret('SMTP_HOST')).secretValue; -export const getSmtpSecure = async () => (await client.getSecret('SMTP_SECURE')).secretValue === 'true' || false; -export const getSmtpPort = async () => parseInt((await client.getSecret('SMTP_PORT')).secretValue) || 587; -export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAME')).secretValue; -export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue; -export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue; -export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical'; +export const getInviteOnlySignup = async () => (await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true" +export const getSaltRounds = async () => parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10; +export const getJwtAuthLifetime = async () => (await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d"; +export const getJwtAuthSecret = async () => (await client.getSecret("JWT_AUTH_SECRET")).secretValue; +export const getJwtMfaLifetime = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m"; +export const getJwtMfaSecret = async () => (await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m"; +export const getJwtRefreshLifetime = async () => (await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d"; +export const getJwtRefreshSecret = async () => (await client.getSecret("JWT_REFRESH_SECRET")).secretValue; +export const getJwtServiceSecret = async () => (await client.getSecret("JWT_SERVICE_SECRET")).secretValue; +export const getJwtSignupLifetime = async () => (await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m"; +export const getJwtProviderAuthSecret = async () => (await client.getSecret("JWT_PROVIDER_AUTH_SECRET")).secretValue; +export const getJwtProviderAuthLifetime = async () => (await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m"; +export const getJwtSignupSecret = async () => (await client.getSecret("JWT_SIGNUP_SECRET")).secretValue; +export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue; +export const getNodeEnv = async () => (await client.getSecret("NODE_ENV")).secretValue || "production"; +export const getVerboseErrorOutput = async () => (await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true; +export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue; +export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue; +export const getClientIdHeroku = async () => (await client.getSecret("CLIENT_ID_HEROKU")).secretValue; +export const getClientIdVercel = async () => (await client.getSecret("CLIENT_ID_VERCEL")).secretValue; +export const getClientIdNetlify = async () => (await client.getSecret("CLIENT_ID_NETLIFY")).secretValue; +export const getClientIdGitHub = async () => (await client.getSecret("CLIENT_ID_GITHUB")).secretValue; +export const getClientIdGitLab = async () => (await client.getSecret("CLIENT_ID_GITLAB")).secretValue; +export const getClientIdGoogle = async () => (await client.getSecret("CLIENT_ID_GOOGLE")).secretValue; +export const getClientSecretAzure = async () => (await client.getSecret("CLIENT_SECRET_AZURE")).secretValue; +export const getClientSecretHeroku = async () => (await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue; +export const getClientSecretVercel = async () => (await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue; +export const getClientSecretNetlify = async () => (await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue; +export const getClientSecretGitHub = async () => (await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue; +export const getClientSecretGitLab = async () => (await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue; +export const getClientSecretGoogle = async () => (await client.getSecret("CLIENT_SECRET_GOOGLE")).secretValue; +export const getClientSlugVercel = async () => (await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue; +export const getPostHogHost = async () => (await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com"; +export const getPostHogProjectApiKey = async () => (await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue || "phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE"; +export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue; +export const getSiteURL = async () => (await client.getSecret("SITE_URL")).secretValue; +export const getSmtpHost = async () => (await client.getSecret("SMTP_HOST")).secretValue; +export const getSmtpSecure = async () => (await client.getSecret("SMTP_SECURE")).secretValue === "true" || false; +export const getSmtpPort = async () => parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587; +export const getSmtpUsername = async () => (await client.getSecret("SMTP_USERNAME")).secretValue; +export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWORD")).secretValue; +export const getSmtpFromAddress = async () => (await client.getSecret("SMTP_FROM_ADDRESS")).secretValue; +export const getSmtpFromName = async () => (await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical"; export const getLicenseKey = async () => { - const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue; - return secretValue === '' ? undefined : secretValue; + const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue; + return secretValue === "" ? undefined : secretValue; } export const getLicenseServerKey = async () => { - const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue; - return secretValue === '' ? undefined : secretValue; + const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue; + return secretValue === "" ? undefined : secretValue; } -export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com'; +export const getLicenseServerUrl = async () => (await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com"; // TODO: deprecate from here -export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue; -export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue; -export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue; -export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue; -export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue; -export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue; +export const getStripeProductStarter = async () => (await client.getSecret("STRIPE_PRODUCT_STARTER")).secretValue; +export const getStripeProductPro = async () => (await client.getSecret("STRIPE_PRODUCT_PRO")).secretValue; +export const getStripeProductTeam = async () => (await client.getSecret("STRIPE_PRODUCT_TEAM")).secretValue; +export const getStripePublishableKey = async () => (await client.getSecret("STRIPE_PUBLISHABLE_KEY")).secretValue; +export const getStripeSecretKey = async () => (await client.getSecret("STRIPE_SECRET_KEY")).secretValue; +export const getStripeWebhookSecret = async () => (await client.getSecret("STRIPE_WEBHOOK_SECRET")).secretValue; -export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true; -export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue; -export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true +export const getTelemetryEnabled = async () => (await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true; +export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue; +export const getSmtpConfigured = async () => (await client.getSecret("SMTP_HOST")).secretValue == "" || (await client.getSecret("SMTP_HOST")).secretValue == undefined ? false : true export const getHttpsEnabled = async () => { if ((await getNodeEnv()) != "production") { // no https for anything other than prod return false } - if ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") { + if ((await client.getSecret("HTTPS_ENABLED")).secretValue == undefined || (await client.getSecret("HTTPS_ENABLED")).secretValue == "") { // default when no value present return true } - return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true + return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true } \ No newline at end of file diff --git a/backend/src/config/request.ts b/backend/src/config/request.ts index e13469657..e69b1baff 100644 --- a/backend/src/config/request.ts +++ b/backend/src/config/request.ts @@ -1,16 +1,16 @@ -import axios from 'axios'; -import axiosRetry from 'axios-retry'; +import axios from "axios"; +import axiosRetry from "axios-retry"; import { - getLicenseServerKeyAuthToken, - setLicenseServerKeyAuthToken, getLicenseKeyAuthToken, - setLicenseKeyAuthToken -} from './storage'; + getLicenseServerKeyAuthToken, + setLicenseKeyAuthToken, + setLicenseServerKeyAuthToken, +} from "./storage"; import { getLicenseKey, getLicenseServerKey, - getLicenseServerUrl -} from './index'; + getLicenseServerUrl, +} from "./index"; // should have JWT to interact with the license server export const licenseServerKeyRequest = axios.create(); @@ -35,8 +35,8 @@ export const refreshLicenseServerKeyToken = async () => { `${licenseServerUrl}/api/auth/v1/license-server-login`, {}, { headers: { - 'X-API-KEY': licenseServerKey - } + "X-API-KEY": licenseServerKey, + }, } ); @@ -53,8 +53,8 @@ export const refreshLicenseKeyToken = async () => { `${licenseServerUrl}/api/auth/v1/license-login`, {}, { headers: { - 'X-API-KEY': licenseKey - } + "X-API-KEY": licenseKey, + }, } ); @@ -86,7 +86,7 @@ licenseServerKeyRequest.interceptors.response.use((response) => { // refresh const token = await refreshLicenseServerKeyToken(); - axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; + axios.defaults.headers.common["Authorization"] = "Bearer " + token; return licenseServerKeyRequest(originalRequest); } @@ -116,7 +116,7 @@ licenseKeyRequest.interceptors.response.use((response) => { // refresh const token = await refreshLicenseKeyToken(); - axios.defaults.headers.common['Authorization'] = 'Bearer ' + token; + axios.defaults.headers.common["Authorization"] = "Bearer " + token; return licenseKeyRequest(originalRequest); } diff --git a/backend/src/config/storage.ts b/backend/src/config/storage.ts index 5638561ac..f3cf27196 100644 --- a/backend/src/config/storage.ts +++ b/backend/src/config/storage.ts @@ -5,7 +5,7 @@ const MemoryLicenseServerKeyTokenStorage = () => { setToken: (token: string) => { authToken = token; }, - getToken: () => authToken + getToken: () => authToken, }; }; @@ -16,7 +16,7 @@ const MemoryLicenseKeyTokenStorage = () => { setToken: (token: string) => { authToken = token; }, - getToken: () => authToken + getToken: () => authToken, }; }; diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 8a70e016e..311d9602d 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -1,36 +1,36 @@ -import { Request, Response } from 'express'; -import fs from 'fs'; -import path from 'path'; -import jwt from 'jsonwebtoken'; -import * as bigintConversion from 'bigint-conversion'; +import { Request, Response } from "express"; +import fs from "fs"; +import path from "path"; +import jwt from "jsonwebtoken"; +import * as bigintConversion from "bigint-conversion"; // eslint-disable-next-line @typescript-eslint/no-var-requires -const jsrp = require('jsrp'); +const jsrp = require("jsrp"); import { - User, - LoginSRPDetail, - TokenVersion -} from '../../models'; -import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; -import { checkUserDevice } from '../../helpers/user'; + LoginSRPDetail, + TokenVersion, + User, +} from "../../models"; +import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; +import { checkUserDevice } from "../../helpers/user"; import { ACTION_LOGIN, ACTION_LOGOUT, - AUTH_MODE_JWT -} from '../../variables'; + AUTH_MODE_JWT, +} from "../../variables"; import { BadRequestError, - UnauthorizedRequestError -} from '../../utils/errors'; -import { EELogService } from '../../ee/services'; -import { getChannelFromUserAgent } from '../../utils/posthog'; + UnauthorizedRequestError, +} from "../../utils/errors"; +import { EELogService } from "../../ee/services"; +import { getChannelFromUserAgent } from "../../utils/posthog"; import { - getJwtRefreshSecret, + getHttpsEnabled, getJwtAuthLifetime, getJwtAuthSecret, - getHttpsEnabled -} from '../../config'; + getJwtRefreshSecret, +} from "../../config"; -declare module 'jsonwebtoken' { +declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; refreshVersion?: number; @@ -46,20 +46,20 @@ declare module 'jsonwebtoken' { export const login1 = async (req: Request, res: Response) => { const { email, - clientPublicKey + clientPublicKey, }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ - email - }).select('+salt +verifier'); + email, + }).select("+salt +verifier"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); const server = new jsrp.server(); server.init( { salt: user.salt, - verifier: user.verifier + verifier: user.verifier, }, async () => { // generate server-side public key @@ -73,7 +73,7 @@ export const login1 = async (req: Request, res: Response) => { return res.status(200).send({ serverPublicKey, - salt: user.salt + salt: user.salt, }); } ); @@ -89,10 +89,10 @@ export const login1 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => { const { email, clientProof } = req.body; const user = await User.findOne({ - email - }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag'); + email, + }).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) @@ -105,7 +105,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetailFromDB.serverBInt + b: loginSRPDetailFromDB.serverBInt, }, async () => { server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); @@ -117,33 +117,33 @@ export const login2 = async (req: Request, res: Response) => { await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { + res.cookie("jid", tokens.refreshToken, { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled(), }); const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id + userId: user._id, }); loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.realIP + channel: getChannelFromUserAgent(req.headers["user-agent"]), + ipAddress: req.realIP, }); // return (access) token in response @@ -152,12 +152,12 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag + tag: user.tag, }); } return res.status(400).send({ - message: 'Failed to authenticate. Try again?' + message: "Failed to authenticate. Try again?", }); } ); @@ -175,51 +175,51 @@ export const logout = async (req: Request, res: Response) => { } // clear httpOnly cookie - res.cookie('jid', '', { + res.cookie("jid", "", { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: (await getHttpsEnabled()) as boolean + path: "/", + sameSite: "strict", + secure: (await getHttpsEnabled()) as boolean, }); const logoutAction = await EELogService.createAction({ name: ACTION_LOGOUT, - userId: req.user._id + userId: req.user._id, }); logoutAction && await EELogService.createLog({ userId: req.user._id, actions: [logoutAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.realIP + channel: getChannelFromUserAgent(req.headers["user-agent"]), + ipAddress: req.realIP, }); return res.status(200).send({ - message: 'Successfully logged out.' + message: "Successfully logged out.", }); }; export const getCommonPasswords = async (req: Request, res: Response) => { const commonPasswords = fs.readFileSync( - path.resolve(__dirname, '../../data/' + 'common_passwords.txt'), - 'utf8' - ).split('\n'); + path.resolve(__dirname, "../../data/" + "common_passwords.txt"), + "utf8" + ).split("\n"); return res.status(200).send(commonPasswords); } export const revokeAllSessions = async (req: Request, res: Response) => { await TokenVersion.updateMany({ - user: req.user._id + user: req.user._id, }, { $inc: { refreshVersion: 1, - accessVersion: 1 - } + accessVersion: 1, + }, }); return res.status(200).send({ - message: 'Successfully revoked all sessions.' + message: "Successfully revoked all sessions.", }); } @@ -231,7 +231,7 @@ export const revokeAllSessions = async (req: Request, res: Response) => { */ export const checkAuth = async (req: Request, res: Response) => { return res.status(200).send({ - message: 'Authenticated' + message: "Authenticated", }); } @@ -245,7 +245,7 @@ export const getNewToken = async (req: Request, res: Response) => { const refreshToken = req.cookies.jid; if (!refreshToken) { - throw new Error('Failed to find refresh token in request cookies'); + throw new Error("Failed to find refresh token in request cookies"); } const decodedToken = ( @@ -253,35 +253,35 @@ export const getNewToken = async (req: Request, res: Response) => { ); const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey +refreshVersion +accessVersion'); + _id: decodedToken.userId, + }).select("+publicKey +refreshVersion +accessVersion"); - if (!user) throw new Error('Failed to authenticate unfound user'); + if (!user) throw new Error("Failed to authenticate unfound user"); if (!user?.publicKey) - throw new Error('Failed to authenticate not fully set up account'); + throw new Error("Failed to authenticate not fully set up account"); const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId); if (!tokenVersion) throw UnauthorizedRequestError({ - message: 'Failed to validate refresh token' + message: "Failed to validate refresh token", }); if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({ - message: 'Failed to validate refresh token' + message: "Failed to validate refresh token", }); const token = createToken({ payload: { userId: decodedToken.userId, tokenVersionId: tokenVersion._id.toString(), - accessVersion: tokenVersion.refreshVersion + accessVersion: tokenVersion.refreshVersion, }, expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret() + secret: await getJwtAuthSecret(), }); return res.status(200).send({ - token + token, }); }; diff --git a/backend/src/controllers/v1/botController.ts b/backend/src/controllers/v1/botController.ts index f155f58a5..b2e757541 100644 --- a/backend/src/controllers/v1/botController.ts +++ b/backend/src/controllers/v1/botController.ts @@ -1,7 +1,7 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { Bot, BotKey } from '../../models'; -import { createBot } from '../../helpers/bot'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Bot, BotKey } from "../../models"; +import { createBot } from "../../helpers/bot"; interface BotKey { encryptedKey: string; @@ -19,20 +19,20 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => { const { workspaceId } = req.params; let bot = await Bot.findOne({ - workspace: workspaceId + workspace: workspaceId, }); if (!bot) { // case: bot doesn't exist for workspace with id [workspaceId] // -> create a new bot and return it bot = await createBot({ - name: 'Infisical Bot', - workspaceId: new Types.ObjectId(workspaceId) + name: "Infisical Bot", + workspaceId: new Types.ObjectId(workspaceId), }); } return res.status(200).send({ - bot + bot, }); }; @@ -49,40 +49,40 @@ export const setBotActiveState = async (req: Request, res: Response) => { // bot state set to active -> share workspace key with bot if (!botKey?.encryptedKey || !botKey?.nonce) { return res.status(400).send({ - message: 'Failed to set bot state to active - missing bot key' + message: "Failed to set bot state to active - missing bot key", }); } await BotKey.findOneAndUpdate({ - workspace: req.bot.workspace + workspace: req.bot.workspace, }, { encryptedKey: botKey.encryptedKey, nonce: botKey.nonce, sender: req.user._id, bot: req.bot._id, - workspace: req.bot.workspace + workspace: req.bot.workspace, }, { upsert: true, - new: true + new: true, }); } else { // case: bot state set to inactive -> delete bot's workspace key await BotKey.deleteOne({ - bot: req.bot._id + bot: req.bot._id, }); } - let bot = await Bot.findOneAndUpdate({ - _id: req.bot._id + const bot = await Bot.findOneAndUpdate({ + _id: req.bot._id, }, { - isActive + isActive, }, { - new: true + new: true, }); - if (!bot) throw new Error('Failed to update bot active state'); + if (!bot) throw new Error("Failed to update bot active state"); return res.status(200).send({ - bot + bot, }); }; diff --git a/backend/src/controllers/v1/index.ts b/backend/src/controllers/v1/index.ts index 1da61835f..5f2523895 100644 --- a/backend/src/controllers/v1/index.ts +++ b/backend/src/controllers/v1/index.ts @@ -1,19 +1,19 @@ -import * as authController from './authController'; -import * as botController from './botController'; -import * as integrationAuthController from './integrationAuthController'; -import * as integrationController from './integrationController'; -import * as keyController from './keyController'; -import * as membershipController from './membershipController'; -import * as membershipOrgController from './membershipOrgController'; -import * as organizationController from './organizationController'; -import * as passwordController from './passwordController'; -import * as secretController from './secretController'; -import * as serviceTokenController from './serviceTokenController'; -import * as signupController from './signupController'; -import * as stripeController from './stripeController'; -import * as userActionController from './userActionController'; -import * as userController from './userController'; -import * as workspaceController from './workspaceController'; +import * as authController from "./authController"; +import * as botController from "./botController"; +import * as integrationAuthController from "./integrationAuthController"; +import * as integrationController from "./integrationController"; +import * as keyController from "./keyController"; +import * as membershipController from "./membershipController"; +import * as membershipOrgController from "./membershipOrgController"; +import * as organizationController from "./organizationController"; +import * as passwordController from "./passwordController"; +import * as secretController from "./secretController"; +import * as serviceTokenController from "./serviceTokenController"; +import * as signupController from "./signupController"; +import * as stripeController from "./stripeController"; +import * as userActionController from "./userActionController"; +import * as userController from "./userController"; +import * as workspaceController from "./workspaceController"; export { authController, @@ -31,5 +31,5 @@ export { stripeController, userActionController, userController, - workspaceController + workspaceController, }; diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index fc79e22cf..f625f10a2 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -1,21 +1,17 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { standardRequest } from "../../config/request"; +import { getApps, getTeams, revokeAccess } from "../../integrations"; +import { Bot, IntegrationAuth } from "../../models"; +import { IntegrationService } from "../../services"; import { - IntegrationAuth, - Bot -} from '../../models'; -import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables'; -import { IntegrationService } from '../../services'; -import { - getApps, - getTeams, - revokeAccess -} from '../../integrations'; -import { - INTEGRATION_VERCEL_API_URL, - INTEGRATION_RAILWAY_API_URL -} from '../../variables'; -import { standardRequest } from '../../config/request'; + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + INTEGRATION_RAILWAY_API_URL, + INTEGRATION_SET, + INTEGRATION_VERCEL_API_URL, + getIntegrationOptions as getIntegrationOptionsFunc +} from "../../variables"; /*** * Return integration authorization with id [integrationAuthId] @@ -23,22 +19,23 @@ import { standardRequest } from '../../config/request'; export const getIntegrationAuth = async (req: Request, res: Response) => { const { integrationAuthId } = req.params; const integrationAuth = await IntegrationAuth.findById(integrationAuthId); - - if (!integrationAuth) return res.status(400).send({ - message: 'Failed to find integration authorization' - }); - return res.status(200).send({ - integrationAuth - }); -} + if (!integrationAuth) + return res.status(400).send({ + message: "Failed to find integration authorization" + }); + + return res.status(200).send({ + integrationAuth + }); +}; export const getIntegrationOptions = async (req: Request, res: Response) => { - const INTEGRATION_OPTIONS = await getIntegrationOptionsFunc(); + const INTEGRATION_OPTIONS = await getIntegrationOptionsFunc(); - return res.status(200).send({ - integrationOptions: INTEGRATION_OPTIONS, - }); + return res.status(200).send({ + integrationOptions: INTEGRATION_OPTIONS + }); }; /** @@ -47,26 +44,22 @@ export const getIntegrationOptions = async (req: Request, res: Response) => { * @param res * @returns */ -export const oAuthExchange = async ( - req: Request, - res: Response -) => { +export const oAuthExchange = async (req: Request, res: Response) => { const { workspaceId, code, integration } = req.body; - if (!INTEGRATION_SET.has(integration)) - throw new Error('Failed to validate integration'); - + if (!INTEGRATION_SET.has(integration)) throw new Error("Failed to validate integration"); + const environments = req.membership.workspace?.environments || []; - if(environments.length === 0){ - throw new Error("Failed to get environments") + if (environments.length === 0) { + throw new Error("Failed to get environments"); } const integrationAuth = await IntegrationService.handleOAuthExchange({ workspaceId, integration, code, - environment: environments[0].slug, + environment: environments[0].slug }); - + return res.status(200).send({ integrationAuth }); @@ -75,69 +68,70 @@ export const oAuthExchange = async ( /** * Save integration access token and (optionally) access id as part of integration * [integration] for workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ -export const saveIntegrationAccessToken = async ( - req: Request, - res: Response -) => { - // TODO: refactor - // TODO: check if access token is valid for each integration +export const saveIntegrationAccessToken = async (req: Request, res: Response) => { + // TODO: refactor + // TODO: check if access token is valid for each integration - let integrationAuth; - const { - workspaceId, - accessId, - accessToken, - url, - namespace, - integration - }: { - workspaceId: string; - accessId: string | null; - accessToken: string; - url: string; - namespace: string; - integration: string; - } = req.body; + let integrationAuth; + const { + workspaceId, + accessId, + accessToken, + url, + namespace, + integration + }: { + workspaceId: string; + accessId: string | null; + accessToken: string; + url: string; + namespace: string; + integration: string; + } = req.body; - const bot = await Bot.findOne({ - workspace: new Types.ObjectId(workspaceId), - isActive: true - }); - - if (!bot) throw new Error('Bot must be enabled to save integration access token'); + const bot = await Bot.findOne({ + workspace: new Types.ObjectId(workspaceId), + isActive: true + }); - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - workspace: new Types.ObjectId(workspaceId), - integration - }, { - workspace: new Types.ObjectId(workspaceId), - integration, - url, - namespace, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true, - upsert: true - }); - - // encrypt and save integration access details - integrationAuth = await IntegrationService.setIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString(), - accessId, - accessToken, - accessExpiresAt: undefined - }); - - if (!integrationAuth) throw new Error('Failed to save integration access token'); - - return res.status(200).send({ - integrationAuth - }); -} + if (!bot) throw new Error("Bot must be enabled to save integration access token"); + + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + workspace: new Types.ObjectId(workspaceId), + integration + }, + { + workspace: new Types.ObjectId(workspaceId), + integration, + url, + namespace, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8 + }, + { + new: true, + upsert: true + } + ); + + // encrypt and save integration access details + integrationAuth = await IntegrationService.setIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString(), + accessId, + accessToken, + accessExpiresAt: undefined + }); + + if (!integrationAuth) throw new Error("Failed to save integration access token"); + + return res.status(200).send({ + integrationAuth + }); +}; /** * Return list of applications allowed for integration with integration authorization id [integrationAuthId] @@ -147,107 +141,108 @@ export const saveIntegrationAccessToken = async ( */ export const getIntegrationAuthApps = async (req: Request, res: Response) => { const teamId = req.query.teamId as string; - + const apps = await getApps({ integrationAuth: req.integrationAuth, accessToken: req.accessToken, - ...teamId && { teamId } + accessId: req.accessId, + ...(teamId && { teamId }) }); - return res.status(200).send({ - apps - }); + return res.status(200).send({ + apps + }); }; /** * Return list of teams allowed for integration with integration authorization id [integrationAuthId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { - const teams = await getTeams({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken - }); - - return res.status(200).send({ - teams - }); -} + const teams = await getTeams({ + integrationAuth: req.integrationAuth, + accessToken: req.accessToken + }); + + return res.status(200).send({ + teams + }); +}; /** * Return list of available Vercel (preview) branches for Vercel project with * id [appId] - * @param req - * @param res + * @param req + * @param res */ export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => { - const { integrationAuthId } = req.params; - const appId = req.query.appId as string; - - interface VercelBranch { - ref: string; - lastCommit: string; - isProtected: boolean; - } + const appId = req.query.appId as string; - const params = new URLSearchParams({ - projectId: appId, - ...(req.integrationAuth.teamId ? { - teamId: req.integrationAuth.teamId - } : {}) - }); + interface VercelBranch { + ref: string; + lastCommit: string; + isProtected: boolean; + } - let branches: string[] = []; - - if (appId && appId !== '') { - const { data }: { data: VercelBranch[] } = await standardRequest.get( - `${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`, - { - params, - headers: { - Authorization: `Bearer ${req.accessToken}`, - 'Accept-Encoding': 'application/json' - } - } - ); - - branches = data.map((b) => b.ref); - } + const params = new URLSearchParams({ + projectId: appId, + ...(req.integrationAuth.teamId + ? { + teamId: req.integrationAuth.teamId + } + : {}) + }); - return res.status(200).send({ - branches - }); -} + let branches: string[] = []; + + if (appId && appId !== "") { + const { data }: { data: VercelBranch[] } = await standardRequest.get( + `${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`, + { + params, + headers: { + Authorization: `Bearer ${req.accessToken}`, + "Accept-Encoding": "application/json" + } + } + ); + + branches = data.map((b) => b.ref); + } + + return res.status(200).send({ + branches + }); +}; /** * Return list of Railway environments for Railway project with * id [appId] - * @param req - * @param res + * @param req + * @param res */ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => { - const { integrationAuthId } = req.params; - const appId = req.query.appId as string; - - interface RailwayEnvironment { - node: { - id: string; - name: string; - isEphemeral: boolean; - } - } - - interface Environment { - environmentId: string; - name: string; - } - - let environments: Environment[] = []; + const appId = req.query.appId as string; - if (appId && appId !== '') { - const query = ` + interface RailwayEnvironment { + node: { + id: string; + name: string; + isEphemeral: boolean; + }; + } + + interface Environment { + environmentId: string; + name: string; + } + + let environments: Environment[] = []; + + if (appId && appId !== "") { + const query = ` query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) { environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) { edges { @@ -260,59 +255,68 @@ export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: R } } `; - - const variables = { - projectId: appId - } - - const { data: { data: { environments: { edges } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { - query, - variables, - }, { - headers: { - 'Authorization': `Bearer ${req.accessToken}`, - 'Content-Type': 'application/json', - }, - }); - - environments = edges.map((e: RailwayEnvironment) => { - return ({ - name: e.node.name, - environmentId: e.node.id - }); - }); - } - - return res.status(200).send({ - environments - }); -} + + const variables = { + projectId: appId + }; + + const { + data: { + data: { + environments: { edges } + } + } + } = await standardRequest.post( + INTEGRATION_RAILWAY_API_URL, + { + query, + variables + }, + { + headers: { + Authorization: `Bearer ${req.accessToken}`, + "Content-Type": "application/json" + } + } + ); + + environments = edges.map((e: RailwayEnvironment) => { + return { + name: e.node.name, + environmentId: e.node.id + }; + }); + } + + return res.status(200).send({ + environments + }); +}; /** * Return list of Railway services for Railway project with id * [appId] - * @param req - * @param res + * @param req + * @param res */ export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => { - const { integrationAuthId } = req.params; - const appId = req.query.appId as string; - - interface RailwayService { - node: { - id: string; - name: string; - } - } - - interface Service { - name: string; - serviceId: string; - } - - let services: Service[] = []; - - const query = ` + const appId = req.query.appId as string; + + interface RailwayService { + node: { + id: string; + name: string; + }; + } + + interface Service { + name: string; + serviceId: string; + } + + let services: Service[] = []; + + const query = ` query project($id: String!) { project(id: $id) { createdAt @@ -340,31 +344,43 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo } `; - if (appId && appId !== '') { - const variables = { - id: appId - } - - const { data: { data: { project: { services: { edges } } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { - query, - variables - }, { - headers: { - 'Authorization': `Bearer ${req.accessToken}`, - 'Content-Type': 'application/json', - }, - }); - - services = edges.map((e: RailwayService) => ({ - name: e.node.name, - serviceId: e.node.id - })); - } - - return res.status(200).send({ - services - }); -} + if (appId && appId !== "") { + const variables = { + id: appId + }; + + const { + data: { + data: { + project: { + services: { edges } + } + } + } + } = await standardRequest.post( + INTEGRATION_RAILWAY_API_URL, + { + query, + variables + }, + { + headers: { + Authorization: `Bearer ${req.accessToken}`, + "Content-Type": "application/json" + } + } + ); + + services = edges.map((e: RailwayService) => ({ + name: e.node.name, + serviceId: e.node.id + })); + } + + return res.status(200).send({ + services + }); +}; /** * Delete integration authorization with id [integrationAuthId] @@ -375,10 +391,10 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo export const deleteIntegrationAuth = async (req: Request, res: Response) => { const integrationAuth = await revokeAccess({ integrationAuth: req.integrationAuth, - accessToken: req.accessToken, + accessToken: req.accessToken }); return res.status(200).send({ - integrationAuth, + integrationAuth }); }; diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 5119761d0..63828ef14 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -1,10 +1,11 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { - Integration -} from '../../models'; -import { EventService } from '../../services'; -import { eventPushSecrets } from '../../events'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Integration } from "../../models"; +import { EventService } from "../../services"; +import { eventPushSecrets } from "../../events"; +import Folder from "../../models/folder"; +import { getFolderByPath } from "../../services/FolderService"; +import { BadRequestError } from "../../utils/errors"; /** * Create/initialize an (empty) integration for integration authorization @@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => { targetServiceId, owner, path, - region + region, + secretPath, } = req.body; - + + const folders = await Folder.findOne({ + workspace: req.integrationAuth.workspace._id, + environment: sourceEnvironment, + }); + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw BadRequestError({ + message: "Path for service token does not exist", + }); + } + } + // TODO: validate [sourceEnvironment] and [targetEnvironment] // initialize new integration after saving integration access token @@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => { owner, path, region, + secretPath, integration: req.integrationAuth.integration, - integrationAuth: new Types.ObjectId(integrationAuthId) + integrationAuth: new Types.ObjectId(integrationAuthId), }).save(); - + if (integration) { // trigger event - push secrets EventService.handleEvent({ event: eventPushSecrets({ workspaceId: integration.workspace, - environment: sourceEnvironment - }) + environment: sourceEnvironment, + }), }); } @@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => { * @returns */ export const updateIntegration = async (req: Request, res: Response) => { - // TODO: add integration-specific validation to ensure that each // integration has the correct fields populated in [Integration] @@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, // github-specific integration param + secretPath, } = req.body; + const folders = await Folder.findOne({ + workspace: req.integration.workspace, + environment, + }); + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw BadRequestError({ + message: "Path for service token does not exist", + }); + } + } + const integration = await Integration.findOneAndUpdate( { _id: req.integration._id, @@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => { appId, targetEnvironment, owner, + secretPath, }, { new: true, @@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => { EventService.handleEvent({ event: eventPushSecrets({ workspaceId: integration.workspace, - environment + environment, }), }); } diff --git a/backend/src/controllers/v1/keyController.ts b/backend/src/controllers/v1/keyController.ts index beb4d0d07..ed82dfdcc 100644 --- a/backend/src/controllers/v1/keyController.ts +++ b/backend/src/controllers/v1/keyController.ts @@ -1,6 +1,6 @@ -import { Request, Response } from 'express'; -import { Key } from '../../models'; -import { findMembership } from '../../helpers/membership'; +import { Request, Response } from "express"; +import { Key } from "../../models"; +import { findMembership } from "../../helpers/membership"; /** * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with @@ -16,11 +16,11 @@ export const uploadKey = async (req: Request, res: Response) => { // validate membership of receiver const receiverMembership = await findMembership({ user: key.userId, - workspace: workspaceId + workspace: workspaceId, }); if (!receiverMembership) { - throw new Error('Failed receiver membership validation for workspace'); + throw new Error("Failed receiver membership validation for workspace"); } await new Key({ @@ -28,11 +28,11 @@ export const uploadKey = async (req: Request, res: Response) => { nonce: key.nonce, sender: req.user._id, receiver: key.userId, - workspace: workspaceId + workspace: workspaceId, }).save(); return res.status(200).send({ - message: 'Successfully uploaded key to workspace' + message: "Successfully uploaded key to workspace", }); }; @@ -48,16 +48,16 @@ export const getLatestKey = async (req: Request, res: Response) => { // get latest key const latestKey = await Key.find({ workspace: workspaceId, - receiver: req.user._id + receiver: req.user._id, }) .sort({ createdAt: -1 }) .limit(1) - .populate('sender', '+publicKey'); + .populate("sender", "+publicKey"); const resObj: any = {}; if (latestKey.length > 0) { - resObj['latestKey'] = latestKey[0]; + resObj["latestKey"] = latestKey[0]; } return res.status(200).send(resObj); diff --git a/backend/src/controllers/v1/membershipController.ts b/backend/src/controllers/v1/membershipController.ts index d7e512adb..d795bcb60 100644 --- a/backend/src/controllers/v1/membershipController.ts +++ b/backend/src/controllers/v1/membershipController.ts @@ -1,12 +1,9 @@ -import { Request, Response } from 'express'; -import { Membership, MembershipOrg, User, Key } from '../../models'; -import { - findMembership, - deleteMembership as deleteMember -} from '../../helpers/membership'; -import { sendMail } from '../../helpers/nodemailer'; -import { ADMIN, MEMBER, ACCEPTED } from '../../variables'; -import { getSiteURL } from '../../config'; +import { Request, Response } from "express"; +import { Key, Membership, MembershipOrg, User } from "../../models"; +import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership"; +import { sendMail } from "../../helpers/nodemailer"; +import { ACCEPTED, ADMIN, MEMBER } from "../../variables"; +import { getSiteURL } from "../../config"; /** * Check that user is a member of workspace with id [workspaceId] @@ -23,12 +20,12 @@ export const validateMembership = async (req: Request, res: Response) => { }); if (!membership) { - throw new Error('Failed to validate membership'); + throw new Error("Failed to validate membership"); } - return res.status(200).send({ - message: 'Workspace membership confirmed' - }); + return res.status(200).send({ + message: "Workspace membership confirmed" + }); }; /** @@ -43,12 +40,10 @@ export const deleteMembership = async (req: Request, res: Response) => { // check if membership to delete exists const membershipToDelete = await Membership.findOne({ _id: membershipId - }).populate('user'); + }).populate("user"); if (!membershipToDelete) { - throw new Error( - "Failed to delete workspace membership that doesn't exist" - ); + throw new Error("Failed to delete workspace membership that doesn't exist"); } // check if user is a member and admin of the workspace @@ -59,12 +54,12 @@ export const deleteMembership = async (req: Request, res: Response) => { }); if (!membership) { - throw new Error('Failed to validate workspace membership'); + throw new Error("Failed to validate workspace membership"); } if (membership.role !== ADMIN) { // user is not an admin member of the workspace - throw new Error('Insufficient role for deleting workspace membership'); + throw new Error("Insufficient role for deleting workspace membership"); } // delete workspace membership @@ -72,9 +67,9 @@ export const deleteMembership = async (req: Request, res: Response) => { membershipId: membershipToDelete._id.toString() }); - return res.status(200).send({ - deletedMembership - }); + return res.status(200).send({ + deletedMembership + }); }; /** @@ -88,7 +83,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => { const { role } = req.body; if (![ADMIN, MEMBER].includes(role)) { - throw new Error('Failed to validate role'); + throw new Error("Failed to validate role"); } // validate target membership @@ -97,7 +92,7 @@ export const changeMembershipRole = async (req: Request, res: Response) => { }); if (!membershipToChangeRole) { - throw new Error('Failed to find membership to change role'); + throw new Error("Failed to find membership to change role"); } // check if user is a member and admin of target membership's @@ -108,20 +103,20 @@ export const changeMembershipRole = async (req: Request, res: Response) => { }); if (!membership) { - throw new Error('Failed to validate membership'); + throw new Error("Failed to validate membership"); } if (membership.role !== ADMIN) { // user is not an admin member of the workspace - throw new Error('Insufficient role for changing member roles'); + throw new Error("Insufficient role for changing member roles"); } membershipToChangeRole.role = role; await membershipToChangeRole.save(); - return res.status(200).send({ - membership: membershipToChangeRole - }); + return res.status(200).send({ + membership: membershipToChangeRole + }); }; /** @@ -136,10 +131,9 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { const invitee = await User.findOne({ email - }).select('+publicKey'); + }).select("+publicKey"); - if (!invitee || !invitee?.publicKey) - throw new Error('Failed to validate invitee'); + if (!invitee || !invitee?.publicKey) throw new Error("Failed to validate invitee"); // validate invitee's workspace membership - ensure member isn't // already a member of the workspace @@ -148,8 +142,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { workspace: workspaceId }); - if (inviteeMembership) - throw new Error('Failed to add existing member of workspace'); + if (inviteeMembership) throw new Error("Failed to add existing member of workspace"); // validate invitee's organization membership - ensure that only // (accepted) organization members can be added to the workspace @@ -159,8 +152,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { status: ACCEPTED }); - if (!membershipOrg) - throw new Error("Failed to validate invitee's organization membership"); + if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership"); // get latest key const latestKey = await Key.findOne({ @@ -168,29 +160,29 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => { receiver: req.user._id }) .sort({ createdAt: -1 }) - .populate('sender', '+publicKey'); + .populate("sender", "+publicKey"); // create new workspace membership - const m = await new Membership({ + await new Membership({ user: invitee._id, workspace: workspaceId, role: MEMBER }).save(); await sendMail({ - template: 'workspaceInvitation.handlebars', - subjectLine: 'Infisical workspace invitation', + template: "workspaceInvitation.handlebars", + subjectLine: "Infisical workspace invitation", recipients: [invitee.email], substitutions: { inviterFirstName: req.user.firstName, inviterEmail: req.user.email, workspaceName: req.membership.workspace.name, - callback_url: (await getSiteURL()) + '/login' + callback_url: (await getSiteURL()) + "/login" } }); - return res.status(200).send({ - invitee, - latestKey - }); + return res.status(200).send({ + invitee, + latestKey + }); }; diff --git a/backend/src/controllers/v1/membershipOrgController.ts b/backend/src/controllers/v1/membershipOrgController.ts index eafd7fae2..b5669a671 100644 --- a/backend/src/controllers/v1/membershipOrgController.ts +++ b/backend/src/controllers/v1/membershipOrgController.ts @@ -1,15 +1,27 @@ -import { Types } from 'mongoose'; -import { Request, Response } from 'express'; -import { MembershipOrg, Organization, User } from '../../models'; -import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; -import { createToken } from '../../helpers/auth'; -import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; -import { sendMail } from '../../helpers/nodemailer'; -import { TokenService } from '../../services'; -import { EELicenseService } from '../../ee/services'; -import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; -import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; -import { validateUserEmail } from '../../validation'; +import { Types } from "mongoose"; +import { Request, Response } from "express"; +import { MembershipOrg, Organization, User } from "../../models"; +import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg"; +import { createToken } from "../../helpers/auth"; +import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; +import { sendMail } from "../../helpers/nodemailer"; +import { TokenService } from "../../services"; +import { EELicenseService } from "../../ee/services"; +import { + ACCEPTED, + ADMIN, + INVITED, + MEMBER, + OWNER, + TOKEN_EMAIL_ORG_INVITATION +} from "../../variables"; +import { + getJwtSignupLifetime, + getJwtSignupSecret, + getSiteURL, + getSmtpConfigured +} from "../../config"; +import { validateUserEmail } from "../../validation"; /** * Delete organization membership with id [membershipOrgId] from organization @@ -17,18 +29,16 @@ import { validateUserEmail } from '../../validation'; * @param res * @returns */ -export const deleteMembershipOrg = async (req: Request, res: Response) => { +export const deleteMembershipOrg = async (req: Request, _res: Response) => { const { membershipOrgId } = req.params; // check if organization membership to delete exists const membershipOrgToDelete = await MembershipOrg.findOne({ _id: membershipOrgId - }).populate('user'); + }).populate("user"); if (!membershipOrgToDelete) { - throw new Error( - "Failed to delete organization membership that doesn't exist" - ); + throw new Error("Failed to delete organization membership that doesn't exist"); } // check if user is a member and admin of the organization @@ -39,16 +49,16 @@ export const deleteMembershipOrg = async (req: Request, res: Response) => { }); if (!membershipOrg) { - throw new Error('Failed to validate organization membership'); + throw new Error("Failed to validate organization membership"); } if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { // user is not an admin member of the organization - throw new Error('Insufficient role for deleting organization membership'); + throw new Error("Insufficient role for deleting organization membership"); } // delete organization membership - const deletedMembershipOrg = await deleteMemberFromOrg({ + await deleteMemberFromOrg({ membershipOrgId: membershipOrgToDelete._id.toString() }); @@ -56,7 +66,7 @@ export const deleteMembershipOrg = async (req: Request, res: Response) => { organizationId: membershipOrg.organization.toString() }); - return membershipOrgToDelete; + return membershipOrgToDelete; }; /** @@ -66,14 +76,14 @@ export const deleteMembershipOrg = async (req: Request, res: Response) => { * @returns */ export const changeMembershipOrgRole = async (req: Request, res: Response) => { - // change role for (target) organization membership with id - // [membershipOrgId] + // change role for (target) organization membership with id + // [membershipOrgId] - let membershipToChangeRole; + let membershipToChangeRole; - return res.status(200).send({ - membershipOrg: membershipToChangeRole - }); + return res.status(200).send({ + membershipOrg: membershipToChangeRole + }); }; /** @@ -84,7 +94,7 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => { * @returns */ export const inviteUserToOrganization = async (req: Request, res: Response) => { - let invitee, inviteeMembershipOrg, completeInviteLink; + let inviteeMembershipOrg, completeInviteLink; const { organizationId, inviteeEmail } = req.body; const host = req.headers.host; const siteUrl = `${req.protocol}://${host}`; @@ -96,25 +106,26 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { }); if (!membershipOrg) { - throw new Error('Failed to validate organization membership'); + throw new Error("Failed to validate organization membership"); } - - const plan = await EELicenseService.getOrganizationPlan(organizationId); - + + const plan = await EELicenseService.getPlan(organizationId); + if (plan.memberLimit !== null) { // case: limit imposed on number of members allowed - + if (plan.membersUsed >= plan.memberLimit) { // case: number of members used exceeds the number of members allowed return res.status(400).send({ - message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.' + message: + "Failed to invite member due to member limit reached. Upgrade plan to invite more members." }); } } - invitee = await User.findOne({ + const invitee = await User.findOne({ email: inviteeEmail - }).select('+publicKey'); + }).select("+publicKey"); if (invitee) { // case: invitee is an existing user @@ -125,13 +136,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { }); if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { - throw new Error( - 'Failed to invite an existing member of the organization' - ); + throw new Error("Failed to invite an existing member of the organization"); } if (!inviteeMembershipOrg) { - await new MembershipOrg({ user: invitee, inviteEmail: inviteeEmail, @@ -149,7 +157,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { if (!inviteeMembershipOrg) { // case: invitee has never been invited before - + // validate that email is not disposable validateUserEmail(inviteeEmail); @@ -165,7 +173,6 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { const organization = await Organization.findOne({ _id: organizationId }); if (organization) { - const token = await TokenService.createToken({ type: TOKEN_EMAIL_ORG_INVITATION, email: inviteeEmail, @@ -173,8 +180,8 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { }); await sendMail({ - template: 'organizationInvitation.handlebars', - subjectLine: 'Infisical organization invitation', + template: "organizationInvitation.handlebars", + subjectLine: "Infisical organization invitation", recipients: [inviteeEmail], substitutions: { inviterFirstName: req.user.firstName, @@ -183,21 +190,23 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { email: inviteeEmail, organizationId: organization._id.toString(), token, - callback_url: (await getSiteURL()) + '/signupinvite' + callback_url: (await getSiteURL()) + "/signupinvite" } }); if (!(await getSmtpConfigured())) { - completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}` + completeInviteLink = `${ + siteUrl + "/signupinvite" + }?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`; } } await updateSubscriptionOrgQuantity({ organizationId }); - return res.status(200).send({ - message: `Sent an invite link to ${req.body.inviteeEmail}`, - completeInviteLink - }); + return res.status(200).send({ + message: `Sent an invite link to ${req.body.inviteeEmail}`, + completeInviteLink + }); }; /** @@ -208,14 +217,10 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => { * @returns */ export const verifyUserToOrganization = async (req: Request, res: Response) => { - let user; - const { - email, - organizationId, - code - } = req.body; + let user; + const { email, organizationId, code } = req.body; - user = await User.findOne({ email }).select('+publicKey'); + user = await User.findOne({ email }).select("+publicKey"); const membershipOrg = await MembershipOrg.findOne({ inviteEmail: email, @@ -223,8 +228,7 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { organization: new Types.ObjectId(organizationId) }); - if (!membershipOrg) - throw new Error('Failed to find any invitations for email'); + if (!membershipOrg) throw new Error("Failed to find any invitations for email"); await TokenService.validateToken({ type: TOKEN_EMAIL_ORG_INVITATION, @@ -238,14 +242,14 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { // membership can be approved and redirected to login/dashboard membershipOrg.status = ACCEPTED; await membershipOrg.save(); - + await updateSubscriptionOrgQuantity({ organizationId }); return res.status(200).send({ - message: 'Successfully verified email', - user, + message: "Successfully verified email", + user }); } @@ -265,9 +269,9 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => { secret: await getJwtSignupSecret() }); - return res.status(200).send({ - message: 'Successfully verified email', - user, - token - }); + return res.status(200).send({ + message: "Successfully verified email", + user, + token + }); }; diff --git a/backend/src/controllers/v1/organizationController.ts b/backend/src/controllers/v1/organizationController.ts index f0c73d75c..304df8b43 100644 --- a/backend/src/controllers/v1/organizationController.ts +++ b/backend/src/controllers/v1/organizationController.ts @@ -1,28 +1,27 @@ -import { Request, Response } from 'express'; -import Stripe from 'stripe'; +import { Request, Response } from "express"; +import Stripe from "stripe"; import { + IncidentContactOrg, Membership, MembershipOrg, Organization, Workspace, - IncidentContactOrg -} from '../../models'; -import { createOrganization as create } from '../../helpers/organization'; -import { addMembershipsOrg } from '../../helpers/membershipOrg'; -import { OWNER, ACCEPTED } from '../../variables'; -import _ from 'lodash'; -import { getStripeSecretKey, getSiteURL } from '../../config'; +} from "../../models"; +import { createOrganization as create } from "../../helpers/organization"; +import { addMembershipsOrg } from "../../helpers/membershipOrg"; +import { ACCEPTED, OWNER } from "../../variables"; +import { getSiteURL, getStripeSecretKey } from "../../config"; export const getOrganizations = async (req: Request, res: Response) => { const organizations = ( await MembershipOrg.find({ user: req.user._id, - status: ACCEPTED - }).populate('organization') + status: ACCEPTED, + }).populate("organization") ).map((m) => m.organization); return res.status(200).send({ - organizations + organizations, }); }; @@ -37,24 +36,24 @@ export const createOrganization = async (req: Request, res: Response) => { const { organizationName } = req.body; if (organizationName.length < 1) { - throw new Error('Organization names must be at least 1-character long'); + throw new Error("Organization names must be at least 1-character long"); } // create organization and add user as member const organization = await create({ email: req.user.email, - name: organizationName + name: organizationName, }); await addMembershipsOrg({ userIds: [req.user._id.toString()], organizationId: organization._id.toString(), roles: [OWNER], - statuses: [ACCEPTED] + statuses: [ACCEPTED], }); return res.status(200).send({ - organization + organization, }); }; @@ -67,7 +66,7 @@ export const createOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => { const organization = req.organization return res.status(200).send({ - organization + organization, }); }; @@ -81,11 +80,11 @@ export const getOrganizationMembers = async (req: Request, res: Response) => { const { organizationId } = req.params; const users = await MembershipOrg.find({ - organization: organizationId - }).populate('user', '+publicKey'); + organization: organizationId, + }).populate("user", "+publicKey"); return res.status(200).send({ - users + users, }); }; @@ -105,23 +104,23 @@ export const getOrganizationWorkspaces = async ( ( await Workspace.find( { - organization: organizationId + organization: organizationId, }, - '_id' + "_id" ) ).map((w) => w._id.toString()) ); const workspaces = ( await Membership.find({ - user: req.user._id - }).populate('workspace') + user: req.user._id, + }).populate("workspace") ) .filter((m) => workspacesSet.has(m.workspace._id.toString())) .map((m) => m.workspace); return res.status(200).send({ - workspaces + workspaces, }); }; @@ -137,19 +136,19 @@ export const changeOrganizationName = async (req: Request, res: Response) => { const organization = await Organization.findOneAndUpdate( { - _id: organizationId + _id: organizationId, }, { - name + name, }, { - new: true + new: true, } ); return res.status(200).send({ - message: 'Successfully changed organization name', - organization + message: "Successfully changed organization name", + organization, }); }; @@ -166,11 +165,11 @@ export const getOrganizationIncidentContacts = async ( const { organizationId } = req.params; const incidentContactsOrg = await IncidentContactOrg.find({ - organization: organizationId + organization: organizationId, }); return res.status(200).send({ - incidentContactsOrg + incidentContactsOrg, }); }; @@ -194,7 +193,7 @@ export const addOrganizationIncidentContact = async ( ); return res.status(200).send({ - incidentContactOrg + incidentContactOrg, }); }; @@ -213,12 +212,12 @@ export const deleteOrganizationIncidentContact = async ( const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ email, - organization: organizationId + organization: organizationId, }); return res.status(200).send({ - message: 'Successfully deleted organization incident contact', - incidentContactOrg + message: "Successfully deleted organization incident contact", + incidentContactOrg, }); }; @@ -235,28 +234,28 @@ export const createOrganizationPortalSession = async ( ) => { let session; const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' + apiVersion: "2022-08-01", }); // check if there is a payment method on file const paymentMethods = await stripe.paymentMethods.list({ customer: req.organization.customerId, - type: 'card' + type: "card", }); if (paymentMethods.data.length < 1) { // case: no payment method on file session = await stripe.checkout.sessions.create({ customer: req.organization.customerId, - mode: 'setup', - payment_method_types: ['card'], - success_url: (await getSiteURL()) + '/dashboard', - cancel_url: (await getSiteURL()) + '/dashboard' + mode: "setup", + payment_method_types: ["card"], + success_url: (await getSiteURL()) + "/dashboard", + cancel_url: (await getSiteURL()) + "/dashboard", }); } else { session = await stripe.billingPortal.sessions.create({ customer: req.organization.customerId, - return_url: (await getSiteURL()) + '/dashboard' + return_url: (await getSiteURL()) + "/dashboard", }); } @@ -274,15 +273,15 @@ export const getOrganizationSubscriptions = async ( res: Response ) => { const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' + apiVersion: "2022-08-01", }); const subscriptions = await stripe.subscriptions.list({ - customer: req.organization.customerId + customer: req.organization.customerId, }); return res.status(200).send({ - subscriptions + subscriptions, }); }; @@ -302,16 +301,16 @@ export const getOrganizationMembersAndTheirWorkspaces = async ( const workspacesSet = ( await Workspace.find( { - organization: organizationId + organization: organizationId, }, - '_id' + "_id" ) ).map((w) => w._id.toString()); const memberships = ( await Membership.find({ - workspace: { $in: workspacesSet } - }).populate('workspace') + workspace: { $in: workspacesSet }, + }).populate("workspace") ); const userToWorkspaceIds: any = {}; diff --git a/backend/src/controllers/v1/passwordController.ts b/backend/src/controllers/v1/passwordController.ts index bdeae0fd4..5a2699225 100644 --- a/backend/src/controllers/v1/passwordController.ts +++ b/backend/src/controllers/v1/passwordController.ts @@ -1,85 +1,77 @@ -import { Request, Response } from 'express'; +import { Request, Response } from "express"; // eslint-disable-next-line @typescript-eslint/no-var-requires -const jsrp = require('jsrp'); -import * as bigintConversion from 'bigint-conversion'; -import { User, BackupPrivateKey, LoginSRPDetail } from '../../models'; +const jsrp = require("jsrp"); +import * as bigintConversion from "bigint-conversion"; +import { BackupPrivateKey, LoginSRPDetail, User } from "../../models"; +import { clearTokens, createToken, sendMail } from "../../helpers"; +import { TokenService } from "../../services"; +import { AUTH_MODE_JWT, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables"; +import { BadRequestError } from "../../utils/errors"; import { - createToken, - sendMail, - clearTokens -} from '../../helpers'; -import { TokenService } from '../../services'; -import { - TOKEN_EMAIL_PASSWORD_RESET, - AUTH_MODE_JWT -} from '../../variables'; -import { BadRequestError } from '../../utils/errors'; -import { - getSiteURL, - getJwtSignupLifetime, - getJwtSignupSecret, - getHttpsEnabled -} from '../../config'; + getHttpsEnabled, + getJwtSignupLifetime, + getJwtSignupSecret, + getSiteURL +} from "../../config"; /** - * Password reset step 1: Send email verification link to email [email] + * Password reset step 1: Send email verification link to email [email] * for account recovery. * @param req * @param res * @returns */ export const emailPasswordReset = async (req: Request, res: Response) => { - let email: string; - email = req.body.email; + const email: string = req.body.email; - const user = await User.findOne({ email }).select('+publicKey'); + const user = await User.findOne({ email }).select("+publicKey"); if (!user || !user?.publicKey) { // case: user has already completed account - return res.status(403).send({ + return res.status(200).send({ message: "If an account exists with this email, a password reset link has been sent" }); } - + const token = await TokenService.createToken({ type: TOKEN_EMAIL_PASSWORD_RESET, email }); - + await sendMail({ - template: 'passwordReset.handlebars', - subjectLine: 'Infisical password reset', + template: "passwordReset.handlebars", + subjectLine: "Infisical password reset", recipients: [email], substitutions: { email, token, - callback_url: (await getSiteURL()) + '/password-reset' + callback_url: (await getSiteURL()) + "/password-reset" } }); - return res.status(200).send({ - message:"If an account exists with this email, a password reset link has been sent" - }); -} + return res.status(200).send({ + message: "If an account exists with this email, a password reset link has been sent" + }); +}; /** * Password reset step 2: Verify email verification link sent to email [email] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const emailPasswordResetVerify = async (req: Request, res: Response) => { const { email, code } = req.body; - const user = await User.findOne({ email }).select('+publicKey'); + const user = await User.findOne({ email }).select("+publicKey"); if (!user || !user?.publicKey) { - // case: user doesn't exist with email [email] or + // case: user doesn't exist with email [email] or // hasn't even completed their account return res.status(403).send({ - error: 'Failed email verification for password reset' + error: "Failed email verification for password reset" }); } - + await TokenService.validateToken({ type: TOKEN_EMAIL_PASSWORD_RESET, email, @@ -95,12 +87,12 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { secret: await getJwtSignupSecret() }); - return res.status(200).send({ - message: 'Successfully verified email', - user, - token - }); -} + return res.status(200).send({ + message: "Successfully verified email", + user, + token + }); +}; /** * Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol @@ -109,14 +101,14 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => { * @returns */ export const srp1 = async (req: Request, res: Response) => { - // return salt, serverPublicKey as part of first step of SRP protocol - + // return salt, serverPublicKey as part of first step of SRP protocol + const { clientPublicKey } = req.body; const user = await User.findOne({ email: req.user.email - }).select('+salt +verifier'); + }).select("+salt +verifier"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); const server = new jsrp.server(); server.init( @@ -128,11 +120,15 @@ export const srp1 = async (req: Request, res: Response) => { // generate server-side public key const serverPublicKey = server.getPublicKey(); - await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, { - email: req.user.email, - clientPublicKey: clientPublicKey, - serverBInt: bigintConversion.bigintToBuf(server.bInt), - }, { upsert: true, returnNewDocument: false }) + await LoginSRPDetail.findOneAndReplace( + { email: req.user.email }, + { + email: req.user.email, + clientPublicKey: clientPublicKey, + serverBInt: bigintConversion.bigintToBuf(server.bInt) + }, + { upsert: true, returnNewDocument: false } + ); return res.status(200).send({ serverPublicKey, @@ -140,8 +136,7 @@ export const srp1 = async (req: Request, res: Response) => { }); } ); -} - +}; /** * Change account SRP authentication information for user @@ -152,8 +147,8 @@ export const srp1 = async (req: Request, res: Response) => { * @returns */ export const changePassword = async (req: Request, res: Response) => { - const { - clientProof, + const { + clientProof, protectedKey, protectedKeyIV, protectedKeyTag, @@ -166,14 +161,18 @@ export const changePassword = async (req: Request, res: Response) => { const user = await User.findOne({ email: req.user.email - }).select('+salt +verifier'); + }).select("+salt +verifier"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }); if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + return BadRequestError( + Error( + "It looks like some details from the first login are not found. Please try login one again" + ) + ); } const server = new jsrp.server(); @@ -207,27 +206,31 @@ export const changePassword = async (req: Request, res: Response) => { new: true } ); - - if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) { - await clearTokens(req.authData.tokenVersionId) + + if ( + req.authData.authMode === AUTH_MODE_JWT && + req.authData.authPayload instanceof User && + req.authData.tokenVersionId + ) { + await clearTokens(req.authData.tokenVersionId); } // clear httpOnly cookie - - res.cookie('jid', '', { + + res.cookie("jid", "", { httpOnly: true, - path: '/', - sameSite: 'strict', + path: "/", + sameSite: "strict", secure: (await getHttpsEnabled()) as boolean }); return res.status(200).send({ - message: 'Successfully changed password' + message: "Successfully changed password" }); } return res.status(400).send({ - error: 'Failed to change password. Try again?' + error: "Failed to change password. Try again?" }); } ); @@ -240,22 +243,25 @@ export const changePassword = async (req: Request, res: Response) => { * @returns */ export const createBackupPrivateKey = async (req: Request, res: Response) => { - // create/change backup private key - // requires verifying [clientProof] as part of second step of SRP protocol - // as initiated in /srp1 + // create/change backup private key + // requires verifying [clientProof] as part of second step of SRP protocol + // as initiated in /srp1 - const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = - req.body; + const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = req.body; const user = await User.findOne({ email: req.user.email - }).select('+salt +verifier'); + }).select("+salt +verifier"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); - const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) + const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }); if (!loginSRPDetailFromDB) { - return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) + return BadRequestError( + Error( + "It looks like some details from the first login are not found. Please try login one again" + ) + ); } const server = new jsrp.server(); @@ -266,9 +272,7 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { b: loginSRPDetailFromDB.serverBInt }, async () => { - server.setClientPublicKey( - loginSRPDetailFromDB.clientPublicKey - ); + server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); // compare server and client shared keys if (server.checkClientProof(clientProof)) { @@ -285,17 +289,17 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { verifier }, { upsert: true, new: true } - ).select('+user, encryptedPrivateKey'); + ).select("+user, encryptedPrivateKey"); // issue tokens return res.status(200).send({ - message: 'Successfully updated backup private key', + message: "Successfully updated backup private key", backupPrivateKey }); } return res.status(400).send({ - message: 'Failed to update backup private key' + message: "Failed to update backup private key" }); } ); @@ -303,21 +307,21 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => { /** * Return backup private key for user - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getBackupPrivateKey = async (req: Request, res: Response) => { const backupPrivateKey = await BackupPrivateKey.findOne({ user: req.user._id - }).select('+encryptedPrivateKey +iv +tag'); + }).select("+encryptedPrivateKey +iv +tag"); - if (!backupPrivateKey) throw new Error('Failed to find backup private key'); + if (!backupPrivateKey) throw new Error("Failed to find backup private key"); - return res.status(200).send({ - backupPrivateKey - }); -} + return res.status(200).send({ + backupPrivateKey + }); +}; export const resetPassword = async (req: Request, res: Response) => { const { @@ -328,7 +332,7 @@ export const resetPassword = async (req: Request, res: Response) => { encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt, - verifier, + verifier } = req.body; await User.findByIdAndUpdate( @@ -337,7 +341,7 @@ export const resetPassword = async (req: Request, res: Response) => { encryptionVersion: 2, protectedKey, protectedKeyIV, - protectedKeyTag, + protectedKeyTag, encryptedPrivateKey, iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, @@ -349,7 +353,7 @@ export const resetPassword = async (req: Request, res: Response) => { } ); - return res.status(200).send({ - message: 'Successfully reset password' - }); -} + return res.status(200).send({ + message: "Successfully reset password" + }); +}; diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index 074e29454..bcb00d209 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -1,30 +1,30 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { Key, Secret } from '../../models'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Key } from "../../models"; import { - v1PushSecrets as push, - pullSecrets as pull, - reformatPullSecrets -} from '../../helpers/secret'; -import { pushKeys } from '../../helpers/key'; -import { eventPushSecrets } from '../../events'; -import { EventService } from '../../services'; -import { TelemetryService } from '../../services'; + pullSecrets as pull, + v1PushSecrets as push, + reformatPullSecrets +} from "../../helpers/secret"; +import { pushKeys } from "../../helpers/key"; +import { eventPushSecrets } from "../../events"; +import { EventService } from "../../services"; +import { TelemetryService } from "../../services"; interface PushSecret { - ciphertextKey: string; - ivKey: string; - tagKey: string; - hashKey: string; - ciphertextValue: string; - ivValue: string; - tagValue: string; - hashValue: string; - ciphertextComment: string; - ivComment: string; - tagComment: string; - hashComment: string; - type: 'shared' | 'personal'; + ciphertextKey: string; + ivKey: string; + tagKey: string; + hashKey: string; + ciphertextValue: string; + ivValue: string; + tagValue: string; + hashValue: string; + ciphertextComment: string; + ivComment: string; + tagComment: string; + hashComment: string; + type: "shared" | "personal"; } /** @@ -35,7 +35,7 @@ interface PushSecret { * @returns */ export const pushSecrets = async (req: Request, res: Response) => { - // upload (encrypted) secrets to workspace with id [workspaceId] + // upload (encrypted) secrets to workspace with id [workspaceId] const postHogClient = await TelemetryService.getPostHogClient(); let { secrets }: { secrets: PushSecret[] } = req.body; const { keys, environment, channel } = req.body; @@ -44,13 +44,11 @@ export const pushSecrets = async (req: Request, res: Response) => { // validate environment const workspaceEnvs = req.membership.workspace.environments; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); + throw new Error("Failed to validate environment"); } // sanitize secrets - secrets = secrets.filter( - (s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== '' - ); + secrets = secrets.filter((s: PushSecret) => s.ciphertextKey !== "" && s.ciphertextValue !== ""); await push({ userId: req.user._id, @@ -64,17 +62,16 @@ export const pushSecrets = async (req: Request, res: Response) => { workspaceId, keys }); - - + if (postHogClient) { postHogClient.capture({ - event: 'secrets pushed', + event: "secrets pushed", distinctId: req.user.email, properties: { numberOfSecrets: secrets.length, environment, workspaceId, - channel: channel ? channel : 'cli' + channel: channel ? channel : "cli" } }); } @@ -87,9 +84,9 @@ export const pushSecrets = async (req: Request, res: Response) => { }) }); - return res.status(200).send({ - message: 'Successfully uploaded workspace secrets' - }); + return res.status(200).send({ + message: "Successfully uploaded workspace secrets" + }); }; /** @@ -100,57 +97,56 @@ export const pushSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecrets = async (req: Request, res: Response) => { - let secrets; - let key; + let secrets; - const postHogClient = await TelemetryService.getPostHogClient(); - const environment: string = req.query.environment as string; - const channel: string = req.query.channel as string; - const { workspaceId } = req.params; + const postHogClient = await TelemetryService.getPostHogClient(); + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; - // validate environment - const workspaceEnvs = req.membership.workspace.environments; - if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); - } + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error("Failed to validate environment"); + } - secrets = await pull({ - userId: req.user._id.toString(), - workspaceId, - environment, - channel: channel ? channel : 'cli', - ipAddress: req.realIP - }); + secrets = await pull({ + userId: req.user._id.toString(), + workspaceId, + environment, + channel: channel ? channel : "cli", + ipAddress: req.realIP + }); - key = await Key.findOne({ - workspace: workspaceId, - receiver: req.user._id - }) - .sort({ createdAt: -1 }) - .populate('sender', '+publicKey'); - - if (channel !== 'cli') { - secrets = reformatPullSecrets({ secrets }); - } + const key = await Key.findOne({ + workspace: workspaceId, + receiver: req.user._id + }) + .sort({ createdAt: -1 }) + .populate("sender", "+publicKey"); - if (postHogClient) { - // capture secrets pushed event in production - postHogClient.capture({ - distinctId: req.user.email, - event: 'secrets pulled', - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } + if (channel !== "cli") { + secrets = reformatPullSecrets({ secrets }); + } - return res.status(200).send({ - secrets, - key - }); + if (postHogClient) { + // capture secrets pushed event in production + postHogClient.capture({ + distinctId: req.user.email, + event: "secrets pulled", + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : "cli" + } + }); + } + + return res.status(200).send({ + secrets, + key + }); }; /** @@ -162,54 +158,51 @@ export const pullSecrets = async (req: Request, res: Response) => { * @returns */ export const pullSecretsServiceToken = async (req: Request, res: Response) => { - let secrets; - let key; + const postHogClient = await TelemetryService.getPostHogClient(); + const environment: string = req.query.environment as string; + const channel: string = req.query.channel as string; + const { workspaceId } = req.params; - const postHogClient = await TelemetryService.getPostHogClient(); - const environment: string = req.query.environment as string; - const channel: string = req.query.channel as string; - const { workspaceId } = req.params; + // validate environment + const workspaceEnvs = req.membership.workspace.environments; + if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { + throw new Error("Failed to validate environment"); + } - // validate environment - const workspaceEnvs = req.membership.workspace.environments; - if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); - } + const secrets = await pull({ + userId: req.serviceToken.user._id.toString(), + workspaceId, + environment, + channel: "cli", + ipAddress: req.realIP + }); - secrets = await pull({ - userId: req.serviceToken.user._id.toString(), - workspaceId, - environment, - channel: 'cli', - ipAddress: req.realIP - }); + const key = { + encryptedKey: req.serviceToken.encryptedKey, + nonce: req.serviceToken.nonce, + sender: { + publicKey: req.serviceToken.publicKey + }, + receiver: req.serviceToken.user, + workspace: req.serviceToken.workspace + }; - key = { - encryptedKey: req.serviceToken.encryptedKey, - nonce: req.serviceToken.nonce, - sender: { - publicKey: req.serviceToken.publicKey - }, - receiver: req.serviceToken.user, - workspace: req.serviceToken.workspace - }; + if (postHogClient) { + // capture secrets pulled event in production + postHogClient.capture({ + distinctId: req.serviceToken.user.email, + event: "secrets pulled", + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, + channel: channel ? channel : "cli" + } + }); + } - if (postHogClient) { - // capture secrets pulled event in production - postHogClient.capture({ - distinctId: req.serviceToken.user.email, - event: 'secrets pulled', - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: channel ? channel : 'cli' - } - }); - } - - return res.status(200).send({ - secrets: reformatPullSecrets({ secrets }), - key - }); + return res.status(200).send({ + secrets: reformatPullSecrets({ secrets }), + key + }); }; diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 06d68a41d..6b31fd111 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -5,12 +5,13 @@ import { BadRequestError } from "../../utils/errors"; import { appendFolder, deleteFolderById, - getAllFolderIds, - searchByFolderIdWithDir, - searchByFolderId, - validateFolderName, generateFolderId, + getAllFolderIds, + getFolderByPath, getParentFromFolderId, + searchByFolderId, + searchByFolderIdWithDir, + validateFolderName, } from "../../services/FolderService"; import { ADMIN, MEMBER } from "../../variables"; import { validateMembership } from "../../helpers/membership"; @@ -177,11 +178,13 @@ export const deleteFolder = async (req: Request, res: Response) => { // TODO: validate workspace export const getFolders = async (req: Request, res: Response) => { - const { workspaceId, environment, parentFolderId } = req.query as { - workspaceId: string; - environment: string; - parentFolderId?: string; - }; + const { workspaceId, environment, parentFolderId, parentFolderPath } = + req.query as { + workspaceId: string; + environment: string; + parentFolderId?: string; + parentFolderPath?: string; + }; const folders = await Folder.findOne({ workspace: workspaceId, environment }); if (!folders) { @@ -196,6 +199,20 @@ export const getFolders = async (req: Request, res: Response) => { acceptedRoles: [ADMIN, MEMBER], }); + // if instead of parentFolderId given a path like /folder1/folder2 + if (parentFolderPath) { + const folder = getFolderByPath(folders.nodes, parentFolderPath); + if (!folder) { + res.send({ folders: [], dir: [] }); + return; + } + // dir is not needed at present as this is only used in overview section of secrets + res.send({ + folders: folder.children.map(({ id, name }) => ({ id, name })), + dir: [{ name: folder.name, id: folder.id }], + }); + } + if (!parentFolderId) { const rootFolders = folders.nodes.children.map(({ id, name }) => ({ id, diff --git a/backend/src/controllers/v1/serviceTokenController.ts b/backend/src/controllers/v1/serviceTokenController.ts index 86a87f372..c1b753a90 100644 --- a/backend/src/controllers/v1/serviceTokenController.ts +++ b/backend/src/controllers/v1/serviceTokenController.ts @@ -1,7 +1,7 @@ -import { Request, Response } from 'express'; -import { ServiceToken } from '../../models'; -import { createToken } from '../../helpers/auth'; -import { getJwtServiceSecret } from '../../config'; +import { Request, Response } from "express"; +import { ServiceToken } from "../../models"; +import { createToken } from "../../helpers/auth"; +import { getJwtServiceSecret } from "../../config"; /** * Return service token on request @@ -11,7 +11,7 @@ import { getJwtServiceSecret } from '../../config'; */ export const getServiceToken = async (req: Request, res: Response) => { return res.status(200).send({ - serviceToken: req.serviceToken + serviceToken: req.serviceToken, }); }; @@ -31,13 +31,13 @@ export const createServiceToken = async (req: Request, res: Response) => { expiresIn, publicKey, encryptedKey, - nonce + nonce, } = req.body; // validate environment const workspaceEnvs = req.membership.workspace.environments; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); + throw new Error("Failed to validate environment"); } // compute access token expiration date @@ -52,24 +52,24 @@ export const createServiceToken = async (req: Request, res: Response) => { expiresAt, publicKey, encryptedKey, - nonce + nonce, }).save(); token = createToken({ payload: { serviceTokenId: serviceToken._id.toString(), - workspaceId + workspaceId, }, expiresIn: expiresIn, - secret: await getJwtServiceSecret() + secret: await getJwtServiceSecret(), }); } catch (err) { return res.status(400).send({ - message: 'Failed to create service token' + message: "Failed to create service token", }); } return res.status(200).send({ - token + token, }); }; \ No newline at end of file diff --git a/backend/src/controllers/v1/signupController.ts b/backend/src/controllers/v1/signupController.ts index 19bf94e61..b545320a6 100644 --- a/backend/src/controllers/v1/signupController.ts +++ b/backend/src/controllers/v1/signupController.ts @@ -1,13 +1,15 @@ -import { Request, Response } from 'express'; -import { User } from '../../models'; +import { Request, Response } from "express"; +import { User } from "../../models"; +import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup"; +import { createToken } from "../../helpers/auth"; +import { BadRequestError } from "../../utils/errors"; import { - sendEmailVerification, - checkEmailVerification, -} from '../../helpers/signup'; -import { createToken } from '../../helpers/auth'; -import { BadRequestError } from '../../utils/errors'; -import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; -import { validateUserEmail } from '../../validation'; + getInviteOnlySignup, + getJwtSignupLifetime, + getJwtSignupSecret, + getSmtpConfigured +} from "../../config"; +import { validateUserEmail } from "../../validation"; /** * Signup step 1: Initialize account for user under email [email] and send a verification code @@ -17,27 +19,26 @@ import { validateUserEmail } from '../../validation'; * @returns */ export const beginEmailSignup = async (req: Request, res: Response) => { - let email: string; - email = req.body.email; - + const email: string = req.body.email; + // validate that email is not disposable validateUserEmail(email); - const user = await User.findOne({ email }).select('+publicKey'); + const user = await User.findOne({ email }).select("+publicKey"); if (user && user?.publicKey) { // case: user has already completed account return res.status(403).send({ - error: 'Failed to send email verification code for complete account' + error: "Failed to send email verification code for complete account" }); } // send send verification email await sendEmailVerification({ email }); - return res.status(200).send({ - message: `Sent an email verification code to ${email}` - }); + return res.status(200).send({ + message: `Sent an email verification code to ${email}` + }); }; /** @@ -48,23 +49,25 @@ export const beginEmailSignup = async (req: Request, res: Response) => { * @returns */ export const verifyEmailSignup = async (req: Request, res: Response) => { - let user, token; + let user; const { email, code } = req.body; // initialize user account - user = await User.findOne({ email }).select('+publicKey'); + user = await User.findOne({ email }).select("+publicKey"); if (user && user?.publicKey) { // case: user has already completed account return res.status(403).send({ - error: 'Failed email verification for complete user' + error: "Failed email verification for complete user" }); } if (await getInviteOnlySignup()) { // Only one user can create an account without being invited. The rest need to be invited in order to make an account - const userCount = await User.countDocuments({}) + const userCount = await User.countDocuments({}); if (userCount != 0) { - throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) + throw BadRequestError({ + message: "New user sign ups are not allowed at this time. You must be invited to sign up." + }); } } @@ -83,7 +86,7 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { } // generate temporary signup token - token = createToken({ + const token = createToken({ payload: { userId: user._id.toString() }, @@ -91,9 +94,9 @@ export const verifyEmailSignup = async (req: Request, res: Response) => { secret: await getJwtSignupSecret() }); - return res.status(200).send({ - message: 'Successfuly verified email', - user, - token - }); + return res.status(200).send({ + message: "Successfuly verified email", + user, + token + }); }; diff --git a/backend/src/controllers/v1/stripeController.ts b/backend/src/controllers/v1/stripeController.ts index 4aa7adce9..2acb52abd 100644 --- a/backend/src/controllers/v1/stripeController.ts +++ b/backend/src/controllers/v1/stripeController.ts @@ -1,6 +1,6 @@ -import { Request, Response } from 'express'; -import Stripe from 'stripe'; -import { getStripeSecretKey, getStripeWebhookSecret } from '../../config'; +import { Request, Response } from "express"; +import Stripe from "stripe"; +import { getStripeSecretKey, getStripeWebhookSecret } from "../../config"; /** * Handle service provisioning/un-provisioning via Stripe @@ -11,10 +11,10 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config'; export const handleWebhook = async (req: Request, res: Response) => { // check request for valid stripe signature const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' + apiVersion: "2022-08-01", }); - const sig = req.headers['stripe-signature'] as string; + const sig = req.headers["stripe-signature"] as string; const event = stripe.webhooks.constructEvent( req.body, sig, @@ -22,7 +22,7 @@ export const handleWebhook = async (req: Request, res: Response) => { ); switch (event.type) { - case '': + case "": break; default: } diff --git a/backend/src/controllers/v1/userActionController.ts b/backend/src/controllers/v1/userActionController.ts index cba78c7b8..9e7354ce1 100644 --- a/backend/src/controllers/v1/userActionController.ts +++ b/backend/src/controllers/v1/userActionController.ts @@ -1,5 +1,5 @@ -import { Request, Response } from 'express'; -import { UserAction } from '../../models'; +import { Request, Response } from "express"; +import { UserAction } from "../../models"; /** * Add user action [action] @@ -15,18 +15,18 @@ export const addUserAction = async (req: Request, res: Response) => { const userAction = await UserAction.findOneAndUpdate( { user: req.user._id, - action + action, }, { user: req.user._id, action }, { new: true, - upsert: true + upsert: true, } ); return res.status(200).send({ - message: 'Successfully recorded user action', - userAction + message: "Successfully recorded user action", + userAction, }); }; @@ -42,10 +42,10 @@ export const getUserAction = async (req: Request, res: Response) => { const userAction = await UserAction.findOne({ user: req.user._id, - action + action, }); return res.status(200).send({ - userAction + userAction, }); }; diff --git a/backend/src/controllers/v1/userController.ts b/backend/src/controllers/v1/userController.ts index d194c6217..398b24f08 100644 --- a/backend/src/controllers/v1/userController.ts +++ b/backend/src/controllers/v1/userController.ts @@ -1,4 +1,4 @@ -import { Request, Response } from 'express'; +import { Request, Response } from "express"; /** * Return user on request @@ -8,6 +8,6 @@ import { Request, Response } from 'express'; */ export const getUser = async (req: Request, res: Response) => { return res.status(200).send({ - user: req.user + user: req.user, }); }; diff --git a/backend/src/controllers/v1/workspaceController.ts b/backend/src/controllers/v1/workspaceController.ts index 796a7bebc..f26078ace 100644 --- a/backend/src/controllers/v1/workspaceController.ts +++ b/backend/src/controllers/v1/workspaceController.ts @@ -1,19 +1,18 @@ import { Request, Response } from "express"; import { - Workspace, - Membership, - MembershipOrg, + IUser, Integration, IntegrationAuth, - IUser, + Membership, + MembershipOrg, ServiceToken, - ServiceTokenData, + Workspace, } from "../../models"; import { createWorkspace as create, deleteWorkspace as deleteWork, } from "../../helpers/workspace"; -import { EELicenseService } from '../../ee/services'; +import { EELicenseService } from "../../ee/services"; import { addMemberships } from "../../helpers/membership"; import { ADMIN } from "../../variables"; @@ -116,14 +115,14 @@ export const createWorkspace = async (req: Request, res: Response) => { throw new Error("Failed to validate organization membership"); } - const plan = await EELicenseService.getOrganizationPlan(organizationId); + const plan = await EELicenseService.getPlan(organizationId); if (plan.workspaceLimit !== null) { // case: limit imposed on number of workspaces allowed if (plan.workspacesUsed >= plan.workspaceLimit) { // case: number of workspaces used exceeds the number of workspaces allowed return res.status(400).send({ - message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.' + message: "Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.", }); } } diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index 73fd1afbf..ee19b0e4c 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -1,74 +1,72 @@ -import { Request, Response } from 'express'; -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; -import { - APIKeyData -} from '../../models'; -import { getSaltRounds } from '../../config'; +import { Request, Response } from "express"; +import crypto from "crypto"; +import bcrypt from "bcrypt"; +import { APIKeyData } from "../../models"; +import { getSaltRounds } from "../../config"; /** * Return API key data for user with id [req.user_id] * @param req - * @param res - * @returns + * @param res + * @returns */ export const getAPIKeyData = async (req: Request, res: Response) => { - const apiKeyData = await APIKeyData.find({ - user: req.user._id - }); - - return res.status(200).send({ - apiKeyData - }); -} + const apiKeyData = await APIKeyData.find({ + user: req.user._id, + }); + + return res.status(200).send({ + apiKeyData, + }); +}; /** * Create new API key data for user with id [req.user._id] - * @param req - * @param res + * @param req + * @param res */ export const createAPIKeyData = async (req: Request, res: Response) => { - const { name, expiresIn } = req.body; - - const secret = crypto.randomBytes(16).toString('hex'); - const secretHash = await bcrypt.hash(secret, await getSaltRounds()); - - const expiresAt = new Date(); - expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - - let apiKeyData = await new APIKeyData({ - name, - lastUsed: new Date(), - expiresAt, - user: req.user._id, - secretHash - }).save(); - - // return api key data without sensitive data - // FIX: fix this any - apiKeyData = await APIKeyData.findById(apiKeyData._id) as any - - if (!apiKeyData) throw new Error('Failed to find API key data'); - - const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; - - return res.status(200).send({ - apiKey, - apiKeyData - }); -} + const { name, expiresIn } = req.body; + + const secret = crypto.randomBytes(16).toString("hex"); + const secretHash = await bcrypt.hash(secret, await getSaltRounds()); + + const expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + + let apiKeyData = await new APIKeyData({ + name, + lastUsed: new Date(), + expiresAt, + user: req.user._id, + secretHash, + }).save(); + + // return api key data without sensitive data + // FIX: fix this any + apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any; + + if (!apiKeyData) throw new Error("Failed to find API key data"); + + const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; + + return res.status(200).send({ + apiKey, + apiKeyData, + }); +}; /** * Delete API key data with id [apiKeyDataId]. - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const deleteAPIKeyData = async (req: Request, res: Response) => { - const { apiKeyDataId } = req.params; - const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); - - return res.status(200).send({ - apiKeyData - }); -} + const { apiKeyDataId } = req.params; + const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId); + + return res.status(200).send({ + apiKeyData, + }); +}; diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 0718ec8dc..282c15288 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -1,27 +1,27 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import { Request, Response } from 'express'; -import jwt from 'jsonwebtoken'; -import * as bigintConversion from 'bigint-conversion'; -const jsrp = require('jsrp'); -import { User, LoginSRPDetail } from '../../models'; -import { issueAuthTokens, createToken } from '../../helpers/auth'; -import { checkUserDevice } from '../../helpers/user'; -import { sendMail } from '../../helpers/nodemailer'; -import { TokenService } from '../../services'; -import { EELogService } from '../../ee/services'; -import { BadRequestError, InternalServerError } from '../../utils/errors'; +import { Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import * as bigintConversion from "bigint-conversion"; +const jsrp = require("jsrp"); +import { LoginSRPDetail, User } from "../../models"; +import { createToken, issueAuthTokens } from "../../helpers/auth"; +import { checkUserDevice } from "../../helpers/user"; +import { sendMail } from "../../helpers/nodemailer"; +import { TokenService } from "../../services"; +import { EELogService } from "../../ee/services"; +import { BadRequestError, InternalServerError } from "../../utils/errors"; import { + ACTION_LOGIN, TOKEN_EMAIL_MFA, - ACTION_LOGIN -} from '../../variables'; -import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this +} from "../../variables"; +import { getChannelFromUserAgent } from "../../utils/posthog"; // TODO: move this import { + getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret, - getHttpsEnabled -} from '../../config'; +} from "../../config"; -declare module 'jsonwebtoken' { +declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; } @@ -36,20 +36,20 @@ declare module 'jsonwebtoken' { export const login1 = async (req: Request, res: Response) => { const { email, - clientPublicKey + clientPublicKey, }: { email: string; clientPublicKey: string } = req.body; const user = await User.findOne({ - email - }).select('+salt +verifier'); + email, + }).select("+salt +verifier"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); const server = new jsrp.server(); server.init( { salt: user.salt, - verifier: user.verifier + verifier: user.verifier, }, async () => { // generate server-side public key @@ -63,7 +63,7 @@ export const login1 = async (req: Request, res: Response) => { return res.status(200).send({ serverPublicKey, - salt: user.salt + salt: user.salt, }); } ); @@ -78,14 +78,14 @@ export const login1 = async (req: Request, res: Response) => { * @returns */ export const login2 = async (req: Request, res: Response) => { - if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); + if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" }); const { email, clientProof } = req.body; const user = await User.findOne({ - email - }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices'); + email, + }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) @@ -98,7 +98,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetail.serverBInt + b: loginSRPDetail.serverBInt, }, async () => { server.setClientPublicKey(loginSRPDetail.clientPublicKey); @@ -111,52 +111,52 @@ export const login2 = async (req: Request, res: Response) => { // generate temporary MFA token const token = createToken({ payload: { - userId: user._id.toString() + userId: user._id.toString(), }, expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret() + secret: await getJwtMfaSecret(), }); const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, - email + email, }); // send MFA code [code] to [email] await sendMail({ - template: 'emailMfa.handlebars', - subjectLine: 'Infisical MFA code', + template: "emailMfa.handlebars", + subjectLine: "Infisical MFA code", recipients: [email], substitutions: { - code - } + code, + }, }); return res.status(200).send({ mfaEnabled: true, - token + token, }); } await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); // issue tokens const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { + res.cookie("jid", tokens.refreshToken, { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled(), }); // case: user does not have MFA enabled @@ -182,7 +182,7 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag + tag: user.tag, } if ( @@ -197,21 +197,21 @@ export const login2 = async (req: Request, res: Response) => { const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id + userId: user._id, }); loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.ip + channel: getChannelFromUserAgent(req.headers["user-agent"]), + ipAddress: req.ip, }); return res.status(200).send(response); } return res.status(400).send({ - message: 'Failed to authenticate. Try again?' + message: "Failed to authenticate. Try again?", }); } ); @@ -227,21 +227,21 @@ export const sendMfaToken = async (req: Request, res: Response) => { const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, - email + email, }); // send MFA code [code] to [email] await sendMail({ - template: 'emailMfa.handlebars', - subjectLine: 'Infisical MFA code', + template: "emailMfa.handlebars", + subjectLine: "Infisical MFA code", recipients: [email], substitutions: { - code - } + code, + }, }); return res.status(200).send({ - message: 'Successfully sent new MFA code' + message: "Successfully sent new MFA code", }); } @@ -257,36 +257,36 @@ export const verifyMfaToken = async (req: Request, res: Response) => { await TokenService.validateToken({ type: TOKEN_EMAIL_MFA, email, - token: mfaToken + token: mfaToken, }); const user = await User.findOne({ - email - }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices'); + email, + }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); await LoginSRPDetail.deleteOne({ userId: user.id }) await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); // issue tokens const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { + res.cookie("jid", tokens.refreshToken, { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled(), }); interface VerifyMfaTokenRes { @@ -319,7 +319,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => { publicKey: user.publicKey as string, encryptedPrivateKey: user.encryptedPrivateKey as string, iv: user.iv as string, - tag: user.tag as string + tag: user.tag as string, } if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { @@ -330,14 +330,14 @@ export const verifyMfaToken = async (req: Request, res: Response) => { const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id + userId: user._id, }); loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.realIP + channel: getChannelFromUserAgent(req.headers["user-agent"]), + ipAddress: req.realIP, }); return res.status(200).send(resObj); diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index d4f91bace..e1381db54 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -1,16 +1,17 @@ -import { Request, Response } from 'express'; +import { Request, Response } from "express"; import { + Integration, + Membership, Secret, ServiceToken, - Workspace, - Integration, ServiceTokenData, - Membership, -} from '../../models'; -import { SecretVersion } from '../../ee/models'; -import { BadRequestError } from '../../utils/errors'; -import _ from 'lodash'; -import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; + Workspace, +} from "../../models"; +import { SecretVersion } from "../../ee/models"; +import { EELicenseService } from "../../ee/services"; +import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; +import _ from "lodash"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; /** * Create new workspace environment named [environmentName] under workspace with id @@ -22,16 +23,33 @@ export const createWorkspaceEnvironment = async ( req: Request, res: Response ) => { + const { workspaceId } = req.params; const { environmentName, environmentSlug } = req.body; const workspace = await Workspace.findById(workspaceId).exec(); + + if (!workspace) throw WorkspaceNotFoundError(); + + const plan = await EELicenseService.getPlan(workspace.organization.toString()); + + if (plan.environmentLimit !== null) { + // case: limit imposed on number of environments allowed + if (workspace.environments.length >= plan.environmentLimit) { + // case: number of environments used exceeds the number of environments allowed + + return res.status(400).send({ + message: "Failed to create environment due to environment limit reached. Upgrade plan to create more environments.", + }); + } + } + if ( !workspace || workspace?.environments.find( ({ name, slug }) => slug === environmentSlug || environmentName === name ) ) { - throw new Error('Failed to create workspace environment'); + throw new Error("Failed to create workspace environment"); } workspace?.environments.push({ @@ -40,8 +58,10 @@ export const createWorkspaceEnvironment = async ( }); await workspace.save(); + await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId); + return res.status(200).send({ - message: 'Successfully created new environment', + message: "Successfully created new environment", workspace: workspaceId, environment: { name: environmentName, @@ -65,13 +85,13 @@ export const renameWorkspaceEnvironment = async ( const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; // user should pass both new slug and env name if (!environmentSlug || !environmentName) { - throw new Error('Invalid environment given.'); + throw new Error("Invalid environment given."); } // atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) { - throw new Error('Failed to create workspace environment'); + throw new Error("Failed to create workspace environment"); } const isEnvExist = workspace.environments.some( @@ -80,14 +100,14 @@ export const renameWorkspaceEnvironment = async ( (name === environmentName || slug === environmentSlug) ); if (isEnvExist) { - throw new Error('Invalid environment given'); + throw new Error("Invalid environment given"); } const envIndex = workspace?.environments.findIndex( ({ slug }) => slug === oldEnvironmentSlug ); if (envIndex === -1) { - throw new Error('Invalid environment given'); + throw new Error("Invalid environment given"); } workspace.environments[envIndex].name = environmentName; @@ -117,7 +137,7 @@ export const renameWorkspaceEnvironment = async ( await Membership.updateMany( { workspace: workspaceId, - "deniedPermissions.environmentSlug": oldEnvironmentSlug + "deniedPermissions.environmentSlug": oldEnvironmentSlug, }, { $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } }, { arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] } @@ -125,7 +145,7 @@ export const renameWorkspaceEnvironment = async ( return res.status(200).send({ - message: 'Successfully update environment', + message: "Successfully update environment", workspace: workspaceId, environment: { name: environmentName, @@ -149,14 +169,14 @@ export const deleteWorkspaceEnvironment = async ( // atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) { - throw new Error('Failed to create workspace environment'); + throw new Error("Failed to create workspace environment"); } const envIndex = workspace?.environments.findIndex( ({ slug }) => slug === environmentSlug ); if (envIndex === -1) { - throw new Error('Invalid environment given'); + throw new Error("Invalid environment given"); } workspace.environments.splice(envIndex, 1); @@ -186,10 +206,12 @@ export const deleteWorkspaceEnvironment = async ( await Membership.updateMany( { workspace: workspaceId }, { $pull: { deniedPermissions: { environmentSlug: environmentSlug } } } - ) + ); + + await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId); return res.status(200).send({ - message: 'Successfully deleted environment', + message: "Successfully deleted environment", workspace: workspaceId, environment: environmentSlug, }); @@ -203,7 +225,7 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async ( const { workspaceId } = req.params; const workspacesUserIsMemberOf = await Membership.findOne({ workspace: workspaceId, - user: req.user + user: req.user, }) if (!workspacesUserIsMemberOf) { @@ -227,7 +249,7 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async ( name: environment.name, slug: environment.slug, isWriteDenied: isWriteBlocked, - isReadDenied: isReadBlocked + isReadDenied: isReadBlocked, }) } }) diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index db78fa503..5496097db 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -1,15 +1,15 @@ -import * as authController from './authController'; -import * as signupController from './signupController'; -import * as usersController from './usersController'; -import * as organizationsController from './organizationsController'; -import * as workspaceController from './workspaceController'; -import * as serviceTokenDataController from './serviceTokenDataController'; -import * as apiKeyDataController from './apiKeyDataController'; -import * as secretController from './secretController'; -import * as secretsController from './secretsController'; -import * as serviceAccountsController from './serviceAccountsController'; -import * as environmentController from './environmentController'; -import * as tagController from './tagController'; +import * as authController from "./authController"; +import * as signupController from "./signupController"; +import * as usersController from "./usersController"; +import * as organizationsController from "./organizationsController"; +import * as workspaceController from "./workspaceController"; +import * as serviceTokenDataController from "./serviceTokenDataController"; +import * as apiKeyDataController from "./apiKeyDataController"; +import * as secretController from "./secretController"; +import * as secretsController from "./secretsController"; +import * as serviceAccountsController from "./serviceAccountsController"; +import * as environmentController from "./environmentController"; +import * as tagController from "./tagController"; export { authController, @@ -23,5 +23,5 @@ export { secretsController, serviceAccountsController, environmentController, - tagController + tagController, } diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index 3bfd9085e..301cac9d0 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -1,13 +1,13 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; import { - MembershipOrg, Membership, + MembershipOrg, + ServiceAccount, Workspace, - ServiceAccount -} from '../../models'; -import { deleteMembershipOrg } from '../../helpers/membershipOrg'; -import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; +} from "../../models"; +import { deleteMembershipOrg } from "../../helpers/membershipOrg"; +import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; /** * Return memberships for organization with id [organizationId] @@ -51,11 +51,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) => const { organizationId } = req.params; const memberships = await MembershipOrg.find({ - organization: organizationId - }).populate('user', '+publicKey'); + organization: organizationId, + }).populate("user", "+publicKey"); return res.status(200).send({ - memberships + memberships, }); } @@ -124,14 +124,14 @@ export const updateOrganizationMembership = async (req: Request, res: Response) const membership = await MembershipOrg.findByIdAndUpdate( membershipId, { - role + role, }, { - new: true + new: true, } ); return res.status(200).send({ - membership + membership, }); } @@ -182,15 +182,15 @@ export const deleteOrganizationMembership = async (req: Request, res: Response) // delete organization membership const membership = await deleteMembershipOrg({ - membershipOrgId: membershipId + membershipOrgId: membershipId, }); await updateSubscriptionOrgQuantity({ - organizationId: membership.organization.toString() + organizationId: membership.organization.toString(), }); return res.status(200).send({ - membership + membership, }); } @@ -240,23 +240,23 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) => ( await Workspace.find( { - organization: organizationId + organization: organizationId, }, - '_id' + "_id" ) ).map((w) => w._id.toString()) ); const workspaces = ( await Membership.find({ - user: req.user._id - }).populate('workspace') + user: req.user._id, + }).populate("workspace") ) .filter((m) => workspacesSet.has(m.workspace._id.toString())) .map((m) => m.workspace); return res.status(200).send({ - workspaces + workspaces, }); } @@ -269,10 +269,10 @@ export const getOrganizationServiceAccounts = async (req: Request, res: Response const { organizationId } = req.params; const serviceAccounts = await ServiceAccount.find({ - organization: new Types.ObjectId(organizationId) + organization: new Types.ObjectId(organizationId), }); return res.status(200).send({ - serviceAccounts + serviceAccounts, }); } diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index fecba43d6..1b6038f0e 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -2,24 +2,39 @@ import to from "await-to-js"; import { Request, Response } from "express"; import mongoose, { Types } from "mongoose"; import Secret, { ISecret } from "../../models/secret"; -import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret"; +import { + CreateSecretRequestBody, + ModifySecretRequestBody, + SanitizedSecretForCreate, + SanitizedSecretModify +} from "../../types/secret"; const { ValidationError } = mongoose.Error; -import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; -import { AnyBulkWriteOperation } from 'mongodb'; -import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; -import { TelemetryService } from '../../services'; +import { + BadRequestError, + InternalServerError, + ValidationError as RouteValidationError, + UnauthorizedRequestError +} from "../../utils/errors"; +import { AnyBulkWriteOperation } from "mongodb"; +import { + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + SECRET_PERSONAL, + SECRET_SHARED +} from "../../variables"; +import { TelemetryService } from "../../services"; import { User } from "../../models"; -import { AccountNotFoundError } from '../../utils/errors'; +import { AccountNotFoundError } from "../../utils/errors"; /** * Create secret for workspace with id [workspaceId] and environment [environment] - * @param req - * @param res + * @param req + * @param res */ export const createSecret = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); const secretToCreate: CreateSecretRequestBody = req.body.secret; - const { workspaceId, environment } = req.params + const { workspaceId, environment } = req.params; const sanitizedSecret: SanitizedSecretForCreate = { secretKeyCiphertext: secretToCreate.secretKeyCiphertext, secretKeyIV: secretToCreate.secretKeyIV, @@ -39,45 +54,44 @@ export const createSecret = async (req: Request, res: Response) => { user: new Types.ObjectId(req.user._id), algorithm: ALGORITHM_AES_256_GCM, keyEncoding: ENCODING_SCHEME_UTF8 - } + }; - - const [error, secret] = await to(Secret.create(sanitizedSecret).then()) + const [error, secret] = await to(Secret.create(sanitizedSecret).then()); if (error instanceof ValidationError) { - throw RouteValidationError({ message: error.message, stack: error.stack }) + throw RouteValidationError({ message: error.message, stack: error.stack }); } if (postHogClient) { postHogClient.capture({ - event: 'secrets added', + event: "secrets added", distinctId: req.user.email, properties: { numberOfSecrets: 1, workspaceId, environment, - channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - userAgent: req.headers?.['user-agent'] + channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", + userAgent: req.headers?.["user-agent"] } }); } res.status(200).send({ secret - }) -} + }); +}; /** * Create many secrets for workspace wiht id [workspaceId] and environment [environment] - * @param req - * @param res + * @param req + * @param res */ export const createSecrets = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; - const { workspaceId, environment } = req.params - const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] + const { workspaceId, environment } = req.params; + const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []; - secretsToCreate.forEach(rawSecret => { + secretsToCreate.forEach((rawSecret) => { const safeUpdateFields: SanitizedSecretForCreate = { secretKeyCiphertext: rawSecret.secretKeyCiphertext, secretKeyIV: rawSecret.secretKeyIV, @@ -97,140 +111,163 @@ export const createSecrets = async (req: Request, res: Response) => { user: new Types.ObjectId(req.user._id), algorithm: ALGORITHM_AES_256_GCM, keyEncoding: ENCODING_SCHEME_UTF8 - } + }; - sanitizedSecretesToCreate.push(safeUpdateFields) - }) + sanitizedSecretesToCreate.push(safeUpdateFields); + }); - const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()) + const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then()); if (bulkCreateError) { if (bulkCreateError instanceof ValidationError) { - throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack }) + throw RouteValidationError({ + message: bulkCreateError.message, + stack: bulkCreateError.stack + }); } - throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack }) + throw InternalServerError({ + message: "Unable to process your batch create request. Please try again", + stack: bulkCreateError.stack + }); } if (postHogClient) { postHogClient.capture({ - event: 'secrets added', + event: "secrets added", distinctId: req.user.email, properties: { numberOfSecrets: (secretsToCreate ?? []).length, workspaceId, environment, - channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - userAgent: req.headers?.['user-agent'] + channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", + userAgent: req.headers?.["user-agent"] } }); } res.status(200).send({ secrets - }) -} + }); +}; /** * Delete secrets in workspace with id [workspaceId] and environment [environment] - * @param req - * @param res + * @param req + * @param res */ export const deleteSecrets = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); - const { workspaceId, environmentName } = req.params - const secretIdsToDelete: string[] = req.body.secretIds + const { workspaceId, environmentName } = req.params; + const secretIdsToDelete: string[] = req.body.secretIds; - const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to( + Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then() + ); if (secretIdsUserCanDeleteError) { - throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` }) + throw InternalServerError({ + message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` + }); } - const secretsUserCanDeleteSet: Set = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString())); - const deleteOperationsToPerform: AnyBulkWriteOperation[] = [] + const secretsUserCanDeleteSet: Set = new Set( + secretIdsUserCanDelete.map((objectId) => objectId._id.toString()) + ); + const deleteOperationsToPerform: AnyBulkWriteOperation[] = []; let numSecretsDeleted = 0; - secretIdsToDelete.forEach(secretIdToDelete => { + secretIdsToDelete.forEach((secretIdToDelete) => { if (secretsUserCanDeleteSet.has(secretIdToDelete)) { - const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } } - deleteOperationsToPerform.push(deleteOperation) + const deleteOperation = { + deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } + }; + deleteOperationsToPerform.push(deleteOperation); numSecretsDeleted++; } else { - throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" }) + throw RouteValidationError({ + message: "You cannot delete secrets that you do not have access to" + }); } - }) + }); - const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()) + const [bulkDeleteError] = await to(Secret.bulkWrite(deleteOperationsToPerform).then()); if (bulkDeleteError) { if (bulkDeleteError instanceof ValidationError) { - throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack }) + throw RouteValidationError({ + message: "Unable to apply modifications, please try again", + stack: bulkDeleteError.stack + }); } - throw InternalServerError() + throw InternalServerError(); } if (postHogClient) { postHogClient.capture({ - event: 'secrets deleted', + event: "secrets deleted", distinctId: req.user.email, properties: { numberOfSecrets: numSecretsDeleted, environment: environmentName, workspaceId, - channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - userAgent: req.headers?.['user-agent'] + channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", + userAgent: req.headers?.["user-agent"] } }); } - res.status(200).send() -} + res.status(200).send(); +}; /** * Delete secret with id [secretId] - * @param req + * @param req * @param res */ export const deleteSecret = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); - await Secret.findByIdAndDelete(req._secret._id) + await Secret.findByIdAndDelete(req._secret._id); if (postHogClient) { postHogClient.capture({ - event: 'secrets deleted', + event: "secrets deleted", distinctId: req.user.email, properties: { numberOfSecrets: 1, workspaceId: req._secret.workspace.toString(), environment: req._secret.environment, - channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - userAgent: req.headers?.['user-agent'] + channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", + userAgent: req.headers?.["user-agent"] } }); } res.status(200).send({ secret: req._secret - }) -} + }); +}; /** * Update secrets for workspace with id [workspaceId] and environment [environment] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateSecrets = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); - const { workspaceId, environmentName } = req.params + const { workspaceId, environmentName } = req.params; const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; - const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to( + Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then() + ); if (secretIdsUserCanModifyError) { - throw InternalServerError({ message: "Unable to fetch secrets you own" }) + throw InternalServerError({ message: "Unable to fetch secrets you own" }); } - const secretsUserCanModifySet: Set = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString())); - const updateOperationsToPerform: any = [] + const secretsUserCanModifySet: Set = new Set( + secretIdsUserCanModify.map((objectId) => objectId._id.toString()) + ); + const updateOperationsToPerform: any = []; - secretsModificationsRequested.forEach(userModifiedSecret => { + secretsModificationsRequested.forEach((userModifiedSecret) => { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { const sanitizedSecret: SanitizedSecretModify = { secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, @@ -244,56 +281,70 @@ export const updateSecrets = async (req: Request, res: Response) => { secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, secretCommentIV: userModifiedSecret.secretCommentIV, secretCommentTag: userModifiedSecret.secretCommentTag, - secretCommentHash: userModifiedSecret.secretCommentHash, - } + secretCommentHash: userModifiedSecret.secretCommentHash + }; - const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } } - updateOperationsToPerform.push(updateOperation) + const updateOperation = { + updateOne: { + filter: { _id: userModifiedSecret._id, workspace: workspaceId }, + update: { $inc: { version: 1 }, $set: sanitizedSecret } + } + }; + updateOperationsToPerform.push(updateOperation); } else { - throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" }) + throw UnauthorizedRequestError({ + message: "You do not have permission to modify one or more of the requested secrets" + }); } - }) + }); - const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then()) + const [bulkModificationInfoError, bulkModificationInfo] = await to( + Secret.bulkWrite(updateOperationsToPerform).then() + ); if (bulkModificationInfoError) { if (bulkModificationInfoError instanceof ValidationError) { - throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack }) + throw RouteValidationError({ + message: "Unable to apply modifications, please try again", + stack: bulkModificationInfoError.stack + }); } - throw InternalServerError() + throw InternalServerError(); } if (postHogClient) { postHogClient.capture({ - event: 'secrets modified', + event: "secrets modified", distinctId: req.user.email, properties: { numberOfSecrets: (secretsModificationsRequested ?? []).length, environment: environmentName, workspaceId, - channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - userAgent: req.headers?.['user-agent'] + channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", + userAgent: req.headers?.["user-agent"] } }); } - return res.status(200).send() -} + return res.status(200).send(); +}; /** * Update a secret within workspace with id [workspaceId] and environment [environment] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const updateSecret = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); - const { workspaceId, environmentName } = req.params + const { workspaceId, environmentName } = req.params; const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; - const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) + const [secretIdUserCanModifyError, secretIdUserCanModify] = await to( + Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then() + ); if (secretIdUserCanModifyError && !secretIdUserCanModify) { - throw BadRequestError() + throw BadRequestError(); } const sanitizedSecret: SanitizedSecretModify = { @@ -308,45 +359,53 @@ export const updateSecret = async (req: Request, res: Response) => { secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext, secretCommentIV: secretModificationsRequested.secretCommentIV, secretCommentTag: secretModificationsRequested.secretCommentTag, - secretCommentHash: secretModificationsRequested.secretCommentHash, - } + secretCommentHash: secretModificationsRequested.secretCommentHash + }; - const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then()) + const [error, singleModificationUpdate] = await to( + Secret.updateOne( + { _id: secretModificationsRequested._id, workspace: workspaceId }, + { $inc: { version: 1 }, $set: sanitizedSecret } + ).then() + ); if (error instanceof ValidationError) { - throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack }) + throw RouteValidationError({ + message: "Unable to apply modifications, please try again", + stack: error.stack + }); } if (postHogClient) { postHogClient.capture({ - event: 'secrets modified', + event: "secrets modified", distinctId: req.user.email, properties: { numberOfSecrets: 1, environment: environmentName, workspaceId, - channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - userAgent: req.headers?.['user-agent'] + channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", + userAgent: req.headers?.["user-agent"] } }); } - return res.status(200).send(singleModificationUpdate) -} + return res.status(200).send(singleModificationUpdate); +}; /** * Return secrets for workspace with id [workspaceId], environment [environment] and user * with id [req.user._id] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getSecrets = async (req: Request, res: Response) => { const postHogClient = await TelemetryService.getPostHogClient(); const { environment } = req.query; const { workspaceId } = req.params; - let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user - let userEmail: string | undefined = undefined // used for posthog + let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user + let userEmail: string | undefined = undefined; // used for posthog if (req.user) { userId = req.user._id; userEmail = req.user.email; @@ -354,47 +413,50 @@ export const getSecrets = async (req: Request, res: Response) => { if (req.serviceTokenData) { userId = req.serviceTokenData.user; - - const user = await User.findById(req.serviceTokenData.user, 'email'); + + const user = await User.findById(req.serviceTokenData.user, "email"); if (!user) throw AccountNotFoundError(); userEmail = user.email; } - const [err, secrets] = await to(Secret.find( - { + const [err, secrets] = await to( + Secret.find({ workspace: workspaceId, environment, $or: [{ user: userId }, { user: { $exists: false } }], type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } - } - ).then()) + }).then() + ); if (err) { - throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack }) + throw RouteValidationError({ + message: "Failed to get secrets, please try again", + stack: err.stack + }); } if (postHogClient) { postHogClient.capture({ - event: 'secrets pulled', + event: "secrets pulled", distinctId: userEmail, properties: { numberOfSecrets: (secrets ?? []).length, environment, workspaceId, - channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli', - userAgent: req.headers?.['user-agent'] + channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", + userAgent: req.headers?.["user-agent"] } }); } - return res.json(secrets) -} + return res.json(secrets); +}; /** * Return secret with id [secretId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getSecret = async (req: Request, res: Response) => { // if (postHogClient) { @@ -414,4 +476,4 @@ export const getSecret = async (req: Request, res: Response) => { return res.status(200).send({ secret: req._secret }); -} \ No newline at end of file +}; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index ae1de8cfe..a760a511c 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -3,19 +3,19 @@ import { Request, Response } from "express"; import { ISecret, Secret, ServiceTokenData } from "../../models"; import { IAction, SecretVersion } from "../../ee/models"; import { - SECRET_PERSONAL, ACTION_ADD_SECRETS, + ACTION_DELETE_SECRETS, ACTION_READ_SECRETS, ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, + SECRET_PERSONAL, } from "../../variables"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { EventService } from "../../services"; import { eventPushSecrets } from "../../events"; -import { EESecretService, EELogService } from "../../ee/services"; -import { TelemetryService, SecretService } from "../../services"; +import { EELogService, EESecretService } from "../../ee/services"; +import { SecretService, TelemetryService } from "../../services"; import { getChannelFromUserAgent } from "../../utils/posthog"; import { PERMISSION_WRITE_SECRETS } from "../../variables"; import { @@ -25,7 +25,7 @@ import { } from "../../ee/helpers/checkMembershipPermissions"; import Tag from "../../models/tag"; import _ from "lodash"; -import { BatchSecretRequest, BatchSecret } from "../../types/secret"; +import { BatchSecret, BatchSecretRequest } from "../../types/secret"; import Folder from "../../models/folder"; import { getFolderByPath, @@ -700,11 +700,15 @@ export const getSecrets = async (req: Request, res: Response) => { (!folders && folderId && folderId !== "root") || (!folders && secretPath) ) { - throw BadRequestError({ message: "Folder not found" }); + res.send({ secrets: [] }); + return; } if (folders && folderId !== "root") { const folder = searchByFolderId(folders.nodes, folderId as string); - if (!folder) throw BadRequestError({ message: "Folder not found" }); + if (!folder) { + res.send({ secrets: [] }); + return; + } } if (req.authData.authPayload instanceof ServiceTokenData) { @@ -720,10 +724,11 @@ export const getSecrets = async (req: Request, res: Response) => { } if (folders && secretPath) { - if (!folders) throw BadRequestError({ message: "Folder not found" }); + // avoid throwing error and send empty list const folder = getFolderByPath(folders.nodes, secretPath as string); if (!folder) { - throw BadRequestError({ message: "Secret path not found" }); + res.send({ secrets: [] }); + return; } folderId = folder.id; } diff --git a/backend/src/controllers/v2/serviceAccountsController.ts b/backend/src/controllers/v2/serviceAccountsController.ts index d0ec62e1b..0e655782a 100644 --- a/backend/src/controllers/v2/serviceAccountsController.ts +++ b/backend/src/controllers/v2/serviceAccountsController.ts @@ -1,18 +1,18 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import crypto from "crypto"; +import bcrypt from "bcrypt"; import { ServiceAccount, ServiceAccountKey, ServiceAccountOrganizationPermission, - ServiceAccountWorkspacePermission -} from '../../models'; + ServiceAccountWorkspacePermission, +} from "../../models"; import { - CreateServiceAccountDto -} from '../../interfaces/serviceAccounts/dto'; -import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors'; -import { getSaltRounds } from '../../config'; + CreateServiceAccountDto, +} from "../../interfaces/serviceAccounts/dto"; +import { BadRequestError, ServiceAccountNotFoundError } from "../../utils/errors"; +import { getSaltRounds } from "../../config"; /** * Return service account tied to the request (service account) client @@ -23,11 +23,11 @@ export const getCurrentServiceAccount = async (req: Request, res: Response) => { const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id); if (!serviceAccount) { - throw ServiceAccountNotFoundError({ message: 'Failed to find service account' }); + throw ServiceAccountNotFoundError({ message: "Failed to find service account" }); } return res.status(200).send({ - serviceAccount + serviceAccount, }); } @@ -42,11 +42,11 @@ export const getServiceAccountById = async (req: Request, res: Response) => { const serviceAccount = await ServiceAccount.findById(serviceAccountId); if (!serviceAccount) { - throw ServiceAccountNotFoundError({ message: 'Failed to find service account' }); + throw ServiceAccountNotFoundError({ message: "Failed to find service account" }); } return res.status(200).send({ - serviceAccount + serviceAccount, }); } @@ -71,7 +71,7 @@ export const createServiceAccount = async (req: Request, res: Response) => { expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); } - const secret = crypto.randomBytes(16).toString('base64'); + const secret = crypto.randomBytes(16).toString("base64"); const secretHash = await bcrypt.hash(secret, await getSaltRounds()); // create service account @@ -82,7 +82,7 @@ export const createServiceAccount = async (req: Request, res: Response) => { publicKey, lastUsed: new Date(), expiresAt, - secretHash + secretHash, }).save(); const serviceAccountObj = serviceAccount.toObject(); @@ -91,14 +91,14 @@ export const createServiceAccount = async (req: Request, res: Response) => { // provision default org-level permission for service account await new ServiceAccountOrganizationPermission({ - serviceAccount: serviceAccount._id + serviceAccount: serviceAccount._id, }).save(); - const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64'); + const secretId = Buffer.from(serviceAccount._id.toString(), "hex").toString("base64"); return res.status(200).send({ serviceAccountAccessKey: `sa.${secretId}.${secret}`, - serviceAccount: serviceAccountObj + serviceAccount: serviceAccountObj, }); } @@ -114,18 +114,18 @@ export const changeServiceAccountName = async (req: Request, res: Response) => { const serviceAccount = await ServiceAccount.findOneAndUpdate( { - _id: new Types.ObjectId(serviceAccountId) + _id: new Types.ObjectId(serviceAccountId), }, { - name + name, }, { - new: true + new: true, } ); return res.status(200).send({ - serviceAccount + serviceAccount, }); } @@ -140,7 +140,7 @@ export const addServiceAccountKey = async (req: Request, res: Response) => { const { workspaceId, encryptedKey, - nonce + nonce, } = req.body; const serviceAccountKey = await new ServiceAccountKey({ @@ -148,7 +148,7 @@ export const addServiceAccountKey = async (req: Request, res: Response) => { nonce, sender: req.user._id, serviceAccount: req.serviceAccount._d, - workspace: new Types.ObjectId(workspaceId) + workspace: new Types.ObjectId(workspaceId), }).save(); return serviceAccountKey; @@ -161,11 +161,11 @@ export const addServiceAccountKey = async (req: Request, res: Response) => { */ export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => { const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({ - serviceAccount: req.serviceAccount._id - }).populate('workspace'); + serviceAccount: req.serviceAccount._id, + }).populate("workspace"); return res.status(200).send({ - serviceAccountWorkspacePermissions + serviceAccountWorkspacePermissions, }); } @@ -182,34 +182,34 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re read = false, write = false, encryptedKey, - nonce + nonce, } = req.body; if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) { return res.status(400).send({ - message: 'Failed to validate workspace environment' + message: "Failed to validate workspace environment", }); } const existingPermission = await ServiceAccountWorkspacePermission.findOne({ serviceAccount: new Types.ObjectId(serviceAccountId), workspace: new Types.ObjectId(workspaceId), - environment + environment, }); - if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' }); + if (existingPermission) throw BadRequestError({ message: "Failed to add workspace permission to service account due to already-existing " }); const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({ serviceAccount: new Types.ObjectId(serviceAccountId), workspace: new Types.ObjectId(workspaceId), environment, read, - write + write, }).save(); const existingServiceAccountKey = await ServiceAccountKey.findOne({ serviceAccount: new Types.ObjectId(serviceAccountId), - workspace: new Types.ObjectId(workspaceId) + workspace: new Types.ObjectId(workspaceId), }); if (!existingServiceAccountKey) { @@ -218,12 +218,12 @@ export const addServiceAccountWorkspacePermission = async (req: Request, res: Re nonce, sender: req.user._id, serviceAccount: new Types.ObjectId(serviceAccountId), - workspace: new Types.ObjectId(workspaceId) + workspace: new Types.ObjectId(workspaceId), }).save(); } return res.status(200).send({ - serviceAccountWorkspacePermission + serviceAccountWorkspacePermission, }); } @@ -240,19 +240,19 @@ export const deleteServiceAccountWorkspacePermission = async (req: Request, res: const { serviceAccount, workspace } = serviceAccountWorkspacePermission; const count = await ServiceAccountWorkspacePermission.countDocuments({ serviceAccount, - workspace + workspace, }); if (count === 0) { await ServiceAccountKey.findOneAndDelete({ serviceAccount, - workspace + workspace, }); } } return res.status(200).send({ - serviceAccountWorkspacePermission + serviceAccountWorkspacePermission, }); } @@ -269,20 +269,20 @@ export const deleteServiceAccount = async (req: Request, res: Response) => { if (serviceAccount) { await ServiceAccountKey.deleteMany({ - serviceAccount: serviceAccount._id + serviceAccount: serviceAccount._id, }); await ServiceAccountOrganizationPermission.deleteMany({ - serviceAccount: new Types.ObjectId(serviceAccountId) + serviceAccount: new Types.ObjectId(serviceAccountId), }); await ServiceAccountWorkspacePermission.deleteMany({ - serviceAccount: new Types.ObjectId(serviceAccountId) + serviceAccount: new Types.ObjectId(serviceAccountId), }); } return res.status(200).send({ - serviceAccount + serviceAccount, }); } @@ -297,10 +297,10 @@ export const getServiceAccountKeys = async (req: Request, res: Response) => { const serviceAccountKeys = await ServiceAccountKey.find({ serviceAccount: req.serviceAccount._id, - ...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}) + ...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}), }); return res.status(200).send({ - serviceAccountKeys + serviceAccountKeys, }); } \ No newline at end of file diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index 21cc29b04..471ac1f3f 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -1,13 +1,10 @@ import { Request, Response } from "express"; import crypto from "crypto"; import bcrypt from "bcrypt"; -import { User, ServiceAccount, ServiceTokenData } from "../../models"; -import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions"; +import { ServiceAccount, ServiceTokenData, User } from "../../models"; import { - PERMISSION_READ_SECRETS, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, } from "../../variables"; import { getSaltRounds } from "../../config"; import { BadRequestError } from "../../utils/errors"; diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index ada119ee9..45a7db963 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -1,14 +1,14 @@ -import { Request, Response } from 'express'; -import { User, MembershipOrg } from '../../models'; -import { completeAccount } from '../../helpers/user'; +import { Request, Response } from "express"; +import { MembershipOrg, User } from "../../models"; +import { completeAccount } from "../../helpers/user"; import { - initializeDefaultOrg -} from '../../helpers/signup'; -import { issueAuthTokens } from '../../helpers/auth'; -import { INVITED, ACCEPTED } from '../../variables'; -import { standardRequest } from '../../config/request'; -import { getLoopsApiKey, getHttpsEnabled } from '../../config'; -import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; + initializeDefaultOrg, +} from "../../helpers/signup"; +import { issueAuthTokens } from "../../helpers/auth"; +import { ACCEPTED, INVITED } from "../../variables"; +import { standardRequest } from "../../config/request"; +import { getHttpsEnabled, getLoopsApiKey } from "../../config"; +import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; /** * Complete setting up user by adding their personal and auth information as part of the @@ -32,7 +32,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { encryptedPrivateKeyTag, salt, verifier, - organizationName + organizationName, }: { email: string; firstName: string; @@ -56,7 +56,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // case 1: user doesn't exist. // case 2: user has already completed account return res.status(403).send({ - error: 'Failed to complete account for complete user' + error: "Failed to complete account for complete user", }); } @@ -74,28 +74,28 @@ export const completeAccountSignup = async (req: Request, res: Response) => { encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt, - verifier + verifier, }); if (!user) - throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null + throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null // initialize default organization and workspace await initializeDefaultOrg({ organizationName, - user + user, }); // update organization membership statuses that are // invited to completed with user attached const membershipsToUpdate = await MembershipOrg.find({ inviteEmail: email, - status: INVITED + status: INVITED, }); membershipsToUpdate.forEach(async (membership) => { await updateSubscriptionOrgQuantity({ - organizationId: membership.organization.toString() + organizationId: membership.organization.toString(), }); }); @@ -104,11 +104,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => { await MembershipOrg.updateMany( { inviteEmail: email, - status: INVITED + status: INVITED, }, { user, - status: ACCEPTED + status: ACCEPTED, } ); @@ -116,7 +116,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); token = tokens.token; @@ -127,27 +127,27 @@ export const completeAccountSignup = async (req: Request, res: Response) => { "email": email, "eventName": "Sign Up", "firstName": firstName, - "lastName": lastName + "lastName": lastName, }, { headers: { "Accept": "application/json", - "Authorization": "Bearer " + (await getLoopsApiKey()) + "Authorization": "Bearer " + (await getLoopsApiKey()), }, }); } // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { + res.cookie("jid", tokens.refreshToken, { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled(), }); return res.status(200).send({ - message: 'Successfully set up account', + message: "Successfully set up account", user, - token + token, }); }; @@ -172,7 +172,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => { encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt, - verifier + verifier, } = req.body; // get user @@ -182,16 +182,16 @@ export const completeAccountInvite = async (req: Request, res: Response) => { // case 1: user doesn't exist. // case 2: user has already completed account return res.status(403).send({ - error: 'Failed to complete account for complete user' + error: "Failed to complete account for complete user", }); } const membershipOrg = await MembershipOrg.findOne({ inviteEmail: email, - status: INVITED + status: INVITED, }); - if (!membershipOrg) throw new Error('Failed to find invitations for email'); + if (!membershipOrg) throw new Error("Failed to find invitations for email"); // complete setting up user's account user = await completeAccount({ @@ -207,33 +207,33 @@ export const completeAccountInvite = async (req: Request, res: Response) => { encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt, - verifier + verifier, }); if (!user) - throw new Error('Failed to complete account for non-existent user'); + throw new Error("Failed to complete account for non-existent user"); // update organization membership statuses that are // invited to completed with user attached const membershipsToUpdate = await MembershipOrg.find({ inviteEmail: email, - status: INVITED + status: INVITED, }); membershipsToUpdate.forEach(async (membership) => { await updateSubscriptionOrgQuantity({ - organizationId: membership.organization.toString() + organizationId: membership.organization.toString(), }); }); await MembershipOrg.updateMany( { inviteEmail: email, - status: INVITED + status: INVITED, }, { user, - status: ACCEPTED + status: ACCEPTED, } ); @@ -241,22 +241,22 @@ export const completeAccountInvite = async (req: Request, res: Response) => { const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); token = tokens.token; // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { + res.cookie("jid", tokens.refreshToken, { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled(), }); return res.status(200).send({ - message: 'Successfully set up account', + message: "Successfully set up account", user, - token + token, }); }; diff --git a/backend/src/controllers/v2/tagController.ts b/backend/src/controllers/v2/tagController.ts index 0175b359a..926df0ca5 100644 --- a/backend/src/controllers/v2/tagController.ts +++ b/backend/src/controllers/v2/tagController.ts @@ -1,71 +1,65 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { - Membership, Secret, -} from '../../models'; -import Tag, { ITag } from '../../models/tag'; -import { Builder } from "builder-pattern" -import to from 'await-to-js'; -import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors'; -import { MongoError } from 'mongodb'; -import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Membership, Secret } from "../../models"; +import Tag, { ITag } from "../../models/tag"; +import { Builder } from "builder-pattern"; +import to from "await-to-js"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { MongoError } from "mongodb"; export const createWorkspaceTag = async (req: Request, res: Response) => { - const { workspaceId } = req.params - const { name, slug } = req.body - const sanitizedTagToCreate = Builder() - .name(name) - .workspace(new Types.ObjectId(workspaceId)) - .slug(slug) - .user(new Types.ObjectId(req.user._id)) - .build(); + const { workspaceId } = req.params; + const { name, slug } = req.body; + const sanitizedTagToCreate = Builder() + .name(name) + .workspace(new Types.ObjectId(workspaceId)) + .slug(slug) + .user(new Types.ObjectId(req.user._id)) + .build(); - const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate)) + const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate)); - if (err) { - if ((err as MongoError).code === 11000) { - throw BadRequestError({ message: "Tags must be unique in a workspace" }) - } + if (err) { + if ((err as MongoError).code === 11000) { + throw BadRequestError({ message: "Tags must be unique in a workspace" }); + } - throw err - } + throw err; + } - res.json(createdTag) -} + res.json(createdTag); +}; export const deleteWorkspaceTag = async (req: Request, res: Response) => { - const { tagId } = req.params + const { tagId } = req.params; - const tagFromDB = await Tag.findById(tagId) - if (!tagFromDB) { - throw BadRequestError() - } + const tagFromDB = await Tag.findById(tagId); + if (!tagFromDB) { + throw BadRequestError(); + } - // can only delete if the request user is one that belongs to the same workspace as the tag - const membership = await Membership.findOne({ - user: req.user, - workspace: tagFromDB.workspace - }); + // can only delete if the request user is one that belongs to the same workspace as the tag + const membership = await Membership.findOne({ + user: req.user, + workspace: tagFromDB.workspace + }); - if (!membership) { - UnauthorizedRequestError({ message: 'Failed to validate membership' }); - } + if (!membership) { + UnauthorizedRequestError({ message: "Failed to validate membership" }); + } - const result = await Tag.findByIdAndDelete(tagId); + const result = await Tag.findByIdAndDelete(tagId); - // remove the tag from secrets - await Secret.updateMany( - { tags: { $in: [tagId] } }, - { $pull: { tags: tagId } } - ); + // remove the tag from secrets + await Secret.updateMany({ tags: { $in: [tagId] } }, { $pull: { tags: tagId } }); - res.json(result); -} + res.json(result); +}; export const getWorkspaceTags = async (req: Request, res: Response) => { - const { workspaceId } = req.params - const workspaceTags = await Tag.find({ workspace: workspaceId }) - return res.json({ - workspaceTags - }) -} + const { workspaceId } = req.params; + const workspaceTags = await Tag.find({ workspace: workspaceId }); + return res.json({ + workspaceTags + }); +}; diff --git a/backend/src/controllers/v2/usersController.ts b/backend/src/controllers/v2/usersController.ts index 2d5cdc51a..2b4784b29 100644 --- a/backend/src/controllers/v2/usersController.ts +++ b/backend/src/controllers/v2/usersController.ts @@ -1,8 +1,8 @@ -import { Request, Response } from 'express'; +import { Request, Response } from "express"; import { + MembershipOrg, User, - MembershipOrg -} from '../../models'; +} from "../../models"; /** * Return the current user. @@ -38,10 +38,10 @@ export const getMe = async (req: Request, res: Response) => { */ const user = await User .findById(req.user._id) - .select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag'); + .select("+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"); return res.status(200).send({ - user + user, }); } @@ -60,7 +60,7 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => { if (isMfaEnabled) { // TODO: adapt this route/controller // to work for different forms of MFA - req.user.mfaMethods = ['email']; + req.user.mfaMethods = ["email"]; } else { req.user.mfaMethods = []; } @@ -70,7 +70,7 @@ export const updateMyMfaEnabled = async (req: Request, res: Response) => { const user = req.user; return res.status(200).send({ - user + user, }); } @@ -109,11 +109,11 @@ export const getMyOrganizations = async (req: Request, res: Response) => { */ const organizations = ( await MembershipOrg.find({ - user: req.user._id - }).populate('organization') + user: req.user._id, + }).populate("organization") ).map((m) => m.organization); return res.status(200).send({ - organizations + organizations, }); } diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 20778491d..c0d46f851 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -1,25 +1,19 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; import { - Workspace, - Secret, - Membership, - MembershipOrg, - Integration, - IntegrationAuth, Key, - IUser, - ServiceToken, - ServiceTokenData -} from '../../models'; + Membership, + ServiceTokenData, + Workspace, +} from "../../models"; import { - v2PushSecrets as push, pullSecrets as pull, - reformatPullSecrets -} from '../../helpers/secret'; -import { pushKeys } from '../../helpers/key'; -import { TelemetryService, EventService } from '../../services'; -import { eventPushSecrets } from '../../events'; + v2PushSecrets as push, + reformatPullSecrets, +} from "../../helpers/secret"; +import { pushKeys } from "../../helpers/key"; +import { EventService, TelemetryService } from "../../services"; +import { eventPushSecrets } from "../../events"; interface V2PushSecret { type: string; // personal or shared @@ -54,12 +48,12 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { // validate environment const workspaceEnvs = req.membership.workspace.environments; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); + throw new Error("Failed to validate environment"); } // sanitize secrets secrets = secrets.filter( - (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' + (s: V2PushSecret) => s.secretKeyCiphertext !== "" && s.secretValueCiphertext !== "" ); await push({ @@ -67,26 +61,26 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { workspaceId, environment, secrets, - channel: channel ? channel : 'cli', - ipAddress: req.realIP + channel: channel ? channel : "cli", + ipAddress: req.realIP, }); await pushKeys({ userId: req.user._id, workspaceId, - keys + keys, }); if (postHogClient) { postHogClient.capture({ - event: 'secrets pushed', + event: "secrets pushed", distinctId: req.user.email, properties: { numberOfSecrets: secrets.length, environment, workspaceId, - channel: channel ? channel : 'cli' - } + channel: channel ? channel : "cli", + }, }); } @@ -94,12 +88,12 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { EventService.handleEvent({ event: eventPushSecrets({ workspaceId: new Types.ObjectId(workspaceId), - environment - }) + environment, + }), }); return res.status(200).send({ - message: 'Successfully uploaded workspace secrets' + message: "Successfully uploaded workspace secrets", }); }; @@ -126,18 +120,18 @@ export const pullSecrets = async (req: Request, res: Response) => { // validate environment const workspaceEnvs = req.membership.workspace.environments; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { - throw new Error('Failed to validate environment'); + throw new Error("Failed to validate environment"); } secrets = await pull({ userId, workspaceId, environment, - channel: channel ? channel : 'cli', - ipAddress: req.realIP + channel: channel ? channel : "cli", + ipAddress: req.realIP, }); - if (channel !== 'cli') { + if (channel !== "cli") { secrets = reformatPullSecrets({ secrets }); } @@ -145,18 +139,18 @@ export const pullSecrets = async (req: Request, res: Response) => { // capture secrets pushed event in production postHogClient.capture({ distinctId: req.user.email, - event: 'secrets pulled', + event: "secrets pulled", properties: { numberOfSecrets: secrets.length, environment, workspaceId, - channel: channel ? channel : 'cli' - } + channel: channel ? channel : "cli", + }, }); } return res.status(200).send({ - secrets + secrets, }); }; @@ -194,10 +188,10 @@ export const getWorkspaceKey = async (req: Request, res: Response) => { key = await Key.findOne({ workspace: workspaceId, - receiver: req.user._id - }).populate('sender', '+publicKey'); + receiver: req.user._id, + }).populate("sender", "+publicKey"); - if (!key) throw new Error('Failed to find workspace key'); + if (!key) throw new Error("Failed to find workspace key"); return res.status(200).json(key); } @@ -209,12 +203,12 @@ export const getWorkspaceServiceTokenData = async ( const serviceTokenData = await ServiceTokenData .find({ - workspace: workspaceId + workspace: workspaceId, }) - .select('+encryptedKey +iv +tag'); + .select("+encryptedKey +iv +tag"); return res.status(200).send({ - serviceTokenData + serviceTokenData, }); } @@ -261,11 +255,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => { const { workspaceId } = req.params; const memberships = await Membership.find({ - workspace: workspaceId - }).populate('user', '+publicKey'); + workspace: workspaceId, + }).populate("user", "+publicKey"); return res.status(200).send({ - memberships + memberships, }); } @@ -330,21 +324,21 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) => } */ const { - membershipId + membershipId, } = req.params; const { role } = req.body; const membership = await Membership.findByIdAndUpdate( membershipId, { - role + role, }, { - new: true + new: true, } ); return res.status(200).send({ - membership + membership, }); } @@ -392,20 +386,20 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) => } */ const { - membershipId + membershipId, } = req.params; const membership = await Membership.findByIdAndDelete(membershipId); - if (!membership) throw new Error('Failed to delete workspace membership'); + if (!membership) throw new Error("Failed to delete workspace membership"); await Key.deleteMany({ receiver: membership.user, - workspace: membership.workspace + workspace: membership.workspace, }); return res.status(200).send({ - membership + membership, }); } @@ -421,18 +415,18 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => { const workspace = await Workspace.findOneAndUpdate( { - _id: workspaceId + _id: workspaceId, }, { - autoCapitalization + autoCapitalization, }, { - new: true + new: true, } ); return res.status(200).send({ - message: 'Successfully changed autoCapitalization setting', - workspace + message: "Successfully changed autoCapitalization setting", + workspace, }); }; diff --git a/backend/src/controllers/v3/authController.ts b/backend/src/controllers/v3/authController.ts index afb670269..7a70f171c 100644 --- a/backend/src/controllers/v3/authController.ts +++ b/backend/src/controllers/v3/authController.ts @@ -1,29 +1,29 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -import { Request, Response } from 'express'; -import jwt from 'jsonwebtoken'; -import * as Sentry from '@sentry/node'; -import * as bigintConversion from 'bigint-conversion'; -const jsrp = require('jsrp'); -import { User, LoginSRPDetail } from '../../models'; -import { issueAuthTokens, createToken, validateProviderAuthToken } from '../../helpers/auth'; -import { checkUserDevice } from '../../helpers/user'; -import { sendMail } from '../../helpers/nodemailer'; -import { TokenService } from '../../services'; -import { EELogService } from '../../ee/services'; -import { BadRequestError, InternalServerError } from '../../utils/errors'; +import { Request, Response } from "express"; +import jwt from "jsonwebtoken"; +import * as Sentry from "@sentry/node"; +import * as bigintConversion from "bigint-conversion"; +const jsrp = require("jsrp"); +import { LoginSRPDetail, User } from "../../models"; +import { createToken, issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth"; +import { checkUserDevice } from "../../helpers/user"; +import { sendMail } from "../../helpers/nodemailer"; +import { TokenService } from "../../services"; +import { EELogService } from "../../ee/services"; +import { BadRequestError, InternalServerError } from "../../utils/errors"; import { + ACTION_LOGIN, TOKEN_EMAIL_MFA, - ACTION_LOGIN -} from '../../variables'; -import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this +} from "../../variables"; +import { getChannelFromUserAgent } from "../../utils/posthog"; // TODO: move this import { + getHttpsEnabled, getJwtMfaLifetime, getJwtMfaSecret, - getHttpsEnabled, -} from '../../config'; -import { AuthProvider } from '../../models/user'; +} from "../../config"; +import { AuthProvider } from "../../models/user"; -declare module 'jsonwebtoken' { +declare module "jsonwebtoken" { export interface ProviderAuthJwtPayload extends jwt.JwtPayload { userId: string; email: string; @@ -43,7 +43,7 @@ export const login1 = async (req: Request, res: Response) => { const { email, providerAuthToken, - clientPublicKey + clientPublicKey, }: { email: string; clientPublicKey: string, @@ -52,9 +52,9 @@ export const login1 = async (req: Request, res: Response) => { const user = await User.findOne({ email, - }).select('+salt +verifier'); + }).select("+salt +verifier"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); if (user.authProvider) { await validateProviderAuthToken({ @@ -68,13 +68,13 @@ export const login1 = async (req: Request, res: Response) => { server.init( { salt: user.salt, - verifier: user.verifier + verifier: user.verifier, }, async () => { // generate server-side public key const serverPublicKey = server.getPublicKey(); await LoginSRPDetail.findOneAndReplace({ - email: email + email: email, }, { email, userId: user.id, @@ -84,7 +84,7 @@ export const login1 = async (req: Request, res: Response) => { return res.status(200).send({ serverPublicKey, - salt: user.salt + salt: user.salt, }); } ); @@ -92,7 +92,7 @@ export const login1 = async (req: Request, res: Response) => { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to start authentication process' + message: "Failed to start authentication process", }); } }; @@ -107,15 +107,15 @@ export const login1 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => { try { - if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); + if (!req.headers["user-agent"]) throw InternalServerError({ message: "User-Agent header is required" }); const { email, clientProof, providerAuthToken } = req.body; const user = await User.findOne({ email, - }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices'); + }).select("+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"); - if (!user) throw new Error('Failed to find user'); + if (!user) throw new Error("Failed to find user"); if (user.authProvider) { await validateProviderAuthToken({ @@ -136,7 +136,7 @@ export const login2 = async (req: Request, res: Response) => { { salt: user.salt, verifier: user.verifier, - b: loginSRPDetail.serverBInt + b: loginSRPDetail.serverBInt, }, async () => { server.setClientPublicKey(loginSRPDetail.clientPublicKey); @@ -150,52 +150,52 @@ export const login2 = async (req: Request, res: Response) => { // generate temporary MFA token const token = createToken({ payload: { - userId: user._id.toString() + userId: user._id.toString(), }, expiresIn: await getJwtMfaLifetime(), - secret: await getJwtMfaSecret() + secret: await getJwtMfaSecret(), }); const code = await TokenService.createToken({ type: TOKEN_EMAIL_MFA, - email + email, }); // send MFA code [code] to [email] await sendMail({ - template: 'emailMfa.handlebars', - subjectLine: 'Infisical MFA code', + template: "emailMfa.handlebars", + subjectLine: "Infisical MFA code", recipients: [user.email], substitutions: { - code - } + code, + }, }); return res.status(200).send({ mfaEnabled: true, - token + token, }); } await checkUserDevice({ user, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); // issue tokens const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { + res.cookie("jid", tokens.refreshToken, { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled(), }); // case: user does not have MFA enablgged @@ -221,7 +221,7 @@ export const login2 = async (req: Request, res: Response) => { publicKey: user.publicKey, encryptedPrivateKey: user.encryptedPrivateKey, iv: user.iv, - tag: user.tag + tag: user.tag, } if ( @@ -236,21 +236,21 @@ export const login2 = async (req: Request, res: Response) => { const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, - userId: user._id + userId: user._id, }); loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], - channel: getChannelFromUserAgent(req.headers['user-agent']), - ipAddress: req.realIP + channel: getChannelFromUserAgent(req.headers["user-agent"]), + ipAddress: req.realIP, }); return res.status(200).send(response); } return res.status(400).send({ - message: 'Failed to authenticate. Try again?' + message: "Failed to authenticate. Try again?", }); } ); @@ -258,7 +258,7 @@ export const login2 = async (req: Request, res: Response) => { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to authenticate. Try again?' + message: "Failed to authenticate. Try again?", }); } }; diff --git a/backend/src/controllers/v3/index.ts b/backend/src/controllers/v3/index.ts index 9d3f118d3..959bab532 100644 --- a/backend/src/controllers/v3/index.ts +++ b/backend/src/controllers/v3/index.ts @@ -1,7 +1,7 @@ -import * as secretsController from './secretsController'; -import * as workspacesController from './workspacesController'; -import * as authController from './authController'; -import * as signupController from './signupController'; +import * as secretsController from "./secretsController"; +import * as workspacesController from "./workspacesController"; +import * as authController from "./authController"; +import * as signupController from "./signupController"; export { authController, diff --git a/backend/src/controllers/v3/secretsController.ts b/backend/src/controllers/v3/secretsController.ts index c3853d3c2..3c7336241 100644 --- a/backend/src/controllers/v3/secretsController.ts +++ b/backend/src/controllers/v3/secretsController.ts @@ -1,7 +1,241 @@ import { Request, Response } from "express"; import { Types } from "mongoose"; -import { SecretService, EventService } from "../../services"; +import { EventService, SecretService } from "../../services"; import { eventPushSecrets } from "../../events"; +import { BotService } from "../../services"; +import { repackageSecretToRaw } from "../../helpers/secrets"; +import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; + +/** + * Return secrets for workspace with id [workspaceId] and environment + * [environment] in plaintext + * @param req + * @param res + */ +export const getSecretsRaw = async (req: Request, res: Response) => { + const workspaceId = req.query.workspaceId as string; + const environment = req.query.environment as string; + const secretPath = req.query.secretPath as string; + + const secrets = await SecretService.getSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + secretPath, + authData: req.authData, + }); + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId: new Types.ObjectId(workspaceId), + }); + + return res.status(200).send({ + secrets: secrets.map((secret) => { + const rep = repackageSecretToRaw({ + secret, + key, + }); + + return rep; + }), + }); +}; + +/** + * Return secret with name [secretName] in plaintext + * @param req + * @param res + */ +export const getSecretByNameRaw = async (req: Request, res: Response) => { + const { secretName } = req.params; + const workspaceId = req.query.workspaceId as string; + const environment = req.query.environment as string; + const secretPath = req.query.secretPath as string; + const type = req.query.type as "shared" | "personal" | undefined; + + const secret = await SecretService.getSecret({ + secretName, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + secretPath, + authData: req.authData, + }); + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId: new Types.ObjectId(workspaceId), + }); + + return res.status(200).send({ + secret: repackageSecretToRaw({ + secret, + key, + }), + }); +}; + +/** + * Create secret with name [secretName] in plaintext + * @param req + * @param res + */ +export const createSecretRaw = async (req: Request, res: Response) => { + const { secretName } = req.params; + const { + workspaceId, + environment, + type, + secretValue, + secretComment, + secretPath = "/", + } = req.body; + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId: new Types.ObjectId(workspaceId), + }); + + const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({ + plaintext: secretName, + key, + }); + + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ + plaintext: secretValue, + key, + }); + + const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({ + plaintext: secretComment, + key, + }); + + const secret = await SecretService.createSecret({ + secretName, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + authData: req.authData, + secretKeyCiphertext: secretKeyEncrypted.ciphertext, + secretKeyIV: secretKeyEncrypted.iv, + secretKeyTag: secretKeyEncrypted.tag, + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + secretPath, + secretCommentCiphertext: secretCommentEncrypted.ciphertext, + secretCommentIV: secretCommentEncrypted.iv, + secretCommentTag: secretCommentEncrypted.tag, + }); + + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + }), + }); + + const secretWithoutBlindIndex = secret.toObject(); + delete secretWithoutBlindIndex.secretBlindIndex; + + return res.status(200).send({ + secret: repackageSecretToRaw({ + secret: secretWithoutBlindIndex, + key, + }), + }); +} + +/** + * Update secret with name [secretName] + * @param req + * @param res + */ +export const updateSecretByNameRaw = async (req: Request, res: Response) => { + const { secretName } = req.params; + const { + workspaceId, + environment, + type, + secretValue, + secretPath = "/", + } = req.body; + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId: new Types.ObjectId(workspaceId), + }); + + const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ + plaintext: secretValue, + key, + }); + + const secret = await SecretService.updateSecret({ + secretName, + workspaceId, + environment, + type, + authData: req.authData, + secretValueCiphertext: secretValueEncrypted.ciphertext, + secretValueIV: secretValueEncrypted.iv, + secretValueTag: secretValueEncrypted.tag, + secretPath, + }); + + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + }), + }); + + return res.status(200).send({ + secret: repackageSecretToRaw({ + secret, + key, + }), + }); +}; + +/** + * Delete secret with name [secretName] + * @param req + * @param res + */ +export const deleteSecretByNameRaw = async (req: Request, res: Response) => { + const { secretName } = req.params; + const { + workspaceId, + environment, + type, + secretPath = "/", + } = req.body; + + const { secret } = await SecretService.deleteSecret({ + secretName, + workspaceId, + environment, + type, + authData: req.authData, + secretPath, + }); + + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + }), + }); + + const key = await BotService.getWorkspaceKeyWithBot({ + workspaceId: new Types.ObjectId(workspaceId), + }); + + return res.status(200).send({ + secret: repackageSecretToRaw({ + secret, + key, + }), + }); +}; /** * Get secrets for workspace with id [workspaceId] and environment @@ -27,7 +261,7 @@ export const getSecrets = async (req: Request, res: Response) => { }; /** - * Get secret with name [secretName] + * Return secret with name [secretName] * @param req * @param res */ @@ -88,13 +322,9 @@ export const createSecret = async (req: Request, res: Response) => { secretValueIV, secretValueTag, secretPath, - ...(secretCommentCiphertext && secretCommentIV && secretCommentTag - ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - } - : {}), + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, }); await EventService.handleEvent({ @@ -112,6 +342,7 @@ export const createSecret = async (req: Request, res: Response) => { }); }; + /** * Update secret with name [secretName] * @param req @@ -160,7 +391,12 @@ export const updateSecretByName = async (req: Request, res: Response) => { */ export const deleteSecretByName = async (req: Request, res: Response) => { const { secretName } = req.params; - const { workspaceId, environment, type, secretPath = "/" } = req.body; + const { + workspaceId, + environment, + type, + secretPath = "/", + } = req.body; const { secret } = await SecretService.deleteSecret({ secretName, diff --git a/backend/src/controllers/v3/signupController.ts b/backend/src/controllers/v3/signupController.ts index 5944e289a..3d9f897c4 100644 --- a/backend/src/controllers/v3/signupController.ts +++ b/backend/src/controllers/v3/signupController.ts @@ -1,17 +1,17 @@ -import jwt from 'jsonwebtoken'; -import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; -import { User, MembershipOrg } from '../../models'; -import { completeAccount } from '../../helpers/user'; +import jwt from "jsonwebtoken"; +import { Request, Response } from "express"; +import * as Sentry from "@sentry/node"; +import { MembershipOrg, User } from "../../models"; +import { completeAccount } from "../../helpers/user"; import { - initializeDefaultOrg -} from '../../helpers/signup'; -import { issueAuthTokens, validateProviderAuthToken } from '../../helpers/auth'; -import { INVITED, ACCEPTED } from '../../variables'; -import { standardRequest } from '../../config/request'; -import { getLoopsApiKey, getHttpsEnabled, getJwtSignupSecret } from '../../config'; -import { BadRequestError } from '../../utils/errors'; -import { TelemetryService } from '../../services'; + initializeDefaultOrg, +} from "../../helpers/signup"; +import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth"; +import { ACCEPTED, INVITED } from "../../variables"; +import { standardRequest } from "../../config/request"; +import { getHttpsEnabled, getJwtSignupSecret, getLoopsApiKey } from "../../config"; +import { BadRequestError } from "../../utils/errors"; +import { TelemetryService } from "../../services"; /** * Complete setting up user by adding their personal and auth information as part of the @@ -63,7 +63,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { // case 1: user doesn't exist. // case 2: user has already completed account return res.status(403).send({ - error: 'Failed to complete account for complete user' + error: "Failed to complete account for complete user", }); } @@ -74,16 +74,16 @@ export const completeAccountSignup = async (req: Request, res: Response) => { user, }); } else { - const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] + const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] if (AUTH_TOKEN_TYPE === null) { - throw BadRequestError({ message: `Missing Authorization Header in the request header.` }); + throw BadRequestError({ message: "Missing Authorization Header in the request header." }); } - if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') { + if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") { throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` }) } if (AUTH_TOKEN_VALUE === null) { throw BadRequestError({ - message: 'Missing Authorization Body in the request header', + message: "Missing Authorization Body in the request header", }) } @@ -110,16 +110,16 @@ export const completeAccountSignup = async (req: Request, res: Response) => { encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt, - verifier + verifier, }); if (!user) - throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null + throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null // initialize default organization and workspace await initializeDefaultOrg({ organizationName, - user + user, }); // update organization membership statuses that are @@ -127,11 +127,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => { await MembershipOrg.updateMany( { inviteEmail: email, - status: INVITED + status: INVITED, }, { user, - status: ACCEPTED + status: ACCEPTED, } ); @@ -139,7 +139,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { const tokens = await issueAuthTokens({ userId: user._id, ip: req.realIP, - userAgent: req.headers['user-agent'] ?? '' + userAgent: req.headers["user-agent"] ?? "", }); token = tokens.token; @@ -150,45 +150,45 @@ export const completeAccountSignup = async (req: Request, res: Response) => { "email": email, "eventName": "Sign Up", "firstName": firstName, - "lastName": lastName + "lastName": lastName, }, { headers: { "Accept": "application/json", - "Authorization": "Bearer " + (await getLoopsApiKey()) + "Authorization": "Bearer " + (await getLoopsApiKey()), }, }); } // store (refresh) token in httpOnly cookie - res.cookie('jid', tokens.refreshToken, { + res.cookie("jid", tokens.refreshToken, { httpOnly: true, - path: '/', - sameSite: 'strict', - secure: await getHttpsEnabled() + path: "/", + sameSite: "strict", + secure: await getHttpsEnabled(), }); const postHogClient = await TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ - event: 'User Signed Up', + event: "User Signed Up", distinctId: email, properties: { email, - attributionSource - } + attributionSource, + }, }); } } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to complete account setup' + message: "Failed to complete account setup", }); } return res.status(200).send({ - message: 'Successfully set up account', + message: "Successfully set up account", user, - token + token, }); }; diff --git a/backend/src/controllers/v3/workspacesController.ts b/backend/src/controllers/v3/workspacesController.ts index aac6682d7..4298adba2 100644 --- a/backend/src/controllers/v3/workspacesController.ts +++ b/backend/src/controllers/v3/workspacesController.ts @@ -1,7 +1,7 @@ -import { Request, Response } from 'express'; -import { Types } from 'mongoose'; -import { Secret } from '../../models'; -import { SecretService } from'../../services'; +import { Request, Response } from "express"; +import { Types } from "mongoose"; +import { Secret } from "../../models"; +import { SecretService } from"../../services"; /** * Return whether or not all secrets in workspace with id [workspaceId] @@ -16,8 +16,8 @@ export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) const secretsWithoutBlindIndex = await Secret.countDocuments({ workspace: new Types.ObjectId(workspaceId), secretBlindIndex: { - $exists: false - } + $exists: false, + }, }); return res.status(200).send(secretsWithoutBlindIndex === 0); @@ -30,11 +30,11 @@ export const getWorkspaceSecrets = async (req: Request, res: Response) => { const { workspaceId } = req.params; const secrets = await Secret.find({ - workspace: new Types.ObjectId (workspaceId) + workspace: new Types.ObjectId (workspaceId), }); return res.status(200).send({ - secrets + secrets, }); } @@ -51,14 +51,14 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => { const { workspaceId } = req.params; const { - secretsToUpdate + secretsToUpdate, }: { secretsToUpdate: SecretToUpdate[]; } = req.body; // get secret blind index salt const salt = await SecretService.getSecretBlindIndexSalt({ - workspaceId: new Types.ObjectId(workspaceId) + workspaceId: new Types.ObjectId(workspaceId), }); // update secret blind indices @@ -66,18 +66,18 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => { secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => { const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ secretName: secretToUpdate.secretName, - salt + salt, }); return ({ updateOne: { filter: { - _id: new Types.ObjectId(secretToUpdate._id) + _id: new Types.ObjectId(secretToUpdate._id), }, update: { - secretBlindIndex - } - } + secretBlindIndex, + }, + }, }); }) ); @@ -85,6 +85,6 @@ export const nameWorkspaceSecrets = async (req: Request, res: Response) => { await Secret.bulkWrite(operations); return res.status(200).send({ - message: 'Successfully named workspace secrets' + message: "Successfully named workspace secrets", }); } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/actionController.ts b/backend/src/ee/controllers/v1/actionController.ts index 4b3117b20..484c25351 100644 --- a/backend/src/ee/controllers/v1/actionController.ts +++ b/backend/src/ee/controllers/v1/actionController.ts @@ -1,6 +1,6 @@ -import { Request, Response } from 'express'; -import { Action, SecretVersion } from '../../models'; -import { ActionNotFoundError } from '../../../utils/errors'; +import { Request, Response } from "express"; +import { Action } from "../../models"; +import { ActionNotFoundError } from "../../../utils/errors"; export const getAction = async (req: Request, res: Response) => { let action; @@ -10,21 +10,21 @@ export const getAction = async (req: Request, res: Response) => { action = await Action .findById(actionId) .populate([ - 'payload.secretVersions.oldSecretVersion', - 'payload.secretVersions.newSecretVersion' + "payload.secretVersions.oldSecretVersion", + "payload.secretVersions.newSecretVersion", ]); if (!action) throw ActionNotFoundError({ - message: 'Failed to find action' + message: "Failed to find action", }); } catch (err) { throw ActionNotFoundError({ - message: 'Failed to find action' + message: "Failed to find action", }); } return res.status(200).send({ - action + action, }); } diff --git a/backend/src/ee/controllers/v1/cloudProductsController.ts b/backend/src/ee/controllers/v1/cloudProductsController.ts index c7d60ca1a..e66fc66e5 100644 --- a/backend/src/ee/controllers/v1/cloudProductsController.ts +++ b/backend/src/ee/controllers/v1/cloudProductsController.ts @@ -1,7 +1,7 @@ -import { Request, Response } from 'express'; -import { EELicenseService } from '../../services'; -import { getLicenseServerUrl } from '../../../config'; -import { licenseServerKeyRequest } from '../../../config/request'; +import { Request, Response } from "express"; +import { EELicenseService } from "../../services"; +import { getLicenseServerUrl } from "../../../config"; +import { licenseServerKeyRequest } from "../../../config/request"; /** * Return available cloud product information. @@ -11,9 +11,9 @@ import { licenseServerKeyRequest } from '../../../config/request'; * @returns */ export const getCloudProducts = async (req: Request, res: Response) => { - const billingCycle = req.query['billing-cycle'] as string; + const billingCycle = req.query["billing-cycle"] as string; - if (EELicenseService.instanceType === 'cloud') { + if (EELicenseService.instanceType === "cloud") { const { data } = await licenseServerKeyRequest.get( `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` ); @@ -23,6 +23,6 @@ export const getCloudProducts = async (req: Request, res: Response) => { return res.status(200).send({ head: [], - rows: [] + rows: [], }); } diff --git a/backend/src/ee/controllers/v1/index.ts b/backend/src/ee/controllers/v1/index.ts index bf3992b17..c8b9f48a2 100644 --- a/backend/src/ee/controllers/v1/index.ts +++ b/backend/src/ee/controllers/v1/index.ts @@ -1,11 +1,11 @@ -import * as stripeController from './stripeController'; -import * as secretController from './secretController'; -import * as secretSnapshotController from './secretSnapshotController'; -import * as organizationsController from './organizationsController'; -import * as workspaceController from './workspaceController'; -import * as actionController from './actionController'; -import * as membershipController from './membershipController'; -import * as cloudProductsController from './cloudProductsController'; +import * as stripeController from "./stripeController"; +import * as secretController from "./secretController"; +import * as secretSnapshotController from "./secretSnapshotController"; +import * as organizationsController from "./organizationsController"; +import * as workspaceController from "./workspaceController"; +import * as actionController from "./actionController"; +import * as membershipController from "./membershipController"; +import * as cloudProductsController from "./cloudProductsController"; export { stripeController, @@ -15,5 +15,5 @@ export { workspaceController, actionController, membershipController, - cloudProductsController + cloudProductsController, } \ No newline at end of file diff --git a/backend/src/ee/controllers/v1/membershipController.ts b/backend/src/ee/controllers/v1/membershipController.ts index 35534d19c..1a03d36d0 100644 --- a/backend/src/ee/controllers/v1/membershipController.ts +++ b/backend/src/ee/controllers/v1/membershipController.ts @@ -3,7 +3,7 @@ import { Membership, Workspace } from "../../../models"; import { IMembershipPermission } from "../../../models/membership"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; import { ADMIN, MEMBER } from "../../../variables/organization"; -import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables'; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables"; import { Builder } from "builder-pattern" import _ from "lodash"; @@ -39,7 +39,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) => throw BadRequestError({ message: "Something went wrong when locating the related workspace" }) } - const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, 'slug'))); + const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, "slug"))); sanitizedMembershipPermissionsUnique.forEach(permission => { if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) { @@ -59,6 +59,6 @@ export const denyMembershipPermissions = async (req: Request, res: Response) => } res.send({ - permissionsDenied: updatedMembershipWithPermissions.deniedPermissions + permissionsDenied: updatedMembershipWithPermissions.deniedPermissions, }) } diff --git a/backend/src/ee/controllers/v1/organizationsController.ts b/backend/src/ee/controllers/v1/organizationsController.ts index 2dd212e77..54c154c7a 100644 --- a/backend/src/ee/controllers/v1/organizationsController.ts +++ b/backend/src/ee/controllers/v1/organizationsController.ts @@ -1,15 +1,16 @@ -import { Request, Response } from 'express'; -import { getLicenseServerUrl } from '../../../config'; -import { licenseServerKeyRequest } from '../../../config/request'; -import { EELicenseService } from '../../services'; +import { Request, Response } from "express"; +import { getLicenseServerUrl } from "../../../config"; +import { licenseServerKeyRequest } from "../../../config/request"; +import { EELicenseService } from "../../services"; /** * Return the organization's current plan and allowed feature set */ export const getOrganizationPlan = async (req: Request, res: Response) => { const { organizationId } = req.params; + const workspaceId = req.query.workspaceId as string; - const plan = await EELicenseService.getOrganizationPlan(organizationId); + const plan = await EELicenseService.getPlan(organizationId, workspaceId); return res.status(200).send({ plan, @@ -24,13 +25,13 @@ export const getOrganizationPlan = async (req: Request, res: Response) => { */ export const updateOrganizationPlan = async (req: Request, res: Response) => { const { - productId + productId, } = req.body; const { data } = await licenseServerKeyRequest.patch( `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`, { - productId + productId, } ); @@ -46,7 +47,7 @@ export const getOrganizationPmtMethods = async (req: Request, res: Response) => ); return res.status(200).send({ - pmtMethods + pmtMethods, }); } @@ -56,19 +57,19 @@ export const getOrganizationPmtMethods = async (req: Request, res: Response) => export const addOrganizationPmtMethod = async (req: Request, res: Response) => { const { success_url, - cancel_url + cancel_url, } = req.body; const { data: { url } } = await licenseServerKeyRequest.post( `${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`, { success_url, - cancel_url + cancel_url, } ); return res.status(200).send({ - url + url, }); } diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts index 445add15a..c10188b10 100644 --- a/backend/src/ee/controllers/v1/secretSnapshotController.ts +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -17,11 +17,11 @@ export const getSecretSnapshot = async (req: Request, res: Response) => { const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) .lean() .populate<{ secretVersions: ISecretVersion[] }>({ - path: 'secretVersions', + path: "secretVersions", populate: { - path: 'tags', - model: 'Tag' - } + path: "tags", + model: "Tag", + }, }) .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); diff --git a/backend/src/ee/controllers/v1/stripeController.ts b/backend/src/ee/controllers/v1/stripeController.ts index 172df3220..bd3ed7704 100644 --- a/backend/src/ee/controllers/v1/stripeController.ts +++ b/backend/src/ee/controllers/v1/stripeController.ts @@ -1,6 +1,6 @@ -import { Request, Response } from 'express'; -import Stripe from 'stripe'; -import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; +import { Request, Response } from "express"; +import Stripe from "stripe"; +import { getStripeSecretKey, getStripeWebhookSecret } from "../../../config"; /** * Handle service provisioning/un-provisioning via Stripe @@ -10,11 +10,11 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; */ export const handleWebhook = async (req: Request, res: Response) => { const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' + apiVersion: "2022-08-01", }); // check request for valid stripe signature - const sig = req.headers['stripe-signature'] as string; + const sig = req.headers["stripe-signature"] as string; const event = stripe.webhooks.constructEvent( req.body, sig, @@ -22,7 +22,7 @@ export const handleWebhook = async (req: Request, res: Response) => { ); switch (event.type) { - case '': + case "": break; default: } diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 166171ccd..5ec54d676 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -2,11 +2,11 @@ import { Request, Response } from "express"; import { PipelineStage, Types } from "mongoose"; import { Secret } from "../../../models"; import { - SecretSnapshot, - Log, - SecretVersion, - ISecretVersion, FolderVersion, + ISecretVersion, + Log, + SecretSnapshot, + SecretVersion, TFolderRootVersionSchema, } from "../../models"; import { EESecretService } from "../../services"; diff --git a/backend/src/ee/helpers/action.ts b/backend/src/ee/helpers/action.ts index 94e6bd24d..3279e86ca 100644 --- a/backend/src/ee/helpers/action.ts +++ b/backend/src/ee/helpers/action.ts @@ -1,17 +1,17 @@ -import { Types } from 'mongoose'; -import { Action } from '../models'; +import { Types } from "mongoose"; +import { Action } from "../models"; import { + getLatestNSecretSecretVersionIds, getLatestSecretVersionIds, - getLatestNSecretSecretVersionIds -} from '../helpers/secretVersion'; +} from "../helpers/secretVersion"; import { + ACTION_ADD_SECRETS, + ACTION_DELETE_SECRETS, ACTION_LOGIN, ACTION_LOGOUT, - ACTION_ADD_SECRETS, ACTION_READ_SECRETS, - ACTION_DELETE_SECRETS, ACTION_UPDATE_SECRETS, -} from '../../variables'; +} from "../../variables"; /** * Create an (audit) action for updating secrets @@ -26,7 +26,7 @@ const createActionUpdateSecret = async ({ serviceAccountId, serviceTokenDataId, workspaceId, - secretIds + secretIds, }: { name: string; userId?: Types.ObjectId; @@ -37,11 +37,11 @@ const createActionUpdateSecret = async ({ }) => { const latestSecretVersions = (await getLatestNSecretSecretVersionIds({ secretIds, - n: 2 + n: 2, })) .map((s) => ({ oldSecretVersion: s.versions[0]._id, - newSecretVersion: s.versions[1]._id + newSecretVersion: s.versions[1]._id, })); const action = await new Action({ @@ -51,8 +51,8 @@ const createActionUpdateSecret = async ({ serviceTokenData: serviceTokenDataId, workspace: workspaceId, payload: { - secretVersions: latestSecretVersions - } + secretVersions: latestSecretVersions, + }, }).save(); return action; @@ -72,7 +72,7 @@ const createActionSecret = async ({ serviceAccountId, serviceTokenDataId, workspaceId, - secretIds + secretIds, }: { name: string; userId?: Types.ObjectId; @@ -84,10 +84,10 @@ const createActionSecret = async ({ // case: action is adding, deleting, or reading secrets // -> add new secret versions const latestSecretVersions = (await getLatestSecretVersionIds({ - secretIds + secretIds, })) .map((s) => ({ - newSecretVersion: s.versionId + newSecretVersion: s.versionId, })); const action = await new Action({ @@ -97,8 +97,8 @@ const createActionSecret = async ({ serviceTokenData: serviceTokenDataId, workspace: workspaceId, payload: { - secretVersions: latestSecretVersions - } + secretVersions: latestSecretVersions, + }, }).save(); return action; @@ -116,7 +116,7 @@ const createActionClient = ({ name, userId, serviceAccountId, - serviceTokenDataId + serviceTokenDataId, }: { name: string; userId?: Types.ObjectId; @@ -127,7 +127,7 @@ const createActionClient = ({ name, user: userId, serviceAccount: serviceAccountId, - serviceTokenData: serviceTokenDataId + serviceTokenData: serviceTokenDataId, }).save(); return action; @@ -162,27 +162,27 @@ const createActionHelper = async ({ case ACTION_LOGOUT: action = await createActionClient({ name, - userId + userId, }); break; case ACTION_ADD_SECRETS: case ACTION_READ_SECRETS: case ACTION_DELETE_SECRETS: - if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); + if (!workspaceId || !secretIds) throw new Error("Missing required params workspace id or secret ids to create action secret"); action = await createActionSecret({ name, userId, workspaceId, - secretIds + secretIds, }); break; case ACTION_UPDATE_SECRETS: - if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); + if (!workspaceId || !secretIds) throw new Error("Missing required params workspace id or secret ids to create action secret"); action = await createActionUpdateSecret({ name, userId, workspaceId, - secretIds + secretIds, }); break; } @@ -191,5 +191,5 @@ const createActionHelper = async ({ } export { - createActionHelper + createActionHelper, }; diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts index c97a51619..4f6ddc771 100644 --- a/backend/src/ee/helpers/checkMembershipPermissions.ts +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -1,7 +1,7 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import _ from "lodash"; import { Membership } from "../../models"; -import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts index 5b6d78f31..feb52d151 100644 --- a/backend/src/ee/helpers/log.ts +++ b/backend/src/ee/helpers/log.ts @@ -1,8 +1,8 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { + IAction, Log, - IAction -} from '../models'; +} from "../models"; /** * Create an (audit) log @@ -21,7 +21,7 @@ const createLogHelper = async ({ workspaceId, actions, channel, - ipAddress + ipAddress, }: { userId?: Types.ObjectId; serviceAccountId?: Types.ObjectId; @@ -39,12 +39,12 @@ const createLogHelper = async ({ actionNames: actions.map((a) => a.name), actions, channel, - ipAddress + ipAddress, }).save(); return log; } export { - createLogHelper + createLogHelper, } diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index b315310cb..54b26f56e 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -1,10 +1,10 @@ import { Types } from "mongoose"; -import { Secret, ISecret } from "../../models"; +import { Secret } from "../../models"; import { + FolderVersion, + ISecretVersion, SecretSnapshot, SecretVersion, - ISecretVersion, - FolderVersion, } from "../models"; /** diff --git a/backend/src/ee/middleware/index.ts b/backend/src/ee/middleware/index.ts index ff9267965..a984d924b 100644 --- a/backend/src/ee/middleware/index.ts +++ b/backend/src/ee/middleware/index.ts @@ -1,7 +1,7 @@ -import requireLicenseAuth from './requireLicenseAuth'; -import requireSecretSnapshotAuth from './requireSecretSnapshotAuth'; +import requireLicenseAuth from "./requireLicenseAuth"; +import requireSecretSnapshotAuth from "./requireSecretSnapshotAuth"; export { requireLicenseAuth, - requireSecretSnapshotAuth + requireSecretSnapshotAuth, } \ No newline at end of file diff --git a/backend/src/ee/middleware/requireLicenseAuth.ts b/backend/src/ee/middleware/requireLicenseAuth.ts index c577563f3..476a3e82b 100644 --- a/backend/src/ee/middleware/requireLicenseAuth.ts +++ b/backend/src/ee/middleware/requireLicenseAuth.ts @@ -1,4 +1,4 @@ -import { Request, Response, NextFunction } from 'express'; +import { NextFunction, Request, Response } from "express"; /** * Validate if organization hosting meets license requirements to @@ -7,7 +7,7 @@ import { Request, Response, NextFunction } from 'express'; * @param {String[]} obj.acceptedTiers */ const requireLicenseAuth = ({ - acceptedTiers + acceptedTiers, }: { acceptedTiers: string[]; }) => { diff --git a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts index af4d1e21c..7cc5d1de7 100644 --- a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts +++ b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts @@ -1,9 +1,9 @@ -import { Request, Response, NextFunction } from 'express'; -import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors'; -import { SecretSnapshot } from '../models'; +import { NextFunction, Request, Response } from "express"; +import { SecretSnapshotNotFoundError } from "../../utils/errors"; +import { SecretSnapshot } from "../models"; import { - validateMembership -} from '../../helpers/membership'; + validateMembership, +} from "../../helpers/membership"; /** * Validate if user on request has proper membership for secret snapshot @@ -15,7 +15,7 @@ import { const requireSecretSnapshotAuth = ({ acceptedRoles, }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { secretSnapshotId } = req.params; @@ -24,14 +24,14 @@ const requireSecretSnapshotAuth = ({ if (!secretSnapshot) { return next(SecretSnapshotNotFoundError({ - message: 'Failed to find secret snapshot' + message: "Failed to find secret snapshot", })); } await validateMembership({ userId: req.user._id, workspaceId: secretSnapshot.workspace, - acceptedRoles + acceptedRoles, }); req.secretSnapshot = secretSnapshot as any; diff --git a/backend/src/ee/models/action.ts b/backend/src/ee/models/action.ts index 055e144fd..2e8432914 100644 --- a/backend/src/ee/models/action.ts +++ b/backend/src/ee/models/action.ts @@ -1,12 +1,12 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; import { + ACTION_ADD_SECRETS, + ACTION_DELETE_SECRETS, ACTION_LOGIN, ACTION_LOGOUT, - ACTION_ADD_SECRETS, - ACTION_UPDATE_SECRETS, ACTION_READ_SECRETS, - ACTION_DELETE_SECRETS -} from '../../variables'; + ACTION_UPDATE_SECRETS, +} from "../../variables"; export interface IAction { name: string; @@ -30,42 +30,42 @@ const actionSchema = new Schema( ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, ACTION_READ_SECRETS, - ACTION_DELETE_SECRETS - ] + ACTION_DELETE_SECRETS, + ], }, user: { type: Schema.Types.ObjectId, - ref: 'User' + ref: "User", }, serviceAccount: { type: Schema.Types.ObjectId, - ref: 'ServiceAccount' + ref: "ServiceAccount", }, serviceTokenData: { type: Schema.Types.ObjectId, - ref: 'ServiceTokenData' + ref: "ServiceTokenData", }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace' + ref: "Workspace", }, payload: { secretVersions: [{ oldSecretVersion: { type: Schema.Types.ObjectId, - ref: 'SecretVersion' + ref: "SecretVersion", }, newSecretVersion: { type: Schema.Types.ObjectId, - ref: 'SecretVersion' - } - }] - } + ref: "SecretVersion", + }, + }], + }, }, { - timestamps: true + timestamps: true, } ); -const Action = model('Action', actionSchema); +const Action = model("Action", actionSchema); export default Action; \ No newline at end of file diff --git a/backend/src/ee/models/folderVersion.ts b/backend/src/ee/models/folderVersion.ts index f0fa5afbf..4bfa2f67c 100644 --- a/backend/src/ee/models/folderVersion.ts +++ b/backend/src/ee/models/folderVersion.ts @@ -1,4 +1,4 @@ -import { model, Schema, Types } from "mongoose"; +import { Schema, Types, model } from "mongoose"; export type TFolderRootVersionSchema = { _id: Types.ObjectId; diff --git a/backend/src/ee/models/log.ts b/backend/src/ee/models/log.ts index 9ed552640..5e3d0dbbb 100644 --- a/backend/src/ee/models/log.ts +++ b/backend/src/ee/models/log.ts @@ -1,12 +1,12 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; import { + ACTION_ADD_SECRETS, + ACTION_DELETE_SECRETS, ACTION_LOGIN, ACTION_LOGOUT, - ACTION_ADD_SECRETS, - ACTION_UPDATE_SECRETS, ACTION_READ_SECRETS, - ACTION_DELETE_SECRETS -} from '../../variables'; + ACTION_UPDATE_SECRETS, +} from "../../variables"; export interface ILog { _id: Types.ObjectId; @@ -24,19 +24,19 @@ const logSchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: 'User' + ref: "User", }, serviceAccount: { type: Schema.Types.ObjectId, - ref: 'ServiceAccount' + ref: "ServiceAccount", }, serviceTokenData: { type: Schema.Types.ObjectId, - ref: 'ServiceTokenData' + ref: "ServiceTokenData", }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace' + ref: "Workspace", }, actionNames: { type: [String], @@ -46,28 +46,28 @@ const logSchema = new Schema( ACTION_ADD_SECRETS, ACTION_UPDATE_SECRETS, ACTION_READ_SECRETS, - ACTION_DELETE_SECRETS + ACTION_DELETE_SECRETS, ], - required: true + required: true, }, actions: [{ type: Schema.Types.ObjectId, - ref: 'Action', - required: true + ref: "Action", + required: true, }], channel: { type: String, - enum: ['web', 'cli', 'auto', 'k8-operator', 'other'], - required: true + enum: ["web", "cli", "auto", "k8-operator", "other"], + required: true, }, ipAddress: { - type: String - } + type: String, + }, }, { - timestamps: true + timestamps: true, } ); -const Log = model('Log', logSchema); +const Log = model("Log", logSchema); export default Log; \ No newline at end of file diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts index cfc5b03b7..d0fb61110 100644 --- a/backend/src/ee/models/secretSnapshot.ts +++ b/backend/src/ee/models/secretSnapshot.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from "mongoose"; +import { Schema, Types, model } from "mongoose"; export interface ISecretSnapshot { workspace: Types.ObjectId; diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index b915e640e..1922d4539 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -1,10 +1,10 @@ -import { Schema, model, Types } from "mongoose"; +import { Schema, Types, model } from "mongoose"; import { - SECRET_SHARED, - SECRET_PERSONAL, ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8, + SECRET_PERSONAL, + SECRET_SHARED, } from "../../variables"; export interface ISecretVersion { @@ -114,9 +114,9 @@ const secretVersionSchema = new Schema( required: true, }, tags: { - ref: 'Tag', + ref: "Tag", type: [Schema.Types.ObjectId], - default: [] + default: [], }, }, { diff --git a/backend/src/ee/routes/v1/action.ts b/backend/src/ee/routes/v1/action.ts index 5dca83cf9..b77a84188 100644 --- a/backend/src/ee/routes/v1/action.ts +++ b/backend/src/ee/routes/v1/action.ts @@ -1,15 +1,15 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - validateRequest -} from '../../../middleware'; -import { param } from 'express-validator'; -import { actionController } from '../../controllers/v1'; + validateRequest, +} from "../../../middleware"; +import { param } from "express-validator"; +import { actionController } from "../../controllers/v1"; // TODO: put into action controller router.get( - '/:actionId', - param('actionId').exists().trim(), + "/:actionId", + param("actionId").exists().trim(), validateRequest, actionController.getAction ); diff --git a/backend/src/ee/routes/v1/cloudProducts.ts b/backend/src/ee/routes/v1/cloudProducts.ts index 73af00aca..a9be34747 100644 --- a/backend/src/ee/routes/v1/cloudProducts.ts +++ b/backend/src/ee/routes/v1/cloudProducts.ts @@ -1,18 +1,18 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, - validateRequest -} from '../../../middleware'; -import { query } from 'express-validator'; -import { cloudProductsController } from '../../controllers/v1'; + validateRequest, +} from "../../../middleware"; +import { query } from "express-validator"; +import { cloudProductsController } from "../../controllers/v1"; router.get( - '/', + "/", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), - query('billing-cycle').exists().isIn(['monthly', 'yearly']), + query("billing-cycle").exists().isIn(["monthly", "yearly"]), validateRequest, cloudProductsController.getCloudProducts ); diff --git a/backend/src/ee/routes/v1/index.ts b/backend/src/ee/routes/v1/index.ts index 7568e45d6..c68196c64 100644 --- a/backend/src/ee/routes/v1/index.ts +++ b/backend/src/ee/routes/v1/index.ts @@ -1,9 +1,9 @@ -import secret from './secret'; -import secretSnapshot from './secretSnapshot'; -import organizations from './organizations'; -import workspace from './workspace'; -import action from './action'; -import cloudProducts from './cloudProducts'; +import secret from "./secret"; +import secretSnapshot from "./secretSnapshot"; +import organizations from "./organizations"; +import workspace from "./workspace"; +import action from "./action"; +import cloudProducts from "./cloudProducts"; export { secret, @@ -11,5 +11,5 @@ export { organizations, workspace, action, - cloudProducts + cloudProducts, } \ No newline at end of file diff --git a/backend/src/ee/routes/v1/organizations.ts b/backend/src/ee/routes/v1/organizations.ts index fd9fd08e9..5a9c603b1 100644 --- a/backend/src/ee/routes/v1/organizations.ts +++ b/backend/src/ee/routes/v1/organizations.ts @@ -1,85 +1,86 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, requireOrganizationAuth, - validateRequest -} from '../../../middleware'; -import { param, body } from 'express-validator'; -import { organizationsController } from '../../controllers/v1'; + validateRequest, +} from "../../../middleware"; +import { body, param, query } from "express-validator"; +import { organizationsController } from "../../controllers/v1"; import { - OWNER, ADMIN, MEMBER, ACCEPTED -} from '../../../variables'; + ACCEPTED, ADMIN, MEMBER, OWNER, +} from "../../../variables"; router.get( - '/:organizationId/plan', + "/:organizationId/plan", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), + query("workspaceId").optional().isString(), validateRequest, organizationsController.getOrganizationPlan ); router.patch( - '/:organizationId/plan', + "/:organizationId/plan", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), - body('productId').exists().isString(), + param("organizationId").exists().trim(), + body("productId").exists().isString(), validateRequest, organizationsController.updateOrganizationPlan ); router.get( - '/:organizationId/billing-details/payment-methods', + "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationsController.getOrganizationPmtMethods ); router.post( - '/:organizationId/billing-details/payment-methods', + "/:organizationId/billing-details/payment-methods", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), - body('success_url').exists().isString(), - body('cancel_url').exists().isString(), + param("organizationId").exists().trim(), + body("success_url").exists().isString(), + body("cancel_url").exists().isString(), validateRequest, organizationsController.addOrganizationPmtMethod ); router.delete( - '/:organizationId/billing-details/payment-methods/:pmtMethodId', + "/:organizationId/billing-details/payment-methods/:pmtMethodId", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationsController.deleteOrganizationPmtMethod ); diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 3e956a388..7be6f311a 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -1,46 +1,46 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, requireSecretAuth, - validateRequest -} from '../../../middleware'; -import { query, param, body } from 'express-validator'; -import { secretController } from '../../controllers/v1'; + validateRequest, +} from "../../../middleware"; +import { body, param, query } from "express-validator"; +import { secretController } from "../../controllers/v1"; import { ADMIN, MEMBER, PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS -} from '../../../variables'; + PERMISSION_WRITE_SECRETS, +} from "../../../variables"; router.get( - '/:secretId/secret-versions', + "/:secretId/secret-versions", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS] + requiredPermissions: [PERMISSION_READ_SECRETS], }), - param('secretId').exists().trim(), - query('offset').exists().isInt(), - query('limit').exists().isInt(), + param("secretId").exists().trim(), + query("offset").exists().isInt(), + query("limit").exists().isInt(), validateRequest, secretController.getSecretVersions ); router.post( - '/:secretId/secret-versions/rollback', + "/:secretId/secret-versions/rollback", requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: ["jwt", "apiKey"], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS], }), - param('secretId').exists().trim(), - body('version').exists().isInt(), + param("secretId").exists().trim(), + body("version").exists().isInt(), secretController.rollbackSecretVersion ); diff --git a/backend/src/ee/routes/v1/secretSnapshot.ts b/backend/src/ee/routes/v1/secretSnapshot.ts index 80aa7d1ee..fe0c2690c 100644 --- a/backend/src/ee/routes/v1/secretSnapshot.ts +++ b/backend/src/ee/routes/v1/secretSnapshot.ts @@ -1,25 +1,25 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - requireSecretSnapshotAuth -} from '../../middleware'; + requireSecretSnapshotAuth, +} from "../../middleware"; import { requireAuth, - validateRequest -} from '../../../middleware'; -import { param } from 'express-validator'; -import { ADMIN, MEMBER } from '../../../variables'; -import { secretSnapshotController } from '../../controllers/v1'; + validateRequest, +} from "../../../middleware"; +import { param } from "express-validator"; +import { ADMIN, MEMBER } from "../../../variables"; +import { secretSnapshotController } from "../../controllers/v1"; router.get( - '/:secretSnapshotId', + "/:secretSnapshotId", requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ["jwt"], }), requireSecretSnapshotAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - param('secretSnapshotId').exists().trim(), + param("secretSnapshotId").exists().trim(), validateRequest, secretSnapshotController.getSecretSnapshot ); diff --git a/backend/src/ee/routes/v1/stripe.ts b/backend/src/ee/routes/v1/stripe.ts index 02d68c4ea..101f44f24 100644 --- a/backend/src/ee/routes/v1/stripe.ts +++ b/backend/src/ee/routes/v1/stripe.ts @@ -1,7 +1,7 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { stripeController } from '../../controllers/v1'; +import { stripeController } from "../../controllers/v1"; -router.post('/webhook', stripeController.handleWebhook); +router.post("/webhook", stripeController.handleWebhook); export default router; \ No newline at end of file diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 2b840892f..40392b45f 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -5,7 +5,7 @@ import { requireWorkspaceAuth, validateRequest, } from "../../../middleware"; -import { param, query, body } from "express-validator"; +import { body, param, query } from "express-validator"; import { ADMIN, MEMBER } from "../../../variables"; import { workspaceController } from "../../controllers/v1"; diff --git a/backend/src/ee/services/EELicenseService.ts b/backend/src/ee/services/EELicenseService.ts index 527a84730..6417eeb1c 100644 --- a/backend/src/ee/services/EELicenseService.ts +++ b/backend/src/ee/services/EELicenseService.ts @@ -1,34 +1,35 @@ -import * as Sentry from '@sentry/node'; -import NodeCache from 'node-cache'; +import * as Sentry from "@sentry/node"; +import NodeCache from "node-cache"; import { getLicenseKey, getLicenseServerKey, - getLicenseServerUrl -} from '../../config'; + getLicenseServerUrl, +} from "../../config"; import { licenseKeyRequest, licenseServerKeyRequest, + refreshLicenseKeyToken, refreshLicenseServerKeyToken, - refreshLicenseKeyToken -} from '../../config/request'; -import { Organization } from '../../models'; -import { OrganizationNotFoundError } from '../../utils/errors'; +} from "../../config/request"; +import { Organization } from "../../models"; +import { OrganizationNotFoundError } from "../../utils/errors"; interface FeatureSet { _id: string | null; - slug: 'starter' | 'team' | 'pro' | 'enterprise' | null; + slug: "starter" | "team" | "pro" | "enterprise" | null; tier: number; workspaceLimit: number | null; workspacesUsed: number; memberLimit: number | null; membersUsed: number; + environmentLimit: number | null; + environmentsUsed: number; secretVersioning: boolean; pitRecovery: boolean; rbac: boolean; customRateLimits: boolean; customAlerts: boolean; auditLogs: boolean; - envLimit?: number | null; } /** @@ -41,7 +42,7 @@ class EELicenseService { private readonly _isLicenseValid: boolean; // TODO: deprecate - public instanceType: 'self-hosted' | 'enterprise-self-hosted' | 'cloud' = 'self-hosted'; + public instanceType: "self-hosted" | "enterprise-self-hosted" | "cloud" = "self-hosted"; public globalFeatureSet: FeatureSet = { _id: null, @@ -51,13 +52,14 @@ class EELicenseService { workspacesUsed: 0, memberLimit: null, membersUsed: 0, + environmentLimit: null, + environmentsUsed: 0, secretVersioning: true, pitRecovery: true, rbac: true, customRateLimits: true, customAlerts: true, auditLogs: false, - envLimit: null } public localFeatureSet: NodeCache; @@ -65,14 +67,14 @@ class EELicenseService { constructor() { this._isLicenseValid = true; this.localFeatureSet = new NodeCache({ - stdTTL: 300 + stdTTL: 300, }); } - public async getOrganizationPlan(organizationId: string): Promise { + public async getPlan(organizationId: string, workspaceId?: string): Promise { try { - if (this.instanceType === 'cloud') { - const cachedPlan = this.localFeatureSet.get(organizationId); + if (this.instanceType === "cloud") { + const cachedPlan = this.localFeatureSet.get(`${organizationId}-${workspaceId ?? ""}`); if (cachedPlan) { return cachedPlan; } @@ -80,12 +82,16 @@ class EELicenseService { const organization = await Organization.findById(organizationId); if (!organization) throw OrganizationNotFoundError(); - const { data: { currentPlan } } = await licenseServerKeyRequest.get( - `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan` - ); + let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`; + + if (workspaceId) { + url += `?workspaceId=${workspaceId}`; + } + + const { data: { currentPlan } } = await licenseServerKeyRequest.get(url); // cache fetched plan for organization - this.localFeatureSet.set(organizationId, currentPlan); + this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ""}`, currentPlan); return currentPlan; } @@ -95,6 +101,13 @@ class EELicenseService { return this.globalFeatureSet; } + + public async refreshPlan(organizationId: string, workspaceId?: string) { + if (this.instanceType === "cloud") { + this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ""}`); + await this.getPlan(organizationId, workspaceId); + } + } public async initGlobalFeatureSet() { const licenseServerKey = await getLicenseServerKey(); @@ -106,7 +119,7 @@ class EELicenseService { const token = await refreshLicenseServerKeyToken() if (token) { - this.instanceType = 'cloud'; + this.instanceType = "cloud"; } return; @@ -122,7 +135,7 @@ class EELicenseService { ); this.globalFeatureSet = currentPlan; - this.instanceType = 'enterprise-self-hosted'; + this.instanceType = "enterprise-self-hosted"; } } } catch (err) { diff --git a/backend/src/ee/services/EELogService.ts b/backend/src/ee/services/EELogService.ts index 81d26765f..70f702953 100644 --- a/backend/src/ee/services/EELogService.ts +++ b/backend/src/ee/services/EELogService.ts @@ -1,14 +1,14 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IAction -} from '../models'; + IAction, +} from "../models"; import { - createLogHelper -} from '../helpers/log'; + createLogHelper, +} from "../helpers/log"; import { - createActionHelper -} from '../helpers/action'; -import EELicenseService from './EELicenseService'; + createActionHelper, +} from "../helpers/action"; +import EELicenseService from "./EELicenseService"; /** * Class to handle Enterprise Edition log actions @@ -31,7 +31,7 @@ class EELogService { workspaceId, actions, channel, - ipAddress + ipAddress, }: { userId?: Types.ObjectId; serviceAccountId?: Types.ObjectId; @@ -49,7 +49,7 @@ class EELogService { workspaceId, actions, channel, - ipAddress + ipAddress, }) } @@ -68,7 +68,7 @@ class EELogService { serviceAccountId, serviceTokenDataId, workspaceId, - secretIds + secretIds, }: { name: string; userId?: Types.ObjectId; @@ -83,7 +83,7 @@ class EELogService { serviceAccountId, serviceTokenDataId, workspaceId, - secretIds + secretIds, }); } } diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts index a0803dd36..5a1c4d3cb 100644 --- a/backend/src/ee/services/EESecretService.ts +++ b/backend/src/ee/services/EESecretService.ts @@ -1,11 +1,11 @@ -import { Types } from 'mongoose'; -import { ISecretVersion } from '../models'; +import { Types } from "mongoose"; +import { ISecretVersion } from "../models"; import { - takeSecretSnapshotHelper, addSecretVersionsHelper, markDeletedSecretVersionsHelper, -} from '../helpers/secret'; -import EELicenseService from './EELicenseService'; + takeSecretSnapshotHelper, +} from "../helpers/secret"; +import EELicenseService from "./EELicenseService"; /** * Class to handle Enterprise Edition secret actions diff --git a/backend/src/ee/services/index.ts b/backend/src/ee/services/index.ts index b3544bcff..afc3fb80e 100644 --- a/backend/src/ee/services/index.ts +++ b/backend/src/ee/services/index.ts @@ -5,5 +5,5 @@ import EELogService from "./EELogService"; export { EELicenseService, EESecretService, - EELogService + EELogService, } \ No newline at end of file diff --git a/backend/src/events/index.ts b/backend/src/events/index.ts index 461a3ece6..d8198d2fb 100644 --- a/backend/src/events/index.ts +++ b/backend/src/events/index.ts @@ -1,5 +1,5 @@ import { eventPushSecrets } from "./secret" export { - eventPushSecrets + eventPushSecrets, } \ No newline at end of file diff --git a/backend/src/events/secret.ts b/backend/src/events/secret.ts index 6007dd682..23ff9f59c 100644 --- a/backend/src/events/secret.ts +++ b/backend/src/events/secret.ts @@ -1,8 +1,8 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { + EVENT_PULL_SECRETS, EVENT_PUSH_SECRETS, - EVENT_PULL_SECRETS -} from '../variables'; +} from "../variables"; interface PushSecret { ciphertextKey: string; @@ -13,7 +13,7 @@ interface PushSecret { ivValue: string; tagValue: string; hashValue: string; - type: 'shared' | 'personal'; + type: "shared" | "personal"; } /** @@ -24,7 +24,7 @@ interface PushSecret { */ const eventPushSecrets = ({ workspaceId, - environment + environment, }: { workspaceId: Types.ObjectId; environment?: string; @@ -35,7 +35,7 @@ const eventPushSecrets = ({ environment, payload: { - } + }, }); } @@ -55,10 +55,10 @@ const eventPullSecrets = ({ workspaceId, payload: { - } + }, }); } export { - eventPushSecrets + eventPushSecrets, } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index ce61f3650..00ed43aba 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,36 +1,36 @@ -import { Types } from 'mongoose'; -import jwt from 'jsonwebtoken'; -import bcrypt from 'bcrypt'; +import { Types } from "mongoose"; +import jwt from "jsonwebtoken"; +import bcrypt from "bcrypt"; import { - IUser, - User, - ServiceTokenData, - ServiceAccount, APIKeyData, + ITokenVersion, + IUser, + ServiceAccount, + ServiceTokenData, TokenVersion, - ITokenVersion -} from '../models'; + User, +} from "../models"; import { - AccountNotFoundError, - ServiceTokenDataNotFoundError, - ServiceAccountNotFoundError, APIKeyDataNotFoundError, + AccountNotFoundError, + BadRequestError, + ServiceAccountNotFoundError, + ServiceTokenDataNotFoundError, UnauthorizedRequestError, - BadRequestError -} from '../utils/errors'; +} from "../utils/errors"; import { getJwtAuthLifetime, getJwtAuthSecret, getJwtProviderAuthSecret, getJwtRefreshLifetime, - getJwtRefreshSecret -} from '../config'; + getJwtRefreshSecret, +} from "../config"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; /** * @@ -39,41 +39,41 @@ import { */ export const validateAuthMode = ({ headers, - acceptedAuthModes + acceptedAuthModes, }: { headers: { [key: string]: string | string[] | undefined }, acceptedAuthModes: string[] }) => { - const apiKey = headers['x-api-key']; - const authHeader = headers['authorization']; + const apiKey = headers["x-api-key"]; + const authHeader = headers["authorization"]; let authMode, authTokenValue; if (apiKey === undefined && authHeader === undefined) { // case: no auth or X-API-KEY header present - throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' }); + throw BadRequestError({ message: "Missing Authorization or X-API-KEY in request header." }); } - if (typeof apiKey === 'string') { + if (typeof apiKey === "string") { // case: treat request authentication type as via X-API-KEY (i.e. API Key) authMode = AUTH_MODE_API_KEY; authTokenValue = apiKey; } - if (typeof authHeader === 'string') { + if (typeof authHeader === "string") { // case: treat request authentication type as via Authorization header (i.e. either JWT or service token) - const [tokenType, tokenValue] = <[string, string]>authHeader.split(' ', 2) ?? [null, null] + const [tokenType, tokenValue] = <[string, string]>authHeader.split(" ", 2) ?? [null, null] if (tokenType === null) - throw BadRequestError({ message: `Missing Authorization Header in the request header.` }); - if (tokenType.toLowerCase() !== 'bearer') + throw BadRequestError({ message: "Missing Authorization Header in the request header." }); + if (tokenType.toLowerCase() !== "bearer") throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` }); if (tokenValue === null) - throw BadRequestError({ message: 'Missing Authorization Body in the request header.' }); + throw BadRequestError({ message: "Missing Authorization Body in the request header." }); - switch (tokenValue.split('.', 1)[0]) { - case 'st': + switch (tokenValue.split(".", 1)[0]) { + case "st": authMode = AUTH_MODE_SERVICE_TOKEN; break; - case 'sa': + case "sa": authMode = AUTH_MODE_SERVICE_ACCOUNT; break; default: @@ -83,13 +83,13 @@ export const validateAuthMode = ({ authTokenValue = tokenValue; } - if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' }); + if (!authMode || !authTokenValue) throw BadRequestError({ message: "Missing valid Authorization or X-API-KEY in request header." }); - if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' }); + if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: "The provided authentication type is not supported." }); return ({ authMode, - authTokenValue + authTokenValue, }); } @@ -100,7 +100,7 @@ export const validateAuthMode = ({ * @returns {User} user - user corresponding to JWT token */ export const getAuthUserPayload = async ({ - authTokenValue + authTokenValue, }: { authTokenValue: string; }) => { @@ -109,31 +109,31 @@ export const getAuthUserPayload = async ({ ); const user = await User.findOne({ - _id: new Types.ObjectId(decodedToken.userId) - }).select('+publicKey +accessVersion'); + _id: new Types.ObjectId(decodedToken.userId), + }).select("+publicKey +accessVersion"); - if (!user) throw AccountNotFoundError({ message: 'Failed to find user' }); + if (!user) throw AccountNotFoundError({ message: "Failed to find user" }); - if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' }); + if (!user?.publicKey) throw UnauthorizedRequestError({ message: "Failed to authenticate user with partially set up account" }); const tokenVersion = await TokenVersion.findOneAndUpdate({ _id: new Types.ObjectId(decodedToken.tokenVersionId), - user: user._id + user: user._id, }, { - lastUsed: new Date() + lastUsed: new Date(), }); if (!tokenVersion) throw UnauthorizedRequestError({ - message: 'Failed to validate access token' + message: "Failed to validate access token", }); if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({ - message: 'Failed to validate access token' + message: "Failed to validate access token", }); return ({ user, - tokenVersionId: tokenVersion._id + tokenVersionId: tokenVersion._id, }); } @@ -144,41 +144,41 @@ export const getAuthUserPayload = async ({ * @returns {ServiceTokenData} serviceTokenData - service token data */ export const getAuthSTDPayload = async ({ - authTokenValue + authTokenValue, }: { authTokenValue: string; }) => { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); let serviceTokenData = await ServiceTokenData - .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt'); + .findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt"); if (!serviceTokenData) { - throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" }); } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { // case: service token expired await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); throw UnauthorizedRequestError({ - message: 'Failed to authenticate expired service token' + message: "Failed to authenticate expired service token", }); } const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); if (!isMatch) throw UnauthorizedRequestError({ - message: 'Failed to authenticate service token' + message: "Failed to authenticate service token", }); serviceTokenData = await ServiceTokenData .findOneAndUpdate({ - _id: new Types.ObjectId(TOKEN_IDENTIFIER) + _id: new Types.ObjectId(TOKEN_IDENTIFIER), }, { - lastUsed: new Date() + lastUsed: new Date(), }, { - new: true + new: true, }) - .select('+encryptedKey +iv +tag'); + .select("+encryptedKey +iv +tag"); - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: "Failed to find service token data" }); return serviceTokenData; } @@ -190,23 +190,23 @@ export const getAuthSTDPayload = async ({ * @returns {ServiceAccount} serviceAccount */ export const getAuthSAAKPayload = async ({ - authTokenValue + authTokenValue, }: { authTokenValue: string; }) => { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); const serviceAccount = await ServiceAccount.findById( - Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex') - ).select('+secretHash'); + Buffer.from(TOKEN_IDENTIFIER, "base64").toString("hex") + ).select("+secretHash"); if (!serviceAccount) { - throw ServiceAccountNotFoundError({ message: 'Failed to find service account' }); + throw ServiceAccountNotFoundError({ message: "Failed to find service account" }); } const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash); if (!result) throw UnauthorizedRequestError({ - message: 'Failed to authenticate service account access key' + message: "Failed to authenticate service account access key", }); return serviceAccount; @@ -219,48 +219,48 @@ export const getAuthSAAKPayload = async ({ * @returns {APIKeyData} apiKeyData - API key data */ export const getAuthAPIKeyPayload = async ({ - authTokenValue + authTokenValue, }: { authTokenValue: string; }) => { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split(".", 3); let apiKeyData = await APIKeyData - .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') - .populate<{ user: IUser }>('user', '+publicKey'); + .findById(TOKEN_IDENTIFIER, "+secretHash +expiresAt") + .populate<{ user: IUser }>("user", "+publicKey"); if (!apiKeyData) { - throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); + throw APIKeyDataNotFoundError({ message: "Failed to find API key data" }); } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { // case: API key expired await APIKeyData.findByIdAndDelete(apiKeyData._id); throw UnauthorizedRequestError({ - message: 'Failed to authenticate expired API key' + message: "Failed to authenticate expired API key", }); } const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash); if (!isMatch) throw UnauthorizedRequestError({ - message: 'Failed to authenticate API key' + message: "Failed to authenticate API key", }); apiKeyData = await APIKeyData.findOneAndUpdate({ - _id: new Types.ObjectId(TOKEN_IDENTIFIER) + _id: new Types.ObjectId(TOKEN_IDENTIFIER), }, { - lastUsed: new Date() + lastUsed: new Date(), }, { - new: true + new: true, }); if (!apiKeyData) { - throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); + throw APIKeyDataNotFoundError({ message: "Failed to find API key data" }); } - const user = await User.findById(apiKeyData.user).select('+publicKey'); + const user = await User.findById(apiKeyData.user).select("+publicKey"); if (!user) { throw AccountNotFoundError({ - message: 'Failed to find user' + message: "Failed to find user", }); } @@ -278,7 +278,7 @@ export const getAuthAPIKeyPayload = async ({ export const issueAuthTokens = async ({ userId, ip, - userAgent + userAgent, }: { userId: Types.ObjectId; ip: string; @@ -290,7 +290,7 @@ export const issueAuthTokens = async ({ tokenVersion = await TokenVersion.findOne({ user: userId, ip, - userAgent + userAgent, }); if (!tokenVersion) { @@ -302,7 +302,7 @@ export const issueAuthTokens = async ({ accessVersion: 0, ip, userAgent, - lastUsed: new Date() + lastUsed: new Date(), }).save(); } @@ -311,25 +311,25 @@ export const issueAuthTokens = async ({ payload: { userId, tokenVersionId: tokenVersion._id.toString(), - accessVersion: tokenVersion.accessVersion + accessVersion: tokenVersion.accessVersion, }, expiresIn: await getJwtAuthLifetime(), - secret: await getJwtAuthSecret() + secret: await getJwtAuthSecret(), }); const refreshToken = createToken({ payload: { userId, tokenVersionId: tokenVersion._id.toString(), - refreshVersion: tokenVersion.refreshVersion + refreshVersion: tokenVersion.refreshVersion, }, expiresIn: await getJwtRefreshLifetime(), - secret: await getJwtRefreshSecret() + secret: await getJwtRefreshSecret(), }); return { token, - refreshToken + refreshToken, }; }; @@ -342,12 +342,12 @@ export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise // increment refreshVersion on user by 1 await TokenVersion.findOneAndUpdate({ - _id: tokenVersionId + _id: tokenVersionId, }, { $inc: { refreshVersion: 1, - accessVersion: 1 - } + accessVersion: 1, + }, }); }; @@ -362,14 +362,14 @@ export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise export const createToken = ({ payload, expiresIn, - secret + secret, }: { payload: any; expiresIn: string | number; secret: string; }) => { return jwt.sign(payload, secret, { - expiresIn + expiresIn, }); }; @@ -383,7 +383,7 @@ export const validateProviderAuthToken = async ({ providerAuthToken?: string; }) => { if (!providerAuthToken) { - throw new Error('Invalid authentication request.'); + throw new Error("Invalid authentication request."); } const decodedToken = ( @@ -394,6 +394,6 @@ export const validateProviderAuthToken = async ({ decodedToken.authProvider !== user.authProvider || decodedToken.email !== email ) { - throw new Error('Invalid authentication credentials.') + throw new Error("Invalid authentication credentials.") } } \ No newline at end of file diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index e93ab6a8b..c7c5904c7 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -1,29 +1,21 @@ import { Types } from "mongoose"; +import { Bot, BotKey, ISecret, IUser, Secret } from "../models"; import { - Bot, - BotKey, - Secret, - ISecret, - IUser -} from "../models"; -import { - generateKeyPair, - encryptSymmetric128BitHexKeyUTF8, + decryptAsymmetric, decryptSymmetric128BitHexKeyUTF8, - decryptAsymmetric -} from '../utils/crypto'; + encryptSymmetric128BitHexKeyUTF8, + generateKeyPair, +} from "../utils/crypto"; import { - SECRET_SHARED, ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + SECRET_SHARED, } from "../variables"; -import { - getEncryptionKey, - getRootEncryptionKey, - client -} from "../config"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; import { InternalServerError } from "../utils/errors"; +import Folder from "../models/folder"; +import { getFolderByPath } from "../services/FolderService"; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -40,15 +32,14 @@ export const createBot = async ({ }) => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); - + const { publicKey, privateKey } = generateKeyPair(); - + if (rootEncryptionKey) { - const { - ciphertext, - iv, - tag - } = client.encryptSymmetric(privateKey, rootEncryptionKey); + const { ciphertext, iv, tag } = client.encryptSymmetric( + privateKey, + rootEncryptionKey + ); return await new Bot({ name, @@ -59,9 +50,8 @@ export const createBot = async ({ iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 + keyEncoding: ENCODING_SCHEME_BASE64, }).save(); - } else if (encryptionKey) { const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: privateKey, @@ -77,15 +67,27 @@ export const createBot = async ({ iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, }).save(); } throw InternalServerError({ - message: 'Failed to create new bot due to missing encryption key' + message: "Failed to create new bot due to missing encryption key", }); }; +/** + * Return whether or not workspace with id [workspaceId] is end-to-end encrypted + * @param {Types.ObjectId} workspaceId - id of workspace to check + */ +export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { + const botKey = await BotKey.exists({ + workspace: workspaceId, + }); + + return botKey ? false : true; +}; + /** * Return decrypted secrets for workspace with id [workspaceId] * and [environment] using bot @@ -96,16 +98,38 @@ export const createBot = async ({ export const getSecretsBotHelper = async ({ workspaceId, environment, + secretPath, }: { workspaceId: Types.ObjectId; environment: string; + secretPath: string; }) => { const content = {} as any; - const key = await getKey({ workspaceId: workspaceId.toString() }); + const key = await getKey({ workspaceId: workspaceId }); + + let folderId = "root"; + const folders = await Folder.findOne({ + workspace: workspaceId, + environment, + }); + + if (!folders && secretPath !== "/") { + throw InternalServerError({ message: "Folder not found" }); + } + + if (folders) { + const folder = getFolderByPath(folders.nodes, secretPath); + if (!folder) { + throw InternalServerError({ message: "Folder not found" }); + } + folderId = folder.id; + } + const secrets = await Secret.find({ workspace: workspaceId, environment, type: SECRET_SHARED, + folder: folderId, }); secrets.forEach((secret: ISecret) => { @@ -136,14 +160,17 @@ export const getSecretsBotHelper = async ({ * @param {String} obj.workspaceId - id of workspace * @returns {String} key - decrypted workspace key */ -export const getKey = async ({ workspaceId }: { workspaceId: string }) => { +export const getKey = async ({ + workspaceId, +}: { + workspaceId: Types.ObjectId; +}) => { const encryptionKey = await getEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey(); const botKey = await BotKey.findOne({ workspace: workspaceId, - }) - .populate<{ sender: IUser }>("sender", "publicKey"); + }).populate<{ sender: IUser }>("sender", "publicKey"); if (!botKey) throw new Error("Failed to find bot key"); @@ -156,7 +183,12 @@ export const getKey = async ({ workspaceId }: { workspaceId: string }) => { if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) { // case: encoding scheme is base64 - const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag); + const privateKeyBot = client.decryptSymmetric( + bot.encryptedPrivateKey, + rootEncryptionKey, + bot.iv, + bot.tag + ); return decryptAsymmetric({ ciphertext: botKey.encryptedKey, @@ -165,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: string }) => { privateKey: privateKeyBot, }); } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { - // case: encoding scheme is utf8 const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, - key: encryptionKey + key: encryptionKey, }); - + return decryptAsymmetric({ ciphertext: botKey.encryptedKey, nonce: botKey.nonce, @@ -183,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: string }) => { } throw InternalServerError({ - message: "Failed to obtain bot's copy of workspace key needed for bot operations" + message: + "Failed to obtain bot's copy of workspace key needed for bot operations", }); }; @@ -201,7 +233,7 @@ export const encryptSymmetricHelper = async ({ workspaceId: Types.ObjectId; plaintext: string; }) => { - const key = await getKey({ workspaceId: workspaceId.toString() }); + const key = await getKey({ workspaceId: workspaceId }); const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext, key, @@ -233,7 +265,7 @@ export const decryptSymmetricHelper = async ({ iv: string; tag: string; }) => { - const key = await getKey({ workspaceId: workspaceId.toString() }); + const key = await getKey({ workspaceId: workspaceId }); const plaintext = decryptSymmetric128BitHexKeyUTF8({ ciphertext, iv, @@ -242,4 +274,4 @@ export const decryptSymmetricHelper = async ({ }); return plaintext; -}; \ No newline at end of file +}; diff --git a/backend/src/helpers/database.ts b/backend/src/helpers/database.ts index b9284bbf8..4780be346 100644 --- a/backend/src/helpers/database.ts +++ b/backend/src/helpers/database.ts @@ -1,5 +1,5 @@ -import mongoose from 'mongoose'; -import { getLogger } from '../utils/logger'; +import mongoose from "mongoose"; +import { getLogger } from "../utils/logger"; /** * Initialize database connection @@ -8,7 +8,7 @@ import { getLogger } from '../utils/logger'; * @returns */ export const initDatabaseHelper = async ({ - mongoURL + mongoURL, }: { mongoURL: string; }) => { @@ -16,7 +16,7 @@ export const initDatabaseHelper = async ({ await mongoose.connect(mongoURL); // allow empty strings to pass the required validator - mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string'); + mongoose.Schema.Types.String.checkRequired(v => typeof v === "string"); (await getLogger("database")).info("Database connection established"); @@ -35,10 +35,10 @@ export const closeDatabaseHelper = async () => { new Promise((resolve) => { if (mongoose.connection && mongoose.connection.readyState == 1) { mongoose.connection.close() - .then(() => resolve('Database connection closed')); + .then(() => resolve("Database connection closed")); } else { - resolve('Database connection already closed'); + resolve("Database connection already closed"); } - }) + }), ]); } \ No newline at end of file diff --git a/backend/src/helpers/event.ts b/backend/src/helpers/event.ts index 47c243749..124da257c 100644 --- a/backend/src/helpers/event.ts +++ b/backend/src/helpers/event.ts @@ -1,5 +1,5 @@ import { Types } from "mongoose"; -import { Bot, IBot } from "../models"; +import { Bot } from "../models"; import { EVENT_PUSH_SECRETS } from "../variables"; import { IntegrationService } from "../services"; diff --git a/backend/src/helpers/index.ts b/backend/src/helpers/index.ts index b9d85f871..f9a0009fc 100644 --- a/backend/src/helpers/index.ts +++ b/backend/src/helpers/index.ts @@ -1,17 +1,17 @@ -export * from './auth'; -export * from './bot'; -export * from './database'; -export * from './event'; -export * from './integration'; -export * from './key'; -export * from './membership'; -export * from './membershipOrg'; -export * from './nodemailer'; -export * from './organization'; -export * from './rateLimiter'; -export * from './secret'; -export * from './secrets'; -export * from './signup'; -export * from './token'; -export * from './user'; -export * from './workspace'; \ No newline at end of file +export * from "./auth"; +export * from "./bot"; +export * from "./database"; +export * from "./event"; +export * from "./integration"; +export * from "./key"; +export * from "./membership"; +export * from "./membershipOrg"; +export * from "./nodemailer"; +export * from "./organization"; +export * from "./rateLimiter"; +export * from "./secret"; +export * from "./secrets"; +export * from "./signup"; +export * from "./token"; +export * from "./user"; +export * from "./workspace"; \ No newline at end of file diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index b6f4b0915..f2dd1ba66 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -1,26 +1,20 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; +import { Bot, Integration, IntegrationAuth } from "../models"; +import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations"; +import { BotService } from "../services"; import { - Bot, - Integration, - IntegrationAuth -} from '../models'; -import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; -import { BotService } from '../services'; -import { - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8 -} from '../variables'; -import { - UnauthorizedRequestError, -} from '../utils/errors'; + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + INTEGRATION_NETLIFY, + INTEGRATION_VERCEL, +} from "../variables"; +import { UnauthorizedRequestError } from "../utils/errors"; interface Update { - workspace: string; - integration: string; - teamId?: string; - accountId?: string; + workspace: string; + integration: string; + teamId?: string; + accountId?: string; } /** @@ -31,78 +25,83 @@ interface Update { * - Create bot sequence for integration * @param {Object} obj * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.integration - name of integration + * @param {String} obj.integration - name of integration * @param {String} obj.code - code * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange -*/ + */ export const handleOAuthExchangeHelper = async ({ - workspaceId, + workspaceId, + integration, + code, + environment, +}: { + workspaceId: string; + integration: string; + code: string; + environment: string; +}) => { + const bot = await Bot.findOne({ + workspace: workspaceId, + isActive: true, + }); + + if (!bot) + throw new Error("Bot must be enabled for OAuth2 code-token exchange"); + + // exchange code for access and refresh tokens + const res = await exchangeCode({ integration, code, - environment -}: { - workspaceId: string; - integration: string; - code: string; - environment: string; -}) => { - const bot = await Bot.findOne({ - workspace: workspaceId, - isActive: true + }); + + const update: Update = { + workspace: workspaceId, + integration, + }; + + switch (integration) { + case INTEGRATION_VERCEL: + update.teamId = res.teamId; + break; + case INTEGRATION_NETLIFY: + update.accountId = res.accountId; + break; + } + + const integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + workspace: workspaceId, + integration, + }, + update, + { + new: true, + upsert: true, + } + ); + + if (res.refreshToken) { + // case: refresh token returned from exchange + // set integration auth refresh token + await setIntegrationAuthRefreshHelper({ + integrationAuthId: integrationAuth._id.toString(), + refreshToken: res.refreshToken, }); - - if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); - - // exchange code for access and refresh tokens - const res = await exchangeCode({ - integration, - code + } + + if (res.accessToken) { + // case: access token returned from exchange + // set integration auth access token + await setIntegrationAuthAccessHelper({ + integrationAuthId: integrationAuth._id.toString(), + accessId: null, + accessToken: res.accessToken, + accessExpiresAt: res.accessExpiresAt, }); - - const update: Update = { - workspace: workspaceId, - integration - } - - switch (integration) { - case INTEGRATION_VERCEL: - update.teamId = res.teamId; - break; - case INTEGRATION_NETLIFY: - update.accountId = res.accountId; - break; - } - - const integrationAuth = await IntegrationAuth.findOneAndUpdate({ - workspace: workspaceId, - integration - }, update, { - new: true, - upsert: true - }); - - if (res.refreshToken) { - // case: refresh token returned from exchange - // set integration auth refresh token - await setIntegrationAuthRefreshHelper({ - integrationAuthId: integrationAuth._id.toString(), - refreshToken: res.refreshToken - }); - } - - if (res.accessToken) { - // case: access token returned from exchange - // set integration auth access token - await setIntegrationAuthAccessHelper({ - integrationAuthId: integrationAuth._id.toString(), - accessId: null, - accessToken: res.accessToken, - accessExpiresAt: res.accessExpiresAt - }); - } - - return integrationAuth; -} + } + + return integrationAuth; +}; /** * Sync/push environment variables in workspace with id [workspaceId] to * all active integrations for that workspace @@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({ * @param {Object} obj.workspaceId - id of workspace */ export const syncIntegrationsHelper = async ({ - workspaceId, - environment + workspaceId, + environment, }: { - workspaceId: Types.ObjectId; - environment?: string; + workspaceId: Types.ObjectId; + environment?: string; }) => { - const integrations = await Integration.find({ - workspace: workspaceId, - ...(environment ? { - environment - } : {}), - isActive: true, - app: { $ne: null } + const integrations = await Integration.find({ + workspace: workspaceId, + ...(environment + ? { + environment, + } + : {}), + isActive: true, + app: { $ne: null }, + }); + + // for each workspace integration, sync/push secrets + // to that integration + for await (const integration of integrations) { + // get workspace, environment (shared) secrets + const secrets = await BotService.getSecrets({ + // issue here? + workspaceId: integration.workspace, + environment: integration.environment, + secretPath: integration.secretPath, }); - // for each workspace integration, sync/push secrets - // to that integration - for await (const integration of integrations) { - // get workspace, environment (shared) secrets - const secrets = await BotService.getSecrets({ // issue here? - workspaceId: integration.workspace, - environment: integration.environment - }); + const integrationAuth = await IntegrationAuth.findById( + integration.integrationAuth + ); + if (!integrationAuth) throw new Error("Failed to find integration auth"); - const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - // get integration auth access token - const access = await getIntegrationAuthAccessHelper({ - integrationAuthId: integration.integrationAuth - }); + // get integration auth access token + const access = await getIntegrationAuthAccessHelper({ + integrationAuthId: integration.integrationAuth, + }); - // sync secrets to integration - await syncSecrets({ - integration, - integrationAuth, - secrets, - accessId: access.accessId === undefined ? null : access.accessId, - accessToken: access.accessToken - }); - } -} + // sync secrets to integration + await syncSecrets({ + integration, + integrationAuth, + secrets, + accessId: access.accessId === undefined ? null : access.accessId, + accessToken: access.accessToken, + }); + } +}; /** * Return decrypted refresh token using the bot's copy @@ -161,22 +166,29 @@ export const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ -export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('+refreshCiphertext +refreshIV +refreshTag'); +export const getIntegrationAuthRefreshHelper = async ({ + integrationAuthId, +}: { + integrationAuthId: Types.ObjectId; +}) => { + const integrationAuth = await IntegrationAuth.findById( + integrationAuthId + ).select("+refreshCiphertext +refreshIV +refreshTag"); - if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); - - const refreshToken = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.refreshCiphertext as string, - iv: integrationAuth.refreshIV as string, - tag: integrationAuth.refreshTag as string + if (!integrationAuth) + throw UnauthorizedRequestError({ + message: "Failed to locate Integration Authentication credentials", }); - - return refreshToken; -} + + const refreshToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.refreshCiphertext as string, + iv: integrationAuth.refreshIV as string, + tag: integrationAuth.refreshTag as string, + }); + + return refreshToken; +}; /** * Return decrypted access token using the bot's copy @@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i * @param {String} obj.integrationAuthId - id of integration auth * @returns {String} accessToken - decrypted access token */ -export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { - let accessId; - let accessToken; - const integrationAuth = await IntegrationAuth - .findById(integrationAuthId) - .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); +export const getIntegrationAuthAccessHelper = async ({ + integrationAuthId, +}: { + integrationAuthId: Types.ObjectId; +}) => { + let accessId; + let accessToken; + const integrationAuth = await IntegrationAuth.findById( + integrationAuthId + ).select( + "workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag" + ); - if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); - - accessToken = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessCiphertext as string, - iv: integrationAuth.accessIV as string, - tag: integrationAuth.accessTag as string + if (!integrationAuth) + throw UnauthorizedRequestError({ + message: "Failed to locate Integration Authentication credentials", }); - if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { - // there is a access token expiration date - // and refresh token to exchange with the OAuth2 server - - if (integrationAuth.accessExpiresAt < new Date()) { - // access token is expired - const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); - accessToken = await exchangeRefresh({ - integrationAuth, - refreshToken - }); - } + accessToken = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessCiphertext as string, + iv: integrationAuth.accessIV as string, + tag: integrationAuth.accessTag as string, + }); + + if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { + // there is a access token expiration date + // and refresh token to exchange with the OAuth2 server + + if (integrationAuth.accessExpiresAt < new Date()) { + // access token is expired + const refreshToken = await getIntegrationAuthRefreshHelper({ + integrationAuthId, + }); + accessToken = await exchangeRefresh({ + integrationAuth, + refreshToken, + }); } - - if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) { - accessId = await BotService.decryptSymmetric({ - workspaceId: integrationAuth.workspace, - ciphertext: integrationAuth.accessIdCiphertext as string, - iv: integrationAuth.accessIdIV as string, - tag: integrationAuth.accessIdTag as string - }); - } - - return ({ - accessId, - accessToken + } + + if ( + integrationAuth?.accessIdCiphertext && + integrationAuth?.accessIdIV && + integrationAuth?.accessIdTag + ) { + accessId = await BotService.decryptSymmetric({ + workspaceId: integrationAuth.workspace, + ciphertext: integrationAuth.accessIdCiphertext as string, + iv: integrationAuth.accessIdIV as string, + tag: integrationAuth.accessIdTag as string, }); -} + } + + return { + accessId, + accessToken, + }; +}; /** * Encrypt refresh token [refreshToken] using the bot's copy @@ -240,41 +267,43 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in * @param {String} obj.refreshToken - refresh token */ export const setIntegrationAuthRefreshHelper = async ({ - integrationAuthId, - refreshToken + integrationAuthId, + refreshToken, }: { - integrationAuthId: string; - refreshToken: string; + integrationAuthId: string; + refreshToken: string; }) => { - - let integrationAuth = await IntegrationAuth - .findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const obj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: refreshToken - }); - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - refreshCiphertext: obj.ciphertext, - refreshIV: obj.iv, - refreshTag: obj.tag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - - return integrationAuth; -} + let integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error("Failed to find integration auth"); + + const obj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: refreshToken, + }); + + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + _id: integrationAuthId, + }, + { + refreshCiphertext: obj.ciphertext, + refreshIV: obj.iv, + refreshTag: obj.tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + { + new: true, + } + ); + + return integrationAuth; +}; /** * Encrypt access token [accessToken] and (optionally) access id [accessId] - * using the bot's copy of the workspace key for workspace belonging to + * using the bot's copy of the workspace key for workspace belonging to * integration auth with id [integrationAuthId] and store it along with [accessExpiresAt] * @param {Object} obj * @param {String} obj.integrationAuthId - id of integration auth @@ -282,48 +311,52 @@ export const setIntegrationAuthRefreshHelper = async ({ * @param {Date} obj.accessExpiresAt - expiration date of access token */ export const setIntegrationAuthAccessHelper = async ({ - integrationAuthId, - accessId, - accessToken, - accessExpiresAt + integrationAuthId, + accessId, + accessToken, + accessExpiresAt, }: { - integrationAuthId: string; - accessId: string | null; - accessToken: string; - accessExpiresAt: Date | undefined; + integrationAuthId: string; + accessId: string | null; + accessToken: string; + accessExpiresAt: Date | undefined; }) => { - let integrationAuth = await IntegrationAuth.findById(integrationAuthId); - - if (!integrationAuth) throw new Error('Failed to find integration auth'); - - const encryptedAccessTokenObj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: accessToken + let integrationAuth = await IntegrationAuth.findById(integrationAuthId); + + if (!integrationAuth) throw new Error("Failed to find integration auth"); + + const encryptedAccessTokenObj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: accessToken, + }); + + let encryptedAccessIdObj; + if (accessId) { + encryptedAccessIdObj = await BotService.encryptSymmetric({ + workspaceId: integrationAuth.workspace, + plaintext: accessId, }); - - let encryptedAccessIdObj; - if (accessId) { - encryptedAccessIdObj = await BotService.encryptSymmetric({ - workspaceId: integrationAuth.workspace, - plaintext: accessId - }); + } + + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { + _id: integrationAuthId, + }, + { + accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, + accessIdIV: encryptedAccessIdObj?.iv ?? undefined, + accessIdTag: encryptedAccessIdObj?.tag ?? undefined, + accessCiphertext: encryptedAccessTokenObj.ciphertext, + accessIV: encryptedAccessTokenObj.iv, + accessTag: encryptedAccessTokenObj.tag, + accessExpiresAt, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + { + new: true, } - - integrationAuth = await IntegrationAuth.findOneAndUpdate({ - _id: integrationAuthId - }, { - accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, - accessIdIV: encryptedAccessIdObj?.iv ?? undefined, - accessIdTag: encryptedAccessIdObj?.tag ?? undefined, - accessCiphertext: encryptedAccessTokenObj.ciphertext, - accessIV: encryptedAccessTokenObj.iv, - accessTag: encryptedAccessTokenObj.tag, - accessExpiresAt, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }, { - new: true - }); - - return integrationAuth; -} \ No newline at end of file + ); + + return integrationAuth; +}; diff --git a/backend/src/helpers/key.ts b/backend/src/helpers/key.ts index afb4f6396..88bf28f47 100644 --- a/backend/src/helpers/key.ts +++ b/backend/src/helpers/key.ts @@ -1,4 +1,4 @@ -import { Key, IKey } from '../models'; +import { IKey, Key } from "../models"; interface Key { encryptedKey: string; @@ -20,7 +20,7 @@ interface Key { export const pushKeys = async ({ userId, workspaceId, - keys + keys, }: { userId: string; workspaceId: string; @@ -31,9 +31,9 @@ export const pushKeys = async ({ ( await Key.find( { - workspace: workspaceId + workspace: workspaceId, }, - 'receiver' + "receiver" ) ).map((k: IKey) => k.receiver.toString()) ); @@ -47,7 +47,7 @@ export const pushKeys = async ({ nonce: k.nonce, sender: userId, receiver: k.userId, - workspace: workspaceId + workspace: workspaceId, })) ); }; \ No newline at end of file diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 976297512..d2fcf5b17 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -1,6 +1,6 @@ import { Types } from "mongoose"; -import { Membership, Key } from "../models"; -import { MembershipNotFoundError, BadRequestError } from "../utils/errors"; +import { Key, Membership } from "../models"; +import { BadRequestError, MembershipNotFoundError } from "../utils/errors"; /** * Validate that user with id [userId] is a member of workspace with id [workspaceId] @@ -62,7 +62,7 @@ export const findMembership = async (queryObj: any) => { export const addMemberships = async ({ userIds, workspaceId, - roles + roles, }: { userIds: string[]; workspaceId: string; @@ -95,7 +95,7 @@ export const addMemberships = async ({ */ export const deleteMembership = async ({ membershipId }: { membershipId: string }) => { const deletedMembership = await Membership.findOneAndDelete({ - _id: membershipId + _id: membershipId, }); // delete keys associated with the membership diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index f29f3cec0..3ed5be088 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -1,14 +1,14 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - MembershipOrg, - Workspace, + Key, Membership, - Key -} from '../models'; + MembershipOrg, + Workspace, +} from "../models"; import { MembershipOrgNotFoundError, - UnauthorizedRequestError -} from '../utils/errors'; + UnauthorizedRequestError, +} from "../utils/errors"; /** * Validate that user with id [userId] is a member of organization with id [organizationId] @@ -22,31 +22,31 @@ export const validateMembershipOrg = async ({ userId, organizationId, acceptedRoles, - acceptedStatuses + acceptedStatuses, }: { userId: Types.ObjectId; organizationId: Types.ObjectId; - acceptedRoles?: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses?: Array<'invited' | 'accepted'>; + acceptedRoles?: Array<"owner" | "admin" | "member">; + acceptedStatuses?: Array<"invited" | "accepted">; }) => { const membershipOrg = await MembershipOrg.findOne({ user: userId, - organization: organizationId + organization: organizationId, }); if (!membershipOrg) { - throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' }); + throw MembershipOrgNotFoundError({ message: "Failed to find organization membership" }); } if (acceptedRoles) { if (!acceptedRoles.includes(membershipOrg.role)) { - throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' }); + throw UnauthorizedRequestError({ message: "Failed to validate organization membership role" }); } } if (acceptedStatuses) { if (!acceptedStatuses.includes(membershipOrg.status)) { - throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' }); + throw UnauthorizedRequestError({ message: "Failed to validate organization membership status" }); } } @@ -76,7 +76,7 @@ export const addMembershipsOrg = async ({ userIds, organizationId, roles, - statuses + statuses, }: { userIds: string[]; organizationId: string; @@ -90,16 +90,16 @@ export const addMembershipsOrg = async ({ user: userId, organization: organizationId, role: roles[idx], - status: statuses[idx] + status: statuses[idx], }, update: { user: userId, organization: organizationId, role: roles[idx], - status: statuses[idx] + status: statuses[idx], }, - upsert: true - } + upsert: true, + }, }; }); @@ -112,15 +112,15 @@ export const addMembershipsOrg = async ({ * @param {String} obj.membershipOrgId - id of organization membership to delete */ export const deleteMembershipOrg = async ({ - membershipOrgId + membershipOrgId, }: { membershipOrgId: string; }) => { const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ - _id: membershipOrgId + _id: membershipOrgId, }); - if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership'); + if (!deletedMembershipOrg) throw new Error("Failed to delete organization membership"); // delete keys associated with organization membership if (deletedMembershipOrg?.user) { @@ -128,22 +128,22 @@ export const deleteMembershipOrg = async ({ const workspaces = ( await Workspace.find({ - organization: deletedMembershipOrg.organization + organization: deletedMembershipOrg.organization, }) ).map((w) => w._id.toString()); await Membership.deleteMany({ user: deletedMembershipOrg.user, workspace: { - $in: workspaces - } + $in: workspaces, + }, }); await Key.deleteMany({ receiver: deletedMembershipOrg.user, workspace: { - $in: workspaces - } + $in: workspaces, + }, }); } diff --git a/backend/src/helpers/nodemailer.ts b/backend/src/helpers/nodemailer.ts index ffd39d7dd..b83d9bf61 100644 --- a/backend/src/helpers/nodemailer.ts +++ b/backend/src/helpers/nodemailer.ts @@ -1,8 +1,8 @@ -import fs from 'fs'; -import path from 'path'; -import handlebars from 'handlebars'; -import nodemailer from 'nodemailer'; -import { getSmtpFromName, getSmtpFromAddress, getSmtpConfigured } from '../config'; +import fs from "fs"; +import path from "path"; +import handlebars from "handlebars"; +import nodemailer from "nodemailer"; +import { getSmtpConfigured, getSmtpFromAddress, getSmtpFromName } from "../config"; let smtpTransporter: nodemailer.Transporter; @@ -17,7 +17,7 @@ export const sendMail = async ({ template, subjectLine, recipients, - substitutions + substitutions, }: { template: string; subjectLine: string; @@ -26,17 +26,17 @@ export const sendMail = async ({ }) => { if (await getSmtpConfigured()) { const html = fs.readFileSync( - path.resolve(__dirname, '../templates/' + template), - 'utf8' + path.resolve(__dirname, "../templates/" + template), + "utf8" ); const temp = handlebars.compile(html); const htmlToSend = temp(substitutions); await smtpTransporter.sendMail({ from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`, - to: recipients.join(', '), + to: recipients.join(", "), subject: subjectLine, - html: htmlToSend + html: htmlToSend, }); } }; diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index 71e8c15ce..642a1233b 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -1,25 +1,25 @@ import Stripe from "stripe"; import { Types } from "mongoose"; -import { Organization, MembershipOrg } from "../models"; +import { MembershipOrg, Organization } from "../models"; import { - ACCEPTED + ACCEPTED, } from "../variables"; import { - getStripeSecretKey, getStripeProductPro, - getStripeProductTeam, getStripeProductStarter, + getStripeProductTeam, + getStripeSecretKey, } from "../config"; import { - EELicenseService -} from '../ee/services'; + EELicenseService, +} from "../ee/services"; import { - getLicenseServerUrl -} from '../config'; + getLicenseServerUrl, +} from "../config"; import { + licenseKeyRequest, licenseServerKeyRequest, - licenseKeyRequest -} from '../config/request'; +} from "../config/request"; /** * Create an organization with name [name] @@ -137,7 +137,7 @@ export const updateSubscriptionOrgQuantity = async ({ }); if (organization && organization.customerId) { - if (EELicenseService.instanceType === 'cloud') { + if (EELicenseService.instanceType === "cloud") { // instance of Infisical is a cloud instance const quantity = await MembershipOrg.countDocuments({ organization: new Types.ObjectId(organizationId), @@ -147,28 +147,30 @@ export const updateSubscriptionOrgQuantity = async ({ await licenseServerKeyRequest.patch( `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`, { - quantity + quantity, } ); EELicenseService.localFeatureSet.del(organizationId); } - - if (EELicenseService.instanceType === 'enterprise-self-hosted') { - // instance of Infisical is an enterprise self-hosted instance - - const usedSeats = await MembershipOrg.countDocuments({ - status: ACCEPTED - }); - - await licenseKeyRequest.patch( - `${await getLicenseServerUrl()}/api/license/v1/license`, - { - usedSeats - } - ); - } } + if (EELicenseService.instanceType === "enterprise-self-hosted") { + // instance of Infisical is an enterprise self-hosted instance + + const usedSeats = await MembershipOrg.countDocuments({ + status: ACCEPTED, + }); + + await licenseKeyRequest.patch( + `${await getLicenseServerUrl()}/api/license/v1/license`, + { + usedSeats, + } + ); + } + + await EELicenseService.refreshPlan(organizationId); + return stripeSubscription; }; \ No newline at end of file diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index 9e9d022eb..853e7c2ed 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -1,62 +1,62 @@ -import rateLimit from 'express-rate-limit'; -const MongoStore = require('rate-limit-mongo'); +import rateLimit from "express-rate-limit"; +// const MongoStore = require('rate-limit-mongo'); // 200 per minute export const apiLimiter = rateLimit({ - store: new MongoStore({ - uri: process.env.MONGO_URL, - expireTimeMs: 1000 * 60, - collectionName: "expressRateRecords-apiLimiter", - errorHandler: console.error.bind(null, 'rate-limit-mongo') - }), - windowMs: 1000 * 60, - max: 200, + // store: new MongoStore({ + // uri: process.env.MONGO_URL, + // expireTimeMs: 1000 * 60, + // collectionName: "expressRateRecords-apiLimiter", + // errorHandler: console.error.bind(null, 'rate-limit-mongo') + // }), + windowMs: 60 * 1000, + max: 240, standardHeaders: true, legacyHeaders: false, skip: (request) => { - return request.path === '/healthcheck' || request.path === '/api/status' + return request.path === "/healthcheck" || request.path === "/api/status" }, keyGenerator: (req, res) => { return req.realIP - } + }, }); // 50 requests per 1 hours const authLimit = rateLimit({ - store: new MongoStore({ - uri: process.env.MONGO_URL, - expireTimeMs: 1000 * 60 * 60, - errorHandler: console.error.bind(null, 'rate-limit-mongo'), - collectionName: "expressRateRecords-authLimit", - }), - windowMs: 1000 * 60 * 60, - max: 50, + // store: new MongoStore({ + // uri: process.env.MONGO_URL, + // expireTimeMs: 1000 * 60 * 60, + // errorHandler: console.error.bind(null, 'rate-limit-mongo'), + // collectionName: "expressRateRecords-authLimit", + // }), + windowMs: 60 * 1000, + max: 10, standardHeaders: true, legacyHeaders: false, keyGenerator: (req, res) => { return req.realIP - } + }, }); // 5 requests per 1 hour export const passwordLimiter = rateLimit({ - store: new MongoStore({ - uri: process.env.MONGO_URL, - expireTimeMs: 1000 * 60 * 60, - errorHandler: console.error.bind(null, 'rate-limit-mongo'), - collectionName: "expressRateRecords-passwordLimiter", - }), - windowMs: 1000 * 60 * 60, - max: 5, + // store: new MongoStore({ + // uri: process.env.MONGO_URL, + // expireTimeMs: 1000 * 60 * 60, + // errorHandler: console.error.bind(null, 'rate-limit-mongo'), + // collectionName: "expressRateRecords-passwordLimiter", + // }), + windowMs: 60 * 60 * 1000, + max: 10, standardHeaders: true, legacyHeaders: false, keyGenerator: (req, res) => { return req.realIP - } + }, }); export const authLimiter = (req: any, res: any, next: any) => { - if (process.env.NODE_ENV === 'production') { + if (process.env.NODE_ENV === "production") { authLimit(req, res, next); } else { next(); diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 31f6c6c9c..c86807f03 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,16 +1,16 @@ import { Types } from "mongoose"; -import { Secret, ISecret } from "../models"; -import { EESecretService, EELogService } from "../ee/services"; +import { ISecret, Secret } from "../models"; +import { EELogService, EESecretService } from "../ee/services"; import { IAction, SecretVersion } from "../ee/models"; import { - SECRET_SHARED, - SECRET_PERSONAL, ACTION_ADD_SECRETS, - ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, ACTION_READ_SECRETS, + ACTION_UPDATE_SECRETS, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, + SECRET_PERSONAL, + SECRET_SHARED, } from "../variables"; interface V1PushSecret { diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 7b866449e..51731bb6a 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -1,51 +1,105 @@ import { Types } from "mongoose"; import { CreateSecretParams, - GetSecretsParams, - GetSecretParams, - UpdateSecretParams, DeleteSecretParams, + GetSecretParams, + GetSecretsParams, + UpdateSecretParams, } from "../interfaces/services/SecretService"; import { - Secret, ISecret, + Secret, SecretBlindIndexData, ServiceTokenData, } from "../models"; import { SecretVersion } from "../ee/models"; import { BadRequestError, - SecretNotFoundError, - SecretBlindIndexDataNotFoundError, InternalServerError, + SecretBlindIndexDataNotFoundError, + SecretNotFoundError, UnauthorizedRequestError, } from "../utils/errors"; import { - SECRET_PERSONAL, - SECRET_SHARED, ACTION_ADD_SECRETS, + ACTION_DELETE_SECRETS, ACTION_READ_SECRETS, ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS, ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8, + SECRET_PERSONAL, + SECRET_SHARED, } from "../variables"; import crypto from "crypto"; import * as argon2 from "argon2"; import { - encryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8, + encryptSymmetric128BitHexKeyUTF8, } from "../utils/crypto"; -import { getEncryptionKey, client, getRootEncryptionKey } from "../config"; import { TelemetryService } from "../services"; -import { EESecretService, EELogService } from "../ee/services"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../config"; +import { EELogService, EESecretService } from "../ee/services"; import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj, } from "../utils/auth"; import { getFolderIdFromServiceToken } from "../services/FolderService"; +/** + * Returns an object containing secret [secret] but with its value, key, comment decrypted. + * + * Precondition: the workspace for secret [secret] must have E2EE disabled + * @param {ISecret} secret - secret to repackage to raw + * @param {String} key - symmetric key to use to decrypt secret + * @returns + */ +export const repackageSecretToRaw = ({ + secret, + key, +}: { + secret: ISecret; + key: string; +}) => { + + const secretKey = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key, + }); + + const secretValue = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key, + }); + + let secretComment = ""; + + if (secret.secretCommentCiphertext && secret.secretCommentIV && secret.secretCommentTag) { + secretComment = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secret.secretCommentCiphertext, + iv: secret.secretCommentIV, + tag: secret.secretCommentTag, + key, + }); + } + + return ({ + _id: secret._id, + version: secret.version, + workspace: secret.workspace, + type: secret.type, + environment: secret.environment, + user: secret.user, + secretKey, + secretValue, + secretComment, + }); +} + /** * Create secret blind index data containing encrypted blind index [salt] * for workspace with id [workspaceId] @@ -271,6 +325,7 @@ export const createSecretHelper = async ({ secretCommentTag, secretPath = "/", }: CreateSecretParams) => { + const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, workspaceId: new Types.ObjectId(workspaceId), @@ -448,7 +503,7 @@ export const getSecretsHelper = async ({ folder: folderId, type: SECRET_PERSONAL, ...getAuthDataPayloadUserObj(authData), - }); + }).populate("tags").lean(); // concat with shared secrets secrets = secrets.concat( @@ -460,7 +515,7 @@ export const getSecretsHelper = async ({ secretBlindIndex: { $nin: secrets.map((secret) => secret.secretBlindIndex), }, - }) + }).populate("tags").lean() ); // (EE) create (audit) log @@ -546,7 +601,7 @@ export const getSecretHelper = async ({ folder: folderId, type: type ?? SECRET_PERSONAL, ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), - }); + }).lean(); if (!secret) { // case: failed to find personal secret matching criteria @@ -557,7 +612,7 @@ export const getSecretHelper = async ({ environment, folder: folderId, type: SECRET_SHARED, - }); + }).lean(); } if (!secret) throw SecretNotFoundError(); @@ -788,6 +843,7 @@ export const deleteSecretHelper = async ({ // if using service token filter towards the folderId by secretpath if (authData.authPayload instanceof ServiceTokenData) { const { secretPath: serviceTkScopedSecretPath } = authData.authPayload; + if (secretPath !== serviceTkScopedSecretPath) { throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); } @@ -807,7 +863,7 @@ export const deleteSecretHelper = async ({ workspaceId: new Types.ObjectId(workspaceId), environment, folder: folderId, - }); + }).lean(); secret = await Secret.findOneAndDelete({ secretBlindIndex, @@ -815,7 +871,7 @@ export const deleteSecretHelper = async ({ environment, type, folder: folderId, - }); + }).lean(); await Secret.deleteMany({ secretBlindIndex, @@ -831,7 +887,7 @@ export const deleteSecretHelper = async ({ environment, type, ...getAuthDataPayloadUserObj(authData), - }); + }).lean(); if (secret) { secrets = [secret]; @@ -852,15 +908,13 @@ export const deleteSecretHelper = async ({ secretIds: secrets.map((secret) => secret._id), }); - // (EE) take a secret snapshot - action && - (await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], - channel: authData.authChannel, - ipAddress: authData.authIP, - })); + action && (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP, + })); // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ @@ -888,8 +942,8 @@ export const deleteSecretHelper = async ({ }); } - return { + return ({ secrets, secret, - }; + }); }; diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index d747de20d..da494f4f7 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -1,10 +1,10 @@ -import { IUser } from '../models'; -import { createOrganization } from './organization'; -import { addMembershipsOrg } from './membershipOrg'; -import { OWNER, ACCEPTED } from '../variables'; -import { sendMail } from '../helpers/nodemailer'; -import { TokenService } from '../services'; -import { TOKEN_EMAIL_CONFIRMATION } from '../variables'; +import { IUser } from "../models"; +import { createOrganization } from "./organization"; +import { addMembershipsOrg } from "./membershipOrg"; +import { ACCEPTED, OWNER } from "../variables"; +import { sendMail } from "../helpers/nodemailer"; +import { TokenService } from "../services"; +import { TOKEN_EMAIL_CONFIRMATION } from "../variables"; /** * Send magic link to verify email to [email] @@ -16,17 +16,17 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables'; export const sendEmailVerification = async ({ email }: { email: string }) => { const token = await TokenService.createToken({ type: TOKEN_EMAIL_CONFIRMATION, - email + email, }); // send mail await sendMail({ - template: 'emailVerification.handlebars', - subjectLine: 'Infisical confirmation code', + template: "emailVerification.handlebars", + subjectLine: "Infisical confirmation code", recipients: [email], substitutions: { - code: token - } + code: token, + }, }); }; @@ -38,7 +38,7 @@ export const sendEmailVerification = async ({ email }: { email: string }) => { */ export const checkEmailVerification = async ({ email, - code + code, }: { email: string; code: string; @@ -46,7 +46,7 @@ export const checkEmailVerification = async ({ await TokenService.validateToken({ type: TOKEN_EMAIL_CONFIRMATION, email, - token: code + token: code, }); }; @@ -59,7 +59,7 @@ export const checkEmailVerification = async ({ */ export const initializeDefaultOrg = async ({ organizationName, - user + user, }: { organizationName: string; user: IUser; @@ -69,14 +69,14 @@ export const initializeDefaultOrg = async ({ // subscription const organization = await createOrganization({ email: user.email, - name: organizationName + name: organizationName, }); await addMembershipsOrg({ userIds: [user._id.toString()], organizationId: organization._id.toString(), roles: [OWNER], - statuses: [ACCEPTED] + statuses: [ACCEPTED], }); } catch (err) { throw new Error(`Failed to initialize default organization and workspace [err=${err}]`); diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index a69b72695..59030ff8c 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,8 +1,8 @@ import { IUser, User, -} from '../models'; -import { sendMail } from './nodemailer'; +} from "../models"; +import { sendMail } from "./nodemailer"; /** * Initialize a user under email [email] @@ -12,7 +12,7 @@ import { sendMail } from './nodemailer'; */ export const setupAccount = async ({ email }: { email: string }) => { const user = await new User({ - email + email, }).save(); return user; @@ -49,7 +49,7 @@ export const completeAccount = async ({ encryptedPrivateKeyIV, encryptedPrivateKeyTag, salt, - verifier + verifier, }: { userId: string; firstName: string; @@ -66,7 +66,7 @@ export const completeAccount = async ({ verifier: string; }) => { const options = { - new: true + new: true, }; const user = await User.findByIdAndUpdate( userId, @@ -82,7 +82,7 @@ export const completeAccount = async ({ iv: encryptedPrivateKeyIV, tag: encryptedPrivateKeyTag, salt, - verifier + verifier, }, options ); @@ -100,7 +100,7 @@ export const completeAccount = async ({ export const checkUserDevice = async ({ user, ip, - userAgent + userAgent, }: { user: IUser; ip: string; @@ -114,22 +114,22 @@ export const checkUserDevice = async ({ user.devices = user.devices.concat([{ ip: String(ip), - userAgent + userAgent, }]); await user.save(); // send MFA code [code] to [email] await sendMail({ - template: 'newDevice.handlebars', - subjectLine: `Successful login from new device`, + template: "newDevice.handlebars", + subjectLine: "Successful login from new device", recipients: [user.email], substitutions: { email: user.email, timestamp: new Date().toString(), ip, - userAgent - } + userAgent, + }, }); } } \ No newline at end of file diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index a1d71e920..d79d6c453 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,12 +1,13 @@ import { - Workspace, Bot, - Membership, Key, - Secret -} from '../models'; -import { createBot } from '../helpers/bot'; -import { SecretService } from '../services'; + Membership, + Secret, + Workspace, +} from "../models"; +import { createBot } from "../helpers/bot"; +import { EELicenseService } from "../ee/services"; +import { SecretService } from "../services"; /** * Create a workspace with name [name] in organization with id [organizationId] @@ -17,29 +18,30 @@ import { SecretService } from '../services'; */ export const createWorkspace = async ({ name, - organizationId + organizationId, }: { name: string; organizationId: string; }) => { - // create workspace - const workspace = await new Workspace({ - name, - organization: organizationId, - autoCapitalization: true - }).save(); + // create workspace + const workspace = await new Workspace({ + name, + organization: organizationId, + autoCapitalization: true, + }).save(); - // initialize bot for workspace - await createBot({ - name: 'Infisical Bot', - workspaceId: workspace._id - }); + // initialize bot for workspace + await createBot({ + name: "Infisical Bot", + workspaceId: workspace._id, + }); - // initialize blind index salt for workspace - await SecretService.createSecretBlindIndexData({ - workspaceId: workspace._id - }); + // initialize blind index salt for workspace + await SecretService.createSecretBlindIndexData({ + workspaceId: workspace._id, + }); + await EELicenseService.refreshPlan(organizationId); return workspace; }; @@ -53,15 +55,15 @@ export const createWorkspace = async ({ export const deleteWorkspace = async ({ id }: { id: string }) => { await Workspace.deleteOne({ _id: id }); await Bot.deleteOne({ - workspace: id + workspace: id, }); await Membership.deleteMany({ - workspace: id + workspace: id, }); await Secret.deleteMany({ - workspace: id + workspace: id, }); await Key.deleteMany({ - workspace: id + workspace: id, }); }; diff --git a/backend/src/index.ts b/backend/src/index.ts index af3e0b24b..d0cd67ae5 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -2,7 +2,7 @@ import dotenv from "dotenv"; dotenv.config(); import express from "express"; // eslint-disable-next-line @typescript-eslint/no-var-requires -require('express-async-errors'); +require("express-async-errors"); import helmet from "helmet"; import cors from "cors"; import { DatabaseService } from "./services"; @@ -15,32 +15,32 @@ const swaggerFile = require("../spec.json"); // eslint-disable-next-line @typescript-eslint/no-var-requires import { apiLimiter } from "./helpers/rateLimiter"; import { - workspace as eeWorkspaceRouter, + action as eeActionRouter, + cloudProducts as eeCloudProductsRouter, + organizations as eeOrganizationsRouter, secret as eeSecretRouter, secretSnapshot as eeSecretSnapshotRouter, - action as eeActionRouter, - organizations as eeOrganizationsRouter, - cloudProducts as eeCloudProductsRouter, + workspace as eeWorkspaceRouter, } from "./ee/routes/v1"; import { - signup as v1SignupRouter, auth as v1AuthRouter, bot as v1BotRouter, - organization as v1OrganizationRouter, - workspace as v1WorkspaceRouter, + integrationAuth as v1IntegrationAuthRouter, + integration as v1IntegrationRouter, + inviteOrg as v1InviteOrgRouter, + key as v1KeyRouter, membershipOrg as v1MembershipOrgRouter, membership as v1MembershipRouter, - key as v1KeyRouter, - inviteOrg as v1InviteOrgRouter, - user as v1UserRouter, - userAction as v1UserActionRouter, - secret as v1SecretRouter, - serviceToken as v1ServiceTokenRouter, + organization as v1OrganizationRouter, password as v1PasswordRouter, - stripe as v1StripeRouter, - integration as v1IntegrationRouter, - integrationAuth as v1IntegrationAuthRouter, + secret as v1SecretRouter, secretsFolder as v1SecretsFolder, + serviceToken as v1ServiceTokenRouter, + signup as v1SignupRouter, + stripe as v1StripeRouter, + userAction as v1UserActionRouter, + user as v1UserRouter, + workspace as v1WorkspaceRouter, } from "./routes/v1"; import { signup as v2SignupRouter, @@ -95,7 +95,7 @@ const main = async () => { app.use((req, res, next) => { // default to IP address provided by Cloudflare - const cfIp = req.headers['cf-connecting-ip']; + const cfIp = req.headers["cf-connecting-ip"]; req.realIP = Array.isArray(cfIp) ? cfIp[0] : (cfIp as string) || req.ip; next(); }); diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index f8ed77277..bc8f01a80 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -2,32 +2,34 @@ import { Octokit } from "@octokit/rest"; import { IIntegrationAuth } from "../models"; import { standardRequest } from "../config/request"; import { - INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_CHECKLY, + INTEGRATION_CHECKLY_API_URL, + INTEGRATION_CIRCLECI, + INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_FLYIO, + INTEGRATION_FLYIO_API_URL, INTEGRATION_GITHUB, INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY, - INTEGRATION_HEROKU_API_URL, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUDFLARE_PAGES_API_URL, INTEGRATION_GITLAB_API_URL, - INTEGRATION_VERCEL_API_URL, + INTEGRATION_HEROKU, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_NETLIFY, INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, + INTEGRATION_RAILWAY, INTEGRATION_RAILWAY_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_CIRCLECI_API_URL, - INTEGRATION_TRAVISCI_API_URL, + INTEGRATION_RENDER, + INTEGRATION_RENDER_API_URL, + INTEGRATION_SUPABASE, INTEGRATION_SUPABASE_API_URL, - INTEGRATION_CHECKLY_API_URL + INTEGRATION_TRAVISCI, + INTEGRATION_TRAVISCI_API_URL, + INTEGRATION_VERCEL, + INTEGRATION_VERCEL_API_URL, } from "../variables"; interface App { @@ -48,10 +50,12 @@ interface App { const getApps = async ({ integrationAuth, accessToken, + accessId, teamId, }: { integrationAuth: IIntegrationAuth; accessToken: string; + accessId?: string; teamId?: string; }) => { let apps: App[] = []; @@ -127,6 +131,12 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_CLOUDFLARE_PAGES: + apps = await getAppsCloudflarePages({ + accessToken, + accountId: accessId + }) + break; } return apps; @@ -212,7 +222,7 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { const params = new URLSearchParams({ page: String(page), per_page: String(perPage), - filter: 'all' + filter: "all", }); const { data } = await standardRequest.get( @@ -636,4 +646,37 @@ const getAppsCheckly = async ({ accessToken }: { accessToken: string }) => { return apps; }; +/** + * Return list of projects for the Cloudflare Pages integration + * @param {Object} obj + * @param {String} obj.accessToken - api key for the Cloudflare API + * @returns {Object[]} apps - Cloudflare Pages projects + * @returns {String} apps.name - name of Cloudflare Pages project + */ +const getAppsCloudflarePages = async ({ + accessToken, + accountId +}: { + accessToken: string; + accountId?: string; +}) => { + const { data } = await standardRequest.get( + `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accountId}/pages/projects`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept": "application/json", + }, + } + ); + + const apps = data.result.map((a: any) => { + return { + name: a.name, + appId: a.id, + }; + }); + return apps; +} + export { getApps }; diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index a4d5f5b06..12948d403 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -1,31 +1,31 @@ import { standardRequest } from "../config/request"; import { INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, INTEGRATION_AZURE_TOKEN_URL, - INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_VERCEL_TOKEN_URL, - INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_GITHUB, INTEGRATION_GITHUB_TOKEN_URL, + INTEGRATION_GITLAB, INTEGRATION_GITLAB_TOKEN_URL, + INTEGRATION_HEROKU, + INTEGRATION_HEROKU_TOKEN_URL, + INTEGRATION_NETLIFY, + INTEGRATION_NETLIFY_TOKEN_URL, + INTEGRATION_VERCEL, + INTEGRATION_VERCEL_TOKEN_URL, } from "../variables"; import { - getSiteURL, getClientIdAzure, - getClientSecretAzure, - getClientSecretHeroku, - getClientIdVercel, - getClientSecretVercel, - getClientIdNetlify, - getClientSecretNetlify, getClientIdGitHub, - getClientSecretGitHub, getClientIdGitLab, + getClientIdNetlify, + getClientIdVercel, + getClientSecretAzure, + getClientSecretGitHub, getClientSecretGitLab, + getClientSecretHeroku, + getClientSecretNetlify, + getClientSecretVercel, + getSiteURL, } from "../config"; interface ExchangeCodeAzureResponse { diff --git a/backend/src/integrations/index.ts b/backend/src/integrations/index.ts index 8439ff3ba..14e2da7f9 100644 --- a/backend/src/integrations/index.ts +++ b/backend/src/integrations/index.ts @@ -1,9 +1,9 @@ -import { exchangeCode } from './exchange'; -import { exchangeRefresh } from './refresh'; -import { getApps } from './apps'; -import { getTeams } from './teams'; -import { syncSecrets } from './sync'; -import { revokeAccess } from './revoke'; +import { exchangeCode } from "./exchange"; +import { exchangeRefresh } from "./refresh"; +import { getApps } from "./apps"; +import { getTeams } from "./teams"; +import { syncSecrets } from "./sync"; +import { revokeAccess } from "./revoke"; export { exchangeCode, @@ -11,5 +11,5 @@ export { getApps, getTeams, syncSecrets, - revokeAccess + revokeAccess, } \ No newline at end of file diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index 0c401bf15..d530ab2f5 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -2,22 +2,22 @@ import { standardRequest } from "../config/request"; import { IIntegrationAuth } from "../models"; import { INTEGRATION_AZURE_KEY_VAULT, - INTEGRATION_HEROKU, INTEGRATION_GITLAB, + INTEGRATION_HEROKU, } from "../variables"; import { INTEGRATION_AZURE_TOKEN_URL, - INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_GITLAB_TOKEN_URL, + INTEGRATION_HEROKU_TOKEN_URL, } from "../variables"; import { IntegrationService } from "../services"; import { - getSiteURL, getClientIdAzure, - getClientSecretAzure, - getClientSecretHeroku, getClientIdGitLab, + getClientSecretAzure, getClientSecretGitLab, + getClientSecretHeroku, + getSiteURL, } from "../config"; interface RefreshTokenAzureResponse { diff --git a/backend/src/integrations/revoke.ts b/backend/src/integrations/revoke.ts index 46c93017d..d4747a52a 100644 --- a/backend/src/integrations/revoke.ts +++ b/backend/src/integrations/revoke.ts @@ -1,21 +1,19 @@ import { IIntegrationAuth, - IntegrationAuth, - Integration, - Bot, - BotKey -} from '../models'; + Integration, + IntegrationAuth, +} from "../models"; import { - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, INTEGRATION_GITHUB, INTEGRATION_GITLAB, -} from '../variables'; + INTEGRATION_HEROKU, + INTEGRATION_NETLIFY, + INTEGRATION_VERCEL, +} from "../variables"; const revokeAccess = async ({ integrationAuth, - accessToken + accessToken, }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -36,12 +34,12 @@ const revokeAccess = async ({ } deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ - _id: integrationAuth._id + _id: integrationAuth._id, }); if (deletedIntegrationAuth) { await Integration.deleteMany({ - integrationAuth: deletedIntegrationAuth._id + integrationAuth: deletedIntegrationAuth._id, }); } diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 76d009944..0cc3a9438 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -1,45 +1,47 @@ -import _ from 'lodash'; -import AWS from 'aws-sdk'; +import _ from "lodash"; +import AWS from "aws-sdk"; import { - SecretsManagerClient, - UpdateSecretCommand, - CreateSecretCommand, + CreateSecretCommand, GetSecretValueCommand, - ResourceNotFoundException -} from '@aws-sdk/client-secrets-manager'; + ResourceNotFoundException, + SecretsManagerClient, + UpdateSecretCommand, +} from "@aws-sdk/client-secrets-manager"; import { Octokit } from "@octokit/rest"; import sodium from "libsodium-wrappers"; import { IIntegration, IIntegrationAuth } from "../models"; import { - INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, - INTEGRATION_GITHUB, - INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE, - INTEGRATION_HEROKU_API_URL, - INTEGRATION_GITLAB_API_URL, - INTEGRATION_VERCEL_API_URL, - INTEGRATION_NETLIFY_API_URL, - INTEGRATION_RENDER_API_URL, - INTEGRATION_RAILWAY_API_URL, - INTEGRATION_FLYIO_API_URL, - INTEGRATION_CIRCLECI_API_URL, - INTEGRATION_TRAVISCI_API_URL, - INTEGRATION_SUPABASE_API_URL, + INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_CHECKLY, INTEGRATION_CHECKLY_API_URL, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_CIRCLECI, + INTEGRATION_CIRCLECI_API_URL, + INTEGRATION_FLYIO, + INTEGRATION_FLYIO_API_URL, + INTEGRATION_GITHUB, + INTEGRATION_GITLAB, + INTEGRATION_GITLAB_API_URL, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HEROKU, + INTEGRATION_HEROKU_API_URL, + INTEGRATION_NETLIFY, + INTEGRATION_NETLIFY_API_URL, + INTEGRATION_RAILWAY, + INTEGRATION_RAILWAY_API_URL, + INTEGRATION_RENDER, + INTEGRATION_RENDER_API_URL, + INTEGRATION_SUPABASE, + INTEGRATION_SUPABASE_API_URL, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_CLOUDFLARE_PAGES_API_URL, + INTEGRATION_TRAVISCI, + INTEGRATION_TRAVISCI_API_URL, + INTEGRATION_VERCEL, + INTEGRATION_VERCEL_API_URL, } from "../variables"; -import { standardRequest} from '../config/request'; +import { standardRequest} from "../config/request"; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -68,7 +70,7 @@ const syncSecrets = async ({ await syncSecretsAzureKeyVault({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_AWS_PARAMETER_STORE: @@ -76,7 +78,7 @@ const syncSecrets = async ({ integration, secrets, accessId, - accessToken + accessToken, }); break; case INTEGRATION_AWS_SECRET_MANAGER: @@ -84,7 +86,7 @@ const syncSecrets = async ({ integration, secrets, accessId, - accessToken + accessToken, }); break; case INTEGRATION_HEROKU: @@ -135,7 +137,7 @@ const syncSecrets = async ({ await syncSecretsRailway({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: @@ -163,7 +165,7 @@ const syncSecrets = async ({ await syncSecretsSupabase({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_FLYIO: @@ -191,7 +193,7 @@ const syncSecrets = async ({ await syncSecretsSupabase({ integration, secrets, - accessToken + accessToken, }); break; case INTEGRATION_CHECKLY: @@ -207,7 +209,15 @@ const syncSecrets = async ({ integrationAuth, secrets, accessId, - accessToken + accessToken, + }); + break; + case INTEGRATION_CLOUDFLARE_PAGES: + await syncSecretsCloudflarePages({ + integration, + secrets, + accessId, + accessToken }); break; } @@ -223,7 +233,7 @@ const syncSecrets = async ({ const syncSecretsAzureKeyVault = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -254,8 +264,8 @@ const syncSecretsAzureKeyVault = async ({ while (url) { const res = await standardRequest.get(url, { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, }); result = result.concat(res.data.value); @@ -271,13 +281,13 @@ const syncSecretsAzureKeyVault = async ({ let lastSlashIndex: number; const res = (await Promise.all(getAzureKeyVaultSecrets.map(async (getAzureKeyVaultSecret) => { if (!lastSlashIndex) { - lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf('/'); + lastSlashIndex = getAzureKeyVaultSecret.id.lastIndexOf("/"); } const azureKeyVaultSecret = await standardRequest.get(`${getAzureKeyVaultSecret.id}?api-version=7.3`, { headers: { - 'Authorization': `Bearer ${accessToken}` - } + "Authorization": `Bearer ${accessToken}`, + }, }); return ({ @@ -287,7 +297,7 @@ const syncSecretsAzureKeyVault = async ({ }))) .reduce((obj: any, secret: any) => ({ ...obj, - [secret.key]: secret + [secret.key]: secret, }), {}); const setSecrets: { @@ -296,19 +306,19 @@ const syncSecretsAzureKeyVault = async ({ }[] = []; Object.keys(secrets).forEach((key) => { - const hyphenatedKey = key.replace(/_/g, '-'); + const hyphenatedKey = key.replace(/_/g, "-"); if (!(hyphenatedKey in res)) { // case: secret has been created setSecrets.push({ key: hyphenatedKey, - value: secrets[key] + value: secrets[key], }); } else { if (secrets[key] !== res[hyphenatedKey].value) { // case: secret has been updated setSecrets.push({ key: hyphenatedKey, - value: secrets[key] + value: secrets[key], }); } } @@ -317,7 +327,7 @@ const syncSecretsAzureKeyVault = async ({ const deleteSecrets: AzureKeyVaultSecret[] = []; Object.keys(res).forEach((key) => { - const underscoredKey = key.replace(/-/g, '_'); + const underscoredKey = key.replace(/-/g, "_"); if (!(underscoredKey in secrets)) { deleteSecrets.push(res[key]); } @@ -327,7 +337,7 @@ const syncSecretsAzureKeyVault = async ({ key, value, integration, - accessToken + accessToken, }: { key: string; value: string; @@ -343,12 +353,12 @@ const syncSecretsAzureKeyVault = async ({ await standardRequest.put( `${integration.app}/secrets/${key}?api-version=7.3`, { - value + value, }, { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, } ); @@ -356,13 +366,13 @@ const syncSecretsAzureKeyVault = async ({ } catch (err) { const error: any = err; - if (error?.response?.data?.error?.innererror?.code === 'ObjectIsDeletedButRecoverable') { + if (error?.response?.data?.error?.innererror?.code === "ObjectIsDeletedButRecoverable") { await standardRequest.post( `${integration.app}/deletedsecrets/${key}/recover?api-version=7.3`, {}, { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, } ); await new Promise(resolve => setTimeout(resolve, 10000)); @@ -381,7 +391,7 @@ const syncSecretsAzureKeyVault = async ({ key, value, integration, - accessToken + accessToken, }); } @@ -389,8 +399,8 @@ const syncSecretsAzureKeyVault = async ({ const { key } = deleteSecret; await standardRequest.delete(`${integration.app}/secrets/${key}?api-version=7.3`, { headers: { - 'Authorization': `Bearer ${accessToken}` - } + "Authorization": `Bearer ${accessToken}`, + }, }); } }; @@ -407,7 +417,7 @@ const syncSecretsAWSParameterStore = async ({ integration, secrets, accessId, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -419,18 +429,18 @@ const syncSecretsAWSParameterStore = async ({ AWS.config.update({ region: integration.region, accessKeyId: accessId, - secretAccessKey: accessToken + secretAccessKey: accessToken, }); const ssm = new AWS.SSM({ - apiVersion: '2014-11-06', - region: integration.region + apiVersion: "2014-11-06", + region: integration.region, }); const params = { Path: integration.path, Recursive: true, - WithDecryption: true + WithDecryption: true, }; const parameterList = (await ssm.getParametersByPath(params).promise()).Parameters @@ -442,7 +452,7 @@ const syncSecretsAWSParameterStore = async ({ if (parameterList) { awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => ({ ...obj, - [secret.Name.split("/").pop()]: secret + [secret.Name.split("/").pop()]: secret, }), {}); } @@ -453,9 +463,9 @@ const syncSecretsAWSParameterStore = async ({ // -> create secret await ssm.putParameter({ Name: `${integration.path}${key}`, - Type: 'SecureString', + Type: "SecureString", Value: secrets[key], - Overwrite: true + Overwrite: true, }).promise(); } else { // case: secret exists in AWS parameter store @@ -465,9 +475,9 @@ const syncSecretsAWSParameterStore = async ({ // -> update secret await ssm.putParameter({ Name: `${integration.path}${key}`, - Type: 'SecureString', + Type: "SecureString", Value: secrets[key], - Overwrite: true + Overwrite: true, }).promise(); } } @@ -479,7 +489,7 @@ const syncSecretsAWSParameterStore = async ({ // case: // -> delete secret await ssm.deleteParameter({ - Name: awsParameterStoreSecretsObj[key].Name + Name: awsParameterStoreSecretsObj[key].Name, }).promise(); } }); @@ -487,7 +497,7 @@ const syncSecretsAWSParameterStore = async ({ AWS.config.update({ region: undefined, accessKeyId: undefined, - secretAccessKey: undefined + secretAccessKey: undefined, }); } @@ -503,7 +513,7 @@ const syncSecretsAWSSecretManager = async ({ integration, secrets, accessId, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -517,20 +527,20 @@ const syncSecretsAWSSecretManager = async ({ AWS.config.update({ region: integration.region, accessKeyId: accessId, - secretAccessKey: accessToken + secretAccessKey: accessToken, }); secretsManager = new SecretsManagerClient({ region: integration.region, credentials: { accessKeyId: accessId, - secretAccessKey: accessToken - } + secretAccessKey: accessToken, + }, }); const awsSecretManagerSecret = await secretsManager.send( new GetSecretValueCommand({ - SecretId: integration.app + SecretId: integration.app, }) ); @@ -543,26 +553,26 @@ const syncSecretsAWSSecretManager = async ({ if (!_.isEqual(awsSecretManagerSecretObj, secrets)) { await secretsManager.send(new UpdateSecretCommand({ SecretId: integration.app, - SecretString: JSON.stringify(secrets) + SecretString: JSON.stringify(secrets), })); } AWS.config.update({ region: undefined, accessKeyId: undefined, - secretAccessKey: undefined + secretAccessKey: undefined, }); } catch (err) { if (err instanceof ResourceNotFoundException && secretsManager) { await secretsManager.send(new CreateSecretCommand({ Name: integration.app, - SecretString: JSON.stringify(secrets) + SecretString: JSON.stringify(secrets), })); } AWS.config.update({ region: undefined, accessKeyId: undefined, - secretAccessKey: undefined + secretAccessKey: undefined, }); } } @@ -590,7 +600,7 @@ const syncSecretsHeroku = async ({ headers: { Accept: "application/vnd.heroku+json; version=3", Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ) @@ -609,7 +619,7 @@ const syncSecretsHeroku = async ({ headers: { Accept: "application/vnd.heroku+json; version=3", Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -657,8 +667,8 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } + "Accept-Encoding": "application/json", + }, } )) .data @@ -669,7 +679,7 @@ const syncSecretsVercel = async ({ return false; } - if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) { + if (integration.targetEnvironment === "preview" && integration.path && integration.path !== secret.gitBranch) { // case: secret on preview environment does not have same target git branch return false; } @@ -682,7 +692,7 @@ const syncSecretsVercel = async ({ const res: { [key: string]: VercelSecret } = {}; for await (const vercelSecret of vercelSecrets) { - if (vercelSecret.type === 'encrypted') { + if (vercelSecret.type === "encrypted") { // case: secret is encrypted -> need to decrypt const decryptedSecret = (await standardRequest.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${vercelSecret.id}`, @@ -690,8 +700,8 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } + "Accept-Encoding": "application/json", + }, } )).data; @@ -715,8 +725,8 @@ const syncSecretsVercel = async ({ type: "encrypted", target: [integration.targetEnvironment], ...(integration.path ? { - gitBranch: integration.path - } : {}) + gitBranch: integration.path, + } : {}), }); } }); @@ -735,8 +745,8 @@ const syncSecretsVercel = async ({ ? [...res[key].target] : [...res[key].target, integration.targetEnvironment], ...(integration.path ? { - gitBranch: integration.path - } : {}) + gitBranch: integration.path, + } : {}), }); } } else { @@ -748,8 +758,8 @@ const syncSecretsVercel = async ({ type: "encrypted", // value doesn't matter target: [integration.targetEnvironment], ...(integration.path ? { - gitBranch: integration.path - } : {}) + gitBranch: integration.path, + } : {}), }); } }); @@ -763,14 +773,14 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); } for await (const secret of updateSecrets) { - if (secret.type !== 'sensitive') { + if (secret.type !== "sensitive") { const { id, ...updatedSecret } = secret; await standardRequest.patch( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, @@ -779,7 +789,7 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -793,7 +803,7 @@ const syncSecretsVercel = async ({ params, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -846,7 +856,7 @@ const syncSecretsNetlify = async ({ params: getParams, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ) @@ -961,7 +971,7 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -979,7 +989,7 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -994,7 +1004,7 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -1009,7 +1019,7 @@ const syncSecretsNetlify = async ({ params: syncParams, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -1061,7 +1071,7 @@ const syncSecretsGitHub = async ({ "GET /repos/{owner}/{repo}/actions/secrets/public-key", { owner: integration.owner, - repo: integration.app + repo: integration.app, } ) ).data; @@ -1151,7 +1161,7 @@ const syncSecretsRender = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' + "Accept-Encoding": "application/json", }, } ); @@ -1167,7 +1177,7 @@ const syncSecretsRender = async ({ const syncSecretsRailway = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -1184,7 +1194,7 @@ const syncSecretsRailway = async ({ environmentId: integration.targetEnvironmentId, ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), replace: true, - variables: secrets + variables: secrets, }; await standardRequest.post(INTEGRATION_RAILWAY_API_URL, { @@ -1194,9 +1204,9 @@ const syncSecretsRailway = async ({ }, }, { headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'Accept-Encoding': 'application/json' + "Authorization": `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", }, }); } @@ -1252,7 +1262,7 @@ const syncSecretsFlyio = async ({ }, { headers: { Authorization: "Bearer " + accessToken, - 'Accept-Encoding': 'application/json', + "Accept-Encoding": "application/json", }, }); @@ -1281,8 +1291,8 @@ const syncSecretsFlyio = async ({ }, { headers: { Authorization: "Bearer " + accessToken, - 'Content-Type': 'application/json', - 'Accept-Encoding': 'application/json', + "Content-Type": "application/json", + "Accept-Encoding": "application/json", }, })).data.data.app.secrets; @@ -1321,7 +1331,7 @@ const syncSecretsFlyio = async ({ headers: { Authorization: "Bearer " + accessToken, "Content-Type": "application/json", - 'Accept-Encoding': 'application/json', + "Accept-Encoding": "application/json", }, }); }; @@ -1432,7 +1442,7 @@ const syncSecretsTravisCI = async ({ ?.env_vars .reduce((obj: any, secret: any) => ({ ...obj, - [secret.name]: secret + [secret.name]: secret, }), {}); // add secrets @@ -1445,8 +1455,8 @@ const syncSecretsTravisCI = async ({ { env_var: { name: key, - value: secrets[key] - } + value: secrets[key], + }, }, { headers: { @@ -1465,7 +1475,7 @@ const syncSecretsTravisCI = async ({ env_var: { name: key, value: secrets[key], - } + }, }, { headers: { @@ -1533,10 +1543,10 @@ const syncSecretsGitLab = async ({ allEnvVariables = [...allEnvVariables, ...response.data]; const linkHeader = response.headers.link; - const nextLink = linkHeader?.split(',').find((part: string) => part.includes('rel="next"')); + const nextLink = linkHeader?.split(",").find((part: string) => part.includes('rel="next"')); if (nextLink) { - url = nextLink.trim().split(';')[0].slice(1, -1); + url = nextLink.trim().split(";")[0].slice(1, -1); } else { url = null; } @@ -1561,7 +1571,7 @@ const syncSecretsGitLab = async ({ protected: false, masked: false, raw: false, - environment_scope: integration.targetEnvironment + environment_scope: integration.targetEnvironment, }, { headers: { @@ -1578,7 +1588,7 @@ const syncSecretsGitLab = async ({ `${INTEGRATION_GITLAB_API_URL}/v4/projects/${integration?.appId}/variables/${existingSecret.key}?filter[environment_scope]=${integration.targetEnvironment}`, { ...existingSecret, - value: secrets[existingSecret.key] + value: secrets[existingSecret.key], }, { headers: { @@ -1618,7 +1628,7 @@ const syncSecretsGitLab = async ({ const syncSecretsSupabase = async ({ integration, secrets, - accessToken + accessToken, }: { integration: IIntegration; secrets: any; @@ -1629,8 +1639,8 @@ const syncSecretsSupabase = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } + "Accept-Encoding": "application/json", + }, } ); @@ -1639,7 +1649,7 @@ const syncSecretsSupabase = async ({ (key) => { return { name: key, - value: secrets[key] + value: secrets[key], }; } ); @@ -1650,8 +1660,8 @@ const syncSecretsSupabase = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } + "Accept-Encoding": "application/json", + }, } ); @@ -1667,10 +1677,10 @@ const syncSecretsSupabase = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'Accept-Encoding': 'application/json' + "Content-Type": "application/json", + "Accept-Encoding": "application/json", }, - data: secretsToDelete + data: secretsToDelete, } ); }; @@ -1700,7 +1710,7 @@ const syncSecretsCheckly = async ({ headers: { "Authorization": `Bearer ${accessToken}`, "Accept-Encoding": "application/json", - "X-Checkly-Account": integration.appId + "X-Checkly-Account": integration.appId, }, } ) @@ -1708,7 +1718,7 @@ const syncSecretsCheckly = async ({ .data .reduce((obj: any, secret: any) => ({ ...obj, - [secret.key]: secret.value + [secret.key]: secret.value, }), {}); // add secrets @@ -1721,14 +1731,14 @@ const syncSecretsCheckly = async ({ `${INTEGRATION_CHECKLY_API_URL}/v1/variables`, { key, - value: secrets[key] + value: secrets[key], }, { headers: { "Authorization": `Bearer ${accessToken}`, "Accept": "application/json", "Content-Type": "application/json", - "X-Checkly-Account": integration.appId + "X-Checkly-Account": integration.appId, }, } ); @@ -1740,14 +1750,14 @@ const syncSecretsCheckly = async ({ await standardRequest.put( `${INTEGRATION_CHECKLY_API_URL}/v1/variables/${key}`, { - value: secrets[key] + value: secrets[key], }, { headers: { "Authorization": `Bearer ${accessToken}`, "Content-Type": "application/json", "Accept": "application/json", - "X-Checkly-Account": integration.appId + "X-Checkly-Account": integration.appId, }, } ); @@ -1764,7 +1774,7 @@ const syncSecretsCheckly = async ({ headers: { "Authorization": `Bearer ${accessToken}`, "Accept": "application/json", - "X-Checkly-Account": integration.appId + "X-Checkly-Account": integration.appId, }, } ); @@ -1805,12 +1815,12 @@ const syncSecretsHashiCorpVault = async ({ `${integrationAuth.url}/v1/auth/approle/login`, { "role_id": accessId, - "secret_id": accessToken + "secret_id": accessToken, }, { headers: { - "X-Vault-Namespace": integrationAuth.namespace - } + "X-Vault-Namespace": integrationAuth.namespace, + }, } ); @@ -1819,7 +1829,7 @@ const syncSecretsHashiCorpVault = async ({ await standardRequest.post( `${integrationAuth.url}/v1/${integration.app}/data/${integration.path}`, { - data: secrets + data: secrets, }, { headers: { @@ -1827,10 +1837,80 @@ const syncSecretsHashiCorpVault = async ({ "Accept": "application/json", "Content-Type": "application/json", "X-Vault-Token": clientToken, - "X-Vault-Namespace": integrationAuth.namespace + "X-Vault-Namespace": integrationAuth.namespace, }, } ); }; +/** + * Sync/push [secrets] to Cloudflare Pages project with name [integration.app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) + * @param {String} obj.accessToken - API token for Cloudflare + */ +const syncSecretsCloudflarePages = async ({ + integration, + secrets, + accessId, + accessToken, +}: { + integration: IIntegration; + secrets: any; + accessId: string | null; + accessToken: string; +}) => { + + // get secrets from cloudflare pages + const getSecretsRes = ( + await standardRequest.get( + `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept": "application/json", + }, + } + ) + ) + .data.result['deployment_configs'][integration.targetEnvironment]['env_vars']; + + // copy the secrets object, so we can set deleted keys to null + const secretsObj: any = {...secrets}; + + for (const [key, val] of Object.entries(secretsObj)) { + secretsObj[key] = { type: "secret_text", value: val }; + } + + if (getSecretsRes) { + for await (const key of Object.keys(getSecretsRes)) { + if (!(key in secrets)) { + // case: secret does not exist in infisical + // -> delete secret from cloudflare pages + secretsObj[key] = null; + } + } + } + + const data = { + "deployment_configs": { + [integration.targetEnvironment]: { + "env_vars": secretsObj + } + } + }; + + await standardRequest.patch( + `${INTEGRATION_CLOUDFLARE_PAGES_API_URL}/client/v4/accounts/${accessId}/pages/projects/${integration.app}`, + data, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept": "application/json", + }, + } + ); +} + export { syncSecrets }; diff --git a/backend/src/integrations/teams.ts b/backend/src/integrations/teams.ts index 74fc0ca86..3b0564322 100644 --- a/backend/src/integrations/teams.ts +++ b/backend/src/integrations/teams.ts @@ -1,11 +1,11 @@ import { - IIntegrationAuth -} from '../models'; + IIntegrationAuth, +} from "../models"; import { INTEGRATION_GITLAB, - INTEGRATION_GITLAB_API_URL -} from '../variables'; -import { standardRequest } from '../config/request'; + INTEGRATION_GITLAB_API_URL, +} from "../variables"; +import { standardRequest } from "../config/request"; interface Team { name: string; @@ -23,7 +23,7 @@ interface Team { */ const getTeams = async ({ integrationAuth, - accessToken + accessToken, }: { integrationAuth: IIntegrationAuth; accessToken: string; @@ -34,7 +34,7 @@ const getTeams = async ({ switch (integrationAuth.integration) { case INTEGRATION_GITLAB: teams = await getTeamsGitLab({ - accessToken + accessToken, }); break; } @@ -51,7 +51,7 @@ const getTeams = async ({ * @returns {String} teams.teamId - id of team */ const getTeamsGitLab = async ({ - accessToken + accessToken, }: { accessToken: string; }) => { @@ -61,19 +61,19 @@ const getTeamsGitLab = async ({ { headers: { Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } + "Accept-Encoding": "application/json", + }, } )).data; teams = res.map((t: any) => ({ name: t.name, - teamId: t.id + teamId: t.id, })); return teams; } export { - getTeams + getTeams, } diff --git a/backend/src/interfaces/middleware/index.ts b/backend/src/interfaces/middleware/index.ts index a368a3be1..3fba92348 100644 --- a/backend/src/interfaces/middleware/index.ts +++ b/backend/src/interfaces/middleware/index.ts @@ -1,9 +1,9 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IUser, IServiceAccount, - IServiceTokenData -} from '../../models'; + IServiceTokenData, + IUser, +} from "../../models"; export interface AuthData { authMode: string; diff --git a/backend/src/interfaces/serviceAccounts/dto/index.ts b/backend/src/interfaces/serviceAccounts/dto/index.ts index 52d8d8342..f69778144 100644 --- a/backend/src/interfaces/serviceAccounts/dto/index.ts +++ b/backend/src/interfaces/serviceAccounts/dto/index.ts @@ -1,7 +1,7 @@ -import CreateServiceAccountDto from './CreateServiceAccountDto'; -import AddServiceAccountPermissionDto from './AddServiceAccountPermissionDto'; +import CreateServiceAccountDto from "./CreateServiceAccountDto"; +import AddServiceAccountPermissionDto from "./AddServiceAccountPermissionDto"; export { CreateServiceAccountDto, - AddServiceAccountPermissionDto + AddServiceAccountPermissionDto, } \ No newline at end of file diff --git a/backend/src/interfaces/services/BotService/index.ts b/backend/src/interfaces/services/BotService/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/interfaces/utils/index.ts b/backend/src/interfaces/utils/index.ts index 7d27c9105..b781a39bb 100644 --- a/backend/src/interfaces/utils/index.ts +++ b/backend/src/interfaces/utils/index.ts @@ -1 +1 @@ -export * from './crypto'; \ No newline at end of file +export * from "./crypto"; \ No newline at end of file diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 039b8612d..bc2cbc969 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,20 +1,20 @@ -import requireAuth from './requireAuth'; -import requireMfaAuth from './requireMfaAuth'; -import requireBotAuth from './requireBotAuth'; -import requireSignupAuth from './requireSignupAuth'; -import requireWorkspaceAuth from './requireWorkspaceAuth'; -import requireMembershipAuth from './requireMembershipAuth'; -import requireMembershipOrgAuth from './requireMembershipOrgAuth'; -import requireOrganizationAuth from './requireOrganizationAuth'; -import requireIntegrationAuth from './requireIntegrationAuth'; -import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth'; -import requireServiceTokenAuth from './requireServiceTokenAuth'; -import requireServiceTokenDataAuth from './requireServiceTokenDataAuth'; -import requireServiceAccountAuth from './requireServiceAccountAuth'; -import requireServiceAccountWorkspacePermissionAuth from './requireServiceAccountWorkspacePermissionAuth'; -import requireSecretAuth from './requireSecretAuth'; -import requireSecretsAuth from './requireSecretsAuth'; -import validateRequest from './validateRequest'; +import requireAuth from "./requireAuth"; +import requireMfaAuth from "./requireMfaAuth"; +import requireBotAuth from "./requireBotAuth"; +import requireSignupAuth from "./requireSignupAuth"; +import requireWorkspaceAuth from "./requireWorkspaceAuth"; +import requireMembershipAuth from "./requireMembershipAuth"; +import requireMembershipOrgAuth from "./requireMembershipOrgAuth"; +import requireOrganizationAuth from "./requireOrganizationAuth"; +import requireIntegrationAuth from "./requireIntegrationAuth"; +import requireIntegrationAuthorizationAuth from "./requireIntegrationAuthorizationAuth"; +import requireServiceTokenAuth from "./requireServiceTokenAuth"; +import requireServiceTokenDataAuth from "./requireServiceTokenDataAuth"; +import requireServiceAccountAuth from "./requireServiceAccountAuth"; +import requireServiceAccountWorkspacePermissionAuth from "./requireServiceAccountWorkspacePermissionAuth"; +import requireSecretAuth from "./requireSecretAuth"; +import requireSecretsAuth from "./requireSecretsAuth"; +import validateRequest from "./validateRequest"; export { requireAuth, @@ -33,5 +33,5 @@ export { requireServiceAccountWorkspacePermissionAuth, requireSecretAuth, requireSecretsAuth, - validateRequest + validateRequest, }; diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 6aa73954b..dca6a82ba 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -1,9 +1,9 @@ -import * as Sentry from '@sentry/node'; -import { ErrorRequestHandler } from 'express'; -import { InternalServerError } from '../utils/errors'; -import { getLogger } from '../utils/logger'; -import RequestError, { LogLevel } from '../utils/requestError'; -import { getNodeEnv } from '../config'; +import * as Sentry from "@sentry/node"; +import { ErrorRequestHandler } from "express"; +import { InternalServerError } from "../utils/errors"; +import { getLogger } from "../utils/logger"; +import RequestError, { LogLevel } from "../utils/requestError"; +import { getNodeEnv } from "../config"; export const requestErrorHandler: ErrorRequestHandler = async ( error: RequestError | Error, @@ -24,7 +24,7 @@ export const requestErrorHandler: ErrorRequestHandler = async ( context: { exception: error.message }, stack: error.stack, }); - (await getLogger('backend-main')).log( + (await getLogger("backend-main")).log( (error).levelName.toLowerCase(), (error).message ); diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index 22de05f68..667524afd 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -1,26 +1,26 @@ -import jwt from 'jsonwebtoken'; -import { Request, Response, NextFunction } from 'express'; +import jwt from "jsonwebtoken"; +import { NextFunction, Request, Response } from "express"; import { - validateAuthMode, - getAuthUserPayload, - getAuthSTDPayload, getAuthAPIKeyPayload, - getAuthSAAKPayload -} from '../helpers/auth'; + getAuthSAAKPayload, + getAuthSTDPayload, + getAuthUserPayload, + validateAuthMode, +} from "../helpers/auth"; import { - IUser, IServiceAccount, - IServiceTokenData -} from '../models'; + IServiceTokenData, + IUser, +} from "../models"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; -import { getChannelFromUserAgent } from '../utils/posthog'; +} from "../variables"; +import { getChannelFromUserAgent } from "../utils/posthog"; -declare module 'jsonwebtoken' { +declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; } @@ -47,32 +47,32 @@ const requireAuth = ({ // and return token type [authTokenType] and value [authTokenValue] const { authMode, authTokenValue } = validateAuthMode({ headers: req.headers, - acceptedAuthModes + acceptedAuthModes, }); let authPayload: IUser | IServiceAccount | IServiceTokenData; switch (authMode) { case AUTH_MODE_SERVICE_ACCOUNT: authPayload = await getAuthSAAKPayload({ - authTokenValue + authTokenValue, }); req.serviceAccount = authPayload; break; case AUTH_MODE_SERVICE_TOKEN: authPayload = await getAuthSTDPayload({ - authTokenValue + authTokenValue, }); req.serviceTokenData = authPayload; break; case AUTH_MODE_API_KEY: authPayload = await getAuthAPIKeyPayload({ - authTokenValue + authTokenValue, }); req.user = authPayload; break; default: const { user, tokenVersionId } = await getAuthUserPayload({ - authTokenValue + authTokenValue, }); authPayload = user; req.user = user; @@ -89,10 +89,10 @@ const requireAuth = ({ req.authData = { authMode, authPayload, // User, ServiceAccount, ServiceTokenData - authChannel: getChannelFromUserAgent(req.headers['user-agent']), + authChannel: getChannelFromUserAgent(req.headers["user-agent"]), authIP: req.realIP, - authUserAgent: req.headers['user-agent'] ?? 'other', - tokenVersionId: req.tokenVersionId + authUserAgent: req.headers["user-agent"] ?? "other", + tokenVersionId: req.tokenVersionId, } return next(); diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts index 2de8217da..b0dc36956 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -1,14 +1,14 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForBot } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForBot } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; const requireBotAuth = ({ acceptedRoles, - locationBotId = 'params' + locationBotId = "params", }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; locationBotId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -17,7 +17,7 @@ const requireBotAuth = ({ req.bot = await validateClientForBot({ authData: req.authData, botId: new Types.ObjectId(botId), - acceptedRoles + acceptedRoles, }); next(); diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index 94d39a6c1..5adc04353 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -1,6 +1,6 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForIntegration } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForIntegration } from "../validation"; /** * Validate if user on request is a member of workspace with proper roles associated @@ -9,9 +9,9 @@ import { validateClientForIntegration } from '../validation'; * @param {String[]} obj.acceptedRoles - accepted workspace roles */ const requireIntegrationAuth = ({ - acceptedRoles + acceptedRoles, }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { integrationId } = req.params; @@ -19,7 +19,7 @@ const requireIntegrationAuth = ({ const { integration, accessToken } = await validateClientForIntegration({ authData: req.authData, integrationId: new Types.ObjectId(integrationId), - acceptedRoles + acceptedRoles, }); if (integration) { diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index 2ffa7e230..ccbe7e8a3 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -1,8 +1,8 @@ -import { Types } from 'mongoose'; -import { Request, Response, NextFunction } from 'express'; -import { validateClientForIntegrationAuth } from '../validation'; +import { Types } from "mongoose"; +import { NextFunction, Request, Response } from "express"; +import { validateClientForIntegrationAuth } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; /** * Validate if user on request is a member of workspace with proper roles associated @@ -14,20 +14,20 @@ type req = 'params' | 'body' | 'query'; const requireIntegrationAuthorizationAuth = ({ acceptedRoles, attachAccessToken = true, - location = 'params' + location = "params", }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; attachAccessToken?: boolean; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { integrationAuthId } = req[location]; - const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({ + const { integrationAuth, accessToken, accessId } = await validateClientForIntegrationAuth({ authData: req.authData, integrationAuthId: new Types.ObjectId(integrationAuthId), acceptedRoles, - attachAccessToken + attachAccessToken, }); if (integrationAuth) { @@ -38,6 +38,10 @@ const requireIntegrationAuthorizationAuth = ({ req.accessToken = accessToken; } + if (accessId) { + req.accessId = accessId; + } + return next(); }; }; diff --git a/backend/src/middleware/requireMembershipAuth.ts b/backend/src/middleware/requireMembershipAuth.ts index e03a6ea12..cc781677b 100644 --- a/backend/src/middleware/requireMembershipAuth.ts +++ b/backend/src/middleware/requireMembershipAuth.ts @@ -1,8 +1,8 @@ -import { Types } from 'mongoose'; -import { Request, Response, NextFunction } from 'express'; -import { validateClientForMembership } from '../validation'; +import { Types } from "mongoose"; +import { NextFunction, Request, Response } from "express"; +import { validateClientForMembership } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; /** * Validate membership with id [membershipId] and that user with id @@ -13,9 +13,9 @@ type req = 'params' | 'body' | 'query'; */ const requireMembershipAuth = ({ acceptedRoles, - locationMembershipId = 'params' + locationMembershipId = "params", }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; locationMembershipId: req }) => { return async ( @@ -28,7 +28,7 @@ const requireMembershipAuth = ({ req.targetMembership = await validateClientForMembership({ authData: req.authData, membershipId: new Types.ObjectId(membershipId), - acceptedRoles + acceptedRoles, }); return next(); diff --git a/backend/src/middleware/requireMembershipOrgAuth.ts b/backend/src/middleware/requireMembershipOrgAuth.ts index dda90cae8..00bcb6de8 100644 --- a/backend/src/middleware/requireMembershipOrgAuth.ts +++ b/backend/src/middleware/requireMembershipOrgAuth.ts @@ -1,8 +1,8 @@ -import { Types } from 'mongoose'; -import { Request, Response, NextFunction } from 'express'; -import { validateClientForMembershipOrg } from '../validation'; +import { Types } from "mongoose"; +import { NextFunction, Request, Response } from "express"; +import { validateClientForMembershipOrg } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; /** * Validate (organization) membership id [membershipId] and that user with id @@ -14,10 +14,10 @@ type req = 'params' | 'body' | 'query'; const requireMembershipOrgAuth = ({ acceptedRoles, acceptedStatuses, - locationMembershipOrgId = 'params' + locationMembershipOrgId = "params", }: { - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; locationMembershipOrgId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -27,7 +27,7 @@ const requireMembershipOrgAuth = ({ authData: req.authData, membershipOrgId: new Types.ObjectId(membershipId), acceptedRoles, - acceptedStatuses + acceptedStatuses, }); return next(); diff --git a/backend/src/middleware/requireMfaAuth.ts b/backend/src/middleware/requireMfaAuth.ts index ca0b3434e..7a7f7db4b 100644 --- a/backend/src/middleware/requireMfaAuth.ts +++ b/backend/src/middleware/requireMfaAuth.ts @@ -1,10 +1,10 @@ -import jwt from 'jsonwebtoken'; -import { Request, Response, NextFunction } from 'express'; -import { User } from '../models'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; -import { getJwtMfaSecret } from '../config'; +import jwt from "jsonwebtoken"; +import { NextFunction, Request, Response } from "express"; +import { User } from "../models"; +import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; +import { getJwtMfaSecret } from "../config"; -declare module 'jsonwebtoken' { +declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; } @@ -20,21 +20,21 @@ const requireMfaAuth = async ( next: NextFunction ) => { // JWT (temporary) authentication middleware for complete signup - const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] - if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) - if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) - if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: "Missing Authorization Header in the request header."})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"})) const decodedToken = ( jwt.verify(AUTH_TOKEN_VALUE, await getJwtMfaSecret()) ); const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + _id: decodedToken.userId, + }).select("+publicKey"); if (!user) - return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'})) + return next(UnauthorizedRequestError({message: "Unable to authenticate for User account completion. Try logging in again"})) req.user = user; return next(); diff --git a/backend/src/middleware/requireOrganizationAuth.ts b/backend/src/middleware/requireOrganizationAuth.ts index 5f7ef151d..f28660d0e 100644 --- a/backend/src/middleware/requireOrganizationAuth.ts +++ b/backend/src/middleware/requireOrganizationAuth.ts @@ -1,8 +1,8 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForOrganization } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForOrganization } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; /** * Validate if user on request is a member with proper roles for organization @@ -14,10 +14,10 @@ type req = 'params' | 'body' | 'query'; const requireOrganizationAuth = ({ acceptedRoles, acceptedStatuses, - locationOrganizationId = 'params' + locationOrganizationId = "params", }: { - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; locationOrganizationId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -27,7 +27,7 @@ const requireOrganizationAuth = ({ authData: req.authData, organizationId: new Types.ObjectId(organizationId), acceptedRoles, - acceptedStatuses + acceptedStatuses, }); if (organization) { diff --git a/backend/src/middleware/requireSecretAuth.ts b/backend/src/middleware/requireSecretAuth.ts index 4fda73a23..06ad6019e 100644 --- a/backend/src/middleware/requireSecretAuth.ts +++ b/backend/src/middleware/requireSecretAuth.ts @@ -1,6 +1,6 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForSecret } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForSecret } from "../validation"; // note: used for old /v1/secret and /v2/secret routes. // newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception @@ -14,9 +14,9 @@ import { validateClientForSecret } from '../validation'; */ const requireSecretAuth = ({ acceptedRoles, - requiredPermissions + requiredPermissions, }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; requiredPermissions: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -26,7 +26,7 @@ const requireSecretAuth = ({ authData: req.authData, secretId: new Types.ObjectId(secretId), acceptedRoles, - requiredPermissions + requiredPermissions, }); req._secret = secret; diff --git a/backend/src/middleware/requireSecretsAuth.ts b/backend/src/middleware/requireSecretsAuth.ts index f25487b97..3dabdb25c 100644 --- a/backend/src/middleware/requireSecretsAuth.ts +++ b/backend/src/middleware/requireSecretsAuth.ts @@ -1,10 +1,10 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForSecrets } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForSecrets } from "../validation"; const requireSecretsAuth = ({ acceptedRoles, - requiredPermissions = [] + requiredPermissions = [], }: { acceptedRoles: string[]; requiredPermissions?: string[]; @@ -13,18 +13,18 @@ const requireSecretsAuth = ({ let secretIds = []; if (Array.isArray(req.body.secrets)) { secretIds = req.body.secrets.map((s: any) => s.id); - } else if (typeof req.body.secrets === 'object') { + } else if (typeof req.body.secrets === "object") { secretIds = [req.body.secrets.id]; } else if (Array.isArray(req.body.secretIds)) { secretIds = req.body.secretIds; - } else if (typeof req.body.secretIds === 'string') { + } else if (typeof req.body.secretIds === "string") { secretIds = [req.body.secretIds]; } req.secrets = await validateClientForSecrets({ authData: req.authData, secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), - requiredPermissions + requiredPermissions, }); return next(); diff --git a/backend/src/middleware/requireServiceAccountAuth.ts b/backend/src/middleware/requireServiceAccountAuth.ts index da690c7bb..f7e423b79 100644 --- a/backend/src/middleware/requireServiceAccountAuth.ts +++ b/backend/src/middleware/requireServiceAccountAuth.ts @@ -1,14 +1,14 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForServiceAccount } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForServiceAccount } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; const requireServiceAccountAuth = ({ acceptedRoles, acceptedStatuses, - locationServiceAccountId = 'params', - requiredPermissions = [] + locationServiceAccountId = "params", + requiredPermissions = [], }: { acceptedRoles: string[]; acceptedStatuses: string[]; @@ -21,7 +21,7 @@ const requireServiceAccountAuth = ({ req.serviceAccount = await validateClientForServiceAccount({ authData: req.authData, serviceAccountId: new Types.ObjectId(serviceAccountId), - requiredPermissions + requiredPermissions, }); next(); diff --git a/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts b/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts index 0ceb4f598..535cd2068 100644 --- a/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts +++ b/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts @@ -1,21 +1,21 @@ -import { Request, Response, NextFunction } from 'express'; -import { ServiceAccount, ServiceAccountWorkspacePermission } from '../models'; +import { NextFunction, Request, Response } from "express"; +import { ServiceAccount, ServiceAccountWorkspacePermission } from "../models"; import { - ServiceAccountNotFoundError -} from '../utils/errors'; + ServiceAccountNotFoundError, +} from "../utils/errors"; import { - validateMembershipOrg -} from '../helpers/membershipOrg'; + validateMembershipOrg, +} from "../helpers/membershipOrg"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; const requireServiceAccountWorkspacePermissionAuth = ({ acceptedRoles, acceptedStatuses, - location = 'params' + location = "params", }: { - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -23,13 +23,13 @@ const requireServiceAccountWorkspacePermissionAuth = ({ const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findById(serviceAccountWorkspacePermissionId); if (!serviceAccountWorkspacePermission) { - return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account workspace permission' })); + return next(ServiceAccountNotFoundError({ message: "Failed to locate Service Account workspace permission" })); } const serviceAccount = await ServiceAccount.findById(serviceAccountWorkspacePermission.serviceAccount); if (!serviceAccount) { - return next(ServiceAccountNotFoundError({ message: 'Failed to locate Service Account' })); + return next(ServiceAccountNotFoundError({ message: "Failed to locate Service Account" })); } if (serviceAccount.user.toString() !== req.user.id.toString()) { @@ -39,7 +39,7 @@ const requireServiceAccountWorkspacePermissionAuth = ({ userId: req.user._id, organizationId: serviceAccount.organization, acceptedRoles, - acceptedStatuses + acceptedStatuses, }); } diff --git a/backend/src/middleware/requireServiceTokenAuth.ts b/backend/src/middleware/requireServiceTokenAuth.ts index 5db0dcda5..340f03cc0 100644 --- a/backend/src/middleware/requireServiceTokenAuth.ts +++ b/backend/src/middleware/requireServiceTokenAuth.ts @@ -1,11 +1,11 @@ -import jwt from 'jsonwebtoken'; -import { Request, Response, NextFunction } from 'express'; -import { ServiceToken } from '../models'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; -import { getJwtServiceSecret } from '../config'; +import jwt from "jsonwebtoken"; +import { NextFunction, Request, Response } from "express"; +import { ServiceToken } from "../models"; +import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; +import { getJwtServiceSecret } from "../config"; // TODO: deprecate -declare module 'jsonwebtoken' { +declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; } @@ -26,23 +26,23 @@ const requireServiceTokenAuth = async ( ) => { // JWT service token middleware - const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] - if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: "Missing Authorization Header in the request header."})) //TODO: Determine what is the actual Token Type for Service Token Authentication (ex. Bearer) //if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(UnauthorizedRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) - if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"})) const decodedToken = ( jwt.verify(AUTH_TOKEN_VALUE, await getJwtServiceSecret()) ); const serviceToken = await ServiceToken.findOne({ - _id: decodedToken.serviceTokenId + _id: decodedToken.serviceTokenId, }) - .populate('user', '+publicKey') - .select('+encryptedKey +publicKey +nonce'); + .populate("user", "+publicKey") + .select("+encryptedKey +publicKey +nonce"); - if (!serviceToken) return next(UnauthorizedRequestError({message: 'The service token does not match the record in the database'})) + if (!serviceToken) return next(UnauthorizedRequestError({message: "The service token does not match the record in the database"})) req.serviceToken = serviceToken; return next(); diff --git a/backend/src/middleware/requireServiceTokenDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts index be93fd799..df5ca5ef1 100644 --- a/backend/src/middleware/requireServiceTokenDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,14 +1,14 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForServiceTokenData } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForServiceTokenData } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; const requireServiceTokenDataAuth = ({ acceptedRoles, - location = 'params' + location = "params", }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { @@ -17,7 +17,7 @@ const requireServiceTokenDataAuth = ({ req.serviceTokenData = await validateClientForServiceTokenData({ authData: req.authData, serviceTokenDataId: new Types.ObjectId(serviceTokenDataId), - acceptedRoles + acceptedRoles, }); next(); diff --git a/backend/src/middleware/requireSignupAuth.ts b/backend/src/middleware/requireSignupAuth.ts index 6a0fd0b6e..3c5c48d12 100644 --- a/backend/src/middleware/requireSignupAuth.ts +++ b/backend/src/middleware/requireSignupAuth.ts @@ -1,10 +1,10 @@ -import jwt from 'jsonwebtoken'; -import { Request, Response, NextFunction } from 'express'; -import { User } from '../models'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; -import { getJwtSignupSecret } from '../config'; +import jwt from "jsonwebtoken"; +import { NextFunction, Request, Response } from "express"; +import { User } from "../models"; +import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; +import { getJwtSignupSecret } from "../config"; -declare module 'jsonwebtoken' { +declare module "jsonwebtoken" { export interface UserIDJwtPayload extends jwt.JwtPayload { userId: string; } @@ -21,21 +21,21 @@ const requireSignupAuth = async ( ) => { // JWT (temporary) authentication middleware for complete signup - const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null] - if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`})) - if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) - if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'})) + const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers["authorization"]?.split(" ", 2) ?? [null, null] + if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: "Missing Authorization Header in the request header."})) + if(AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`})) + if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: "Missing Authorization Body in the request header"})) const decodedToken = ( jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret()) ); const user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + _id: decodedToken.userId, + }).select("+publicKey"); if (!user) - return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'})) + return next(UnauthorizedRequestError({message: "Unable to authenticate for User account completion. Try logging in again"})) req.user = user; return next(); diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index da517b998..197995a65 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,8 +1,8 @@ -import { Request, Response, NextFunction } from 'express'; -import { Types } from 'mongoose'; -import { validateClientForWorkspace } from '../validation'; +import { NextFunction, Request, Response } from "express"; +import { Types } from "mongoose"; +import { validateClientForWorkspace } from "../validation"; -type req = 'params' | 'body' | 'query'; +type req = "params" | "body" | "query"; /** * Validate if user on request is a member with proper roles for workspace @@ -16,13 +16,15 @@ const requireWorkspaceAuth = ({ locationWorkspaceId, locationEnvironment = undefined, requiredPermissions = [], - requireBlindIndicesEnabled = false + requireBlindIndicesEnabled = false, + requireE2EEOff = false, }: { - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; locationWorkspaceId: req; locationEnvironment?: req | undefined; requiredPermissions?: string[]; requireBlindIndicesEnabled?: boolean; + requireE2EEOff?: boolean; }) => { return async (req: Request, res: Response, next: NextFunction) => { const workspaceId = req[locationWorkspaceId]?.workspaceId; @@ -35,7 +37,8 @@ const requireWorkspaceAuth = ({ environment, acceptedRoles, requiredPermissions, - requireBlindIndicesEnabled + requireBlindIndicesEnabled, + requireE2EEOff, }); if (membership) { diff --git a/backend/src/middleware/validateRequest.ts b/backend/src/middleware/validateRequest.ts index 1b0364766..56ea2653c 100644 --- a/backend/src/middleware/validateRequest.ts +++ b/backend/src/middleware/validateRequest.ts @@ -1,6 +1,6 @@ -import { Request, Response, NextFunction } from 'express'; -import { validationResult } from 'express-validator'; -import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors'; +import { NextFunction, Request, Response } from "express"; +import { validationResult } from "express-validator"; +import { UnauthorizedRequestError, ValidationError } from "../utils/errors"; /** * Validate intended inputs on [req] via express-validator @@ -20,7 +20,7 @@ const validate = (req: Request, res: Response, next: NextFunction) => { return next(); } catch (err) { - return next(UnauthorizedRequestError({ message: 'Unauthenticated requests are not allowed. Try logging in' })) + return next(UnauthorizedRequestError({ message: "Unauthenticated requests are not allowed. Try logging in" })) } }; diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/apiKeyData.ts index 1b6831730..622e62be1 100644 --- a/backend/src/models/apiKeyData.ts +++ b/backend/src/models/apiKeyData.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IAPIKeyData { name: string; @@ -12,30 +12,30 @@ const apiKeyDataSchema = new Schema( { name: { type: String, - required: true + required: true, }, user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, lastUsed: { - type: Date + type: Date, }, expiresAt: { - type: Date + type: Date, }, secretHash: { type: String, required: true, - select: false - } + select: false, + }, }, { - timestamps: true + timestamps: true, } ); -const APIKeyData = model('APIKeyData', apiKeyDataSchema); +const APIKeyData = model("APIKeyData", apiKeyDataSchema); export default APIKeyData; diff --git a/backend/src/models/backupPrivateKey.ts b/backend/src/models/backupPrivateKey.ts index 09bcbb588..01f0dae21 100644 --- a/backend/src/models/backupPrivateKey.ts +++ b/backend/src/models/backupPrivateKey.ts @@ -1,9 +1,9 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; import { ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 -} from '../variables'; +} from "../variables"; export interface IBackupPrivateKey { _id: Types.ObjectId; @@ -13,7 +13,7 @@ export interface IBackupPrivateKey { tag: string; salt: string; algorithm: string; - keyEncoding: 'base64' | 'utf8'; + keyEncoding: "base64" | "utf8"; verifier: string; } @@ -21,55 +21,55 @@ const backupPrivateKeySchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, encryptedPrivateKey: { type: String, select: false, - required: true + required: true, }, iv: { type: String, select: false, - required: true + required: true, }, tag: { type: String, select: false, - required: true + required: true, }, algorithm: { // the encryption algorithm used type: String, enum: [ALGORITHM_AES_256_GCM], - required: true + required: true, }, keyEncoding: { type: String, enum: [ ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + ENCODING_SCHEME_BASE64, ], - required: true + required: true, }, salt: { type: String, select: false, - required: true + required: true, }, verifier: { type: String, select: false, - required: true - } + required: true, + }, }, { - timestamps: true + timestamps: true, } ); const BackupPrivateKey = model( - 'BackupPrivateKey', + "BackupPrivateKey", backupPrivateKeySchema ); diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts index 5755bfd8e..96107a231 100644 --- a/backend/src/models/bot.ts +++ b/backend/src/models/bot.ts @@ -1,10 +1,9 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; import { ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_HEX, - ENCODING_SCHEME_BASE64 -} from '../variables'; +} from "../variables"; export interface IBot { _id: Types.ObjectId; @@ -15,66 +14,66 @@ export interface IBot { encryptedPrivateKey: string; iv: string; tag: string; - algorithm: 'aes-256-gcm'; - keyEncoding: 'base64' | 'utf8'; + algorithm: "aes-256-gcm"; + keyEncoding: "base64" | "utf8"; } const botSchema = new Schema( { name: { type: String, - required: true + required: true, }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + ref: "Workspace", + required: true, }, isActive: { type: Boolean, required: true, - default: false + default: false, }, publicKey: { type: String, - required: true + required: true, }, encryptedPrivateKey: { type: String, required: true, - select: false + select: false, }, iv: { type: String, required: true, - select: false + select: false, }, tag: { type: String, required: true, - select: false + select: false, }, algorithm: { // the encryption algorithm used type: String, enum: [ALGORITHM_AES_256_GCM], required: true, - select: false + select: false, }, keyEncoding: { type: String, enum: [ ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + ENCODING_SCHEME_BASE64, ], required: true, - select: false - } + select: false, + }, }, { - timestamps: true + timestamps: true, } ); -const Bot = model('Bot', botSchema); +const Bot = model("Bot", botSchema); export default Bot; diff --git a/backend/src/models/botKey.ts b/backend/src/models/botKey.ts index 79555cd53..b7be364dd 100644 --- a/backend/src/models/botKey.ts +++ b/backend/src/models/botKey.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IBotKey { _id: Types.ObjectId; @@ -13,33 +13,33 @@ const botKeySchema = new Schema( { encryptedKey: { type: String, - required: true + required: true, }, nonce: { type: String, - required: true + required: true, }, sender: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, bot: { type: Schema.Types.ObjectId, - ref: 'Bot', - required: true + ref: "Bot", + required: true, }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - } + ref: "Workspace", + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const BotKey = model('BotKey', botKeySchema); +const BotKey = model("BotKey", botKeySchema); export default BotKey; diff --git a/backend/src/models/folder.ts b/backend/src/models/folder.ts index ac0358287..46f532c7d 100644 --- a/backend/src/models/folder.ts +++ b/backend/src/models/folder.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from "mongoose"; +import { Schema, Types, model } from "mongoose"; export type TFolderRootSchema = { _id: Types.ObjectId; diff --git a/backend/src/models/incidentContactOrg.ts b/backend/src/models/incidentContactOrg.ts index e1a969e11..16e5e4f02 100644 --- a/backend/src/models/incidentContactOrg.ts +++ b/backend/src/models/incidentContactOrg.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IIncidentContactOrg { _id: Types.ObjectId; @@ -10,21 +10,21 @@ const incidentContactOrgSchema = new Schema( { email: { type: String, - required: true + required: true, }, organization: { type: Schema.Types.ObjectId, - ref: 'Organization', - required: true - } + ref: "Organization", + required: true, + }, }, { - timestamps: true + timestamps: true, } ); const IncidentContactOrg = model( - 'IncidentContactOrg', + "IncidentContactOrg", incidentContactOrgSchema ); diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index cb4ee672a..bf997811a 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -1,28 +1,28 @@ -import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey'; -import Bot, { IBot } from './bot'; -import BotKey, { IBotKey } from './botKey'; -import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg'; -import Integration, { IIntegration } from './integration'; -import IntegrationAuth, { IIntegrationAuth } from './integrationAuth'; -import Key, { IKey } from './key'; -import Membership, { IMembership } from './membership'; -import MembershipOrg, { IMembershipOrg } from './membershipOrg'; -import Organization, { IOrganization } from './organization'; -import Secret, { ISecret } from './secret'; -import SecretBlindIndexData, { ISecretBlindIndexData } from './secretBlindIndexData'; -import ServiceToken, { IServiceToken } from './serviceToken'; -import ServiceAccount, { IServiceAccount } from './serviceAccount'; // new -import ServiceAccountKey, { IServiceAccountKey } from './serviceAccountKey'; // new -import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from './serviceAccountOrganizationPermission'; // new -import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from './serviceAccountWorkspacePermission'; // new -import TokenData, { ITokenData } from './tokenData'; -import User,{ AuthProvider, IUser } from './user'; -import UserAction, { IUserAction } from './userAction'; -import Workspace, { IWorkspace } from './workspace'; -import ServiceTokenData, { IServiceTokenData } from './serviceTokenData'; -import APIKeyData, { IAPIKeyData } from './apiKeyData'; -import LoginSRPDetail, { ILoginSRPDetail } from './loginSRPDetail'; -import TokenVersion, { ITokenVersion } from './tokenVersion'; +import BackupPrivateKey, { IBackupPrivateKey } from "./backupPrivateKey"; +import Bot, { IBot } from "./bot"; +import BotKey, { IBotKey } from "./botKey"; +import IncidentContactOrg, { IIncidentContactOrg } from "./incidentContactOrg"; +import Integration, { IIntegration } from "./integration"; +import IntegrationAuth, { IIntegrationAuth } from "./integrationAuth"; +import Key, { IKey } from "./key"; +import Membership, { IMembership } from "./membership"; +import MembershipOrg, { IMembershipOrg } from "./membershipOrg"; +import Organization, { IOrganization } from "./organization"; +import Secret, { ISecret } from "./secret"; +import SecretBlindIndexData, { ISecretBlindIndexData } from "./secretBlindIndexData"; +import ServiceToken, { IServiceToken } from "./serviceToken"; +import ServiceAccount, { IServiceAccount } from "./serviceAccount"; // new +import ServiceAccountKey, { IServiceAccountKey } from "./serviceAccountKey"; // new +import ServiceAccountOrganizationPermission, { IServiceAccountOrganizationPermission } from "./serviceAccountOrganizationPermission"; // new +import ServiceAccountWorkspacePermission, { IServiceAccountWorkspacePermission } from "./serviceAccountWorkspacePermission"; // new +import TokenData, { ITokenData } from "./tokenData"; +import User,{ AuthProvider, IUser } from "./user"; +import UserAction, { IUserAction } from "./userAction"; +import Workspace, { IWorkspace } from "./workspace"; +import ServiceTokenData, { IServiceTokenData } from "./serviceTokenData"; +import APIKeyData, { IAPIKeyData } from "./apiKeyData"; +import LoginSRPDetail, { ILoginSRPDetail } from "./loginSRPDetail"; +import TokenVersion, { ITokenVersion } from "./tokenVersion"; export { AuthProvider, @@ -75,5 +75,5 @@ export { LoginSRPDetail, ILoginSRPDetail, TokenVersion, - ITokenVersion + ITokenVersion, }; diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index d4fbae807..d94cc9faa 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -1,21 +1,22 @@ -import { Schema, model, Types } from "mongoose"; +import { Schema, Types, model } from "mongoose"; import { - INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_CHECKLY, + INTEGRATION_CIRCLECI, + INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, - INTEGRATION_RENDER, + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_HEROKU, + INTEGRATION_NETLIFY, INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, + INTEGRATION_RENDER, INTEGRATION_SUPABASE, - INTEGRATION_CHECKLY, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_TRAVISCI, + INTEGRATION_VERCEL, } from "../variables"; export interface IIntegration { @@ -33,23 +34,25 @@ export interface IIntegration { targetServiceId: string; path: string; region: string; + secretPath: string; integration: - | 'azure-key-vault' - | 'aws-parameter-store' - | 'aws-secret-manager' - | 'heroku' - | 'vercel' - | 'netlify' - | 'github' - | 'gitlab' - | 'render' - | 'railway' - | 'flyio' - | 'circleci' - | 'travisci' - | 'supabase' - | 'checkly' - | 'hashicorp-vault'; + | "azure-key-vault" + | "aws-parameter-store" + | "aws-secret-manager" + | "heroku" + | "vercel" + | "netlify" + | "github" + | "gitlab" + | "render" + | "railway" + | "flyio" + | "circleci" + | "travisci" + | "supabase" + | "checkly" + | "hashicorp-vault" + | "cloudflare-pages"; integrationAuth: Types.ObjectId; } @@ -71,7 +74,7 @@ const integrationSchema = new Schema( url: { // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) type: String, - default: null + default: null, }, app: { // name of app in provider @@ -90,17 +93,17 @@ const integrationSchema = new Schema( }, targetEnvironmentId: { type: String, - default: null + default: null, }, targetService: { // railway-specific service type: String, - default: null + default: null, }, targetServiceId: { // railway-specific service type: String, - default: null + default: null, }, owner: { // github-specific repo owner-login @@ -111,12 +114,12 @@ const integrationSchema = new Schema( // aws-parameter-store-specific path // (also) vercel preview-branch type: String, - default: null + default: null, }, region: { // aws-parameter-store-specific path type: String, - default: null + default: null, }, integration: { type: String, @@ -136,7 +139,8 @@ const integrationSchema = new Schema( INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, ], required: true, }, @@ -145,6 +149,11 @@ const integrationSchema = new Schema( ref: "IntegrationAuth", required: true, }, + secretPath: { + type: String, + required: true, + default: "/", + }, }, { timestamps: true, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index cc28c9fd6..2d56c2063 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,29 +1,30 @@ -import { Schema, model, Types, Document } from "mongoose"; +import { Document, Schema, Types, model } from "mongoose"; import { - INTEGRATION_AZURE_KEY_VAULT, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8, INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_SECRET_MANAGER, - INTEGRATION_HEROKU, - INTEGRATION_VERCEL, - INTEGRATION_NETLIFY, + INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_CIRCLECI, + INTEGRATION_FLYIO, INTEGRATION_GITHUB, INTEGRATION_GITLAB, - INTEGRATION_RENDER, - INTEGRATION_RAILWAY, - INTEGRATION_FLYIO, - INTEGRATION_CIRCLECI, - INTEGRATION_TRAVISCI, - INTEGRATION_SUPABASE, INTEGRATION_HASHICORP_VAULT, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + INTEGRATION_HEROKU, + INTEGRATION_NETLIFY, + INTEGRATION_RAILWAY, + INTEGRATION_RENDER, + INTEGRATION_SUPABASE, + INTEGRATION_CLOUDFLARE_PAGES, + INTEGRATION_TRAVISCI, + INTEGRATION_VERCEL, } from "../variables"; export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly'; + integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager' | 'checkly' | 'cloudflare-pages'; teamId: string; accountId: string; url: string; @@ -37,8 +38,8 @@ export interface IIntegrationAuth extends Document { accessCiphertext?: string; accessIV?: string; accessTag?: string; - algorithm?: 'aes-256-gcm'; - keyEncoding?: 'utf8' | 'base64'; + algorithm?: "aes-256-gcm"; + keyEncoding?: "utf8" | "base64"; accessExpiresAt?: Date; } @@ -66,7 +67,8 @@ const integrationAuthSchema = new Schema( INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES, ], required: true, }, @@ -76,11 +78,11 @@ const integrationAuthSchema = new Schema( }, url: { // for any self-hosted integrations (e.g. self-hosted hashicorp-vault) - type: String + type: String, }, namespace: { // hashicorp-vault-specific integration param - type: String + type: String, }, accountId: { // netlify-specific integration param @@ -100,15 +102,15 @@ const integrationAuthSchema = new Schema( }, accessIdCiphertext: { type: String, - select: false + select: false, }, accessIdIV: { type: String, - select: false + select: false, }, accessIdTag: { type: String, - select: false + select: false, }, accessCiphertext: { type: String, @@ -129,16 +131,16 @@ const integrationAuthSchema = new Schema( algorithm: { // the encryption algorithm used type: String, enum: [ALGORITHM_AES_256_GCM], - required: true + required: true, }, keyEncoding: { type: String, enum: [ ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + ENCODING_SCHEME_BASE64, ], - required: true - } + required: true, + }, }, { timestamps: true, diff --git a/backend/src/models/key.ts b/backend/src/models/key.ts index cd3d3fdd5..faa37cd86 100644 --- a/backend/src/models/key.ts +++ b/backend/src/models/key.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IKey { _id: Types.ObjectId; @@ -13,33 +13,33 @@ const keySchema = new Schema( { encryptedKey: { type: String, - required: true + required: true, }, nonce: { type: String, - required: true + required: true, }, sender: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, receiver: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - } + ref: "Workspace", + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const Key = model('Key', keySchema); +const Key = model("Key", keySchema); export default Key; diff --git a/backend/src/models/loginSRPDetail.ts b/backend/src/models/loginSRPDetail.ts index dde59233d..8e9e121c5 100644 --- a/backend/src/models/loginSRPDetail.ts +++ b/backend/src/models/loginSRPDetail.ts @@ -1,4 +1,4 @@ -import mongoose, { Schema, model, Types } from 'mongoose'; +import mongoose, { Schema, Types, model } from "mongoose"; export interface ILoginSRPDetail { _id: Types.ObjectId; @@ -13,17 +13,17 @@ const loginSRPDetailSchema = new Schema( { clientPublicKey: { type: String, - required: true + required: true, }, email: { type: String, - unique: true + unique: true, }, serverBInt: { type: mongoose.Schema.Types.Buffer }, - expireAt: { type: Date } + expireAt: { type: Date }, } ); -const LoginSRPDetail = model('LoginSRPDetail', loginSRPDetailSchema); +const LoginSRPDetail = model("LoginSRPDetail", loginSRPDetailSchema); export default LoginSRPDetail; diff --git a/backend/src/models/membership.ts b/backend/src/models/membership.ts index 55b8f2048..0fca743b4 100644 --- a/backend/src/models/membership.ts +++ b/backend/src/models/membership.ts @@ -1,5 +1,5 @@ -import { Schema, model, Types } from 'mongoose'; -import { ADMIN, MEMBER } from '../variables'; +import { Schema, Types, model } from "mongoose"; +import { ADMIN, MEMBER } from "../variables"; export interface IMembershipPermission { environmentSlug: string, @@ -11,7 +11,7 @@ export interface IMembership { user: Types.ObjectId; inviteEmail?: string; workspace: Types.ObjectId; - role: 'admin' | 'member'; + role: "admin" | "member"; deniedPermissions: IMembershipPermission[] } @@ -19,15 +19,15 @@ const membershipSchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: 'User' + ref: "User", }, inviteEmail: { - type: String + type: String, }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + ref: "Workspace", + required: true, }, deniedPermissions: { type: [ @@ -35,23 +35,23 @@ const membershipSchema = new Schema( environmentSlug: String, ability: { type: String, - enum: ['read', 'write'] + enum: ["read", "write"], }, }, ], - default: [] + default: [], }, role: { type: String, enum: [ADMIN, MEMBER], - required: true - } + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const Membership = model('Membership', membershipSchema); +const Membership = model("Membership", membershipSchema); export default Membership; diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index 540a4451b..74a09b805 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -1,46 +1,46 @@ -import { Schema, model, Types, Document } from 'mongoose'; -import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables'; +import { Document, Schema, Types, model } from "mongoose"; +import { ACCEPTED, ADMIN, INVITED, MEMBER, OWNER } from "../variables"; export interface IMembershipOrg extends Document { _id: Types.ObjectId; user: Types.ObjectId; inviteEmail: string; organization: Types.ObjectId; - role: 'owner' | 'admin' | 'member'; - status: 'invited' | 'accepted'; + role: "owner" | "admin" | "member"; + status: "invited" | "accepted"; } const membershipOrgSchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: 'User' + ref: "User", }, inviteEmail: { - type: String + type: String, }, organization: { type: Schema.Types.ObjectId, - ref: 'Organization' + ref: "Organization", }, role: { type: String, enum: [OWNER, ADMIN, MEMBER], - required: true + required: true, }, status: { type: String, enum: [INVITED, ACCEPTED], - required: true - } + required: true, + }, }, { - timestamps: true + timestamps: true, } ); const MembershipOrg = model( - 'MembershipOrg', + "MembershipOrg", membershipOrgSchema ); diff --git a/backend/src/models/organization.ts b/backend/src/models/organization.ts index a39d42cd0..bafcc05f8 100644 --- a/backend/src/models/organization.ts +++ b/backend/src/models/organization.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IOrganization { _id: Types.ObjectId; @@ -10,17 +10,17 @@ const organizationSchema = new Schema( { name: { type: String, - required: true + required: true, }, customerId: { - type: String - } + type: String, + }, }, { - timestamps: true + timestamps: true, } ); -const Organization = model('Organization', organizationSchema); +const Organization = model("Organization", organizationSchema); export default Organization; diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index ff34d99b1..34a4d7501 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -1,10 +1,10 @@ -import { Schema, model, Types } from "mongoose"; +import { Schema, Types, model } from "mongoose"; import { - SECRET_SHARED, - SECRET_PERSONAL, ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8, + SECRET_PERSONAL, + SECRET_SHARED, } from "../variables"; export interface ISecret { diff --git a/backend/src/models/secretApprovalRequest.ts b/backend/src/models/secretApprovalRequest.ts index 9fa897e25..008274dd9 100644 --- a/backend/src/models/secretApprovalRequest.ts +++ b/backend/src/models/secretApprovalRequest.ts @@ -1,5 +1,5 @@ -import mongoose, { Schema, model } from 'mongoose'; -import Secret, { ISecret } from './secret'; +import mongoose, { Schema, model } from "mongoose"; +import Secret, { ISecret } from "./secret"; interface ISecretApprovalRequest { secret: mongoose.Types.ObjectId; @@ -18,66 +18,66 @@ interface IApprover { } export enum ApprovalStatus { - PENDING = 'pending', - APPROVED = 'approved', - REJECTED = 'rejected' + PENDING = "pending", + APPROVED = "approved", + REJECTED = "rejected" } export enum RequestType { - UPDATE = 'update', - DELETE = 'delete', - CREATE = 'create' + UPDATE = "update", + DELETE = "delete", + CREATE = "create" } const approverSchema = new mongoose.Schema({ user: { type: mongoose.Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, status: { type: String, enum: [ApprovalStatus], - default: ApprovalStatus.PENDING - } + default: ApprovalStatus.PENDING, + }, }); const secretApprovalRequestSchema = new Schema( { secret: { type: mongoose.Schema.Types.ObjectId, - ref: 'Secret' + ref: "Secret", }, requestedChanges: Secret, requestedBy: { type: mongoose.Schema.Types.ObjectId, - ref: 'User' + ref: "User", }, approvers: [approverSchema], status: { type: String, enum: ApprovalStatus, - default: ApprovalStatus.PENDING + default: ApprovalStatus.PENDING, }, timestamp: { type: Date, - default: Date.now + default: Date.now, }, requestType: { type: String, enum: RequestType, - required: true + required: true, }, requestId: { type: String, - required: false - } + required: false, + }, }, { - timestamps: true + timestamps: true, } ); -const SecretApprovalRequest = model('SecretApprovalRequest', secretApprovalRequestSchema); +const SecretApprovalRequest = model("SecretApprovalRequest", secretApprovalRequestSchema); export default SecretApprovalRequest; diff --git a/backend/src/models/secretBlindIndexData.ts b/backend/src/models/secretBlindIndexData.ts index 885faaff6..ca277d19e 100644 --- a/backend/src/models/secretBlindIndexData.ts +++ b/backend/src/models/secretBlindIndexData.ts @@ -1,9 +1,9 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Document, Schema, Types, model } from "mongoose"; import { ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 -} from '../variables'; +} from "../variables"; export interface ISecretBlindIndexData extends Document { _id: Types.ObjectId; @@ -11,48 +11,48 @@ export interface ISecretBlindIndexData extends Document { encryptedSaltCiphertext: string; saltIV: string; saltTag: string; - algorithm: 'aes-256-gcm'; - keyEncoding: 'base64' | 'utf8' + algorithm: "aes-256-gcm"; + keyEncoding: "base64" | "utf8" } const secretBlindIndexDataSchema = new Schema( { workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + ref: "Workspace", + required: true, }, encryptedSaltCiphertext: { // TODO: make these select: false type: String, - required: true + required: true, }, saltIV: { type: String, - required: true + required: true, }, saltTag: { type: String, - required: true + required: true, }, algorithm: { type: String, enum: [ALGORITHM_AES_256_GCM], required: true, - select: false + select: false, }, keyEncoding: { type: String, enum: [ ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + ENCODING_SCHEME_BASE64, ], required: true, - select: false - } + select: false, + }, } ); -const SecretBlindIndexData = model('SecretBlindIndexData', secretBlindIndexDataSchema); +const SecretBlindIndexData = model("SecretBlindIndexData", secretBlindIndexDataSchema); export default SecretBlindIndexData; \ No newline at end of file diff --git a/backend/src/models/serviceAccount.ts b/backend/src/models/serviceAccount.ts index 9ff9dcb03..090d55c21 100644 --- a/backend/src/models/serviceAccount.ts +++ b/backend/src/models/serviceAccount.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Document, Schema, Types, model } from "mongoose"; export interface IServiceAccount extends Document { _id: Types.ObjectId; @@ -15,39 +15,39 @@ const serviceAccountSchema = new Schema( { name: { type: String, - required: true + required: true, }, organization: { type: Schema.Types.ObjectId, - ref: 'Organization', - required: true + ref: "Organization", + required: true, }, user: { // user who created the service account type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, publicKey: { type: String, - required: true + required: true, }, lastUsed: { - type: Date + type: Date, }, expiresAt: { - type: Date + type: Date, }, secretHash: { type: String, required: true, - select: false - } + select: false, + }, }, { - timestamps: true + timestamps: true, } ); -const ServiceAccount = model('ServiceAccount', serviceAccountSchema); +const ServiceAccount = model("ServiceAccount", serviceAccountSchema); export default ServiceAccount; \ No newline at end of file diff --git a/backend/src/models/serviceAccountKey.ts b/backend/src/models/serviceAccountKey.ts index 637ac188b..d442dcb08 100644 --- a/backend/src/models/serviceAccountKey.ts +++ b/backend/src/models/serviceAccountKey.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IServiceAccountKey { _id: Types.ObjectId; @@ -13,32 +13,32 @@ const serviceAccountKeySchema = new Schema( { encryptedKey: { type: String, - required: true + required: true, }, nonce: { type: String, - required: true + required: true, }, sender: { type: Schema.Types.ObjectId, - required: true + required: true, }, serviceAccount: { type: Schema.Types.ObjectId, - ref: 'ServiceAccount', - required: true + ref: "ServiceAccount", + required: true, }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - } + ref: "Workspace", + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const ServiceAccountKey = model('ServiceAccountKey', serviceAccountKeySchema); +const ServiceAccountKey = model("ServiceAccountKey", serviceAccountKeySchema); export default ServiceAccountKey; diff --git a/backend/src/models/serviceAccountOrganizationPermission.ts b/backend/src/models/serviceAccountOrganizationPermission.ts index 6454bc6a0..4519bd832 100644 --- a/backend/src/models/serviceAccountOrganizationPermission.ts +++ b/backend/src/models/serviceAccountOrganizationPermission.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Document, Schema, Types, model } from "mongoose"; export interface IServiceAccountOrganizationPermission extends Document { _id: Types.ObjectId; @@ -9,15 +9,15 @@ const serviceAccountOrganizationPermissionSchema = new Schema('ServiceAccountOrganizationPermission', serviceAccountOrganizationPermissionSchema); +const ServiceAccountOrganizationPermission = model("ServiceAccountOrganizationPermission", serviceAccountOrganizationPermissionSchema); export default ServiceAccountOrganizationPermission; \ No newline at end of file diff --git a/backend/src/models/serviceAccountWorkspacePermission.ts b/backend/src/models/serviceAccountWorkspacePermission.ts index 01e4c4ba6..5814923e5 100644 --- a/backend/src/models/serviceAccountWorkspacePermission.ts +++ b/backend/src/models/serviceAccountWorkspacePermission.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Document, Schema, Types, model } from "mongoose"; export interface IServiceAccountWorkspacePermission extends Document { _id: Types.ObjectId; @@ -13,32 +13,32 @@ const serviceAccountWorkspacePermissionSchema = new Schema('ServiceAccountWorkspacePermission', serviceAccountWorkspacePermissionSchema); +const ServiceAccountWorkspacePermission = model("ServiceAccountWorkspacePermission", serviceAccountWorkspacePermissionSchema); export default ServiceAccountWorkspacePermission; \ No newline at end of file diff --git a/backend/src/models/serviceToken.ts b/backend/src/models/serviceToken.ts index 9d91b076e..ce5fa3c9b 100644 --- a/backend/src/models/serviceToken.ts +++ b/backend/src/models/serviceToken.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IServiceToken { _id: Types.ObjectId; name: string; @@ -15,47 +15,47 @@ const serviceTokenSchema = new Schema( { name: { type: String, - required: true + required: true, }, user: { // token issuer type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true + ref: "Workspace", + required: true, }, environment: { type: String, - required: true + required: true, }, expiresAt: { - type: Date + type: Date, }, publicKey: { type: String, required: true, - select: true + select: true, }, encryptedKey: { type: String, required: true, - select: true + select: true, }, nonce: { type: String, required: true, - select: true - } + select: true, + }, }, { - timestamps: true + timestamps: true, } ); -const ServiceToken = model('ServiceToken', serviceTokenSchema); +const ServiceToken = model("ServiceToken", serviceTokenSchema); export default ServiceToken; diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index 3587a7ff4..57528a4e9 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, Document } from "mongoose"; +import { Document, Schema, Types, model } from "mongoose"; export interface IServiceTokenData extends Document { _id: Types.ObjectId; diff --git a/backend/src/models/tag.ts b/backend/src/models/tag.ts index 6b02c8b1b..53bf085d3 100644 --- a/backend/src/models/tag.ts +++ b/backend/src/models/tag.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface ITag { _id: Types.ObjectId; @@ -22,28 +22,28 @@ const tagSchema = new Schema( lowercase: true, validate: [ function (value: any) { - return value.indexOf(' ') === -1; + return value.indexOf(" ") === -1; }, - 'slug cannot contain spaces' - ] + "slug cannot contain spaces", + ], }, user: { type: Schema.Types.ObjectId, - ref: 'User' + ref: "User", }, workspace: { type: Schema.Types.ObjectId, - ref: 'Workspace' + ref: "Workspace", }, }, { - timestamps: true + timestamps: true, } ); tagSchema.index({ slug: 1, workspace: 1 }, { unique: true }) tagSchema.index({ workspace: 1 }) -const Tag = model('Tag', tagSchema); +const Tag = model("Tag", tagSchema); export default Tag; diff --git a/backend/src/models/token.ts b/backend/src/models/token.ts index e6f485f55..ab0a69c9e 100644 --- a/backend/src/models/token.ts +++ b/backend/src/models/token.ts @@ -1,4 +1,4 @@ -import { Schema, model } from 'mongoose'; +import { Schema, model } from "mongoose"; export interface IToken { email: string; @@ -10,23 +10,23 @@ export interface IToken { const tokenSchema = new Schema({ email: { type: String, - required: true + required: true, }, token: { type: String, - required: true + required: true, }, createdAt: { type: Date, - default: Date.now + default: Date.now, }, ttl: { type: Number, - } + }, }); tokenSchema.index({ email: 1 }); -const Token = model('Token', tokenSchema); +const Token = model("Token", tokenSchema); export default Token; diff --git a/backend/src/models/tokenData.ts b/backend/src/models/tokenData.ts index 8856a3677..615c9019d 100644 --- a/backend/src/models/tokenData.ts +++ b/backend/src/models/tokenData.ts @@ -1,4 +1,4 @@ -import { Schema, Types, model } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface ITokenData { type: string; @@ -16,40 +16,40 @@ const tokenDataSchema = new Schema({ type: { type: String, enum: [ - 'emailConfirmation', - 'emailMfa', - 'organizationInvitation', - 'passwordReset' + "emailConfirmation", + "emailMfa", + "organizationInvitation", + "passwordReset", ], - required: true + required: true, }, email: { - type: String + type: String, }, phoneNumber: { - type: String + type: String, }, organization: { // organizationInvitation-specific field type: Schema.Types.ObjectId, - ref: 'Organization' + ref: "Organization", }, tokenHash: { type: String, select: false, - required: true + required: true, }, triesLeft: { - type: Number + type: Number, }, expiresAt: { type: Date, expires: 0, - required: true - } + required: true, + }, }, { - timestamps: true + timestamps: true, }); -const TokenData = model('TokenData', tokenDataSchema); +const TokenData = model("TokenData", tokenDataSchema); export default TokenData; diff --git a/backend/src/models/tokenVersion.ts b/backend/src/models/tokenVersion.ts index 890c0a272..1103fc316 100644 --- a/backend/src/models/tokenVersion.ts +++ b/backend/src/models/tokenVersion.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Document, Schema, Types, model } from "mongoose"; export interface ITokenVersion extends Document { user: Types.ObjectId; @@ -13,35 +13,35 @@ const tokenVersionSchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, ip: { type: String, - required: true + required: true, }, userAgent: { type: String, - required: true + required: true, }, refreshVersion: { type: Number, - required: true + required: true, }, accessVersion: { type: Number, - required: true + required: true, }, lastUsed: { type: Date, - required: true - } + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const TokenVersion = model('TokenVersion', tokenVersionSchema); +const TokenVersion = model("TokenVersion", tokenVersionSchema); export default TokenVersion; \ No newline at end of file diff --git a/backend/src/models/user.ts b/backend/src/models/user.ts index 1c12720f8..b5f0027ca 100644 --- a/backend/src/models/user.ts +++ b/backend/src/models/user.ts @@ -1,7 +1,7 @@ -import { Schema, model, Types, Document } from 'mongoose'; +import { Document, Schema, Types, model } from "mongoose"; export enum AuthProvider { - GOOGLE = 'google', + GOOGLE = "google", } export interface IUser extends Document { @@ -44,73 +44,73 @@ const userSchema = new Schema( unique: true, }, firstName: { - type: String + type: String, }, lastName: { - type: String + type: String, }, encryptionVersion: { type: Number, select: false, - default: 1 // to resolve backward-compatibility issues + default: 1, // to resolve backward-compatibility issues }, protectedKey: { // introduced as part of encryption version 2 type: String, - select: false + select: false, }, protectedKeyIV: { // introduced as part of encryption version 2 type: String, - select: false + select: false, }, protectedKeyTag: { // introduced as part of encryption version 2 type: String, - select: false + select: false, }, publicKey: { type: String, - select: false + select: false, }, encryptedPrivateKey: { type: String, - select: false + select: false, }, iv: { // iv of [encryptedPrivateKey] type: String, - select: false + select: false, }, tag: { // tag of [encryptedPrivateKey] type: String, - select: false + select: false, }, salt: { type: String, - select: false + select: false, }, verifier: { type: String, - select: false + select: false, }, isMfaEnabled: { type: Boolean, - default: false + default: false, }, mfaMethods: [{ - type: String + type: String, }], devices: { type: [{ ip: String, - userAgent: String + userAgent: String, }], default: [], - select: false - } + select: false, + }, }, { - timestamps: true + timestamps: true, } ); -const User = model('User', userSchema); +const User = model("User", userSchema); export default User; diff --git a/backend/src/models/userAction.ts b/backend/src/models/userAction.ts index a2c69d46c..11eda05e8 100644 --- a/backend/src/models/userAction.ts +++ b/backend/src/models/userAction.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IUserAction { _id: Types.ObjectId; @@ -10,19 +10,19 @@ const userActionSchema = new Schema( { user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: "User", + required: true, }, action: { type: String, - required: true - } + required: true, + }, }, { - timestamps: true + timestamps: true, } ); -const UserAction = model('UserAction', userActionSchema); +const UserAction = model("UserAction", userActionSchema); export default UserAction; diff --git a/backend/src/models/workspace.ts b/backend/src/models/workspace.ts index ebd55d802..b3dd28b00 100644 --- a/backend/src/models/workspace.ts +++ b/backend/src/models/workspace.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, Types, model } from "mongoose"; export interface IWorkspace { _id: Types.ObjectId; @@ -14,7 +14,7 @@ export interface IWorkspace { const workspaceSchema = new Schema({ name: { type: String, - required: true + required: true, }, autoCapitalization: { type: Boolean, @@ -22,8 +22,8 @@ const workspaceSchema = new Schema({ }, organization: { type: Schema.Types.ObjectId, - ref: 'Organization', - required: true + ref: "Organization", + required: true, }, environments: { type: [ @@ -35,20 +35,20 @@ const workspaceSchema = new Schema({ default: [ { name: "Development", - slug: "dev" + slug: "dev", }, { name: "Staging", - slug: "staging" + slug: "staging", }, { name: "Production", - slug: "prod" - } + slug: "prod", + }, ], }, }); -const Workspace = model('Workspace', workspaceSchema); +const Workspace = model("Workspace", workspaceSchema); export default Workspace; \ No newline at end of file diff --git a/backend/src/routes/status/index.ts b/backend/src/routes/status/index.ts index d3c694b92..6f3be6271 100644 --- a/backend/src/routes/status/index.ts +++ b/backend/src/routes/status/index.ts @@ -1,5 +1,5 @@ -import healthCheck from './status'; +import healthCheck from "./status"; export { - healthCheck + healthCheck, } \ No newline at end of file diff --git a/backend/src/routes/status/status.ts b/backend/src/routes/status/status.ts index 91d0c9e85..0af4bb5c2 100644 --- a/backend/src/routes/status/status.ts +++ b/backend/src/routes/status/status.ts @@ -1,15 +1,15 @@ -import express, { Request, Response } from 'express'; -import { getSmtpConfigured } from '../../config'; +import express, { Request, Response } from "express"; +import { getSmtpConfigured } from "../../config"; const router = express.Router(); router.get( - '/status', + "/status", async (req: Request, res: Response) => { res.status(200).json({ date: new Date(), - message: 'Ok', - emailConfigured: await getSmtpConfigured() + message: "Ok", + emailConfigured: await getSmtpConfigured(), }) } ); diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index ed86a236d..fcf9869cc 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -1,75 +1,75 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body } from 'express-validator'; -import passport from 'passport'; -import { requireAuth, validateRequest } from '../../middleware'; -import { authController } from '../../controllers/v1'; -import { authLimiter } from '../../helpers/rateLimiter'; -import { AUTH_MODE_JWT } from '../../variables'; +import { body } from "express-validator"; +import passport from "passport"; +import { requireAuth, validateRequest } from "../../middleware"; +import { authController } from "../../controllers/v1"; +import { authLimiter } from "../../helpers/rateLimiter"; +import { AUTH_MODE_JWT } from "../../variables"; -router.post('/token', validateRequest, authController.getNewToken); +router.post("/token", validateRequest, authController.getNewToken); router.post( // deprecated (moved to api/v2/auth/login1) - '/login1', + "/login1", authLimiter, - body('email').exists().trim().notEmpty(), - body('clientPublicKey').exists().trim().notEmpty(), + body("email").exists().trim().notEmpty(), + body("clientPublicKey").exists().trim().notEmpty(), validateRequest, authController.login1 ); router.post( // deprecated (moved to api/v2/auth/login2) - '/login2', + "/login2", authLimiter, - body('email').exists().trim().notEmpty(), - body('clientProof').exists().trim().notEmpty(), + body("email").exists().trim().notEmpty(), + body("clientProof").exists().trim().notEmpty(), validateRequest, authController.login2 ); router.post( - '/logout', + "/logout", authLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), authController.logout ); router.post( - '/checkAuth', + "/checkAuth", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), authController.checkAuth ); router.get( - '/redirect/google', + "/redirect/google", authLimiter, - passport.authenticate('google', { - scope: ['profile', 'email'], + passport.authenticate("google", { + scope: ["profile", "email"], session: false, }), ); router.get( - '/callback/google', - passport.authenticate('google', { failureRedirect: '/login/provider/error', session: false }), + "/callback/google", + passport.authenticate("google", { failureRedirect: "/login/provider/error", session: false }), authController.handleAuthProviderCallback, ); router.get( - '/common-passwords', + "/common-passwords", authLimiter, authController.getCommonPasswords ); router.delete( - '/sessions', + "/sessions", authLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), authController.revokeAllSessions ); diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index 83e126dc4..0eafecad0 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -1,39 +1,39 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body, param } from 'express-validator'; +import { body, param } from "express-validator"; import { requireAuth, requireBotAuth, requireWorkspaceAuth, - validateRequest -} from '../../middleware'; -import { botController } from '../../controllers/v1'; -import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables'; + validateRequest, +} from "../../middleware"; +import { botController } from "../../controllers/v1"; +import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables"; router.get( - '/:workspaceId', + "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim().notEmpty(), + param("workspaceId").exists().trim().notEmpty(), validateRequest, botController.getBotByWorkspaceId ); router.patch( - '/:botId/active', + "/:botId/active", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireBotAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - body('isActive').exists().isBoolean(), - body('botKey'), + body("isActive").exists().isBoolean(), + body("botKey"), validateRequest, botController.setBotActiveState ); diff --git a/backend/src/routes/v1/index.ts b/backend/src/routes/v1/index.ts index 62ff08b14..7efc36a95 100644 --- a/backend/src/routes/v1/index.ts +++ b/backend/src/routes/v1/index.ts @@ -1,21 +1,21 @@ -import signup from './signup'; -import bot from './bot'; -import auth from './auth'; -import user from './user'; -import userAction from './userAction'; -import organization from './organization'; -import workspace from './workspace'; -import membershipOrg from './membershipOrg'; -import membership from './membership'; -import key from './key'; -import inviteOrg from './inviteOrg'; -import secret from './secret'; -import serviceToken from './serviceToken'; -import password from './password'; -import stripe from './stripe'; -import integration from './integration'; -import integrationAuth from './integrationAuth'; -import secretsFolder from './secretsFolder'; +import signup from "./signup"; +import bot from "./bot"; +import auth from "./auth"; +import user from "./user"; +import userAction from "./userAction"; +import organization from "./organization"; +import workspace from "./workspace"; +import membershipOrg from "./membershipOrg"; +import membership from "./membership"; +import key from "./key"; +import inviteOrg from "./inviteOrg"; +import secret from "./secret"; +import serviceToken from "./serviceToken"; +import password from "./password"; +import stripe from "./stripe"; +import integration from "./integration"; +import integrationAuth from "./integrationAuth"; +import secretsFolder from "./secretsFolder"; export { signup, @@ -35,5 +35,5 @@ export { stripe, integration, integrationAuth, - secretsFolder + secretsFolder, }; diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index b8e0b38bd..1820f4bb8 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -1,75 +1,77 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - requireAuth, - requireIntegrationAuth, - requireIntegrationAuthorizationAuth, - validateRequest -} from '../../middleware'; + requireAuth, + requireIntegrationAuth, + requireIntegrationAuthorizationAuth, + validateRequest, +} from "../../middleware"; import { - ADMIN, - MEMBER, - AUTH_MODE_JWT, - AUTH_MODE_API_KEY -} from '../../variables'; -import { body, param } from 'express-validator'; -import { integrationController } from '../../controllers/v1'; + ADMIN, + AUTH_MODE_API_KEY, + AUTH_MODE_JWT, + MEMBER, +} from "../../variables"; +import { body, param } from "express-validator"; +import { integrationController } from "../../controllers/v1"; router.post( - '/', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] - }), - requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER], - location: 'body' - }), - body('integrationAuthId').exists().isString().trim(), - body('app').trim(), - body('isActive').exists().isBoolean(), - body('appId').trim(), - body('sourceEnvironment').trim(), - body('targetEnvironment').trim(), - body('targetEnvironmentId').trim(), - body('targetService').trim(), - body('targetServiceId').trim(), - body('owner').trim(), - body('path').trim(), - body('region').trim(), - validateRequest, - integrationController.createIntegration + "/", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER], + location: "body", + }), + body("integrationAuthId").exists().isString().trim(), + body("app").trim(), + body("isActive").exists().isBoolean(), + body("appId").trim(), + body("secretPath").default("/").isString().trim(), + body("sourceEnvironment").trim(), + body("targetEnvironment").trim(), + body("targetEnvironmentId").trim(), + body("targetService").trim(), + body("targetServiceId").trim(), + body("owner").trim(), + body("path").trim(), + body("region").trim(), + validateRequest, + integrationController.createIntegration ); router.patch( - '/:integrationId', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] - }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param('integrationId').exists().trim(), - body('isActive').exists().isBoolean(), - body('app').exists().trim(), - body('environment').exists().trim(), - body('appId').exists(), - body('targetEnvironment').exists(), - body('owner').exists(), - validateRequest, - integrationController.updateIntegration + "/:integrationId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireIntegrationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationId").exists().trim(), + body("isActive").exists().isBoolean(), + body("app").exists().trim(), + body("secretPath").default("/").isString().trim(), + body("environment").exists().trim(), + body("appId").exists(), + body("targetEnvironment").exists(), + body("owner").exists(), + validateRequest, + integrationController.updateIntegration ); router.delete( - '/:integrationId', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] - }), - requireIntegrationAuth({ - acceptedRoles: [ADMIN, MEMBER] - }), - param('integrationId').exists().trim(), - validateRequest, - integrationController.deleteIntegration + "/:integrationId", + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT], + }), + requireIntegrationAuth({ + acceptedRoles: [ADMIN, MEMBER], + }), + param("integrationId").exists().trim(), + validateRequest, + integrationController.deleteIntegration ); export default router; diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index f8fe15a4b..28fa65af8 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -1,156 +1,156 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body, param, query } from 'express-validator'; +import { body, param, query } from "express-validator"; import { requireAuth, - requireWorkspaceAuth, requireIntegrationAuthorizationAuth, - validateRequest -} from '../../middleware'; + requireWorkspaceAuth, + validateRequest, +} from "../../middleware"; import { ADMIN, - MEMBER, + AUTH_MODE_API_KEY, AUTH_MODE_JWT, - AUTH_MODE_API_KEY -} from '../../variables'; -import { integrationAuthController } from '../../controllers/v1'; + MEMBER, +} from "../../variables"; +import { integrationAuthController } from "../../controllers/v1"; router.get( - '/integration-options', + "/integration-options", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), integrationAuthController.getIntegrationOptions ); router.get( - '/:integrationAuthId', + "/:integrationAuthId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - param('integrationAuthId'), + param("integrationAuthId"), validateRequest, integrationAuthController.getIntegrationAuth ); router.post( - '/oauth-token', + "/oauth-token", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' + locationWorkspaceId: "body", }), - body('workspaceId').exists().trim().notEmpty(), - body('code').exists().trim().notEmpty(), - body('integration').exists().trim().notEmpty(), + body("workspaceId").exists().trim().notEmpty(), + body("code").exists().trim().notEmpty(), + body("integration").exists().trim().notEmpty(), validateRequest, integrationAuthController.oAuthExchange ); router.post( - '/access-token', - body('workspaceId').exists().trim().notEmpty(), - body('accessId').trim(), - body('accessToken').exists().trim().notEmpty(), - body('url').trim(), - body('namespace').trim(), - body('integration').exists().trim().notEmpty(), + "/access-token", + body("workspaceId").exists().trim().notEmpty(), + body("accessId").trim(), + body("accessToken").exists().trim().notEmpty(), + body("url").trim(), + body("namespace").trim(), + body("integration").exists().trim().notEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' + locationWorkspaceId: "body", }), integrationAuthController.saveIntegrationAccessToken ); router.get( - '/:integrationAuthId/apps', + "/:integrationAuthId/apps", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - param('integrationAuthId'), - query('teamId'), + param("integrationAuthId"), + query("teamId"), validateRequest, integrationAuthController.getIntegrationAuthApps ); router.get( - '/:integrationAuthId/teams', + "/:integrationAuthId/teams", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - param('integrationAuthId'), + param("integrationAuthId"), validateRequest, integrationAuthController.getIntegrationAuthTeams ); router.get( - '/:integrationAuthId/vercel/branches', + "/:integrationAuthId/vercel/branches", requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ["jwt"], }), requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - param('integrationAuthId').exists().isString(), - query('appId').exists().isString(), - query('teamId').optional().isString(), + param("integrationAuthId").exists().isString(), + query("appId").exists().isString(), + query("teamId").optional().isString(), validateRequest, integrationAuthController.getIntegrationAuthVercelBranches ); router.get( - '/:integrationAuthId/railway/environments', + "/:integrationAuthId/railway/environments", requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ["jwt"], }), requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - param('integrationAuthId').exists().isString(), - query('appId').exists().isString(), + param("integrationAuthId").exists().isString(), + query("appId").exists().isString(), validateRequest, integrationAuthController.getIntegrationAuthRailwayEnvironments ); router.get( - '/:integrationAuthId/railway/services', + "/:integrationAuthId/railway/services", requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ["jwt"], }), requireIntegrationAuthorizationAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], }), - param('integrationAuthId').exists().isString(), - query('appId').exists().isString(), + param("integrationAuthId").exists().isString(), + query("appId").exists().isString(), validateRequest, integrationAuthController.getIntegrationAuthRailwayServices ); router.delete( - '/:integrationAuthId', + "/:integrationAuthId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], - attachAccessToken: false + attachAccessToken: false, }), - param('integrationAuthId'), + param("integrationAuthId"), validateRequest, integrationAuthController.deleteIntegrationAuth ); diff --git a/backend/src/routes/v1/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts index 9b4889bc9..edcb34c87 100644 --- a/backend/src/routes/v1/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -1,26 +1,26 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body } from 'express-validator'; -import { requireAuth, validateRequest } from '../../middleware'; -import { membershipOrgController } from '../../controllers/v1'; -import { AUTH_MODE_JWT } from '../../variables'; +import { body } from "express-validator"; +import { requireAuth, validateRequest } from "../../middleware"; +import { membershipOrgController } from "../../controllers/v1"; +import { AUTH_MODE_JWT } from "../../variables"; router.post( - '/signup', + "/signup", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('inviteeEmail').exists().trim().notEmpty().isEmail(), - body('organizationId').exists().trim().notEmpty(), + body("inviteeEmail").exists().trim().notEmpty().isEmail(), + body("organizationId").exists().trim().notEmpty(), validateRequest, membershipOrgController.inviteUserToOrganization ); router.post( - '/verify', - body('email').exists().trim().notEmpty(), - body('organizationId').exists().trim().notEmpty(), - body('code').exists().trim().notEmpty(), + "/verify", + body("email").exists().trim().notEmpty(), + body("organizationId").exists().trim().notEmpty(), + body("code").exists().trim().notEmpty(), validateRequest, membershipOrgController.verifyUserToOrganization ); diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index be99c9c17..fbd6e3eca 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -1,39 +1,39 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, requireWorkspaceAuth, - validateRequest -} from '../../middleware'; -import { body, param } from 'express-validator'; -import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables'; -import { keyController } from '../../controllers/v1'; + validateRequest, +} from "../../middleware"; +import { body, param } from "express-validator"; +import { ADMIN, AUTH_MODE_JWT, MEMBER } from "../../variables"; +import { keyController } from "../../controllers/v1"; router.post( - '/:workspaceId', + "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('key').exists(), + param("workspaceId").exists().trim(), + body("key").exists(), validateRequest, keyController.uploadKey ); router.get( - '/:workspaceId/latest', + "/:workspaceId/latest", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId'), + param("workspaceId"), validateRequest, keyController.getLatestKey ); diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index e830bd06d..428213768 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -1,50 +1,50 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body, param } from 'express-validator'; -import { requireAuth, validateRequest } from '../../middleware'; -import { membershipController } from '../../controllers/v1'; -import { membershipController as EEMembershipControllers } from '../../ee/controllers/v1'; -import { AUTH_MODE_JWT } from '../../variables'; +import { body, param } from "express-validator"; +import { requireAuth, validateRequest } from "../../middleware"; +import { membershipController } from "../../controllers/v1"; +import { membershipController as EEMembershipControllers } from "../../ee/controllers/v1"; +import { AUTH_MODE_JWT } from "../../variables"; // note: ALL DEPRECIATED (moved to api/v2/workspace/:workspaceId/memberships/:membershipId) router.get( // used for old CLI (deprecate) - '/:workspaceId/connect', + "/:workspaceId/connect", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, membershipController.validateMembership ); router.delete( - '/:membershipId', + "/:membershipId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - param('membershipId').exists().trim(), + param("membershipId").exists().trim(), validateRequest, membershipController.deleteMembership ); router.post( - '/:membershipId/change-role', + "/:membershipId/change-role", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('role').exists().trim(), + body("role").exists().trim(), validateRequest, membershipController.changeMembershipRole ); router.post( - '/:membershipId/deny-permissions', + "/:membershipId/deny-permissions", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - param('membershipId').isMongoId().exists().trim(), - body('permissions').isArray().exists(), + param("membershipId").isMongoId().exists().trim(), + body("permissions").isArray().exists(), validateRequest, EEMembershipControllers.denyMembershipPermissions ); diff --git a/backend/src/routes/v1/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts index 2863c53fb..6b3c7d2e8 100644 --- a/backend/src/routes/v1/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -1,27 +1,27 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { param } from 'express-validator'; -import { requireAuth, validateRequest } from '../../middleware'; -import { membershipOrgController } from '../../controllers/v1'; -import { AUTH_MODE_JWT } from '../../variables'; +import { param } from "express-validator"; +import { requireAuth, validateRequest } from "../../middleware"; +import { membershipOrgController } from "../../controllers/v1"; +import { AUTH_MODE_JWT } from "../../variables"; router.post( // TODO - '/membershipOrg/:membershipOrgId/change-role', + "/membershipOrg/:membershipOrgId/change-role", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - param('membershipOrgId'), + param("membershipOrgId"), validateRequest, membershipOrgController.changeMembershipOrgRole ); router.delete( - '/:membershipOrgId', + "/:membershipOrgId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - param('membershipOrgId').exists().trim(), + param("membershipOrgId").exists().trim(), validateRequest, membershipOrgController.deleteMembershipOrg ); diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index dded53f3a..7cfd0e3fa 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -1,177 +1,177 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body, param } from 'express-validator'; +import { body, param } from "express-validator"; import { requireAuth, requireOrganizationAuth, - validateRequest -} from '../../middleware'; + validateRequest, +} from "../../middleware"; import { - OWNER, + ACCEPTED, ADMIN, - MEMBER, - ACCEPTED, - AUTH_MODE_JWT -} from '../../variables'; -import { organizationController } from '../../controllers/v1'; + AUTH_MODE_JWT, + MEMBER, + OWNER, +} from "../../variables"; +import { organizationController } from "../../controllers/v1"; router.get( // deprecated (moved to api/v2/users/me/organizations) - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), organizationController.getOrganizations ); router.post( // not used on frontend - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('organizationName').exists().trim().notEmpty(), + body("organizationName").exists().trim().notEmpty(), validateRequest, organizationController.createOrganization ); router.get( - '/:organizationId', + "/:organizationId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationController.getOrganization ); router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships) - '/:organizationId/users', + "/:organizationId/users", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationController.getOrganizationMembers ); router.get( - '/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces) + "/:organizationId/my-workspaces", // deprecated (moved to api/v2/organizations/:organizationId/workspaces) requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationController.getOrganizationWorkspaces ); router.patch( - '/:organizationId/name', + "/:organizationId/name", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), - body('name').exists().trim().notEmpty(), + param("organizationId").exists().trim(), + body("name").exists().trim().notEmpty(), validateRequest, organizationController.changeOrganizationName ); router.get( - '/:organizationId/incidentContactOrg', + "/:organizationId/incidentContactOrg", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationController.getOrganizationIncidentContacts ); router.post( - '/:organizationId/incidentContactOrg', + "/:organizationId/incidentContactOrg", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), - body('email').exists().trim().notEmpty(), + param("organizationId").exists().trim(), + body("email").exists().trim().notEmpty(), validateRequest, organizationController.addOrganizationIncidentContact ); router.delete( - '/:organizationId/incidentContactOrg', + "/:organizationId/incidentContactOrg", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), - body('email').exists().trim().notEmpty(), + param("organizationId").exists().trim(), + body("email").exists().trim().notEmpty(), validateRequest, organizationController.deleteOrganizationIncidentContact ); router.post( - '/:organizationId/customer-portal-session', + "/:organizationId/customer-portal-session", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationController.createOrganizationPortalSession ); router.get( - '/:organizationId/subscriptions', + "/:organizationId/subscriptions", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationController.getOrganizationSubscriptions ); router.get( - '/:organizationId/workspace-memberships', + "/:organizationId/workspace-memberships", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), - param('organizationId').exists().trim(), + param("organizationId").exists().trim(), validateRequest, organizationController.getOrganizationMembersAndTheirWorkspaces ); diff --git a/backend/src/routes/v1/password.ts b/backend/src/routes/v1/password.ts index b04fa36af..7268b1a3c 100644 --- a/backend/src/routes/v1/password.ts +++ b/backend/src/routes/v1/password.ts @@ -1,93 +1,93 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body } from 'express-validator'; -import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware'; -import { passwordController } from '../../controllers/v1'; -import { passwordLimiter } from '../../helpers/rateLimiter'; +import { body } from "express-validator"; +import { requireAuth, requireSignupAuth, validateRequest } from "../../middleware"; +import { passwordController } from "../../controllers/v1"; +import { passwordLimiter } from "../../helpers/rateLimiter"; import { - AUTH_MODE_JWT -} from '../../variables'; + AUTH_MODE_JWT, +} from "../../variables"; router.post( - '/srp1', + "/srp1", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('clientPublicKey').exists().isString().trim().notEmpty(), + body("clientPublicKey").exists().isString().trim().notEmpty(), validateRequest, passwordController.srp1 ); router.post( - '/change-password', + "/change-password", passwordLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('clientProof').exists().trim().notEmpty(), - body('protectedKey').exists().isString().trim().notEmpty(), - body('protectedKeyIV').exists().isString().trim().notEmpty(), - body('protectedKeyTag').exists().isString().trim().notEmpty(), - body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // private key encrypted under new pwd - body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), // new iv for private key - body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), // new tag for private key - body('salt').exists().isString().trim().notEmpty(), // part of new pwd - body('verifier').exists().isString().trim().notEmpty(), // part of new pwd + body("clientProof").exists().trim().notEmpty(), + body("protectedKey").exists().isString().trim().notEmpty(), + body("protectedKeyIV").exists().isString().trim().notEmpty(), + body("protectedKeyTag").exists().isString().trim().notEmpty(), + body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // private key encrypted under new pwd + body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), // new iv for private key + body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), // new tag for private key + body("salt").exists().isString().trim().notEmpty(), // part of new pwd + body("verifier").exists().isString().trim().notEmpty(), // part of new pwd validateRequest, passwordController.changePassword ); router.post( - '/email/password-reset', + "/email/password-reset", passwordLimiter, - body('email').exists().isString().trim().notEmpty().isEmail(), + body("email").exists().isString().trim().notEmpty().isEmail(), validateRequest, passwordController.emailPasswordReset ); router.post( - '/email/password-reset-verify', + "/email/password-reset-verify", passwordLimiter, - body('email').exists().isString().trim().notEmpty().isEmail(), - body('code').exists().isString().trim().notEmpty(), + body("email").exists().isString().trim().notEmpty().isEmail(), + body("code").exists().isString().trim().notEmpty(), validateRequest, passwordController.emailPasswordResetVerify ); router.get( - '/backup-private-key', + "/backup-private-key", passwordLimiter, requireSignupAuth, passwordController.getBackupPrivateKey ); router.post( - '/backup-private-key', + "/backup-private-key", passwordLimiter, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('clientProof').exists().isString().trim().notEmpty(), - body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key - body('iv').exists().isString().trim().notEmpty(), // new iv for (backup) private key - body('tag').exists().isString().trim().notEmpty(), // new tag for (backup) private key - body('salt').exists().isString().trim().notEmpty(), // salt generated from strong key - body('verifier').exists().isString().trim().notEmpty(), // salt generated from strong key + body("clientProof").exists().isString().trim().notEmpty(), + body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // (backup) private key encrypted under a strong key + body("iv").exists().isString().trim().notEmpty(), // new iv for (backup) private key + body("tag").exists().isString().trim().notEmpty(), // new tag for (backup) private key + body("salt").exists().isString().trim().notEmpty(), // salt generated from strong key + body("verifier").exists().isString().trim().notEmpty(), // salt generated from strong key validateRequest, passwordController.createBackupPrivateKey ); router.post( - '/password-reset', + "/password-reset", requireSignupAuth, - body('protectedKey').exists().isString().trim().notEmpty(), - body('protectedKeyIV').exists().isString().trim().notEmpty(), - body('protectedKeyTag').exists().isString().trim().notEmpty(), - body('encryptedPrivateKey').exists().isString().trim().notEmpty(), // private key encrypted under new pwd - body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), // new iv for private key - body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), // new tag for private key - body('salt').exists().isString().trim().notEmpty(), // part of new pwd - body('verifier').exists().isString().trim().notEmpty(), // part of new pwd + body("protectedKey").exists().isString().trim().notEmpty(), + body("protectedKeyIV").exists().isString().trim().notEmpty(), + body("protectedKeyTag").exists().isString().trim().notEmpty(), + body("encryptedPrivateKey").exists().isString().trim().notEmpty(), // private key encrypted under new pwd + body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), // new iv for private key + body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), // new tag for private key + body("salt").exists().isString().trim().notEmpty(), // part of new pwd + body("verifier").exists().isString().trim().notEmpty(), // part of new pwd validateRequest, passwordController.resetPassword ); diff --git a/backend/src/routes/v1/secret.ts b/backend/src/routes/v1/secret.ts index e55dfaf43..89ed3c975 100644 --- a/backend/src/routes/v1/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -1,61 +1,61 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, - requireWorkspaceAuth, requireServiceTokenAuth, - validateRequest -} from '../../middleware'; -import { body, query, param } from 'express-validator'; -import { secretController } from '../../controllers/v1'; + requireWorkspaceAuth, + validateRequest, +} from "../../middleware"; +import { body, param, query } from "express-validator"; +import { secretController } from "../../controllers/v1"; import { ADMIN, + AUTH_MODE_JWT, MEMBER, - AUTH_MODE_JWT -} from '../../variables'; +} from "../../variables"; // note to devs: these endpoints will be deprecated in favor of v2 router.post( - '/:workspaceId', + "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - body('secrets').exists(), - body('keys').exists(), - body('environment').exists().trim().notEmpty(), - body('channel'), - param('workspaceId').exists().trim(), + body("secrets").exists(), + body("keys").exists(), + body("environment").exists().trim().notEmpty(), + body("channel"), + param("workspaceId").exists().trim(), validateRequest, secretController.pushSecrets ); router.get( - '/:workspaceId', + "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - query('environment').exists().trim(), - query('channel'), - param('workspaceId').exists().trim(), + query("environment").exists().trim(), + query("channel"), + param("workspaceId").exists().trim(), validateRequest, secretController.pullSecrets ); router.get( - '/:workspaceId/service-token', + "/:workspaceId/service-token", requireServiceTokenAuth, - query('environment').exists().trim(), - query('channel'), - param('workspaceId').exists().trim(), + query("environment").exists().trim(), + query("channel"), + param("workspaceId").exists().trim(), validateRequest, secretController.pullSecretsServiceToken ); diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index 2644a835b..83517f1ab 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -63,6 +63,7 @@ router.get( query("workspaceId").exists().isString().trim(), query("environment").exists().isString().trim(), query("parentFolderId").optional().isString().trim(), + query("parentFolderPath").optional().isString().trim(), validateRequest, getFolders ); diff --git a/backend/src/routes/v1/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts index 2b75e7cbf..b3f3abb70 100644 --- a/backend/src/routes/v1/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -1,43 +1,43 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, - requireWorkspaceAuth, requireServiceTokenAuth, - validateRequest -} from '../../middleware'; -import { body } from 'express-validator'; + requireWorkspaceAuth, + validateRequest, +} from "../../middleware"; +import { body } from "express-validator"; import { ADMIN, + AUTH_MODE_JWT, MEMBER, - AUTH_MODE_JWT -} from '../../variables'; -import { serviceTokenController } from '../../controllers/v1'; +} from "../../variables"; +import { serviceTokenController } from "../../controllers/v1"; // note: deprecate service-token routes in favor of service-token data routes/structure router.get( - '/', + "/", requireServiceTokenAuth, serviceTokenController.getServiceToken ); router.post( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' + locationWorkspaceId: "body", }), - body('name').exists().trim().notEmpty(), - body('workspaceId').exists().trim().notEmpty(), - body('environment').exists().trim().notEmpty(), - body('expiresIn'), // measured in ms - body('publicKey').exists().trim().notEmpty(), - body('encryptedKey').exists().trim().notEmpty(), - body('nonce').exists().trim().notEmpty(), + body("name").exists().trim().notEmpty(), + body("workspaceId").exists().trim().notEmpty(), + body("environment").exists().trim().notEmpty(), + body("expiresIn"), // measured in ms + body("publicKey").exists().trim().notEmpty(), + body("encryptedKey").exists().trim().notEmpty(), + body("nonce").exists().trim().notEmpty(), validateRequest, serviceTokenController.createServiceToken ); diff --git a/backend/src/routes/v1/signup.ts b/backend/src/routes/v1/signup.ts index 35a043c8e..1b82edd3a 100644 --- a/backend/src/routes/v1/signup.ts +++ b/backend/src/routes/v1/signup.ts @@ -1,23 +1,23 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body } from 'express-validator'; -import { validateRequest } from '../../middleware'; -import { signupController } from '../../controllers/v1'; -import { authLimiter } from '../../helpers/rateLimiter'; +import { body } from "express-validator"; +import { validateRequest } from "../../middleware"; +import { signupController } from "../../controllers/v1"; +import { authLimiter } from "../../helpers/rateLimiter"; router.post( - '/email/signup', + "/email/signup", authLimiter, - body('email').exists().trim().notEmpty().isEmail(), + body("email").exists().trim().notEmpty().isEmail(), validateRequest, signupController.beginEmailSignup ); router.post( - '/email/verify', + "/email/verify", authLimiter, - body('email').exists().trim().notEmpty().isEmail(), - body('code').exists().trim().notEmpty(), + body("email").exists().trim().notEmpty().isEmail(), + body("code").exists().trim().notEmpty(), validateRequest, signupController.verifyEmailSignup ); diff --git a/backend/src/routes/v1/stripe.ts b/backend/src/routes/v1/stripe.ts index cfcca77cb..d4ba1255d 100644 --- a/backend/src/routes/v1/stripe.ts +++ b/backend/src/routes/v1/stripe.ts @@ -1,7 +1,7 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { stripeController } from '../../controllers/v1'; +import { stripeController } from "../../controllers/v1"; -router.post('/webhook', stripeController.handleWebhook); +router.post("/webhook", stripeController.handleWebhook); export default router; diff --git a/backend/src/routes/v1/user.ts b/backend/src/routes/v1/user.ts index b9d88dfb1..d499a2377 100644 --- a/backend/src/routes/v1/user.ts +++ b/backend/src/routes/v1/user.ts @@ -1,15 +1,15 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { requireAuth } from '../../middleware'; -import { userController } from '../../controllers/v1'; +import { requireAuth } from "../../middleware"; +import { userController } from "../../controllers/v1"; import { - AUTH_MODE_JWT -} from '../../variables'; + AUTH_MODE_JWT, +} from "../../variables"; router.get( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), userController.getUser ); diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index c8d21f918..29cc811bd 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -1,27 +1,27 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { requireAuth, validateRequest } from '../../middleware'; -import { body, query } from 'express-validator'; -import { userActionController } from '../../controllers/v1'; -import { AUTH_MODE_JWT } from '../../variables'; +import { requireAuth, validateRequest } from "../../middleware"; +import { body, query } from "express-validator"; +import { userActionController } from "../../controllers/v1"; +import { AUTH_MODE_JWT } from "../../variables"; // note: [userAction] will be deprecated in /v2 in favor of [action] router.post( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('action'), + body("action"), validateRequest, userActionController.addUserAction ); router.get( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - query('action'), + query("action"), validateRequest, userActionController.getUserAction ); diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index 431a2e4f9..8b696d4ac 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -1,161 +1,161 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body, param, query } from 'express-validator'; +import { body, param } from "express-validator"; import { requireAuth, requireWorkspaceAuth, - validateRequest -} from '../../middleware'; + validateRequest, +} from "../../middleware"; import { ADMIN, + AUTH_MODE_JWT, MEMBER, - AUTH_MODE_JWT -} from '../../variables'; -import { workspaceController, membershipController } from '../../controllers/v1'; +} from "../../variables"; +import { membershipController, workspaceController } from "../../controllers/v1"; router.get( - '/:workspaceId/keys', + "/:workspaceId/keys", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspacePublicKeys ); router.get( - '/:workspaceId/users', + "/:workspaceId/users", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspaceMemberships ); router.get( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), workspaceController.getWorkspaces ); router.get( - '/:workspaceId', + "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspace ); router.post( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('workspaceName').exists().trim().notEmpty(), - body('organizationId').exists().trim().notEmpty(), + body("workspaceName").exists().trim().notEmpty(), + body("organizationId").exists().trim().notEmpty(), validateRequest, workspaceController.createWorkspace ); router.delete( - '/:workspaceId', + "/:workspaceId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.deleteWorkspace ); router.post( - '/:workspaceId/name', + "/:workspaceId/name", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('name').exists().trim().notEmpty(), + param("workspaceId").exists().trim(), + body("name").exists().trim().notEmpty(), validateRequest, workspaceController.changeWorkspaceName ); router.post( - '/:workspaceId/invite-signup', + "/:workspaceId/invite-signup", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('email').exists().trim().notEmpty(), + param("workspaceId").exists().trim(), + body("email").exists().trim().notEmpty(), validateRequest, membershipController.inviteUserToWorkspace ); router.get( - '/:workspaceId/integrations', + "/:workspaceId/integrations", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspaceIntegrations ); router.get( - '/:workspaceId/authorizations', + "/:workspaceId/authorizations", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspaceIntegrationAuthorizations ); router.get( - '/:workspaceId/service-tokens', // deprecate + "/:workspaceId/service-tokens", // deprecate requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspaceServiceTokens ); diff --git a/backend/src/routes/v2/apiKeyData.ts b/backend/src/routes/v2/apiKeyData.ts index 939bdbe1f..eae8d7ddd 100644 --- a/backend/src/routes/v2/apiKeyData.ts +++ b/backend/src/routes/v2/apiKeyData.ts @@ -1,40 +1,40 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { param, body } from 'express-validator'; +import { body, param } from "express-validator"; import { requireAuth, - validateRequest -} from '../../middleware'; -import { apiKeyDataController } from '../../controllers/v2'; + validateRequest, +} from "../../middleware"; +import { apiKeyDataController } from "../../controllers/v2"; import { - AUTH_MODE_JWT -} from '../../variables'; + AUTH_MODE_JWT, +} from "../../variables"; router.get( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), apiKeyDataController.getAPIKeyData ); router.post( - '/', + "/", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('name').exists().trim(), - body('expiresIn'), // measured in ms + body("name").exists().trim(), + body("expiresIn"), // measured in ms validateRequest, apiKeyDataController.createAPIKeyData ); router.delete( - '/:apiKeyDataId', + "/:apiKeyDataId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - param('apiKeyDataId').exists().trim(), + param("apiKeyDataId").exists().trim(), validateRequest, apiKeyDataController.deleteAPIKeyData ); diff --git a/backend/src/routes/v2/auth.ts b/backend/src/routes/v2/auth.ts index 288004e65..444819f27 100644 --- a/backend/src/routes/v2/auth.ts +++ b/backend/src/routes/v2/auth.ts @@ -1,42 +1,42 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body } from 'express-validator'; -import { requireMfaAuth, validateRequest } from '../../middleware'; -import { authController } from '../../controllers/v2'; -import { authLimiter } from '../../helpers/rateLimiter'; +import { body } from "express-validator"; +import { requireMfaAuth, validateRequest } from "../../middleware"; +import { authController } from "../../controllers/v2"; +import { authLimiter } from "../../helpers/rateLimiter"; router.post( - '/login1', + "/login1", authLimiter, - body('email').isString().trim().notEmpty(), - body('clientPublicKey').isString().trim().notEmpty(), + body("email").isString().trim().notEmpty(), + body("clientPublicKey").isString().trim().notEmpty(), validateRequest, authController.login1 ); router.post( - '/login2', + "/login2", authLimiter, - body('email').isString().trim().notEmpty(), - body('clientProof').isString().trim().notEmpty(), + body("email").isString().trim().notEmpty(), + body("clientProof").isString().trim().notEmpty(), validateRequest, authController.login2 ); router.post( - '/mfa/send', + "/mfa/send", authLimiter, - body('email').isString().trim().notEmpty().isEmail(), + body("email").isString().trim().notEmpty().isEmail(), validateRequest, authController.sendMfaToken ); router.post( - '/mfa/verify', + "/mfa/verify", authLimiter, requireMfaAuth, - body('email').isString().trim().notEmpty(), - body('mfaToken').isString().trim().notEmpty(), + body("email").isString().trim().notEmpty(), + body("mfaToken").isString().trim().notEmpty(), validateRequest, authController.verifyMfaToken ); diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index 0eb4b4a20..f9943f33d 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -1,76 +1,76 @@ -import express, { Response, Request } from 'express'; +import express from "express"; const router = express.Router(); -import { body, param } from 'express-validator'; -import { environmentController } from '../../controllers/v2'; +import { body, param } from "express-validator"; +import { environmentController } from "../../controllers/v2"; import { requireAuth, requireWorkspaceAuth, validateRequest, -} from '../../middleware'; +} from "../../middleware"; import { ADMIN, + AUTH_MODE_JWT, MEMBER, - AUTH_MODE_JWT -} from '../../variables'; +} from "../../variables"; router.post( - '/:workspaceId/environments', + "/:workspaceId/environments", requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('environmentSlug').exists().trim(), - body('environmentName').exists().trim(), + param("workspaceId").exists().trim(), + body("environmentSlug").exists().trim(), + body("environmentName").exists().trim(), validateRequest, environmentController.createWorkspaceEnvironment ); router.put( - '/:workspaceId/environments', + "/:workspaceId/environments", requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('environmentSlug').exists().trim(), - body('environmentName').exists().trim(), - body('oldEnvironmentSlug').exists().trim(), + param("workspaceId").exists().trim(), + body("environmentSlug").exists().trim(), + body("environmentName").exists().trim(), + body("oldEnvironmentSlug").exists().trim(), validateRequest, environmentController.renameWorkspaceEnvironment ); router.delete( - '/:workspaceId/environments', + "/:workspaceId/environments", requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('environmentSlug').exists().trim(), + param("workspaceId").exists().trim(), + body("environmentSlug").exists().trim(), validateRequest, environmentController.deleteWorkspaceEnvironment ); router.get( - '/:workspaceId/environments', + "/:workspaceId/environments", requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, environmentController.getAllAccessibleEnvironmentsOfWorkspace ); diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index c088771eb..fc7353729 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -1,13 +1,13 @@ -import auth from './auth'; -import signup from './signup'; -import users from './users'; -import organizations from './organizations'; -import workspace from './workspace'; -import secret from './secret'; // deprecated -import secrets from './secrets'; -import serviceTokenData from './serviceTokenData'; -import serviceAccounts from './serviceAccounts'; -import apiKeyData from './apiKeyData'; +import auth from "./auth"; +import signup from "./signup"; +import users from "./users"; +import organizations from "./organizations"; +import workspace from "./workspace"; +import secret from "./secret"; // deprecated +import secrets from "./secrets"; +import serviceTokenData from "./serviceTokenData"; +import serviceAccounts from "./serviceAccounts"; +import apiKeyData from "./apiKeyData"; import environment from "./environment" import tags from "./tags" @@ -23,5 +23,5 @@ export { serviceAccounts, apiKeyData, environment, - tags + tags, } \ No newline at end of file diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index eb2cef8eb..46223cf93 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -1,101 +1,101 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, - requireOrganizationAuth, requireMembershipOrgAuth, - validateRequest -} from '../../middleware'; -import { body, param } from 'express-validator'; + requireOrganizationAuth, + validateRequest, +} from "../../middleware"; +import { body, param } from "express-validator"; import { - OWNER, + ACCEPTED, ADMIN, - MEMBER, - ACCEPTED, + AUTH_MODE_API_KEY, AUTH_MODE_JWT, - AUTH_MODE_API_KEY -} from '../../variables'; -import { organizationsController } from '../../controllers/v2'; + MEMBER, + OWNER, +} from "../../variables"; +import { organizationsController } from "../../controllers/v2"; // TODO: /POST to create membership router.get( - '/:organizationId/memberships', - param('organizationId').exists().trim(), + "/:organizationId/memberships", + param("organizationId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), organizationsController.getOrganizationMemberships ); router.patch( - '/:organizationId/memberships/:membershipId', - param('organizationId').exists().trim(), - param('membershipId').exists().trim(), - body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), + "/:organizationId/memberships/:membershipId", + param("organizationId").exists().trim(), + param("membershipId").exists().trim(), + body("role").exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), requireMembershipOrgAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), organizationsController.updateOrganizationMembership ); router.delete( - '/:organizationId/memberships/:membershipId', - param('organizationId').exists().trim(), - param('membershipId').exists().trim(), + "/:organizationId/memberships/:membershipId", + param("organizationId").exists().trim(), + param("membershipId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), requireMembershipOrgAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), organizationsController.deleteOrganizationMembership ); router.get( - '/:organizationId/workspaces', - param('organizationId').exists().trim(), + "/:organizationId/workspaces", + param("organizationId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), organizationsController.getOrganizationWorkspaces ); router.get( - '/:organizationId/service-accounts', - param('organizationId').exists().trim(), + "/:organizationId/service-accounts", + param("organizationId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), organizationsController.getOrganizationServiceAccounts ); diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index f2c825ba8..e577d2a47 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -1,146 +1,146 @@ -import express from 'express'; +import express from "express"; import { requireAuth, - requireWorkspaceAuth, requireSecretAuth, - validateRequest -} from '../../middleware'; -import { body, param, query } from 'express-validator'; + requireWorkspaceAuth, + validateRequest, +} from "../../middleware"; +import { body, param, query } from "express-validator"; import { ADMIN, - MEMBER, AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN, + MEMBER, PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS -} from '../../variables'; -import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret'; -import { secretController } from '../../controllers/v2'; + PERMISSION_WRITE_SECRETS, +} from "../../variables"; +import { CreateSecretRequestBody, ModifySecretRequestBody } from "../../types/secret"; +import { secretController } from "../../controllers/v2"; // note to devs: stop supporting these routes [deprecated] const router = express.Router(); router.post( - '/batch-create/workspace/:workspaceId/environment/:environment', + "/batch-create/workspace/:workspaceId/environment/:environment", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().isMongoId().trim(), - param('environment').exists().trim(), - body('secrets').exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === 'object')), - body('channel'), + param("workspaceId").exists().isMongoId().trim(), + param("environment").exists().trim(), + body("secrets").exists().isArray().custom((value) => value.every((item: CreateSecretRequestBody) => typeof item === "object")), + body("channel"), validateRequest, secretController.createSecrets ); router.post( - '/workspace/:workspaceId/environment/:environment', + "/workspace/:workspaceId/environment/:environment", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().isMongoId().trim(), - param('environment').exists().trim(), - body('secret').exists().isObject(), - body('channel'), + param("workspaceId").exists().isMongoId().trim(), + param("environment").exists().trim(), + body("secret").exists().isObject(), + body("channel"), validateRequest, secretController.createSecret ); router.get( - '/workspace/:workspaceId', - param('workspaceId').exists().trim(), + "/workspace/:workspaceId", + param("workspaceId").exists().trim(), query("environment").exists(), requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - query('channel'), + query("channel"), validateRequest, secretController.getSecrets ); router.get( - '/:secretId', + "/:secretId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS] + requiredPermissions: [PERMISSION_READ_SECRETS], }), validateRequest, secretController.getSecret ); router.delete( - '/batch/workspace/:workspaceId/environment/:environmentName', + "/batch/workspace/:workspaceId/environment/:environmentName", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - param('workspaceId').exists().isMongoId().trim(), - param('environmentName').exists().trim(), - body('secretIds').exists().isArray().custom(array => array.length > 0), + param("workspaceId").exists().isMongoId().trim(), + param("environmentName").exists().trim(), + body("secretIds").exists().isArray().custom(array => array.length > 0), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), validateRequest, secretController.deleteSecrets ); router.delete( - '/:secretId', + "/:secretId", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireSecretAuth({ acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS], }), - param('secretId').isMongoId(), + param("secretId").isMongoId(), validateRequest, secretController.deleteSecret ); router.patch( - '/batch-modify/workspace/:workspaceId/environment/:environmentName', + "/batch-modify/workspace/:workspaceId/environment/:environmentName", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), - param('workspaceId').exists().isMongoId().trim(), - param('environmentName').exists().trim(), + body("secrets").exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), + param("workspaceId").exists().isMongoId().trim(), + param("environmentName").exists().trim(), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), validateRequest, secretController.updateSecrets ); router.patch( - '/workspace/:workspaceId/environment/:environmentName', + "/workspace/:workspaceId/environment/:environmentName", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), - body('secret').isObject(), - param('workspaceId').exists().isMongoId().trim(), - param('environmentName').exists().trim(), + body("secret").isObject(), + param("workspaceId").exists().isMongoId().trim(), + param("environmentName").exists().trim(), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), validateRequest, secretController.updateSecret diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 3cf7e83d5..cd550d99e 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -3,24 +3,24 @@ const router = express.Router(); import { Types } from "mongoose"; import { requireAuth, - requireWorkspaceAuth, requireSecretsAuth, + requireWorkspaceAuth, validateRequest, } from "../../middleware"; import { validateClientForSecrets } from "../../validation"; -import { query, body } from "express-validator"; +import { body, query } from "express-validator"; import { secretsController } from "../../controllers/v2"; import { ADMIN, - MEMBER, - SECRET_PERSONAL, - SECRET_SHARED, - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY, + MEMBER, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, + SECRET_PERSONAL, + SECRET_SHARED, } from "../../variables"; import { BatchSecretRequest } from "../../types/secret"; diff --git a/backend/src/routes/v2/serviceAccounts.ts b/backend/src/routes/v2/serviceAccounts.ts index 6f0db91b7..244739e72 100644 --- a/backend/src/routes/v2/serviceAccounts.ts +++ b/backend/src/routes/v2/serviceAccounts.ts @@ -1,157 +1,157 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, requireOrganizationAuth, - requireWorkspaceAuth, requireServiceAccountAuth, requireServiceAccountWorkspacePermissionAuth, - validateRequest -} from '../../middleware'; -import { param, query, body } from 'express-validator'; + requireWorkspaceAuth, + validateRequest, +} from "../../middleware"; +import { body, param, query } from "express-validator"; import { - OWNER, - ADMIN, - MEMBER, ACCEPTED, + ADMIN, AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT -} from '../../variables'; -import { serviceAccountsController } from '../../controllers/v2'; + AUTH_MODE_SERVICE_ACCOUNT, + MEMBER, + OWNER, +} from "../../variables"; +import { serviceAccountsController } from "../../controllers/v2"; router.get( // TODO: check - '/me', + "/me", requireAuth({ - acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT] + acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT], }), serviceAccountsController.getCurrentServiceAccount ); router.get( - '/:serviceAccountId', - param('serviceAccountId').exists().isString().trim(), + "/:serviceAccountId", + param("serviceAccountId").exists().isString().trim(), requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireServiceAccountAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), serviceAccountsController.getServiceAccountById ); router.post( - '/', - body('organizationId').exists().isString().trim(), - body('name').exists().isString().trim(), - body('publicKey').exists().isString().trim(), - body('expiresIn').isNumeric(), // measured in ms + "/", + body("organizationId").exists().isString().trim(), + body("name").exists().isString().trim(), + body("publicKey").exists().isString().trim(), + body("expiresIn").isNumeric(), // measured in ms validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], acceptedStatuses: [ACCEPTED], - locationOrganizationId: 'body' + locationOrganizationId: "body", }), serviceAccountsController.createServiceAccount ); router.patch( - '/:serviceAccountId/name', - param('serviceAccountId').exists().isString().trim(), + "/:serviceAccountId/name", + param("serviceAccountId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireServiceAccountAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), serviceAccountsController.changeServiceAccountName ); router.delete( - '/:serviceAccountId', - param('serviceAccountId').exists().isString().trim(), + "/:serviceAccountId", + param("serviceAccountId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireServiceAccountAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), serviceAccountsController.deleteServiceAccount ); router.get( - '/:serviceAccountId/permissions/workspace', - param('serviceAccountId').exists().isString().trim(), + "/:serviceAccountId/permissions/workspace", + param("serviceAccountId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireServiceAccountAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), serviceAccountsController.getServiceAccountWorkspacePermissions ); router.post( - '/:serviceAccountId/permissions/workspace', - param('serviceAccountId').exists().isString().trim(), - body('workspaceId').exists().isString().notEmpty(), - body('environment').exists().isString().notEmpty(), - body('read').isBoolean().optional(), - body('write').isBoolean().optional(), - body('encryptedKey').exists().isString().notEmpty(), - body('nonce').exists().isString().notEmpty(), + "/:serviceAccountId/permissions/workspace", + param("serviceAccountId").exists().isString().trim(), + body("workspaceId").exists().isString().notEmpty(), + body("environment").exists().isString().notEmpty(), + body("read").isBoolean().optional(), + body("write").isBoolean().optional(), + body("encryptedKey").exists().isString().notEmpty(), + body("nonce").exists().isString().notEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireServiceAccountAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' + locationWorkspaceId: "body", }), serviceAccountsController.addServiceAccountWorkspacePermission ); router.delete( - '/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId', - param('serviceAccountId').exists().isString().trim(), - param('serviceAccountWorkspacePermissionId').exists().isString().trim(), + "/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId", + param("serviceAccountId").exists().isString().trim(), + param("serviceAccountWorkspacePermissionId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireServiceAccountAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), requireServiceAccountWorkspacePermissionAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), serviceAccountsController.deleteServiceAccountWorkspacePermission ); router.get( - '/:serviceAccountId/keys', - query('workspaceId').optional().isString(), + "/:serviceAccountId/keys", + query("workspaceId").optional().isString(), requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT], }), requireServiceAccountAuth({ acceptedRoles: [OWNER, ADMIN], - acceptedStatuses: [ACCEPTED] + acceptedStatuses: [ACCEPTED], }), serviceAccountsController.getServiceAccountKeys ); diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index e96064715..33ffad3cf 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -2,18 +2,18 @@ import express from "express"; const router = express.Router(); import { requireAuth, - requireWorkspaceAuth, requireServiceTokenDataAuth, + requireWorkspaceAuth, validateRequest, } from "../../middleware"; -import { param, body } from "express-validator"; +import { body, param } from "express-validator"; import { ADMIN, - MEMBER, - PERMISSION_WRITE_SECRETS, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, + MEMBER, + PERMISSION_WRITE_SECRETS, } from "../../variables"; import { serviceTokenDataController } from "../../controllers/v2"; diff --git a/backend/src/routes/v2/signup.ts b/backend/src/routes/v2/signup.ts index 138591879..cc701b034 100644 --- a/backend/src/routes/v2/signup.ts +++ b/backend/src/routes/v2/signup.ts @@ -1,47 +1,47 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body } from 'express-validator'; -import { requireSignupAuth, validateRequest } from '../../middleware'; -import { signupController } from '../../controllers/v2'; -import { authLimiter } from '../../helpers/rateLimiter'; +import { body } from "express-validator"; +import { requireSignupAuth, validateRequest } from "../../middleware"; +import { signupController } from "../../controllers/v2"; +import { authLimiter } from "../../helpers/rateLimiter"; router.post( - '/complete-account/signup', + "/complete-account/signup", authLimiter, requireSignupAuth, - body('email').exists().isString().trim().notEmpty().isEmail(), - body('firstName').exists().isString().trim().notEmpty(), - body('lastName').exists().isString().trim().notEmpty(), - body('protectedKey').exists().isString().trim().notEmpty(), - body('protectedKeyIV').exists().isString().trim().notEmpty(), - body('protectedKeyTag').exists().isString().trim().notEmpty(), - body('publicKey').exists().isString().trim().notEmpty(), - body('encryptedPrivateKey').exists().isString().trim().notEmpty(), - body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), - body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), - body('salt').exists().isString().trim().notEmpty(), - body('verifier').exists().isString().trim().notEmpty(), - body('organizationName').exists().isString().trim().notEmpty(), + body("email").exists().isString().trim().notEmpty().isEmail(), + body("firstName").exists().isString().trim().notEmpty(), + body("lastName").exists().isString().trim().notEmpty(), + body("protectedKey").exists().isString().trim().notEmpty(), + body("protectedKeyIV").exists().isString().trim().notEmpty(), + body("protectedKeyTag").exists().isString().trim().notEmpty(), + body("publicKey").exists().isString().trim().notEmpty(), + body("encryptedPrivateKey").exists().isString().trim().notEmpty(), + body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), + body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), + body("salt").exists().isString().trim().notEmpty(), + body("verifier").exists().isString().trim().notEmpty(), + body("organizationName").exists().isString().trim().notEmpty(), validateRequest, signupController.completeAccountSignup ); router.post( - '/complete-account/invite', + "/complete-account/invite", authLimiter, requireSignupAuth, - body('email').exists().isString().trim().notEmpty().isEmail(), - body('firstName').exists().isString().trim().notEmpty(), - body('lastName').exists().isString().trim().notEmpty(), - body('protectedKey').exists().isString().trim().notEmpty(), - body('protectedKeyIV').exists().isString().trim().notEmpty(), - body('protectedKeyTag').exists().isString().trim().notEmpty(), - body('publicKey').exists().trim().notEmpty(), - body('encryptedPrivateKey').exists().isString().trim().notEmpty(), - body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), - body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), - body('salt').exists().isString().trim().notEmpty(), - body('verifier').exists().isString().trim().notEmpty(), + body("email").exists().isString().trim().notEmpty().isEmail(), + body("firstName").exists().isString().trim().notEmpty(), + body("lastName").exists().isString().trim().notEmpty(), + body("protectedKey").exists().isString().trim().notEmpty(), + body("protectedKeyIV").exists().isString().trim().notEmpty(), + body("protectedKeyTag").exists().isString().trim().notEmpty(), + body("publicKey").exists().trim().notEmpty(), + body("encryptedPrivateKey").exists().isString().trim().notEmpty(), + body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), + body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), + body("salt").exists().isString().trim().notEmpty(), + body("verifier").exists().isString().trim().notEmpty(), validateRequest, signupController.completeAccountInvite ); diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index c9a11c1bc..8974bd9fd 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -1,54 +1,54 @@ -import express, { Response, Request } from 'express'; +import express from "express"; const router = express.Router(); -import { body, param } from 'express-validator'; -import { tagController } from '../../controllers/v2'; +import { body, param } from "express-validator"; +import { tagController } from "../../controllers/v2"; import { requireAuth, requireWorkspaceAuth, - validateRequest -} from '../../middleware'; + validateRequest, +} from "../../middleware"; import { ADMIN, + AUTH_MODE_JWT, MEMBER, - AUTH_MODE_JWT -} from '../../variables'; +} from "../../variables"; router.get( - '/:workspaceId/tags', + "/:workspaceId/tags", requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, tagController.getWorkspaceTags ); router.delete( - '/tags/:tagId', + "/tags/:tagId", requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT], }), - param('tagId').exists().trim(), + param("tagId").exists().trim(), validateRequest, tagController.deleteWorkspaceTag ); router.post( - '/:workspaceId/tags', + "/:workspaceId/tags", requireAuth({ acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('name').exists().trim(), - body('slug').exists().trim(), + param("workspaceId").exists().trim(), + body("name").exists().trim(), + body("slug").exists().trim(), validateRequest, tagController.createWorkspaceTag ); diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index 63ae5eee9..970824eaa 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -1,38 +1,38 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, - validateRequest -} from '../../middleware'; -import { body } from 'express-validator'; -import { usersController } from '../../controllers/v2'; + validateRequest, +} from "../../middleware"; +import { body } from "express-validator"; +import { usersController } from "../../controllers/v2"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, - AUTH_MODE_API_KEY -} from '../../variables'; +} from "../../variables"; router.get( - '/me', + "/me", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), usersController.getMe ); router.patch( - '/me/mfa', + "/me/mfa", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), - body('isMfaEnabled').exists().isBoolean(), + body("isMfaEnabled").exists().isBoolean(), validateRequest, usersController.updateMyMfaEnabled ); router.get( - '/me/organizations', + "/me/organizations", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), usersController.getMyOrganizations ); diff --git a/backend/src/routes/v2/workspace.ts b/backend/src/routes/v2/workspace.ts index e258636fb..c36a0b7f8 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -1,147 +1,147 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body, param, query } from 'express-validator'; +import { body, param, query } from "express-validator"; import { requireAuth, requireMembershipAuth, requireWorkspaceAuth, - validateRequest -} from '../../middleware'; + validateRequest, +} from "../../middleware"; import { ADMIN, - MEMBER, + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../../variables'; -import { workspaceController } from '../../controllers/v2'; + MEMBER, +} from "../../variables"; +import { workspaceController } from "../../controllers/v2"; router.post( - '/:workspaceId/secrets', + "/:workspaceId/secrets", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - body('secrets').exists(), - body('keys').exists(), - body('environment').exists().trim().notEmpty(), - body('channel'), - param('workspaceId').exists().trim(), + body("secrets").exists(), + body("keys").exists(), + body("environment").exists().trim().notEmpty(), + body("channel"), + param("workspaceId").exists().trim(), validateRequest, workspaceController.pushWorkspaceSecrets ); router.get( - '/:workspaceId/secrets', + "/:workspaceId/secrets", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - query('environment').exists().trim(), - query('channel'), - param('workspaceId').exists().trim(), + query("environment").exists().trim(), + query("channel"), + param("workspaceId").exists().trim(), validateRequest, workspaceController.pullSecrets ); router.get( - '/:workspaceId/encrypted-key', + "/:workspaceId/encrypted-key", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspaceKey ); router.get( - '/:workspaceId/service-token-data', + "/:workspaceId/service-token-data", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), + param("workspaceId").exists().trim(), validateRequest, workspaceController.getWorkspaceServiceTokenData ); router.get( // new - TODO: rewire dashboard to this route - '/:workspaceId/memberships', - param('workspaceId').exists().trim(), + "/:workspaceId/memberships", + param("workspaceId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), workspaceController.getWorkspaceMemberships ); router.patch( // TODO - rewire dashboard to this route - '/:workspaceId/memberships/:membershipId', - param('workspaceId').exists().trim(), - param('membershipId').exists().trim(), - body('role').exists().isString().trim().isIn([ADMIN, MEMBER]), + "/:workspaceId/memberships/:membershipId", + param("workspaceId").exists().trim(), + param("membershipId").exists().trim(), + body("role").exists().isString().trim().isIn([ADMIN, MEMBER]), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), requireMembershipAuth({ acceptedRoles: [ADMIN], - locationMembershipId: 'params' + locationMembershipId: "params", }), workspaceController.updateWorkspaceMembership ); router.delete( // TODO - rewire dashboard to this route - '/:workspaceId/memberships/:membershipId', - param('workspaceId').exists().trim(), - param('membershipId').exists().trim(), + "/:workspaceId/memberships/:membershipId", + param("workspaceId").exists().trim(), + param("membershipId").exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), requireMembershipAuth({ acceptedRoles: [ADMIN], - locationMembershipId: 'params' + locationMembershipId: "params", }), workspaceController.deleteWorkspaceMembership ); router.patch( - '/:workspaceId/auto-capitalization', + "/:workspaceId/auto-capitalization", requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), - param('workspaceId').exists().trim(), - body('autoCapitalization').exists().trim().notEmpty(), + param("workspaceId").exists().trim(), + body("autoCapitalization").exists().trim().notEmpty(), validateRequest, workspaceController.toggleAutoCapitalization ); diff --git a/backend/src/routes/v3/auth.ts b/backend/src/routes/v3/auth.ts index 6539b5c0f..12afb5113 100644 --- a/backend/src/routes/v3/auth.ts +++ b/backend/src/routes/v3/auth.ts @@ -1,27 +1,27 @@ -import express from 'express'; -import { body } from 'express-validator'; -import { validateRequest } from '../../middleware'; -import { authController } from '../../controllers/v3'; -import { authLimiter } from '../../helpers/rateLimiter'; +import express from "express"; +import { body } from "express-validator"; +import { validateRequest } from "../../middleware"; +import { authController } from "../../controllers/v3"; +import { authLimiter } from "../../helpers/rateLimiter"; const router = express.Router(); router.post( - '/login1', + "/login1", authLimiter, - body('email').isString().trim(), - body('providerAuthToken').isString().trim().optional({nullable: true}), - body('clientPublicKey').isString().trim().notEmpty(), + body("email").isString().trim(), + body("providerAuthToken").isString().trim().optional({nullable: true}), + body("clientPublicKey").isString().trim().notEmpty(), validateRequest, authController.login1 ); router.post( - '/login2', + "/login2", authLimiter, - body('email').isString().trim(), - body('providerAuthToken').isString().trim().optional({nullable: true}), - body('clientProof').isString().trim().notEmpty(), + body("email").isString().trim(), + body("providerAuthToken").isString().trim().optional({nullable: true}), + body("clientProof").isString().trim().notEmpty(), validateRequest, authController.login2 ); diff --git a/backend/src/routes/v3/index.ts b/backend/src/routes/v3/index.ts index 2560a8f82..f4fcfe55b 100644 --- a/backend/src/routes/v3/index.ts +++ b/backend/src/routes/v3/index.ts @@ -1,7 +1,7 @@ -import auth from './auth'; -import secrets from './secrets'; -import workspaces from './workspaces'; -import signup from './signup'; +import auth from "./auth"; +import secrets from "./secrets"; +import workspaces from "./workspaces"; +import signup from "./signup"; export { auth, diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts index 6a3d7af88..c010151c6 100644 --- a/backend/src/routes/v3/secrets.ts +++ b/backend/src/routes/v3/secrets.ts @@ -8,18 +8,153 @@ import { import { body, param, query } from "express-validator"; import { secretsController } from "../../controllers/v3"; import { - AUTH_MODE_JWT, - AUTH_MODE_API_KEY, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_SERVICE_ACCOUNT, ADMIN, + AUTH_MODE_API_KEY, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, MEMBER, - PERMISSION_WRITE_SECRETS, - SECRET_SHARED, - SECRET_PERSONAL, PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, + SECRET_PERSONAL, + SECRET_SHARED, } from "../../variables"; +router.get( + "/raw", + query("workspaceId").exists().isString().trim(), + query("environment").exists().isString().trim(), + query("secretPath").default("/").isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "query", + locationEnvironment: "query", + requiredPermissions: [PERMISSION_READ_SECRETS], + requireBlindIndicesEnabled: true, + requireE2EEOff: true, + }), + secretsController.getSecretsRaw +); + +router.get( + "/raw/:secretName", + param("secretName").exists().isString().trim(), + query("workspaceId").exists().isString().trim(), + query("environment").exists().isString().trim(), + query("secretPath").default("/").isString().trim(), + query("type").optional().isIn([SECRET_SHARED, SECRET_PERSONAL]), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "query", + locationEnvironment: "query", + requiredPermissions: [PERMISSION_READ_SECRETS], + requireBlindIndicesEnabled: true, + requireE2EEOff: true, + }), + secretsController.getSecretByNameRaw +); + +router.post( + "/raw/:secretName", + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), + body("secretValue").exists().isString().trim(), + body("secretComment").default("").isString().trim(), + body("secretPath").default("/").isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body", + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + requireE2EEOff: true, + }), + secretsController.createSecretRaw +); + +router.patch( + "/raw/:secretName", + param("secretName").exists().isString().trim(), + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), + body("secretValue").exists().isString().trim(), + body("secretPath").default("/").isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body", + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + requireE2EEOff: true, + }), + secretsController.updateSecretByNameRaw +); + +router.delete( + "/raw/:secretName", + param("secretName").exists().isString().trim(), + body("workspaceId").exists().isString().trim(), + body("environment").exists().isString().trim(), + body("secretPath").default("/").isString().trim(), + body("type").exists().isIn([SECRET_SHARED, SECRET_PERSONAL]), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "body", + locationEnvironment: "body", + requiredPermissions: [PERMISSION_WRITE_SECRETS], + requireBlindIndicesEnabled: true, + requireE2EEOff: true, + }), + secretsController.deleteSecretByNameRaw +); + router.get( "/", query("workspaceId").exists().isString().trim(), @@ -40,6 +175,7 @@ router.get( locationEnvironment: "query", requiredPermissions: [PERMISSION_READ_SECRETS], requireBlindIndicesEnabled: true, + requireE2EEOff: false, }), secretsController.getSecrets ); @@ -74,6 +210,7 @@ router.post( locationEnvironment: "body", requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, + requireE2EEOff: false, }), secretsController.createSecret ); @@ -129,6 +266,7 @@ router.patch( locationEnvironment: "body", requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, + requireE2EEOff: false, }), secretsController.updateSecretByName ); @@ -155,6 +293,7 @@ router.delete( locationEnvironment: "body", requiredPermissions: [PERMISSION_WRITE_SECRETS], requireBlindIndicesEnabled: true, + requireE2EEOff: false, }), secretsController.deleteSecretByName ); diff --git a/backend/src/routes/v3/signup.ts b/backend/src/routes/v3/signup.ts index b2fb4af81..eb3ff9023 100644 --- a/backend/src/routes/v3/signup.ts +++ b/backend/src/routes/v3/signup.ts @@ -1,27 +1,27 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); -import { body } from 'express-validator'; -import { signupController } from '../../controllers/v3'; -import { authLimiter } from '../../helpers/rateLimiter'; -import { validateRequest } from '../../middleware'; +import { body } from "express-validator"; +import { signupController } from "../../controllers/v3"; +import { authLimiter } from "../../helpers/rateLimiter"; +import { validateRequest } from "../../middleware"; router.post( - '/complete-account/signup', + "/complete-account/signup", authLimiter, - body('email').exists().isString().trim().notEmpty().isEmail(), - body('firstName').exists().isString().trim().notEmpty(), - body('lastName').exists().isString().trim().optional({nullable: true}), - body('protectedKey').exists().isString().trim().notEmpty(), - body('protectedKeyIV').exists().isString().trim().notEmpty(), - body('protectedKeyTag').exists().isString().trim().notEmpty(), - body('publicKey').exists().isString().trim().notEmpty(), - body('encryptedPrivateKey').exists().isString().trim().notEmpty(), - body('encryptedPrivateKeyIV').exists().isString().trim().notEmpty(), - body('encryptedPrivateKeyTag').exists().isString().trim().notEmpty(), - body('salt').exists().isString().trim().notEmpty(), - body('verifier').exists().isString().trim().notEmpty(), - body('organizationName').exists().isString().trim().notEmpty(), - body('providerAuthToken').isString().trim().optional({nullable: true}), + body("email").exists().isString().trim().notEmpty().isEmail(), + body("firstName").exists().isString().trim().notEmpty(), + body("lastName").exists().isString().trim().optional({nullable: true}), + body("protectedKey").exists().isString().trim().notEmpty(), + body("protectedKeyIV").exists().isString().trim().notEmpty(), + body("protectedKeyTag").exists().isString().trim().notEmpty(), + body("publicKey").exists().isString().trim().notEmpty(), + body("encryptedPrivateKey").exists().isString().trim().notEmpty(), + body("encryptedPrivateKeyIV").exists().isString().trim().notEmpty(), + body("encryptedPrivateKeyTag").exists().isString().trim().notEmpty(), + body("salt").exists().isString().trim().notEmpty(), + body("verifier").exists().isString().trim().notEmpty(), + body("organizationName").exists().isString().trim().notEmpty(), + body("providerAuthToken").isString().trim().optional({nullable: true}), validateRequest, signupController.completeAccountSignup, ); diff --git a/backend/src/routes/v3/workspaces.ts b/backend/src/routes/v3/workspaces.ts index fdd734cf9..7aa909693 100644 --- a/backend/src/routes/v3/workspaces.ts +++ b/backend/src/routes/v3/workspaces.ts @@ -1,76 +1,75 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, requireWorkspaceAuth, - validateRequest -} from '../../middleware'; -import { workspacesController } from '../../controllers/v3'; + validateRequest, +} from "../../middleware"; +import { workspacesController } from "../../controllers/v3"; import { - AUTH_MODE_JWT, ADMIN, - PERMISSION_READ_SECRETS -} from '../../variables'; -import { param, body, validationResult } from 'express-validator'; + AUTH_MODE_JWT, +} from "../../variables"; +import { body, param } from "express-validator"; // -- migration to blind indices endpoints router.get( - '/:workspaceId/secrets/blind-index-status', - param('workspaceId').exists().isString().trim(), + "/:workspaceId/secrets/blind-index-status", + param("workspaceId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - locationWorkspaceId: 'params', + locationWorkspaceId: "params", }), workspacesController.getWorkspaceBlindIndexStatus ); router.get( // allow admins to get all workspace secrets (part of blind indices migration) - '/:workspaceId/secrets', - param('workspaceId').exists().isString().trim(), + "/:workspaceId/secrets", + param("workspaceId").exists().isString().trim(), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - locationWorkspaceId: 'params', + locationWorkspaceId: "params", }), workspacesController.getWorkspaceSecrets ); router.post( // allow admins to name all workspace secrets (part of blind indices migration) - '/:workspaceId/secrets/names', - param('workspaceId').exists().isString().trim(), - body('secretsToUpdate') + "/:workspaceId/secrets/names", + param("workspaceId").exists().isString().trim(), + body("secretsToUpdate") .exists() .isArray() - .withMessage('secretsToUpdate must be an array') + .withMessage("secretsToUpdate must be an array") .customSanitizer((value) => { return value.map((secret: any) => ({ secretName: secret.secretName, - _id: secret._id + _id: secret._id, })); }), - body('secretsToUpdate.*.secretName') + body("secretsToUpdate.*.secretName") .exists() .isString() - .withMessage('secretName must be a string'), - body('secretsToUpdate.*._id') + .withMessage("secretName must be a string"), + body("secretsToUpdate.*._id") .exists() .isString() - .withMessage('secretId must be a string'), + .withMessage("secretId must be a string"), validateRequest, requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT] + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], - locationWorkspaceId: 'params' + locationWorkspaceId: "params", }), workspacesController.nameWorkspaceSecrets ); diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts index 2c0db0355..ca31bf103 100644 --- a/backend/src/services/BotService.ts +++ b/backend/src/services/BotService.ts @@ -1,83 +1,112 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - getSecretsBotHelper, - encryptSymmetricHelper, - decryptSymmetricHelper -} from '../helpers/bot'; + decryptSymmetricHelper, + encryptSymmetricHelper, + getIsWorkspaceE2EEHelper, + getKey, + getSecretsBotHelper, +} from "../helpers/bot"; /** * Class to handle bot actions */ class BotService { - - /** - * Return decrypted secrets for workspace with id [workspaceId] and - * environment [environmen] shared to bot. - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace of secrets - * @param {String} obj.environment - environment for secrets - * @returns {Object} secretObj - object where keys are secret keys and values are secret values - */ - static async getSecrets({ - workspaceId, - environment - }: { - workspaceId: Types.ObjectId; - environment: string; - }) { - return await getSecretsBotHelper({ - workspaceId, - environment - }); - } - - /** - * Return symmetrically encrypted [plaintext] using the - * bot's copy of the workspace key for workspace with id [workspaceId] - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.plaintext - plaintext to encrypt - */ - static async encryptSymmetric({ - workspaceId, - plaintext - }: { - workspaceId: Types.ObjectId; - plaintext: string; - }) { - return await encryptSymmetricHelper({ - workspaceId, - plaintext - }); - } - - /** - * Return symmetrically decrypted [ciphertext] using the - * bot's copy of the workspace key for workspace with id [workspaceId] - * @param {Object} obj - * @param {String} obj.workspaceId - id of workspace - * @param {String} obj.ciphertext - ciphertext to decrypt - * @param {String} obj.iv - iv - * @param {String} obj.tag - tag - */ - static async decryptSymmetric({ - workspaceId, - ciphertext, - iv, - tag - }: { - workspaceId: Types.ObjectId; - ciphertext: string; - iv: string; - tag: string; - }) { - return await decryptSymmetricHelper({ - workspaceId, - ciphertext, - iv, - tag - }); - } + /** + * Return whether or not workspace with id [workspaceId] is end-to-end encrypted + * @param workspaceId - id of workspace + * @returns {Boolean} + */ + static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) { + return await getIsWorkspaceE2EEHelper(workspaceId); + } + + /** + * Get workspace key for workspace with id [workspaceId] shared to bot. + * @param {Object} obj + * @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for + * @returns + */ + static async getWorkspaceKeyWithBot({ + workspaceId, + }: { + workspaceId: Types.ObjectId; + }) { + return await getKey({ + workspaceId, + }); + } + + /** + * Return decrypted secrets for workspace with id [workspaceId] and + * environment [environmen] shared to bot. + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace of secrets + * @param {String} obj.environment - environment for secrets + * @returns {Object} secretObj - object where keys are secret keys and values are secret values + */ + static async getSecrets({ + workspaceId, + environment, + secretPath, + }: { + workspaceId: Types.ObjectId; + environment: string; + secretPath: string; + }) { + return await getSecretsBotHelper({ + workspaceId, + environment, + secretPath, + }); + } + + /** + * Return symmetrically encrypted [plaintext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.plaintext - plaintext to encrypt + */ + static async encryptSymmetric({ + workspaceId, + plaintext, + }: { + workspaceId: Types.ObjectId; + plaintext: string; + }) { + return await encryptSymmetricHelper({ + workspaceId, + plaintext, + }); + } + + /** + * Return symmetrically decrypted [ciphertext] using the + * bot's copy of the workspace key for workspace with id [workspaceId] + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.ciphertext - ciphertext to decrypt + * @param {String} obj.iv - iv + * @param {String} obj.tag - tag + */ + static async decryptSymmetric({ + workspaceId, + ciphertext, + iv, + tag, + }: { + workspaceId: Types.ObjectId; + ciphertext: string; + iv: string; + tag: string; + }) { + return await decryptSymmetricHelper({ + workspaceId, + ciphertext, + iv, + tag, + }); + } } -export default BotService; \ No newline at end of file +export default BotService; diff --git a/backend/src/services/DatabaseService.ts b/backend/src/services/DatabaseService.ts index 616f56c47..4b40863d0 100644 --- a/backend/src/services/DatabaseService.ts +++ b/backend/src/services/DatabaseService.ts @@ -1,7 +1,7 @@ import { + closeDatabaseHelper, initDatabaseHelper, - closeDatabaseHelper -} from '../helpers/database'; +} from "../helpers/database"; /** * Class to handle database actions @@ -15,7 +15,7 @@ class DatabaseService { */ static async initDatabase(MONGO_URL: string) { return await initDatabaseHelper({ - mongoURL: MONGO_URL + mongoURL: MONGO_URL, }); } diff --git a/backend/src/services/EventService.ts b/backend/src/services/EventService.ts index 160086be8..7abc7c1b1 100644 --- a/backend/src/services/EventService.ts +++ b/backend/src/services/EventService.ts @@ -1,5 +1,5 @@ -import { Types } from 'mongoose'; -import { handleEventHelper } from '../helpers/event'; +import { Types } from "mongoose"; +import { handleEventHelper } from "../helpers/event"; interface Event { name: string; @@ -22,7 +22,7 @@ class EventService { */ static async handleEvent({ event }: { event: Event }): Promise { await handleEventHelper({ - event + event, }); } } diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts index 1e7409ad0..b5b8b1d3f 100644 --- a/backend/src/services/IntegrationService.ts +++ b/backend/src/services/IntegrationService.ts @@ -1,12 +1,12 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - handleOAuthExchangeHelper, - syncIntegrationsHelper, - getIntegrationAuthRefreshHelper, getIntegrationAuthAccessHelper, - setIntegrationAuthRefreshHelper, + getIntegrationAuthRefreshHelper, + handleOAuthExchangeHelper, setIntegrationAuthAccessHelper, -} from '../helpers/integration'; + setIntegrationAuthRefreshHelper, + syncIntegrationsHelper, +} from "../helpers/integration"; /** * Class to handle integrations @@ -30,7 +30,7 @@ class IntegrationService { workspaceId, integration, code, - environment + environment, }: { workspaceId: string; integration: string; @@ -41,7 +41,7 @@ class IntegrationService { workspaceId, integration, code, - environment + environment, }); } @@ -53,13 +53,13 @@ class IntegrationService { */ static async syncIntegrations({ workspaceId, - environment + environment, }: { workspaceId: Types.ObjectId; environment?: string; }) { return await syncIntegrationsHelper({ - workspaceId + workspaceId, }); } @@ -72,7 +72,7 @@ class IntegrationService { */ static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: Types.ObjectId}) { return await getIntegrationAuthRefreshHelper({ - integrationAuthId + integrationAuthId, }); } @@ -85,7 +85,7 @@ class IntegrationService { */ static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) { return await getIntegrationAuthAccessHelper({ - integrationAuthId + integrationAuthId, }); } @@ -100,14 +100,14 @@ class IntegrationService { */ static async setIntegrationAuthRefresh({ integrationAuthId, - refreshToken + refreshToken, }: { integrationAuthId: string; refreshToken: string; }) { return await setIntegrationAuthRefreshHelper({ integrationAuthId, - refreshToken + refreshToken, }); } @@ -126,7 +126,7 @@ class IntegrationService { integrationAuthId, accessId, accessToken, - accessExpiresAt + accessExpiresAt, }: { integrationAuthId: string; accessId: string | null; @@ -137,7 +137,7 @@ class IntegrationService { integrationAuthId, accessId, accessToken, - accessExpiresAt + accessExpiresAt, }); } } diff --git a/backend/src/services/SecretService.ts b/backend/src/services/SecretService.ts index d7c1c9644..ef372bddc 100644 --- a/backend/src/services/SecretService.ts +++ b/backend/src/services/SecretService.ts @@ -1,22 +1,21 @@ import { Types } from "mongoose"; -import { ISecret } from "../models"; import { - CreateSecretParams, - GetSecretsParams, - GetSecretParams, - UpdateSecretParams, - DeleteSecretParams, + CreateSecretParams, + DeleteSecretParams, + GetSecretParams, + GetSecretsParams, + UpdateSecretParams, } from "../interfaces/services/SecretService"; -import { - createSecretBlindIndexDataHelper, - getSecretBlindIndexSaltHelper, - generateSecretBlindIndexWithSaltHelper, - generateSecretBlindIndexHelper, - createSecretHelper, - getSecretsHelper, - getSecretHelper, - updateSecretHelper, - deleteSecretHelper, +import { + createSecretBlindIndexDataHelper, + createSecretHelper, + deleteSecretHelper, + generateSecretBlindIndexHelper, + generateSecretBlindIndexWithSaltHelper, + getSecretBlindIndexSaltHelper, + getSecretHelper, + getSecretsHelper, + updateSecretHelper, } from "../helpers/secrets"; class SecretService { diff --git a/backend/src/services/TelemetryService.ts b/backend/src/services/TelemetryService.ts index da90732eb..3db2fdb89 100644 --- a/backend/src/services/TelemetryService.ts +++ b/backend/src/services/TelemetryService.ts @@ -1,24 +1,21 @@ -import { PostHog } from 'posthog-node'; -import { getLogger } from '../utils/logger'; -import { AuthData } from '../interfaces/middleware'; +import { PostHog } from "posthog-node"; +import { getLogger } from "../utils/logger"; +import { AuthData } from "../interfaces/middleware"; import { getNodeEnv, - getTelemetryEnabled, + getPostHogHost, getPostHogProjectApiKey, - getPostHogHost -} from '../config'; + getTelemetryEnabled, +} from "../config"; import { - IUser, - User, - IServiceAccount, ServiceAccount, - IServiceTokenData, - ServiceTokenData -} from '../models'; + ServiceTokenData, + User, +} from "../models"; import { AccountNotFoundError, - BadRequestError -} from '../utils/errors'; + BadRequestError, +} from "../utils/errors"; class Telemetry { /** @@ -31,7 +28,7 @@ class Telemetry { "To improve, Infisical collects telemetry data about general usage.", "This helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth as we support Infisical as open-source software.", "To opt into telemetry, you can set `TELEMETRY_ENABLED=true` within the environment variables.", - ].join('\n')) + ].join("\n")) } } @@ -41,10 +38,10 @@ class Telemetry { */ static getPostHogClient = async () => { let postHogClient: any; - if ((await getNodeEnv()) === 'production' && (await getTelemetryEnabled())) { + if ((await getNodeEnv()) === "production" && (await getTelemetryEnabled())) { // case: enable opt-out telemetry in production postHogClient = new PostHog(await getPostHogProjectApiKey(), { - host: await getPostHogHost() + host: await getPostHogHost(), }); } @@ -52,11 +49,11 @@ class Telemetry { } static getDistinctId = async ({ - authData + authData, }: { authData: AuthData; }) => { - let distinctId = ''; + let distinctId = ""; if (authData.authPayload instanceof User) { distinctId = authData.authPayload.email; } else if (authData.authPayload instanceof ServiceAccount) { @@ -64,7 +61,7 @@ class Telemetry { } else if (authData.authPayload instanceof ServiceTokenData) { if (authData.authPayload.user) { - const user = await User.findById(authData.authPayload.user, 'email'); + const user = await User.findById(authData.authPayload.user, "email"); if (!user) throw AccountNotFoundError(); distinctId = user.email; } else if (authData.authPayload.serviceAccount) { @@ -72,8 +69,8 @@ class Telemetry { } } - if (distinctId === '') throw BadRequestError({ - message: 'Failed to obtain distinct id for logging telemetry' + if (distinctId === "") throw BadRequestError({ + message: "Failed to obtain distinct id for logging telemetry", }); return distinctId; diff --git a/backend/src/services/TokenService.ts b/backend/src/services/TokenService.ts index 6299f1d54..7d0ff881d 100644 --- a/backend/src/services/TokenService.ts +++ b/backend/src/services/TokenService.ts @@ -1,5 +1,5 @@ -import { Types } from 'mongoose'; -import { createTokenHelper, validateTokenHelper } from '../helpers/token'; +import { Types } from "mongoose"; +import { createTokenHelper, validateTokenHelper } from "../helpers/token"; /** * Class to handle token actions @@ -19,9 +19,9 @@ class TokenService { type, email, phoneNumber, - organizationId + organizationId, }: { - type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset'; + type: "emailConfirmation" | "emailMfa" | "organizationInvitation" | "passwordReset"; email?: string; phoneNumber?: string; organizationId?: Types.ObjectId; @@ -30,7 +30,7 @@ class TokenService { type, email, phoneNumber, - organizationId + organizationId, }); } @@ -48,9 +48,9 @@ class TokenService { email, phoneNumber, organizationId, - token + token, }: { - type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset'; + type: "emailConfirmation" | "emailMfa" | "organizationInvitation" | "passwordReset"; email?: string; phoneNumber?: string; organizationId?: Types.ObjectId; @@ -61,7 +61,7 @@ class TokenService { email, phoneNumber, organizationId, - token + token, }); } } diff --git a/backend/src/services/health.ts b/backend/src/services/health.ts index daf3bf962..5ddfb8171 100644 --- a/backend/src/services/health.ts +++ b/backend/src/services/health.ts @@ -1,19 +1,19 @@ -import mongoose from 'mongoose'; -import { createTerminus } from '@godaddy/terminus'; -import { getLogger } from '../utils/logger'; +import mongoose from "mongoose"; +import { createTerminus } from "@godaddy/terminus"; +import { getLogger } from "../utils/logger"; export const setUpHealthEndpoint = (server: T) => { const onSignal = async () => { - (await getLogger('backend-main')).info('Server is starting clean-up'); + (await getLogger("backend-main")).info("Server is starting clean-up"); return Promise.all([ new Promise((resolve) => { if (mongoose.connection && mongoose.connection.readyState == 1) { mongoose.connection.close() - .then(() => resolve('Database connection closed')); + .then(() => resolve("Database connection closed")); } else { - resolve('Database connection already closed'); + resolve("Database connection already closed"); } - }) + }), ]); }; @@ -25,8 +25,8 @@ export const setUpHealthEndpoint = (server: T) => { createTerminus(server, { healthChecks: { - '/healthcheck': healthCheck, - onSignal - } + "/healthcheck": healthCheck, + onSignal, + }, }); }; diff --git a/backend/src/services/index.ts b/backend/src/services/index.ts index db0bd39a3..ad83bf510 100644 --- a/backend/src/services/index.ts +++ b/backend/src/services/index.ts @@ -1,11 +1,11 @@ -import DatabaseService from './DatabaseService'; +import DatabaseService from "./DatabaseService"; // import { logTelemetryMessage, getPostHogClient } from './TelemetryService'; -import TelemetryService from './TelemetryService'; -import BotService from './BotService'; -import EventService from './EventService'; -import IntegrationService from './IntegrationService'; -import TokenService from './TokenService'; -import SecretService from './SecretService'; +import TelemetryService from "./TelemetryService"; +import BotService from "./BotService"; +import EventService from "./EventService"; +import IntegrationService from "./IntegrationService"; +import TokenService from "./TokenService"; +import SecretService from "./SecretService"; export { TelemetryService, @@ -14,5 +14,5 @@ export { EventService, IntegrationService, TokenService, - SecretService + SecretService, } \ No newline at end of file diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index 0231e7f54..4bd58020b 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -1,31 +1,31 @@ -import nodemailer from 'nodemailer'; +import nodemailer from "nodemailer"; import { - SMTP_HOST_SENDGRID, + SMTP_HOST_GMAIL, SMTP_HOST_MAILGUN, + SMTP_HOST_SENDGRID, SMTP_HOST_SOCKETLABS, SMTP_HOST_ZOHOMAIL, - SMTP_HOST_GMAIL -} from '../variables'; -import SMTPConnection from 'nodemailer/lib/smtp-connection'; -import * as Sentry from '@sentry/node'; +} from "../variables"; +import SMTPConnection from "nodemailer/lib/smtp-connection"; +import * as Sentry from "@sentry/node"; import { getSmtpHost, - getSmtpUsername, getSmtpPassword, + getSmtpPort, getSmtpSecure, - getSmtpPort -} from '../config'; + getSmtpUsername, +} from "../config"; export const initSmtp = async () => { const mailOpts: SMTPConnection.Options = { host: await getSmtpHost(), - port: await getSmtpPort() + port: await getSmtpPort(), }; if ((await getSmtpUsername()) && (await getSmtpPassword())) { mailOpts.auth = { user: await getSmtpUsername(), - pass: await getSmtpPassword() + pass: await getSmtpPassword(), }; } @@ -37,31 +37,31 @@ export const initSmtp = async () => { case SMTP_HOST_MAILGUN: mailOpts.requireTLS = true; mailOpts.tls = { - ciphers: 'TLSv1.2' + ciphers: "TLSv1.2", } break; case SMTP_HOST_SOCKETLABS: mailOpts.requireTLS = true; mailOpts.tls = { - ciphers: 'TLSv1.2' + ciphers: "TLSv1.2", } break; case SMTP_HOST_ZOHOMAIL: mailOpts.requireTLS = true; mailOpts.tls = { - ciphers: 'TLSv1.2' + ciphers: "TLSv1.2", } break; case SMTP_HOST_GMAIL: mailOpts.requireTLS = true; mailOpts.tls = { - ciphers: 'TLSv1.2' + ciphers: "TLSv1.2", } break; default: - if ((await getSmtpHost()).includes('amazonaws.com')) { + if ((await getSmtpHost()).includes("amazonaws.com")) { mailOpts.tls = { - ciphers: 'TLSv1.2' + ciphers: "TLSv1.2", } } else { mailOpts.secure = true; @@ -75,7 +75,7 @@ export const initSmtp = async () => { .verify() .then((err) => { Sentry.setUser(null); - Sentry.captureMessage('SMTP - Successfully connected'); + Sentry.captureMessage("SMTP - Successfully connected"); console.log("SMTP - Successfully connected") }) .catch(async (err) => { diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index b5ca0ab28..f3b2700da 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,16 +1,11 @@ -import * as express from 'express'; -import { Types } from 'mongoose'; -import { - IUser, - IServiceAccount, - IServiceTokenData, - ISecret -} from '../../models'; -import { - AuthData -} from '../../interfaces/middleware'; +import { Types } from "mongoose"; -declare module 'express' { + +import { + AuthData, +} from "../../interfaces/middleware"; + +declare module "express" { interface Request { user?: any; } @@ -37,6 +32,7 @@ declare global { serviceToken: any; serviceAccount: any; accessToken: any; + accessId: any; serviceTokenData: any; apiKeyData: any; query?: any; diff --git a/backend/src/types/secret/index.d.ts b/backend/src/types/secret/index.d.ts index deab73e95..13391549e 100644 --- a/backend/src/types/secret/index.d.ts +++ b/backend/src/types/secret/index.d.ts @@ -1,7 +1,5 @@ -import { Types } from "mongoose"; import { Assign, Omit } from "utility-types"; import { ISecret } from "../../models"; -import { mongo } from "mongoose"; // Everything is required, except the omitted types export type CreateSecretRequestBody = Omit< diff --git a/backend/src/utils/addDevelopmentUser.ts b/backend/src/utils/addDevelopmentUser.ts index 2c7866229..d8b668909 100644 --- a/backend/src/utils/addDevelopmentUser.ts +++ b/backend/src/utils/addDevelopmentUser.ts @@ -6,8 +6,8 @@ import { Key, Membership, MembershipOrg, Organization, User, Workspace } from "../models"; import { SecretService } from "../services"; -import { Types } from 'mongoose'; -import { getNodeEnv } from '../config'; +import { Types } from "mongoose"; +import { getNodeEnv } from "../config"; export const testUserEmail = "test@localhost.local" export const testUserPassword = "testInfisical1" @@ -25,71 +25,71 @@ export const createTestUserForDevelopment = async () => { _id: testUserId, email: testUserEmail, refreshVersion: 0, - encryptedPrivateKey: 'ITMdDXtLoxib4+53U/qzvIV/T/UalRwimogFCXv/UsulzEoiKM+aK2aqOb0=', - firstName: 'Jake', - iv: '9fp0dZHI+UuHeKkWMDvD6w==', - lastName: 'Moni', - publicKey: 'cf44BhkybbBfsE0fZHe2jvqtCj6KLXvSq4hVjV0svzk=', - salt: 'd8099dc70958090346910fb9639262b83cf526fc9b4555a171b36a9e1bcd0240', - tag: 'bQ/UTghqcQHRoSMpLQD33g==', - verifier: '12271fcd50937ca4512e1e3166adaf9d9fc7a5cd0e4c4cb3eda89f35572ede4d9eef23f64aef9220367abff9437b0b6fa55792c442f177201d87051cf77dadade254ff667170440327355fb7d6ac4745d4db302f4843632c2ed5919ebdcff343287a4cd552255d9e3ce81177edefe089617b7616683901475d393405f554634b9bf9230c041ac85624f37a60401be20b78044932580ae0868323be3749fbf856df1518153ba375fec628275f0c445f237446ea4aa7f12c1aa1d6b5fd74b7f2e88d062845a19819ec63f2d2ed9e9f37c055149649461d997d2ae1482f53b04f9de7493efbb9686fb19b2d559b9aa2b502c22dec83f9fc43290dfea89a1dc6f03580b3642b3824513853e81a441be9a0b2fde2231bac60f3287872617a36884697805eeea673cf1a351697834484ada0f282e4745015c9c2928d61e6d092f1b9c3a27eda8413175d23bb2edae62f82ccaf52bf5a6a90344a766c7e4ebf65dae9ae90b2ad4ae65dbf16e3a6948e429771cc50307ae86d454f71a746939ed061f080dd3ae369c1a0739819aca17af46a085bac1f2a5d936d198e7951a8ac3bb38b893665fe7312835abd3f61811f81efa2a8761af5070085f9b6adcca80bf9b0d81899c3d41487fba90728bb24eceb98bd69770360a232624133700ceb4d153f2ad702e0a5b7dfaf97d20bc8aa71dc8c20024a58c06a8fecdad18cb5a2f89c51eaf7' + encryptedPrivateKey: "ITMdDXtLoxib4+53U/qzvIV/T/UalRwimogFCXv/UsulzEoiKM+aK2aqOb0=", + firstName: "Jake", + iv: "9fp0dZHI+UuHeKkWMDvD6w==", + lastName: "Moni", + publicKey: "cf44BhkybbBfsE0fZHe2jvqtCj6KLXvSq4hVjV0svzk=", + salt: "d8099dc70958090346910fb9639262b83cf526fc9b4555a171b36a9e1bcd0240", + tag: "bQ/UTghqcQHRoSMpLQD33g==", + verifier: "12271fcd50937ca4512e1e3166adaf9d9fc7a5cd0e4c4cb3eda89f35572ede4d9eef23f64aef9220367abff9437b0b6fa55792c442f177201d87051cf77dadade254ff667170440327355fb7d6ac4745d4db302f4843632c2ed5919ebdcff343287a4cd552255d9e3ce81177edefe089617b7616683901475d393405f554634b9bf9230c041ac85624f37a60401be20b78044932580ae0868323be3749fbf856df1518153ba375fec628275f0c445f237446ea4aa7f12c1aa1d6b5fd74b7f2e88d062845a19819ec63f2d2ed9e9f37c055149649461d997d2ae1482f53b04f9de7493efbb9686fb19b2d559b9aa2b502c22dec83f9fc43290dfea89a1dc6f03580b3642b3824513853e81a441be9a0b2fde2231bac60f3287872617a36884697805eeea673cf1a351697834484ada0f282e4745015c9c2928d61e6d092f1b9c3a27eda8413175d23bb2edae62f82ccaf52bf5a6a90344a766c7e4ebf65dae9ae90b2ad4ae65dbf16e3a6948e429771cc50307ae86d454f71a746939ed061f080dd3ae369c1a0739819aca17af46a085bac1f2a5d936d198e7951a8ac3bb38b893665fe7312835abd3f61811f81efa2a8761af5070085f9b6adcca80bf9b0d81899c3d41487fba90728bb24eceb98bd69770360a232624133700ceb4d153f2ad702e0a5b7dfaf97d20bc8aa71dc8c20024a58c06a8fecdad18cb5a2f89c51eaf7", } const testWorkspaceKey = { _id: new Types.ObjectId(testWorkspaceKeyId), workspace: testWorkspaceId, - encryptedKey: '96ZIRSU21CjVzIQ4Yp994FGWQvDdyK3gq+z+NCaJLK0ByTlvUePmf+AYGFJjkAdz', - nonce: '1jhCGqg9Wx3n0OtVxbDgiYYGq4S3EdgO', - sender: '63cefa6ec8d3175601cfa980', - receiver: '63cefa6ec8d3175601cfa980', + encryptedKey: "96ZIRSU21CjVzIQ4Yp994FGWQvDdyK3gq+z+NCaJLK0ByTlvUePmf+AYGFJjkAdz", + nonce: "1jhCGqg9Wx3n0OtVxbDgiYYGq4S3EdgO", + sender: "63cefa6ec8d3175601cfa980", + receiver: "63cefa6ec8d3175601cfa980", } const testWorkspace = { _id: new Types.ObjectId(testWorkspaceId), - name: 'Example Project', + name: "Example Project", organization: testOrgId, environments: [ { - _id: '63cefb15c8d3175601cfa98a', - name: 'Development', - slug: 'dev' + _id: "63cefb15c8d3175601cfa98a", + name: "Development", + slug: "dev", }, { - _id: '63cefb15c8d3175601cfa98b', - name: 'Test', - slug: 'test' + _id: "63cefb15c8d3175601cfa98b", + name: "Test", + slug: "test", }, { - _id: '63cefb15c8d3175601cfa98c', - name: 'Staging', - slug: 'staging' + _id: "63cefb15c8d3175601cfa98c", + name: "Staging", + slug: "staging", }, { - _id: '63cefb15c8d3175601cfa98d', - name: 'Production', - slug: 'prod' - } + _id: "63cefb15c8d3175601cfa98d", + name: "Production", + slug: "prod", + }, ], } const testOrg = { _id: testOrgId, - name: 'Jake\'s organization' + name: "Jake's organization", } const testMembershipOrg = { _id: testMembershipOrgId, organization: testOrgId, - role: 'owner', - status: 'accepted', + role: "owner", + status: "accepted", user: testUserId, } const testMembership = { _id: testMembershipId, - role: 'admin', + role: "admin", user: testUserId, - workspace: testWorkspaceId + workspace: testWorkspaceId, } try { @@ -124,7 +124,7 @@ export const createTestUserForDevelopment = async () => { // initialize blind index salt for workspace await SecretService.createSecretBlindIndexData({ - workspaceId: workspace._id + workspaceId: workspace._id, }); } diff --git a/backend/src/utils/aes-gcm.ts b/backend/src/utils/aes-gcm.ts index 20d0bb829..4457616a6 100644 --- a/backend/src/utils/aes-gcm.ts +++ b/backend/src/utils/aes-gcm.ts @@ -1,6 +1,6 @@ -import crypto = require('crypto'); +import crypto = require("crypto"); -const ALGORITHM = 'aes-256-gcm'; +const ALGORITHM = "aes-256-gcm"; const BLOCK_SIZE_BYTES = 16; export default class AesGCM { @@ -13,12 +13,12 @@ export default class AesGCM { const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); + let ciphertext = cipher.update(text, "utf8", "base64"); + ciphertext += cipher.final("base64"); return { ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') + iv: iv.toString("base64"), + tag: cipher.getAuthTag().toString("base64"), }; } @@ -31,12 +31,12 @@ export default class AesGCM { const decipher = crypto.createDecipheriv( ALGORITHM, secret, - Buffer.from(iv, 'base64') + Buffer.from(iv, "base64") ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); + decipher.setAuthTag(Buffer.from(tag, "base64")); - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); + let cleartext = decipher.update(ciphertext, "base64", "utf8"); + cleartext += decipher.final("utf8"); return cleartext; } diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 5144af16d..6970292c4 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -1,22 +1,22 @@ -import express from 'express'; -import passport from 'passport'; -import { AuthData } from '../interfaces/middleware'; +import express from "express"; +import passport from "passport"; +import { AuthData } from "../interfaces/middleware"; import { AuthProvider, - User, ServiceAccount, ServiceTokenData, -} from '../models'; -import { createToken } from '../helpers/auth'; + User, +} from "../models"; +import { createToken } from "../helpers/auth"; import { getClientIdGoogle, getClientSecretGoogle, getJwtProviderAuthLifetime, - getJwtProviderAuthSecret -} from '../config'; + getJwtProviderAuthSecret, +} from "../config"; // eslint-disable-next-line @typescript-eslint/no-var-requires -const GoogleStrategy = require('passport-google-oauth20').Strategy; +const GoogleStrategy = require("passport-google-oauth20").Strategy; // TODO: find a more optimal folder structure to store these types of functions @@ -68,8 +68,8 @@ const initializePassport = async () => { passReqToCallback: true, clientID: googleClientId, clientSecret: googleClientSecret, - callbackURL: '/api/v1/auth/callback/google', - scope: ['profile', ' email'], + callbackURL: "/api/v1/auth/callback/google", + scope: ["profile", " email"], }, async ( req: express.Request, accessToken: string, @@ -82,7 +82,7 @@ const initializePassport = async () => { let user = await User.findOne({ authProvider: AuthProvider.GOOGLE, authId: profile.id, - }).select('+publicKey') + }).select("+publicKey") if (!user) { user = await new User({ @@ -97,7 +97,7 @@ const initializePassport = async () => { userId: user._id.toString(), email: user.email, authProvider: user.authProvider, - isUserCompleted: !!user.publicKey + isUserCompleted: !!user.publicKey, }, expiresIn: await getJwtProviderAuthLifetime(), secret: await getJwtProviderAuthSecret(), diff --git a/backend/src/utils/crypto/index.ts b/backend/src/utils/crypto/index.ts index 9c3877717..9194bd7e8 100644 --- a/backend/src/utils/crypto/index.ts +++ b/backend/src/utils/crypto/index.ts @@ -1,20 +1,19 @@ -import crypto from 'crypto'; -import nacl from 'tweetnacl'; -import util from 'tweetnacl-util'; +import crypto from "crypto"; +import nacl from "tweetnacl"; +import util from "tweetnacl-util"; import { - IGenerateKeyPairOutput, + IDecryptAsymmetricInput, + IDecryptSymmetricInput, IEncryptAsymmetricInput, IEncryptAsymmetricOutput, - IDecryptAsymmetricInput, IEncryptSymmetricInput, - IDecryptSymmetricInput -} from '../../interfaces/utils'; -import { BadRequestError } from '../errors'; + IGenerateKeyPairOutput, +} from "../../interfaces/utils"; +import { BadRequestError } from "../errors"; import { - ALGORITHM_AES_256_GCM, - NONCE_BYTES_SIZE, - BLOCK_SIZE_BYTES_16 -} from '../../variables'; + ALGORITHM_AES_256_GCM, + BLOCK_SIZE_BYTES_16, +} from "../../variables"; /** * Return new base64, NaCl, public-private key pair. @@ -27,7 +26,7 @@ const generateKeyPair = (): IGenerateKeyPairOutput => { return ({ publicKey: util.encodeBase64(pair.publicKey), - privateKey: util.encodeBase64(pair.secretKey) + privateKey: util.encodeBase64(pair.secretKey), }); } @@ -45,7 +44,7 @@ const generateKeyPair = (): IGenerateKeyPairOutput => { const encryptAsymmetric = ({ plaintext, publicKey, - privateKey + privateKey, }: IEncryptAsymmetricInput): IEncryptAsymmetricOutput => { const nonce = nacl.randomBytes(24); const ciphertext = nacl.box( @@ -57,7 +56,7 @@ const encryptAsymmetric = ({ return { ciphertext: util.encodeBase64(ciphertext), - nonce: util.encodeBase64(nonce) + nonce: util.encodeBase64(nonce), }; }; @@ -75,7 +74,7 @@ const decryptAsymmetric = ({ ciphertext, nonce, publicKey, - privateKey + privateKey, }: IDecryptAsymmetricInput): string => { const plaintext: Uint8Array | null = nacl.box.open( util.decodeBase64(ciphertext), @@ -85,7 +84,7 @@ const decryptAsymmetric = ({ ); if (plaintext == null) throw BadRequestError({ - message: 'Invalid ciphertext or keys' + message: "Invalid ciphertext or keys", }); return util.encodeUTF8(plaintext); @@ -108,18 +107,18 @@ const decryptAsymmetric = ({ */ const encryptSymmetric128BitHexKeyUTF8 = ({ plaintext, - key + key, }: IEncryptSymmetricInput) => { const iv = crypto.randomBytes(BLOCK_SIZE_BYTES_16); const cipher = crypto.createCipheriv(ALGORITHM_AES_256_GCM, key, iv); - let ciphertext = cipher.update(plaintext, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); + let ciphertext = cipher.update(plaintext, "utf8", "base64"); + ciphertext += cipher.final("base64"); return { ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') + iv: iv.toString("base64"), + tag: cipher.getAuthTag().toString("base64"), }; } /** @@ -141,18 +140,18 @@ const decryptSymmetric128BitHexKeyUTF8 = ({ ciphertext, iv, tag, - key + key, }: IDecryptSymmetricInput) => { const decipher = crypto.createDecipheriv( ALGORITHM_AES_256_GCM, key, - Buffer.from(iv, 'base64') + Buffer.from(iv, "base64") ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); + decipher.setAuthTag(Buffer.from(tag, "base64")); - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); + let cleartext = decipher.update(ciphertext, "base64", "utf8"); + cleartext += decipher.final("utf8"); return cleartext; } @@ -162,5 +161,5 @@ export { encryptAsymmetric, decryptAsymmetric, encryptSymmetric128BitHexKeyUTF8, - decryptSymmetric128BitHexKeyUTF8 + decryptSymmetric128BitHexKeyUTF8, }; diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index 4b698bee3..f99ef27b2 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -4,231 +4,231 @@ import RequestError, { LogLevel, RequestErrorContext } from "./requestError" export const RouteNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'route_not_found', - message: error?.message ?? 'The requested source was not found', + type: error?.type ?? "route_not_found", + message: error?.message ?? "The requested source was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const MethodNotAllowedError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, statusCode: error?.statusCode ?? 405, - type: error?.type ?? 'method_not_allowed', - message: error?.message ?? 'The requested method is not allowed for the resource', + type: error?.type ?? "method_not_allowed", + message: error?.message ?? "The requested method is not allowed for the resource", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const UnauthorizedRequestError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, statusCode: error?.statusCode ?? 401, - type: error?.type ?? 'unauthorized', - message: error?.message ?? 'You are not authorized to access this resource', + type: error?.type ?? "unauthorized", + message: error?.message ?? "You are not authorized to access this resource", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const ForbiddenRequestError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, statusCode: error?.statusCode ?? 403, - type: error?.type ?? 'forbidden', - message: error?.message ?? 'You are not allowed to access this resource', + type: error?.type ?? "forbidden", + message: error?.message ?? "You are not allowed to access this resource", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const BadRequestError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.INFO, statusCode: error?.statusCode ?? 400, - type: error?.type ?? 'bad_request', - message: error?.message ?? 'The request is invalid or cannot be served', + type: error?.type ?? "bad_request", + message: error?.message ?? "The request is invalid or cannot be served", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const InternalServerError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 500, - type: error?.type ?? 'internal_server_error', - message: error?.message ?? 'The server encountered an error while processing the request', + type: error?.type ?? "internal_server_error", + message: error?.message ?? "The server encountered an error while processing the request", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const ServiceUnavailableError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 503, - type: error?.type ?? 'service_unavailable', - message: error?.message ?? 'The service is currently unavailable. Please try again later.', + type: error?.type ?? "service_unavailable", + message: error?.message ?? "The service is currently unavailable. Please try again later.", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const ValidationError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 400, - type: error?.type ?? 'validation_error', - message: error?.message ?? 'The request failed validation', + type: error?.type ?? "validation_error", + message: error?.message ?? "The request failed validation", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[INTEGRATION AUTH ERRORS]<----- export const IntegrationAuthNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'integration_auth_not_found_error', - message: error?.message ?? 'The requested integration authorization was not found', + type: error?.type ?? "integration_auth_not_found_error", + message: error?.message ?? "The requested integration authorization was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[INTEGRATION ERRORS]<----- export const IntegrationNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'integration_not_found_error', - message: error?.message ?? 'The requested integration was not found', + type: error?.type ?? "integration_not_found_error", + message: error?.message ?? "The requested integration was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[WORKSPACE ERRORS]<----- export const WorkspaceNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'workspace_not_found_error', - message: error?.message ?? 'The requested workspace was not found', + type: error?.type ?? "workspace_not_found_error", + message: error?.message ?? "The requested workspace was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[WORKSPACE MEMBERSHIP ERRORS]<----- export const MembershipNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'workspace_membership_not_found_error', - message: error?.message ?? 'The requested membership was not found', + type: error?.type ?? "workspace_membership_not_found_error", + message: error?.message ?? "The requested membership was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[ORGANIZATION ERRORS]<----- export const OrganizationNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'organization_not_found_error', - message: error?.message ?? 'The requested organization was not found', + type: error?.type ?? "organization_not_found_error", + message: error?.message ?? "The requested organization was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[MEMBERSHIP ORGANIZATION ERRORS]<----- export const MembershipOrgNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'organization_membership_not_found_error', - message: error?.message ?? 'The requested organization membership was not found', + type: error?.type ?? "organization_membership_not_found_error", + message: error?.message ?? "The requested organization membership was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[ACCOUNT ERRORS]<----- export const AccountNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'account_not_found_error', - message: error?.message ?? 'The requested account was not found', + type: error?.type ?? "account_not_found_error", + message: error?.message ?? "The requested account was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[SECRET ERRORS]<----- export const SecretNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'secret_not_found_error', - message: error?.message ?? 'The requested secret was not found', + type: error?.type ?? "secret_not_found_error", + message: error?.message ?? "The requested secret was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[SECRET BLIND INDEX DATA ERRORS]<----- export const SecretBlindIndexDataNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'secret_blind_index_data_not_found_error', - message: error?.message ?? 'The requested secret was not found', + type: error?.type ?? "secret_blind_index_data_not_found_error", + message: error?.message ?? "The requested secret was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[SECRET SNAPSHOT ERRORS]<----- export const SecretSnapshotNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'secret_snapshot_not_found_error', - message: error?.message ?? 'The requested secret snapshot was not found', + type: error?.type ?? "secret_snapshot_not_found_error", + message: error?.message ?? "The requested secret snapshot was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[ACTION ERRORS]<----- export const ActionNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'action_not_found_error', - message: error?.message ?? 'The requested action was not found', + type: error?.type ?? "action_not_found_error", + message: error?.message ?? "The requested action was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[SERVICE TOKEN DATA ERRORS]<----- export const ServiceTokenDataNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'service_token_data_not_found_error', - message: error?.message ?? 'The requested service token data was not found', + type: error?.type ?? "service_token_data_not_found_error", + message: error?.message ?? "The requested service token data was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }) //* ----->[API KEY DATA ERRORS]<----- export const APIKeyDataNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'api_key_data_not_found_error', - message: error?.message ?? 'The requested service token data was not found', + type: error?.type ?? "api_key_data_not_found_error", + message: error?.message ?? "The requested service token data was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); //* ----->[SERVICE_ACCOUNT ERRORS]<----- export const ServiceAccountNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'service_account_not_found_error', - message: error?.message ?? 'The requested service account was not found', + type: error?.type ?? "service_account_not_found_error", + message: error?.message ?? "The requested service account was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }); export const ServiceAccountKeyNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'service_account_key_not_found_error', - message: error?.message ?? 'The requested service account key was not found', + type: error?.type ?? "service_account_key_not_found_error", + message: error?.message ?? "The requested service account key was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }) export const BotNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, statusCode: error?.statusCode ?? 404, - type: error?.type ?? 'bot_not_found_error', - message: error?.message ?? 'The requested bot was not found', + type: error?.type ?? "bot_not_found_error", + message: error?.message ?? "The requested bot was not found", context: error?.context, - stack: error?.stack + stack: error?.stack, }) //* ----->[MISC ERRORS]<----- diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts index 15335a619..9673f1115 100644 --- a/backend/src/utils/logger.ts +++ b/backend/src/utils/logger.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ -import { createLogger, format, transports } from 'winston'; -import LokiTransport from 'winston-loki'; -import { getLokiHost, getNodeEnv } from '../config'; +import { createLogger, format, transports } from "winston"; +import LokiTransport from "winston-loki"; +import { getLokiHost, getNodeEnv } from "../config"; const { combine, colorize, label, printf, splat, timestamp } = format; @@ -13,7 +13,7 @@ const logFormat = (prefix: string) => combine( ); const createLoggerWithLabel = async (level: string, label: string) => { - const _level = level.toLowerCase() || 'info' + const _level = level.toLowerCase() || "info" //* Always add Console output to transports const _transports: any[] = [ new transports.Console({ @@ -21,8 +21,8 @@ const createLoggerWithLabel = async (level: string, label: string) => { colorize(), logFormat(label), // format.json() - ) - }) + ), + }), ] //* Add LokiTransport if it's enabled if((await getLokiHost()) !== undefined){ @@ -40,9 +40,9 @@ const createLoggerWithLabel = async (level: string, label: string) => { labels: { app: process.env.npm_package_name, version: process.env.npm_package_version, - environment: await getNodeEnv() + environment: await getNodeEnv(), }, - onConnectionError: (err: Error)=> console.error('Connection error while connecting to Loki Server.\n', err) + onConnectionError: (err: Error)=> console.error("Connection error while connecting to Loki Server.\n", err), }) ) } @@ -53,15 +53,15 @@ const createLoggerWithLabel = async (level: string, label: string) => { transports: _transports, format: format.combine( logFormat(label), - format.metadata({ fillExcept: ['message', 'level', 'timestamp', 'label'] }) - ) + format.metadata({ fillExcept: ["message", "level", "timestamp", "label"] }) + ), }); } -export const getLogger = async (loggerName: 'backend-main' | 'database') => { +export const getLogger = async (loggerName: "backend-main" | "database") => { const logger = { - "backend-main": await createLoggerWithLabel('info', '[IFSC:backend-main]'), - "database": await createLoggerWithLabel('info', '[IFSC:database]'), + "backend-main": await createLoggerWithLabel("info", "[IFSC:backend-main]"), + "database": await createLoggerWithLabel("info", "[IFSC:database]"), } return logger[loggerName] } diff --git a/backend/src/utils/posthog.ts b/backend/src/utils/posthog.ts index 85a411ca7..06c404888 100644 --- a/backend/src/utils/posthog.ts +++ b/backend/src/utils/posthog.ts @@ -7,7 +7,7 @@ export const getChannelFromUserAgent = function (userAgent: string | undefined) return "cli" } else if (userAgent == K8_OPERATOR_AGENT_NAME) { return "k8-operator" - } else if (userAgent.toLowerCase().includes('mozilla')) { + } else if (userAgent.toLowerCase().includes("mozilla")) { return "web" } else { return "other" diff --git a/backend/src/utils/requestError.ts b/backend/src/utils/requestError.ts index 570ed132e..0c4e093b2 100644 --- a/backend/src/utils/requestError.ts +++ b/backend/src/utils/requestError.ts @@ -1,5 +1,5 @@ -import { Request } from 'express' -import { getVerboseErrorOutput } from '../config'; +import { Request } from "express" +import { getVerboseErrorOutput } from "../config"; export enum LogLevel { DEBUG = 100, @@ -44,7 +44,7 @@ export default class RequestError extends Error{ if(stack) this.stack = stack else Error.captureStackTrace(this, this.constructor) - this.stacktrace = this.stack?.split('\n') + this.stacktrace = this.stack?.split("\n") } static convertFrom(error: Error) { @@ -52,13 +52,13 @@ export default class RequestError extends Error{ return new RequestError({ logLevel: LogLevel.ERROR, statusCode: 500, - type: 'internal_server_error', - message: 'This error was not handled by error handler. Please report this incident to the staff', + type: "internal_server_error", + message: "This error was not handled by error handler. Please report this incident to the staff", context: { message: error.message, - name: error.name + name: error.name, }, - stack: error.stack + stack: error.stack, }) } @@ -66,7 +66,7 @@ export default class RequestError extends Error{ get levelName(){ return this._logName } withTags(...tags: string[]|number[]){ - this.context['tags'] = Object.assign(tags, this.context['tags']) + this.context["tags"] = Object.assign(tags, this.context["tags"]) return this } @@ -83,14 +83,14 @@ export default class RequestError extends Error{ public async format(req: Request){ let _context = Object.assign({ - stacktrace: this.stacktrace + stacktrace: this.stacktrace, }, this.context) //* Omit sensitive information from context that can leak internal workings of this program if user is not developer if(!(await getVerboseErrorOutput())){ _context = this._omit(_context, [ - 'stacktrace', - 'exception', + "stacktrace", + "exception", ]) } @@ -102,9 +102,9 @@ export default class RequestError extends Error{ level_name: this.levelName, status_code: this.statusCode, datetime_iso: new Date().toISOString(), - application: process.env.npm_package_name || 'unknown', + application: process.env.npm_package_name || "unknown", request_id: req.headers["Request-Id"], - extra: this.extra + extra: this.extra, } return formatObject diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index 4bb163aae..a77d3ee27 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -5,21 +5,22 @@ import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; import { EESecretService } from "../../ee/services"; import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models"; import { - Secret, - ISecret, - SecretBlindIndexData, - Workspace, - Bot, BackupPrivateKey, + Bot, + ISecret, + Integration, IntegrationAuth, + Secret, + SecretBlindIndexData, ServiceTokenData, + Workspace, } from "../../models"; import { generateKeyPair } from "../../utils/crypto"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64, + ENCODING_SCHEME_UTF8, } from "../../variables"; import { InternalServerError } from "../errors"; @@ -424,3 +425,19 @@ export const backfillServiceToken = async () => { ); console.log("Migration: Service token migration v1 complete"); }; + +export const backfillIntegration = async () => { + await Integration.updateMany( + { + secretPath: { + $exists: false, + }, + }, + { + $set: { + secretPath: "/", + }, + } + ); + console.log("Migration: Integration migration v1 complete"); +}; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index bec142e9e..16d9ee1ae 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -7,11 +7,12 @@ import { createTestUserForDevelopment } from "../addDevelopmentUser"; // eslint-disable-next-line @typescript-eslint/no-var-requires import { validateEncryptionKeysConfig } from "./validateConfig"; import { - backfillSecretVersions, backfillBots, - backfillSecretBlindIndexData, backfillEncryptionMetadata, + backfillIntegration, + backfillSecretBlindIndexData, backfillSecretFolders, + backfillSecretVersions, backfillServiceToken, } from "./backfillData"; import { @@ -19,11 +20,11 @@ import { reencryptSecretBlindIndexDataSalts, } from "./reencryptData"; import { - getNodeEnv, - getMongoURL, - getSentryDSN, - getClientSecretGoogle, getClientIdGoogle, + getClientSecretGoogle, + getMongoURL, + getNodeEnv, + getSentryDSN, } from "../../config"; import { initializePassport } from "../auth"; @@ -77,6 +78,7 @@ export const setup = async () => { await backfillEncryptionMetadata(); await backfillSecretFolders(); await backfillServiceToken(); + await backfillIntegration(); // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY diff --git a/backend/src/utils/setup/reencryptData.ts b/backend/src/utils/setup/reencryptData.ts index 0741b44f4..7ea5db202 100644 --- a/backend/src/utils/setup/reencryptData.ts +++ b/backend/src/utils/setup/reencryptData.ts @@ -2,19 +2,19 @@ import { Bot, IBot, ISecretBlindIndexData, - SecretBlindIndexData -} from '../../models'; -import { decryptSymmetric128BitHexKeyUTF8 } from '../../utils/crypto'; + SecretBlindIndexData, +} from "../../models"; +import { decryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto"; import { client, getEncryptionKey, - getRootEncryptionKey -} from '../../config'; + getRootEncryptionKey, +} from "../../config"; import { ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 -} from '../../variables'; +} from "../../variables"; /** * Re-encrypt bot private keys from hex 128-bit ENCRYPTION_KEY @@ -28,8 +28,8 @@ export const reencryptBotPrivateKeys = async () => { // 1: re-encrypt bot private keys under ROOT_ENCRYPTION_KEY const bots = await Bot.find({ algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }).select('+encryptedPrivateKey iv tag algorithm keyEncoding'); + keyEncoding: ENCODING_SCHEME_UTF8, + }).select("+encryptedPrivateKey iv tag algorithm keyEncoding"); if (bots.length === 0) return; @@ -40,28 +40,28 @@ export const reencryptBotPrivateKeys = async () => { ciphertext: bot.encryptedPrivateKey, iv: bot.iv, tag: bot.tag, - key: encryptionKey + key: encryptionKey, }); const { ciphertext: encryptedPrivateKey, iv, - tag + tag, } = client.encryptSymmetric(privateKey, rootEncryptionKey); return ({ updateOne: { filter: { - _id: bot._id + _id: bot._id, }, update: { encryptedPrivateKey, iv, tag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 - } - } + keyEncoding: ENCODING_SCHEME_BASE64, + }, + }, }) }) ); @@ -81,8 +81,8 @@ export const reencryptSecretBlindIndexDataSalts = async () => { if (encryptionKey && rootEncryptionKey) { const secretBlindIndexData = await SecretBlindIndexData.find({ algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }).select('+encryptedSaltCiphertext +saltIV +saltTag +algorithm +keyEncoding'); + keyEncoding: ENCODING_SCHEME_UTF8, + }).select("+encryptedSaltCiphertext +saltIV +saltTag +algorithm +keyEncoding"); if (secretBlindIndexData.length == 0) return; @@ -93,28 +93,28 @@ export const reencryptSecretBlindIndexDataSalts = async () => { ciphertext: secretBlindIndexDatum.encryptedSaltCiphertext, iv: secretBlindIndexDatum.saltIV, tag: secretBlindIndexDatum.saltTag, - key: encryptionKey + key: encryptionKey, }); const { ciphertext: encryptedSaltCiphertext, iv: saltIV, - tag: saltTag + tag: saltTag, } = client.encryptSymmetric(salt, rootEncryptionKey); return ({ updateOne: { filter: { - _id: secretBlindIndexDatum._id + _id: secretBlindIndexDatum._id, }, update: { encryptedSaltCiphertext, saltIV, saltTag, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 - } - } + keyEncoding: ENCODING_SCHEME_BASE64, + }, + }, }) }) ); diff --git a/backend/src/utils/setup/validateConfig.ts b/backend/src/utils/setup/validateConfig.ts index f8ebd3c17..3a3974791 100644 --- a/backend/src/utils/setup/validateConfig.ts +++ b/backend/src/utils/setup/validateConfig.ts @@ -1,10 +1,10 @@ import { getEncryptionKey, - getRootEncryptionKey -} from '../../config'; + getRootEncryptionKey, +} from "../../config"; import { - InternalServerError -} from '../../utils/errors'; + InternalServerError, +} from "../../utils/errors"; /** * Validate ENCRYPTION_KEY and ROOT_ENCRYPTION_KEY. Specifically: @@ -26,7 +26,7 @@ export const validateEncryptionKeysConfig = async () => { (encryptionKey === undefined || encryptionKey === "") && (rootEncryptionKey === undefined || rootEncryptionKey === "") ) throw InternalServerError({ - message: "Failed to find required root encryption key environment variable. Please make sure that you're passing in a ROOT_ENCRYPTION_KEY environment variable." + message: "Failed to find required root encryption key environment variable. Please make sure that you're passing in a ROOT_ENCRYPTION_KEY environment variable.", }); // if (encryptionKey && encryptionKey !== '') { @@ -44,18 +44,18 @@ export const validateEncryptionKeysConfig = async () => { // }); // } - if (rootEncryptionKey && rootEncryptionKey !== '') { + if (rootEncryptionKey && rootEncryptionKey !== "") { // validate [rootEncryptionKey] - const keyBuffer = Buffer.from(rootEncryptionKey, 'base64') - const decoded = keyBuffer.toString('base64'); + const keyBuffer = Buffer.from(rootEncryptionKey, "base64") + const decoded = keyBuffer.toString("base64"); if (decoded !== rootEncryptionKey) throw InternalServerError({ - message: 'Failed to validate that the root encryption key is correctly encoded in base64' + message: "Failed to validate that the root encryption key is correctly encoded in base64", }); if (keyBuffer.length !== 32) throw InternalServerError({ - message: 'Failed to validate that the encryption key is a 256-bit base64 string' + message: "Failed to validate that the encryption key is a 256-bit base64 string", }); } } \ No newline at end of file diff --git a/backend/src/validation/bot.ts b/backend/src/validation/bot.ts index 7104eec33..7e19ca770 100644 --- a/backend/src/validation/bot.ts +++ b/backend/src/validation/bot.ts @@ -1,25 +1,25 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IUser, + Bot, IServiceAccount, IServiceTokenData, - Bot, - User, + IUser, ServiceAccount, - ServiceTokenData -} from '../models'; -import { validateServiceAccountClientForWorkspace } from './serviceAccount'; -import { validateUserClientForWorkspace } from './user'; + ServiceTokenData, + User, +} from "../models"; +import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; +import { validateUserClientForWorkspace } from "./user"; import { + BotNotFoundError, UnauthorizedRequestError, - BotNotFoundError -} from '../utils/errors'; +} from "../utils/errors"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; /** * Validate authenticated clients for bot with id [botId] based diff --git a/backend/src/validation/index.ts b/backend/src/validation/index.ts index 177c716ea..4cc25450f 100644 --- a/backend/src/validation/index.ts +++ b/backend/src/validation/index.ts @@ -1,11 +1,11 @@ -export * from './user'; -export * from './workspace'; -export * from './bot'; -export * from './integration'; -export * from './integrationAuth'; -export * from './membership'; -export * from './membershipOrg'; -export * from './organization'; -export * from './secrets'; -export * from './serviceAccount'; -export * from './serviceTokenData'; \ No newline at end of file +export * from "./user"; +export * from "./workspace"; +export * from "./bot"; +export * from "./integration"; +export * from "./integrationAuth"; +export * from "./membership"; +export * from "./membershipOrg"; +export * from "./organization"; +export * from "./secrets"; +export * from "./serviceAccount"; +export * from "./serviceTokenData"; \ No newline at end of file diff --git a/backend/src/validation/integration.ts b/backend/src/validation/integration.ts index 5b2f4ad3c..3f39a87b6 100644 --- a/backend/src/validation/integration.ts +++ b/backend/src/validation/integration.ts @@ -1,28 +1,28 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IUser, IServiceAccount, IServiceTokenData, + IUser, Integration, IntegrationAuth, - User, ServiceAccount, - ServiceTokenData -} from '../models'; -import { validateServiceAccountClientForWorkspace } from './serviceAccount'; -import { validateUserClientForWorkspace } from './user'; -import { IntegrationService } from '../services'; + ServiceTokenData, + User, +} from "../models"; +import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; +import { validateUserClientForWorkspace } from "./user"; +import { IntegrationService } from "../services"; import { - IntegrationNotFoundError, IntegrationAuthNotFoundError, - UnauthorizedRequestError -} from '../utils/errors'; + IntegrationNotFoundError, + UnauthorizedRequestError, +} from "../utils/errors"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; /** * Validate authenticated clients for integration with id [integrationId] based @@ -37,14 +37,14 @@ import { export const validateClientForIntegration = async ({ authData, integrationId, - acceptedRoles + acceptedRoles, }: { authData: { authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }; integrationId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; }) => { const integration = await Integration.findById(integrationId); @@ -53,20 +53,20 @@ export const validateClientForIntegration = async ({ const integrationAuth = await IntegrationAuth .findById(integration.integrationAuth) .select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" ); if (!integrationAuth) throw IntegrationAuthNotFoundError(); const accessToken = (await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id + integrationAuthId: integrationAuth._id, })).accessToken; if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: integration.workspace, - acceptedRoles + acceptedRoles, }); return ({ integration, accessToken }); @@ -75,7 +75,7 @@ export const validateClientForIntegration = async ({ if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { await validateServiceAccountClientForWorkspace({ serviceAccount: authData.authPayload, - workspaceId: integration.workspace + workspaceId: integration.workspace, }); return ({ integration, accessToken }); @@ -83,7 +83,7 @@ export const validateClientForIntegration = async ({ if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { throw UnauthorizedRequestError({ - message: 'Failed service token authorization for integration' + message: "Failed service token authorization for integration", }); } @@ -91,13 +91,13 @@ export const validateClientForIntegration = async ({ await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: integration.workspace, - acceptedRoles + acceptedRoles, }); return ({ integration, accessToken }); } throw UnauthorizedRequestError({ - message: 'Failed client authorization for integration' + message: "Failed client authorization for integration", }); } \ No newline at end of file diff --git a/backend/src/validation/integrationAuth.ts b/backend/src/validation/integrationAuth.ts index b43dd4cbd..fa77f3d62 100644 --- a/backend/src/validation/integrationAuth.ts +++ b/backend/src/validation/integrationAuth.ts @@ -1,27 +1,27 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IntegrationAuth, - IUser, - User, IServiceAccount, - ServiceAccount, IServiceTokenData, + IUser, + IWorkspace, + IntegrationAuth, + ServiceAccount, ServiceTokenData, - IWorkspace -} from '../models'; + User, +} from "../models"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; import { IntegrationAuthNotFoundError, - UnauthorizedRequestError -} from '../utils/errors'; -import { IntegrationService } from '../services'; -import { validateUserClientForWorkspace } from './user'; -import { validateServiceAccountClientForWorkspace } from './serviceAccount'; + UnauthorizedRequestError, +} from "../utils/errors"; +import { IntegrationService } from "../services"; +import { validateUserClientForWorkspace } from "./user"; +import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; /** * Validate authenticated clients for integration authorization with id [integrationAuthId] based @@ -36,55 +36,58 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount'; authData, integrationAuthId, acceptedRoles, - attachAccessToken + attachAccessToken, }: { authData: { authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }; integrationAuthId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; attachAccessToken?: boolean; }) => { const integrationAuth = await IntegrationAuth .findById(integrationAuthId) - .populate<{ workspace: IWorkspace }>('workspace') + .populate<{ workspace: IWorkspace }>("workspace") .select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + "+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt" ); if (!integrationAuth) throw IntegrationAuthNotFoundError(); - let accessToken; + let accessToken, accessId; if (attachAccessToken) { - accessToken = (await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id - })).accessToken; + const access = (await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id, + })); + + accessToken = access.accessToken; + accessId = access.accessId; } if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: integrationAuth.workspace._id, - acceptedRoles + acceptedRoles, }); - return ({ integrationAuth, accessToken }); + return ({ integrationAuth, accessToken, accessId }); } if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { await validateServiceAccountClientForWorkspace({ serviceAccount: authData.authPayload, - workspaceId: integrationAuth.workspace._id + workspaceId: integrationAuth.workspace._id, }); - return ({ integrationAuth, accessToken }); + return ({ integrationAuth, accessToken, accessId }); } if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { throw UnauthorizedRequestError({ - message: 'Failed service token authorization for integration authorization' + message: "Failed service token authorization for integration authorization", }); } @@ -92,17 +95,17 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount'; await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: integrationAuth.workspace._id, - acceptedRoles + acceptedRoles, }); - return ({ integrationAuth, accessToken }); + return ({ integrationAuth, accessToken, accessId }); } throw UnauthorizedRequestError({ - message: 'Failed client authorization for integration authorization' + message: "Failed client authorization for integration authorization", }); } export { - validateClientForIntegrationAuth + validateClientForIntegrationAuth, }; \ No newline at end of file diff --git a/backend/src/validation/membership.ts b/backend/src/validation/membership.ts index ab4f8dc76..aee5f8bca 100644 --- a/backend/src/validation/membership.ts +++ b/backend/src/validation/membership.ts @@ -1,26 +1,26 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IUser, IServiceAccount, IServiceTokenData, + IUser, Membership, - User, ServiceAccount, - ServiceTokenData -} from '../models'; -import { validateServiceAccountClientForWorkspace } from './serviceAccount'; -import { validateUserClientForWorkspace } from './user'; -import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData'; + ServiceTokenData, + User, +} from "../models"; +import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; +import { validateUserClientForWorkspace } from "./user"; +import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; import { MembershipNotFoundError, - UnauthorizedRequestError -} from '../utils/errors'; + UnauthorizedRequestError, +} from "../utils/errors"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; /** * Validate authenticated clients for membership with id [membershipId] based @@ -34,27 +34,27 @@ import { export const validateClientForMembership = async ({ authData, membershipId, - acceptedRoles + acceptedRoles, }: { authData: { authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }; membershipId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; }) => { const membership = await Membership.findById(membershipId); if (!membership) throw MembershipNotFoundError({ - message: 'Failed to find membership' + message: "Failed to find membership", }); if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: membership.workspace, - acceptedRoles + acceptedRoles, }); return membership; @@ -63,7 +63,7 @@ export const validateClientForMembership = async ({ if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { await validateServiceAccountClientForWorkspace({ serviceAccount: authData.authPayload, - workspaceId: membership.workspace + workspaceId: membership.workspace, }); return membership; @@ -72,7 +72,7 @@ export const validateClientForMembership = async ({ if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { await validateServiceTokenDataClientForWorkspace({ serviceTokenData: authData.authPayload, - workspaceId: new Types.ObjectId(membership.workspace) + workspaceId: new Types.ObjectId(membership.workspace), }); return membership; @@ -82,13 +82,13 @@ export const validateClientForMembership = async ({ await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: membership.workspace, - acceptedRoles + acceptedRoles, }); return membership; } throw UnauthorizedRequestError({ - message: 'Failed client authorization for membership' + message: "Failed client authorization for membership", }); } \ No newline at end of file diff --git a/backend/src/validation/membershipOrg.ts b/backend/src/validation/membershipOrg.ts index 7fd86a374..444f2aa2a 100644 --- a/backend/src/validation/membershipOrg.ts +++ b/backend/src/validation/membershipOrg.ts @@ -1,26 +1,26 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IUser, IServiceAccount, IServiceTokenData, + IUser, MembershipOrg, - User, ServiceAccount, - ServiceTokenData -} from '../models'; + ServiceTokenData, + User, +} from "../models"; import { - validateMembershipOrg -} from '../helpers/membershipOrg'; + validateMembershipOrg, +} from "../helpers/membershipOrg"; import { MembershipOrgNotFoundError, - UnauthorizedRequestError -} from '../utils/errors'; + UnauthorizedRequestError, +} from "../utils/errors"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; /** * Validate authenticated clients for organization membership with id [membershipOrgId] based @@ -35,20 +35,20 @@ export const validateClientForMembershipOrg = async ({ authData, membershipOrgId, acceptedRoles, - acceptedStatuses + acceptedStatuses, }: { authData: { authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }; membershipOrgId: Types.ObjectId; - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; }) => { const membershipOrg = await MembershipOrg.findById(membershipOrgId); if (!membershipOrg) throw MembershipOrgNotFoundError({ - message: 'Failed to find organization membership ' + message: "Failed to find organization membership ", }); if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { @@ -56,7 +56,7 @@ export const validateClientForMembershipOrg = async ({ userId: authData.authPayload._id, organizationId: membershipOrg.organization, acceptedRoles, - acceptedStatuses + acceptedStatuses, }); return membershipOrg; @@ -64,7 +64,7 @@ export const validateClientForMembershipOrg = async ({ if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({ - message: 'Failed service account client authorization for organization membership' + message: "Failed service account client authorization for organization membership", }); return membershipOrg; @@ -72,7 +72,7 @@ export const validateClientForMembershipOrg = async ({ if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { throw UnauthorizedRequestError({ - message: 'Failed service account client authorization for organization membership' + message: "Failed service account client authorization for organization membership", }); } @@ -81,13 +81,13 @@ export const validateClientForMembershipOrg = async ({ userId: authData.authPayload._id, organizationId: membershipOrg.organization, acceptedRoles, - acceptedStatuses + acceptedStatuses, }); return membershipOrg; } throw UnauthorizedRequestError({ - message: 'Failed client authorization for organization membership' + message: "Failed client authorization for organization membership", }); } \ No newline at end of file diff --git a/backend/src/validation/organization.ts b/backend/src/validation/organization.ts index 239517b75..35e0aab04 100644 --- a/backend/src/validation/organization.ts +++ b/backend/src/validation/organization.ts @@ -1,25 +1,25 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IUser, IServiceAccount, IServiceTokenData, + IUser, Organization, - User, ServiceAccount, - ServiceTokenData -} from '../models'; + ServiceTokenData, + User, +} from "../models"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; import { OrganizationNotFoundError, - UnauthorizedRequestError -} from '../utils/errors'; -import { validateUserClientForOrganization } from './user'; -import { validateServiceAccountClientForOrganization } from './serviceAccount'; + UnauthorizedRequestError, +} from "../utils/errors"; +import { validateUserClientForOrganization } from "./user"; +import { validateServiceAccountClientForOrganization } from "./serviceAccount"; /** * Validate accepted clients for organization with id [organizationId] diff --git a/backend/src/validation/secrets.ts b/backend/src/validation/secrets.ts index 272fe4545..0de983dbe 100644 --- a/backend/src/validation/secrets.ts +++ b/backend/src/validation/secrets.ts @@ -1,26 +1,26 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { ISecret, Secret, - User, ServiceAccount, - ServiceTokenData -} from '../models'; -import { validateServiceAccountClientForWorkspace, validateServiceAccountClientForSecrets } from './serviceAccount'; -import { validateUserClientForSecret, validateUserClientForSecrets } from './user'; -import { validateServiceTokenDataClientForWorkspace, validateServiceTokenDataClientForSecrets } from './serviceTokenData'; -import { AuthData } from '../interfaces/middleware'; + ServiceTokenData, + User, +} from "../models"; +import { validateServiceAccountClientForSecrets, validateServiceAccountClientForWorkspace } from "./serviceAccount"; +import { validateUserClientForSecret, validateUserClientForSecrets } from "./user"; +import { validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; +import { AuthData } from "../interfaces/middleware"; import { + BadRequestError, SecretNotFoundError, UnauthorizedRequestError, - BadRequestError -} from '../utils/errors'; +} from "../utils/errors"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; /** * Validate authenticated clients for secrets with id [secretId] based @@ -35,17 +35,17 @@ export const validateClientForSecret = async ({ authData, secretId, acceptedRoles, - requiredPermissions + requiredPermissions, }: { authData: AuthData; secretId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; requiredPermissions: string[]; }) => { const secret = await Secret.findById(secretId); if (!secret) throw SecretNotFoundError({ - message: 'Failed to find secret' + message: "Failed to find secret", }); if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { @@ -53,7 +53,7 @@ export const validateClientForSecret = async ({ user: authData.authPayload, secret, acceptedRoles, - requiredPermissions + requiredPermissions, }); return secret; @@ -64,7 +64,7 @@ export const validateClientForSecret = async ({ serviceAccount: authData.authPayload, workspaceId: secret.workspace, environment: secret.environment, - requiredPermissions + requiredPermissions, }); return secret; @@ -74,7 +74,7 @@ export const validateClientForSecret = async ({ await validateServiceTokenDataClientForWorkspace({ serviceTokenData: authData.authPayload, workspaceId: secret.workspace, - environment: secret.environment + environment: secret.environment, }); return secret; @@ -85,14 +85,14 @@ export const validateClientForSecret = async ({ user: authData.authPayload, secret, acceptedRoles, - requiredPermissions + requiredPermissions, }); return secret; } throw UnauthorizedRequestError({ - message: 'Failed client authorization for secret' + message: "Failed client authorization for secret", }); } @@ -109,7 +109,7 @@ export const validateClientForSecret = async ({ export const validateClientForSecrets = async ({ authData, secretIds, - requiredPermissions + requiredPermissions, }: { authData: AuthData; secretIds: Types.ObjectId[]; @@ -120,19 +120,19 @@ export const validateClientForSecrets = async ({ secrets = await Secret.find({ _id: { - $in: secretIds - } + $in: secretIds, + }, }); if (secrets.length != secretIds.length) { - throw BadRequestError({ message: 'Failed to validate non-existent secrets' }) + throw BadRequestError({ message: "Failed to validate non-existent secrets" }) } if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { await validateUserClientForSecrets({ user: authData.authPayload, secrets, - requiredPermissions + requiredPermissions, }); return secrets; @@ -142,7 +142,7 @@ export const validateClientForSecrets = async ({ await validateServiceAccountClientForSecrets({ serviceAccount: authData.authPayload, secrets, - requiredPermissions + requiredPermissions, }); return secrets; @@ -152,7 +152,7 @@ export const validateClientForSecrets = async ({ await validateServiceTokenDataClientForSecrets({ serviceTokenData: authData.authPayload, secrets, - requiredPermissions + requiredPermissions, }); return secrets; @@ -162,13 +162,13 @@ export const validateClientForSecrets = async ({ await validateUserClientForSecrets({ user: authData.authPayload, secrets, - requiredPermissions + requiredPermissions, }); return secrets; } throw UnauthorizedRequestError({ - message: 'Failed client authorization for secrets resource' + message: "Failed client authorization for secrets resource", }); } \ No newline at end of file diff --git a/backend/src/validation/serviceAccount.ts b/backend/src/validation/serviceAccount.ts index 11997763c..78f2c9e69 100644 --- a/backend/src/validation/serviceAccount.ts +++ b/backend/src/validation/serviceAccount.ts @@ -1,35 +1,35 @@ -import _ from 'lodash'; -import { Types } from 'mongoose'; +import _ from "lodash"; +import { Types } from "mongoose"; import { - User, + IOrganization, + ISecret, + IServiceAccount, + IServiceTokenData, IUser, ServiceAccount, - IServiceAccount, + ServiceAccountWorkspacePermission, ServiceTokenData, - IServiceTokenData, - ISecret, - IOrganization, - ServiceAccountWorkspacePermission -} from '../models'; -import { validateUserClientForServiceAccount } from './user'; + User, +} from "../models"; +import { validateUserClientForServiceAccount } from "./user"; import { BadRequestError, + ServiceAccountNotFoundError, UnauthorizedRequestError, - ServiceAccountNotFoundError -} from '../utils/errors'; +} from "../utils/errors"; import { - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, +} from "../variables"; export const validateClientForServiceAccount = async ({ authData, serviceAccountId, - requiredPermissions + requiredPermissions, }: { authData: { authMode: string; @@ -42,7 +42,7 @@ export const validateClientForServiceAccount = async ({ if (!serviceAccount) { throw ServiceAccountNotFoundError({ - message: 'Failed to find service account' + message: "Failed to find service account", }); } @@ -50,7 +50,7 @@ export const validateClientForServiceAccount = async ({ await validateUserClientForServiceAccount({ user: authData.authPayload, serviceAccount, - requiredPermissions + requiredPermissions, }); return serviceAccount; @@ -60,7 +60,7 @@ export const validateClientForServiceAccount = async ({ await validateServiceAccountClientForServiceAccount({ serviceAccount: authData.authPayload, targetServiceAccount: serviceAccount, - requiredPermissions + requiredPermissions, }); return serviceAccount; @@ -68,7 +68,7 @@ export const validateClientForServiceAccount = async ({ if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { throw UnauthorizedRequestError({ - message: 'Failed service token authorization for service account resource' + message: "Failed service token authorization for service account resource", }); } @@ -76,14 +76,14 @@ export const validateClientForServiceAccount = async ({ await validateUserClientForServiceAccount({ user: authData.authPayload, serviceAccount, - requiredPermissions + requiredPermissions, }); return serviceAccount; } throw UnauthorizedRequestError({ - message: 'Failed client authorization for service account resource' + message: "Failed client authorization for service account resource", }); } @@ -101,7 +101,7 @@ export const validateServiceAccountClientForWorkspace = async ({ serviceAccount, workspaceId, environment, - requiredPermissions + requiredPermissions, }: { serviceAccount: IServiceAccount; workspaceId: Types.ObjectId; @@ -115,11 +115,11 @@ export const validateServiceAccountClientForWorkspace = async ({ const permission = await ServiceAccountWorkspacePermission.findOne({ serviceAccount, workspace: new Types.ObjectId(workspaceId), - environment + environment, }); if (!permission) throw UnauthorizedRequestError({ - message: 'Failed service account authorization for the given workspace environment' + message: "Failed service account authorization for the given workspace environment", }); let runningIsDisallowed = false; @@ -137,7 +137,7 @@ export const validateServiceAccountClientForWorkspace = async ({ if (runningIsDisallowed) { throw UnauthorizedRequestError({ - message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`, }); } }); @@ -149,11 +149,11 @@ export const validateServiceAccountClientForWorkspace = async ({ const permission = await ServiceAccountWorkspacePermission.findOne({ serviceAccount, - workspace: new Types.ObjectId(workspaceId) + workspace: new Types.ObjectId(workspaceId), }); if (!permission) throw UnauthorizedRequestError({ - message: 'Failed service account authorization for the given workspace' + message: "Failed service account authorization for the given workspace", }); } } @@ -169,7 +169,7 @@ export const validateServiceAccountClientForWorkspace = async ({ export const validateServiceAccountClientForSecrets = async ({ serviceAccount, secrets, - requiredPermissions + requiredPermissions, }: { serviceAccount: IServiceAccount; secrets: ISecret[]; @@ -177,7 +177,7 @@ export const validateServiceAccountClientForSecrets = async ({ }) => { const permissions = await ServiceAccountWorkspacePermission.find({ - serviceAccount: serviceAccount._id + serviceAccount: serviceAccount._id, }); const permissionsObj = _.keyBy(permissions, (p) => { @@ -188,7 +188,7 @@ export const validateServiceAccountClientForSecrets = async ({ const permission = permissionsObj[`${secret.workspace.toString()}-${secret.environment}`]; if (!permission) throw BadRequestError({ - message: 'Failed to find any permission for the secret workspace and environment' + message: "Failed to find any permission for the secret workspace and environment", }); requiredPermissions?.forEach((requiredPermission: string) => { @@ -207,7 +207,7 @@ export const validateServiceAccountClientForSecrets = async ({ if (runningIsDisallowed) { throw UnauthorizedRequestError({ - message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`, }); } }); @@ -226,7 +226,7 @@ export const validateServiceAccountClientForSecrets = async ({ export const validateServiceAccountClientForServiceAccount = ({ serviceAccount, targetServiceAccount, - requiredPermissions + requiredPermissions, }: { serviceAccount: IServiceAccount; targetServiceAccount: IServiceAccount; @@ -234,7 +234,7 @@ export const validateServiceAccountClientForServiceAccount = ({ }) => { if (!serviceAccount.organization.equals(targetServiceAccount.organization)) { throw UnauthorizedRequestError({ - message: 'Failed service account authorization for the given service account' + message: "Failed service account authorization for the given service account", }); } } @@ -247,14 +247,14 @@ export const validateServiceAccountClientForServiceAccount = ({ */ export const validateServiceAccountClientForOrganization = async ({ serviceAccount, - organization + organization, }: { serviceAccount: IServiceAccount; organization: IOrganization; }) => { if (!serviceAccount.organization.equals(organization._id)) { throw UnauthorizedRequestError({ - message: 'Failed service account authorization for the given organization' + message: "Failed service account authorization for the given organization", }); } } \ No newline at end of file diff --git a/backend/src/validation/serviceTokenData.ts b/backend/src/validation/serviceTokenData.ts index 8713e000f..0ade7f96e 100644 --- a/backend/src/validation/serviceTokenData.ts +++ b/backend/src/validation/serviceTokenData.ts @@ -1,25 +1,25 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { ISecret, - IServiceTokenData, - ServiceTokenData, - IUser, - User, IServiceAccount, + IServiceTokenData, + IUser, ServiceAccount, -} from '../models'; + ServiceTokenData, + User, +} from "../models"; import { + ServiceTokenDataNotFoundError, UnauthorizedRequestError, - ServiceTokenDataNotFoundError -} from '../utils/errors'; +} from "../utils/errors"; import { - AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; -import { validateUserClientForWorkspace } from './user'; -import { validateServiceAccountClientForWorkspace } from './serviceAccount'; +} from "../variables"; +import { validateUserClientForWorkspace } from "./user"; +import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; /** * Validate authenticated clients for service token with id [serviceTokenId] based @@ -32,29 +32,29 @@ import { validateServiceAccountClientForWorkspace } from './serviceAccount'; export const validateClientForServiceTokenData = async ({ authData, serviceTokenDataId, - acceptedRoles + acceptedRoles, }: { authData: { authMode: string; authPayload: IUser | IServiceAccount | IServiceTokenData; }; serviceTokenDataId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; }) => { const serviceTokenData = await ServiceTokenData .findById(serviceTokenDataId) - .select('+encryptedKey +iv +tag') - .populate<{ user: IUser }>('user'); + .select("+encryptedKey +iv +tag") + .populate<{ user: IUser }>("user"); if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ - message: 'Failed to find service token data' + message: "Failed to find service token data", }); if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: serviceTokenData.workspace, - acceptedRoles + acceptedRoles, }); return serviceTokenData; @@ -63,7 +63,7 @@ export const validateClientForServiceTokenData = async ({ if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { await validateServiceAccountClientForWorkspace({ serviceAccount: authData.authPayload, - workspaceId: serviceTokenData.workspace + workspaceId: serviceTokenData.workspace, }); return serviceTokenData; @@ -71,7 +71,7 @@ export const validateClientForServiceTokenData = async ({ if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { throw UnauthorizedRequestError({ - message: 'Failed service token authorization for service token data' + message: "Failed service token authorization for service token data", }); } @@ -79,14 +79,14 @@ export const validateClientForServiceTokenData = async ({ await validateUserClientForWorkspace({ user: authData.authPayload, workspaceId: serviceTokenData.workspace, - acceptedRoles + acceptedRoles, }); return serviceTokenData; } throw UnauthorizedRequestError({ - message: 'Failed client authorization for service token data' + message: "Failed client authorization for service token data", }); } @@ -104,7 +104,7 @@ export const validateServiceTokenDataClientForWorkspace = async ({ serviceTokenData, workspaceId, environment, - requiredPermissions + requiredPermissions, }: { serviceTokenData: IServiceTokenData; workspaceId: Types.ObjectId; @@ -114,7 +114,7 @@ export const validateServiceTokenDataClientForWorkspace = async ({ if (!serviceTokenData.workspace.equals(workspaceId)) { // case: invalid workspaceId passed throw UnauthorizedRequestError({ - message: 'Failed service token authorization for the given workspace' + message: "Failed service token authorization for the given workspace", }); } @@ -124,14 +124,14 @@ export const validateServiceTokenDataClientForWorkspace = async ({ if (serviceTokenData.environment !== environment) { // case: invalid environment passed throw UnauthorizedRequestError({ - message: 'Failed service token authorization for the given workspace environment' + message: "Failed service token authorization for the given workspace environment", }); } requiredPermissions?.forEach((permission) => { if (!serviceTokenData.permissions.includes(permission)) { throw UnauthorizedRequestError({ - message: `Failed service token authorization for the given workspace environment action: ${permission}` + message: `Failed service token authorization for the given workspace environment action: ${permission}`, }); } }); @@ -149,7 +149,7 @@ export const validateServiceTokenDataClientForWorkspace = async ({ export const validateServiceTokenDataClientForSecrets = async ({ serviceTokenData, secrets, - requiredPermissions + requiredPermissions, }: { serviceTokenData: IServiceTokenData; secrets: ISecret[]; @@ -160,21 +160,21 @@ export const validateServiceTokenDataClientForSecrets = async ({ if (!serviceTokenData.workspace.equals(secret.workspace)) { // case: invalid workspaceId passed throw UnauthorizedRequestError({ - message: 'Failed service token authorization for the given workspace' + message: "Failed service token authorization for the given workspace", }); } if (serviceTokenData.environment !== secret.environment) { // case: invalid environment passed throw UnauthorizedRequestError({ - message: 'Failed service token authorization for the given workspace environment' + message: "Failed service token authorization for the given workspace environment", }); } requiredPermissions?.forEach((permission) => { if (!serviceTokenData.permissions.includes(permission)) { throw UnauthorizedRequestError({ - message: `Failed service token authorization for the given workspace environment action: ${permission}` + message: `Failed service token authorization for the given workspace environment action: ${permission}`, }); } }); diff --git a/backend/src/validation/user.ts b/backend/src/validation/user.ts index 4c412e52f..f1edad3bf 100644 --- a/backend/src/validation/user.ts +++ b/backend/src/validation/user.ts @@ -1,37 +1,37 @@ -import fs from 'fs'; -import path from 'path'; -import { Types } from 'mongoose'; +import fs from "fs"; +import path from "path"; +import { Types } from "mongoose"; import { - IUser, + IOrganization, ISecret, IServiceAccount, + IUser, Membership, - IOrganization, -} from '../models'; -import { validateMembership } from '../helpers/membership'; -import _ from 'lodash'; -import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../utils/errors'; +} from "../models"; +import { validateMembership } from "../helpers/membership"; +import _ from "lodash"; +import { BadRequestError, UnauthorizedRequestError, ValidationError } from "../utils/errors"; import { - validateMembershipOrg -} from '../helpers/membershipOrg'; + validateMembershipOrg, +} from "../helpers/membershipOrg"; import { PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS -} from '../variables'; + PERMISSION_WRITE_SECRETS, +} from "../variables"; /** * Validate that email [email] is not disposable * @param email - email to validate */ export const validateUserEmail = (email: string) => { - const emailDomain = email.split('@')[1]; + const emailDomain = email.split("@")[1]; const disposableEmails = fs.readFileSync( - path.resolve(__dirname, '../data/' + 'disposable_emails.txt'), - 'utf8' - ).split('\n'); + path.resolve(__dirname, "../data/" + "disposable_emails.txt"), + "utf8" + ).split("\n"); if (disposableEmails.includes(emailDomain)) throw ValidationError({ - message: 'Failed to validate email as non-disposable' + message: "Failed to validate email as non-disposable", }); } @@ -50,12 +50,12 @@ export const validateUserClientForWorkspace = async ({ workspaceId, environment, acceptedRoles, - requiredPermissions + requiredPermissions, }: { user: IUser; workspaceId: Types.ObjectId; environment?: string; - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; requiredPermissions?: string[]; }) => { @@ -63,7 +63,7 @@ export const validateUserClientForWorkspace = async ({ const membership = await validateMembership({ userId: user._id, workspaceId, - acceptedRoles + acceptedRoles, }); let runningIsDisallowed = false; @@ -81,7 +81,7 @@ export const validateUserClientForWorkspace = async ({ if (runningIsDisallowed) { throw UnauthorizedRequestError({ - message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`, }); } }); @@ -101,17 +101,17 @@ export const validateUserClientForSecret = async ({ user, secret, acceptedRoles, - requiredPermissions + requiredPermissions, }: { user: IUser; secret: ISecret; - acceptedRoles?: Array<'admin' | 'member'>; + acceptedRoles?: Array<"admin" | "member">; requiredPermissions?: string[]; }) => { const membership = await validateMembership({ userId: user._id, workspaceId: secret.workspace, - acceptedRoles + acceptedRoles, }); if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { @@ -119,7 +119,7 @@ export const validateUserClientForSecret = async ({ if (isDisallowed) { throw UnauthorizedRequestError({ - message: 'You do not have the required permissions to perform this action' + message: "You do not have the required permissions to perform this action", }); } } @@ -136,7 +136,7 @@ export const validateUserClientForSecret = async ({ export const validateUserClientForSecrets = async ({ user, secrets, - requiredPermissions + requiredPermissions, }: { user: IUser; secrets: ISecret[]; @@ -146,14 +146,14 @@ export const validateUserClientForSecrets = async ({ // TODO: add acceptedRoles? const userMemberships = await Membership.find({ user: user._id }) - const userMembershipById = _.keyBy(userMemberships, 'workspace'); + const userMembershipById = _.keyBy(userMemberships, "workspace"); const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString())); // for each secret check if the secret belongs to a workspace the user is a member of secrets.forEach((secret: ISecret) => { if (!workspaceIdsSet.has(secret.workspace.toString())) { throw BadRequestError({ - message: 'Failed authorization for the secret' + message: "Failed authorization for the secret", }); } @@ -163,7 +163,7 @@ export const validateUserClientForSecrets = async ({ if (isDisallowed) { throw UnauthorizedRequestError({ - message: 'You do not have the required permissions to perform this action' + message: "You do not have the required permissions to perform this action", }); } } @@ -181,7 +181,7 @@ export const validateUserClientForSecrets = async ({ export const validateUserClientForServiceAccount = async ({ user, serviceAccount, - requiredPermissions + requiredPermissions, }: { user: IUser; serviceAccount: IServiceAccount; @@ -194,7 +194,7 @@ export const validateUserClientForServiceAccount = async ({ userId: user._id, organizationId: serviceAccount.organization, acceptedRoles: [], - acceptedStatuses: [] + acceptedStatuses: [], }); } } @@ -209,18 +209,18 @@ export const validateUserClientForOrganization = async ({ user, organization, acceptedRoles, - acceptedStatuses + acceptedStatuses, }: { user: IUser; organization: IOrganization; - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; }) => { const membershipOrg = await validateMembershipOrg({ userId: user._id, organizationId: organization._id, acceptedRoles, - acceptedStatuses + acceptedStatuses, }); return membershipOrg; diff --git a/backend/src/validation/workspace.ts b/backend/src/validation/workspace.ts index b7a04634f..cdc2771f4 100644 --- a/backend/src/validation/workspace.ts +++ b/backend/src/validation/workspace.ts @@ -1,27 +1,29 @@ -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - IUser, IServiceAccount, IServiceTokenData, - Workspace, - User, + IUser, + SecretBlindIndexData, ServiceAccount, ServiceTokenData, - SecretBlindIndexData -} from '../models'; -import { validateServiceAccountClientForWorkspace } from './serviceAccount'; -import { validateUserClientForWorkspace } from './user'; -import { validateServiceTokenDataClientForWorkspace } from './serviceTokenData'; + User, + Workspace, +} from "../models"; +import { validateServiceAccountClientForWorkspace } from "./serviceAccount"; +import { validateUserClientForWorkspace } from "./user"; +import { validateServiceTokenDataClientForWorkspace } from "./serviceTokenData"; import { + BadRequestError, UnauthorizedRequestError, - WorkspaceNotFoundError -} from '../utils/errors'; + WorkspaceNotFoundError, +} from "../utils/errors"; import { + AUTH_MODE_API_KEY, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; +} from "../variables"; +import { BotService } from "../services"; /** * Validate authenticated clients for workspace with id [workspaceId] based @@ -39,7 +41,8 @@ export const validateClientForWorkspace = async ({ environment, acceptedRoles, requiredPermissions, - requireBlindIndicesEnabled + requireBlindIndicesEnabled, + requireE2EEOff, }: { authData: { authMode: string; @@ -47,14 +50,15 @@ export const validateClientForWorkspace = async ({ }; workspaceId: Types.ObjectId; environment?: string; - acceptedRoles: Array<'admin' | 'member'>; + acceptedRoles: Array<"admin" | "member">; requiredPermissions?: string[]; requireBlindIndicesEnabled: boolean; + requireE2EEOff: boolean; }) => { const workspace = await Workspace.findById(workspaceId); if (!workspace) throw WorkspaceNotFoundError({ - message: 'Failed to find workspace' + message: "Failed to find workspace", }); if (requireBlindIndicesEnabled) { @@ -63,11 +67,19 @@ export const validateClientForWorkspace = async ({ // and no admin has enabled it) const secretBlindIndexData = await SecretBlindIndexData.exists({ - workspace: new Types.ObjectId(workspaceId) + workspace: new Types.ObjectId(workspaceId), }); if (!secretBlindIndexData) throw UnauthorizedRequestError({ - message: 'Failed workspace authorization due to blind indices not being enabled' + message: "Failed workspace authorization due to blind indices not being enabled", + }); + } + + if (requireE2EEOff) { + const isWorkspaceE2EE = await BotService.getIsWorkspaceE2EE(workspaceId); + + if (isWorkspaceE2EE) throw BadRequestError({ + message: "Failed workspace authorization due to end-to-end encryption not being disabled", }); } @@ -77,7 +89,7 @@ export const validateClientForWorkspace = async ({ workspaceId, environment, acceptedRoles, - requiredPermissions + requiredPermissions, }); return ({ membership, workspace }); @@ -88,7 +100,7 @@ export const validateClientForWorkspace = async ({ serviceAccount: authData.authPayload, workspaceId, environment, - requiredPermissions + requiredPermissions, }); return {}; @@ -99,7 +111,7 @@ export const validateClientForWorkspace = async ({ serviceTokenData: authData.authPayload, workspaceId, environment, - requiredPermissions + requiredPermissions, }); return {}; @@ -111,13 +123,13 @@ export const validateClientForWorkspace = async ({ workspaceId, environment, acceptedRoles, - requiredPermissions + requiredPermissions, }); return ({ membership, workspace }); } throw UnauthorizedRequestError({ - message: 'Failed client authorization for workspace' + message: "Failed client authorization for workspace", }); } diff --git a/backend/src/variables/action.ts b/backend/src/variables/action.ts index 9d4fb18d9..033be15b0 100644 --- a/backend/src/variables/action.ts +++ b/backend/src/variables/action.ts @@ -1,6 +1,6 @@ -export const ACTION_LOGIN = 'login'; -export const ACTION_LOGOUT = 'logout'; -export const ACTION_ADD_SECRETS = 'addSecrets'; -export const ACTION_DELETE_SECRETS = 'deleteSecrets'; -export const ACTION_UPDATE_SECRETS = 'updateSecrets'; -export const ACTION_READ_SECRETS = 'readSecrets'; \ No newline at end of file +export const ACTION_LOGIN = "login"; +export const ACTION_LOGOUT = "logout"; +export const ACTION_ADD_SECRETS = "addSecrets"; +export const ACTION_DELETE_SECRETS = "deleteSecrets"; +export const ACTION_UPDATE_SECRETS = "updateSecrets"; +export const ACTION_READ_SECRETS = "readSecrets"; \ No newline at end of file diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts index ac19d2756..38bfdfc2c 100644 --- a/backend/src/variables/authentication.ts +++ b/backend/src/variables/authentication.ts @@ -1,4 +1,4 @@ -export const AUTH_MODE_JWT = 'jwt'; -export const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount'; -export const AUTH_MODE_SERVICE_TOKEN = 'serviceToken'; -export const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate \ No newline at end of file +export const AUTH_MODE_JWT = "jwt"; +export const AUTH_MODE_SERVICE_ACCOUNT = "serviceAccount"; +export const AUTH_MODE_SERVICE_TOKEN = "serviceToken"; +export const AUTH_MODE_API_KEY = "apiKey"; // TODO: deprecate \ No newline at end of file diff --git a/backend/src/variables/crypto.ts b/backend/src/variables/crypto.ts index bd2ae110c..64dde2c22 100644 --- a/backend/src/variables/crypto.ts +++ b/backend/src/variables/crypto.ts @@ -1,7 +1,7 @@ -export const ALGORITHM_AES_256_GCM = 'aes-256-gcm'; +export const ALGORITHM_AES_256_GCM = "aes-256-gcm"; export const NONCE_BYTES_SIZE = 12; export const BLOCK_SIZE_BYTES_16 = 16; -export const ENCODING_SCHEME_UTF8 = 'utf8'; -export const ENCODING_SCHEME_HEX = 'hex'; -export const ENCODING_SCHEME_BASE64 = 'base64'; \ No newline at end of file +export const ENCODING_SCHEME_UTF8 = "utf8"; +export const ENCODING_SCHEME_HEX = "hex"; +export const ENCODING_SCHEME_BASE64 = "base64"; \ No newline at end of file diff --git a/backend/src/variables/environment.ts b/backend/src/variables/environment.ts index b068b3d36..d0c8220ef 100644 --- a/backend/src/variables/environment.ts +++ b/backend/src/variables/environment.ts @@ -1,6 +1,6 @@ // environments -export const ENV_DEV = 'dev'; -export const ENV_TESTING = 'test'; -export const ENV_STAGING = 'staging'; -export const ENV_PROD = 'prod'; +export const ENV_DEV = "dev"; +export const ENV_TESTING = "test"; +export const ENV_STAGING = "staging"; +export const ENV_PROD = "prod"; export const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]); \ No newline at end of file diff --git a/backend/src/variables/event.ts b/backend/src/variables/event.ts index 1c55eb3ec..c5de005d1 100644 --- a/backend/src/variables/event.ts +++ b/backend/src/variables/event.ts @@ -1,2 +1,2 @@ -export const EVENT_PUSH_SECRETS = 'pushSecrets'; -export const EVENT_PULL_SECRETS = 'pullSecrets'; \ No newline at end of file +export const EVENT_PUSH_SECRETS = "pushSecrets"; +export const EVENT_PULL_SECRETS = "pullSecrets"; \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index 281145a99..24e86b8fe 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -1,13 +1,13 @@ -export * from './action'; -export * from './authentication'; -export * from './crypto'; -export * from './environment'; -export * from './event'; -export * from './integration'; -export * from './organization'; -export * from './permission'; -export * from './secret'; -export * from './smtp'; -export * from './stripe'; -export * from './token'; -export * from './user'; +export * from "./action"; +export * from "./authentication"; +export * from "./crypto"; +export * from "./environment"; +export * from "./event"; +export * from "./integration"; +export * from "./organization"; +export * from "./permission"; +export * from "./secret"; +export * from "./smtp"; +export * from "./stripe"; +export * from "./token"; +export * from "./user"; diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 26e9f2544..c87d2eb97 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -1,16 +1,16 @@ import { - getClientIdHeroku, - getClientSlugVercel, - getClientIdNetlify, getClientIdAzure, + getClientIdGitHub, getClientIdGitLab, - getClientIdGitHub -} from '../config'; + getClientIdHeroku, + getClientIdNetlify, + getClientSlugVercel, +} from "../config"; // integrations -export const INTEGRATION_AZURE_KEY_VAULT = 'azure-key-vault'; -export const INTEGRATION_AWS_PARAMETER_STORE = 'aws-parameter-store'; -export const INTEGRATION_AWS_SECRET_MANAGER = 'aws-secret-manager'; +export const INTEGRATION_AZURE_KEY_VAULT = "azure-key-vault"; +export const INTEGRATION_AWS_PARAMETER_STORE = "aws-parameter-store"; +export const INTEGRATION_AWS_SECRET_MANAGER = "aws-secret-manager"; export const INTEGRATION_HEROKU = "heroku"; export const INTEGRATION_VERCEL = "vercel"; export const INTEGRATION_NETLIFY = "netlify"; @@ -24,6 +24,7 @@ export const INTEGRATION_TRAVISCI = "travisci"; export const INTEGRATION_SUPABASE = 'supabase'; export const INTEGRATION_CHECKLY = 'checkly'; export const INTEGRATION_HASHICORP_VAULT = 'hashicorp-vault'; +export const INTEGRATION_CLOUDFLARE_PAGES = 'cloudflare-pages'; export const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -37,15 +38,16 @@ export const INTEGRATION_SET = new Set([ INTEGRATION_TRAVISCI, INTEGRATION_SUPABASE, INTEGRATION_CHECKLY, - INTEGRATION_HASHICORP_VAULT + INTEGRATION_HASHICORP_VAULT, + INTEGRATION_CLOUDFLARE_PAGES ]); // integration types export const INTEGRATION_OAUTH2 = "oauth2"; // integration oauth endpoints -export const INTEGRATION_AZURE_TOKEN_URL = `https://login.microsoftonline.com/common/oauth2/v2.0/token`; -export const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token'; +export const INTEGRATION_AZURE_TOKEN_URL = "https://login.microsoftonline.com/common/oauth2/v2.0/token"; +export const INTEGRATION_HEROKU_TOKEN_URL = "https://id.heroku.com/oauth/token"; export const INTEGRATION_VERCEL_TOKEN_URL = "https://api.vercel.com/v2/oauth/access_token"; export const INTEGRATION_NETLIFY_TOKEN_URL = "https://api.netlify.com/oauth/token"; @@ -65,162 +67,172 @@ export const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; export const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; export const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; export const INTEGRATION_CHECKLY_API_URL = 'https://api.checklyhq.com'; +export const INTEGRATION_CLOUDFLARE_PAGES_API_URL = 'https://api.cloudflare.com'; export const getIntegrationOptions = async () => { const INTEGRATION_OPTIONS = [ { - name: 'Heroku', - slug: 'heroku', - image: 'Heroku.png', + name: "Heroku", + slug: "heroku", + image: "Heroku.png", isAvailable: true, - type: 'oauth', + type: "oauth", clientId: await getClientIdHeroku(), - docsLink: '' + docsLink: "", }, { - name: 'Vercel', - slug: 'vercel', - image: 'Vercel.png', + name: "Vercel", + slug: "vercel", + image: "Vercel.png", isAvailable: true, - type: 'oauth', - clientId: '', + type: "oauth", + clientId: "", clientSlug: await getClientSlugVercel(), - docsLink: '' + docsLink: "", }, { - name: 'Netlify', - slug: 'netlify', - image: 'Netlify.png', + name: "Netlify", + slug: "netlify", + image: "Netlify.png", isAvailable: true, - type: 'oauth', + type: "oauth", clientId: await getClientIdNetlify(), - docsLink: '' + docsLink: "", }, { - name: 'GitHub', - slug: 'github', - image: 'GitHub.png', + name: "GitHub", + slug: "github", + image: "GitHub.png", isAvailable: true, - type: 'oauth', + type: "oauth", clientId: await getClientIdGitHub(), - docsLink: '' + docsLink: "", }, { - name: 'Render', - slug: 'render', - image: 'Render.png', + name: "Render", + slug: "render", + image: "Render.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'Railway', - slug: 'railway', - image: 'Railway.png', + name: "Railway", + slug: "railway", + image: "Railway.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'Fly.io', - slug: 'flyio', - image: 'Flyio.svg', + name: "Fly.io", + slug: "flyio", + image: "Flyio.svg", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'AWS Parameter Store', - slug: 'aws-parameter-store', - image: 'Amazon Web Services.png', + name: "AWS Parameter Store", + slug: "aws-parameter-store", + image: "Amazon Web Services.png", isAvailable: true, - type: 'custom', - clientId: '', - docsLink: '' + type: "custom", + clientId: "", + docsLink: "", }, { - name: 'AWS Secret Manager', - slug: 'aws-secret-manager', - image: 'Amazon Web Services.png', + name: "AWS Secret Manager", + slug: "aws-secret-manager", + image: "Amazon Web Services.png", isAvailable: true, - type: 'custom', - clientId: '', - docsLink: '' + type: "custom", + clientId: "", + docsLink: "", }, { - name: 'Azure Key Vault', - slug: 'azure-key-vault', - image: 'Microsoft Azure.png', + name: "Azure Key Vault", + slug: "azure-key-vault", + image: "Microsoft Azure.png", isAvailable: true, - type: 'oauth', + type: "oauth", clientId: await getClientIdAzure(), - docsLink: '' + docsLink: "", }, { - name: 'Circle CI', - slug: 'circleci', - image: 'Circle CI.png', + name: "Circle CI", + slug: "circleci", + image: "Circle CI.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'GitLab', - slug: 'gitlab', - image: 'GitLab.png', + name: "GitLab", + slug: "gitlab", + image: "GitLab.png", isAvailable: true, - type: 'custom', + type: "custom", clientId: await getClientIdGitLab(), - docsLink: '' + docsLink: "", }, { - name: 'Travis CI', - slug: 'travisci', - image: 'Travis CI.png', + name: "Travis CI", + slug: "travisci", + image: "Travis CI.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'Supabase', - slug: 'supabase', - image: 'Supabase.png', + name: "Supabase", + slug: "supabase", + image: "Supabase.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'Checkly', - slug: 'checkly', - image: 'Checkly.png', + name: "Checkly", + slug: "checkly", + image: "Checkly.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'HashiCorp Vault', - slug: 'hashicorp-vault', - image: 'Vault.png', + name: "HashiCorp Vault", + slug: "hashicorp-vault", + image: "Vault.png", isAvailable: true, - type: 'pat', - clientId: '', - docsLink: '' + type: "pat", + clientId: "", + docsLink: "", }, { - name: 'Google Cloud Platform', - slug: 'gcp', - image: 'Google Cloud Platform.png', + name: "Google Cloud Platform", + slug: "gcp", + image: "Google Cloud Platform.png", isAvailable: false, type: '', clientId: '', docsLink: '' + }, + { + name: 'Cloudflare Pages', + slug: 'cloudflare-pages', + image: 'Cloudflare.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' } ] diff --git a/backend/src/variables/permission.ts b/backend/src/variables/permission.ts index 98c9ef538..9dc4c61a3 100644 --- a/backend/src/variables/permission.ts +++ b/backend/src/variables/permission.ts @@ -1,2 +1,2 @@ -export const PERMISSION_READ_SECRETS = 'read'; -export const PERMISSION_WRITE_SECRETS = 'write'; \ No newline at end of file +export const PERMISSION_READ_SECRETS = "read"; +export const PERMISSION_WRITE_SECRETS = "write"; \ No newline at end of file diff --git a/backend/src/variables/secret.ts b/backend/src/variables/secret.ts index 571e66b9d..74be3a1e9 100644 --- a/backend/src/variables/secret.ts +++ b/backend/src/variables/secret.ts @@ -1,3 +1,3 @@ // secrets -export const SECRET_SHARED = 'shared'; -export const SECRET_PERSONAL = 'personal'; \ No newline at end of file +export const SECRET_SHARED = "shared"; +export const SECRET_PERSONAL = "personal"; \ No newline at end of file diff --git a/backend/src/variables/smtp.ts b/backend/src/variables/smtp.ts index f5dd05d0b..8a0e752eb 100644 --- a/backend/src/variables/smtp.ts +++ b/backend/src/variables/smtp.ts @@ -1,5 +1,5 @@ -export const SMTP_HOST_SENDGRID = 'smtp.sendgrid.net'; -export const SMTP_HOST_MAILGUN = 'smtp.mailgun.org'; -export const SMTP_HOST_SOCKETLABS = 'smtp.socketlabs.com'; -export const SMTP_HOST_ZOHOMAIL = 'smtp.zoho.com'; -export const SMTP_HOST_GMAIL = 'smtp.gmail.com'; +export const SMTP_HOST_SENDGRID = "smtp.sendgrid.net"; +export const SMTP_HOST_MAILGUN = "smtp.mailgun.org"; +export const SMTP_HOST_SOCKETLABS = "smtp.socketlabs.com"; +export const SMTP_HOST_ZOHOMAIL = "smtp.zoho.com"; +export const SMTP_HOST_GMAIL = "smtp.gmail.com"; diff --git a/backend/src/variables/stripe.ts b/backend/src/variables/stripe.ts index 7b6ae8fa1..fa0fdae07 100644 --- a/backend/src/variables/stripe.ts +++ b/backend/src/variables/stripe.ts @@ -1,2 +1,2 @@ -export const PLAN_STARTER = 'starter'; -export const PLAN_PRO = 'pro'; \ No newline at end of file +export const PLAN_STARTER = "starter"; +export const PLAN_PRO = "pro"; \ No newline at end of file diff --git a/backend/src/variables/token.ts b/backend/src/variables/token.ts index 2ced95d9c..187e2534b 100644 --- a/backend/src/variables/token.ts +++ b/backend/src/variables/token.ts @@ -1,4 +1,4 @@ -export const TOKEN_EMAIL_CONFIRMATION = 'emailConfirmation'; -export const TOKEN_EMAIL_MFA = 'emailMfa'; -export const TOKEN_EMAIL_ORG_INVITATION = 'organizationInvitation'; -export const TOKEN_EMAIL_PASSWORD_RESET = 'passwordReset'; \ No newline at end of file +export const TOKEN_EMAIL_CONFIRMATION = "emailConfirmation"; +export const TOKEN_EMAIL_MFA = "emailMfa"; +export const TOKEN_EMAIL_ORG_INVITATION = "organizationInvitation"; +export const TOKEN_EMAIL_PASSWORD_RESET = "passwordReset"; \ No newline at end of file diff --git a/backend/src/variables/user.ts b/backend/src/variables/user.ts index 7e5b53c4b..a688b115d 100644 --- a/backend/src/variables/user.ts +++ b/backend/src/variables/user.ts @@ -1 +1 @@ -export const MFA_METHOD_EMAIL = 'email'; \ No newline at end of file +export const MFA_METHOD_EMAIL = "email"; \ No newline at end of file diff --git a/backend/swagger/index.ts b/backend/swagger/index.ts index 051f9d64f..35ed20790 100644 --- a/backend/swagger/index.ts +++ b/backend/swagger/index.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-var-requires */ -const swaggerAutogen = require('swagger-autogen')({ openapi: '3.0.0' }); -const fs = require('fs').promises; -const yaml = require('js-yaml'); +const swaggerAutogen = require("swagger-autogen")({ openapi: "3.0.0" }); +const fs = require("fs").promises; +const yaml = require("js-yaml"); /** * Generates OpenAPI specs for all Infisical API endpoints: @@ -11,216 +11,216 @@ const yaml = require('js-yaml'); const generateOpenAPISpec = async () => { const doc = { info: { - title: 'Infisical API', - description: 'List of all available APIs that can be consumed', + title: "Infisical API", + description: "List of all available APIs that can be consumed", }, - host: ['https://infisical.com'], + host: ["https://infisical.com"], servers: [ { - url: 'https://infisical.com', - description: 'Production server' + url: "https://infisical.com", + description: "Production server", }, { - url: 'http://localhost:8080', - description: 'Local server' - } + url: "http://localhost:8080", + description: "Local server", + }, ], securityDefinitions: { bearerAuth: { - type: 'http', - scheme: 'bearer', - bearerFormat: 'JWT', - description: "This security definition uses the HTTP 'bearer' scheme, which allows the client to authenticate using a JSON Web Token (JWT) that is passed in the Authorization header of the request." + type: "http", + scheme: "bearer", + bearerFormat: "JWT", + description: "This security definition uses the HTTP 'bearer' scheme, which allows the client to authenticate using a JSON Web Token (JWT) that is passed in the Authorization header of the request.", }, apiKeyAuth: { - type: 'apiKey', - in: 'header', - name: 'X-API-Key', - description: 'This security definition uses an API key, which is passed in the header of the request as the value of the "X-API-Key" header. The client must provide a valid key in order to access the API.' - } + type: "apiKey", + in: "header", + name: "X-API-Key", + description: 'This security definition uses an API key, which is passed in the header of the request as the value of the "X-API-Key" header. The client must provide a valid key in order to access the API.', + }, }, definitions: { CurrentUser: { - _id: '', - email: 'johndoe@gmail.com', - firstName: 'John', - lastName: 'Doe', - publicKey: 'johns_nacl_public_key', - encryptedPrivateKey: 'johns_enc_nacl_private_key', - iv: 'iv_of_enc_nacl_private_key', - tag: 'tag_of_enc_nacl_private_key', - updatedAt: '2023-01-13T14:16:12.210Z', - createdAt: '2023-01-13T14:16:12.210Z' + _id: "", + email: "johndoe@gmail.com", + firstName: "John", + lastName: "Doe", + publicKey: "johns_nacl_public_key", + encryptedPrivateKey: "johns_enc_nacl_private_key", + iv: "iv_of_enc_nacl_private_key", + tag: "tag_of_enc_nacl_private_key", + updatedAt: "2023-01-13T14:16:12.210Z", + createdAt: "2023-01-13T14:16:12.210Z", }, Membership: { user: { - _id: '', - email: 'johndoe@gmail.com', - firstName: 'John', - lastName: 'Doe', - publicKey: 'johns_nacl_public_key', - updatedAt: '2023-01-13T14:16:12.210Z', - createdAt: '2023-01-13T14:16:12.210Z' + _id: "", + email: "johndoe@gmail.com", + firstName: "John", + lastName: "Doe", + publicKey: "johns_nacl_public_key", + updatedAt: "2023-01-13T14:16:12.210Z", + createdAt: "2023-01-13T14:16:12.210Z", }, - workspace: '', - role: 'admin' + workspace: "", + role: "admin", }, MembershipOrg: { user: { - _id: '', - email: 'johndoe@gmail.com', - firstName: 'John', - lastName: 'Doe', - publicKey: 'johns_nacl_public_key', - updatedAt: '2023-01-13T14:16:12.210Z', - createdAt: '2023-01-13T14:16:12.210Z' + _id: "", + email: "johndoe@gmail.com", + firstName: "John", + lastName: "Doe", + publicKey: "johns_nacl_public_key", + updatedAt: "2023-01-13T14:16:12.210Z", + createdAt: "2023-01-13T14:16:12.210Z", }, - organization: '', - role: 'owner', - status: 'accepted' + organization: "", + role: "owner", + status: "accepted", }, Organization: { - _id: '', - name: 'Acme Corp.', - customerId: '' + _id: "", + name: "Acme Corp.", + customerId: "", }, Project: { - name: 'My Project', - organization: '', + name: "My Project", + organization: "", environments: [{ - name: 'development', - slug: 'dev' - }] + name: "development", + slug: "dev", + }], }, ProjectKey: { - encryptedkey: '', - nonce: '', + encryptedkey: "", + nonce: "", sender: { - publicKey: 'senders_nacl_public_key' + publicKey: "senders_nacl_public_key", }, - receiver: '', - workspace: '' + receiver: "", + workspace: "", }, CreateSecret: { - type: 'shared', - secretKeyCiphertext: '', - secretKeyIV: '', - secretKeyTag: '', - secretValueCiphertext: '', - secretValueIV: '', - secretValueTag: '', - secretCommentCiphertext: '', - secretCommentIV: '', - secretCommentTag: '' + type: "shared", + secretKeyCiphertext: "", + secretKeyIV: "", + secretKeyTag: "", + secretValueCiphertext: "", + secretValueIV: "", + secretValueTag: "", + secretCommentCiphertext: "", + secretCommentIV: "", + secretCommentTag: "", }, UpdateSecret: { - id: '', - secretKeyCiphertext: '', - secretKeyIV: '', - secretKeyTag: '', - secretValueCiphertext: '', - secretValueIV: '', - secretValueTag: '', - secretCommentCiphertext: '', - secretCommentIV: '', - secretCommentTag: '' + id: "", + secretKeyCiphertext: "", + secretKeyIV: "", + secretKeyTag: "", + secretValueCiphertext: "", + secretValueIV: "", + secretValueTag: "", + secretCommentCiphertext: "", + secretCommentIV: "", + secretCommentTag: "", }, Secret: { - _id: '', + _id: "", version: 1, - workspace : '', - type: 'shared', + workspace : "", + type: "shared", user: null, - secretKeyCiphertext: '', - secretKeyIV: '', - secretKeyTag: '', - secretValueCiphertext: '', - secretValueIV: '', - secretValueTag: '', - secretCommentCiphertext: '', - secretCommentIV: '', - secretCommentTag: '', - updatedAt: '2023-01-13T14:16:12.210Z', - createdAt: '2023-01-13T14:16:12.210Z' + secretKeyCiphertext: "", + secretKeyIV: "", + secretKeyTag: "", + secretValueCiphertext: "", + secretValueIV: "", + secretValueTag: "", + secretCommentCiphertext: "", + secretCommentIV: "", + secretCommentTag: "", + updatedAt: "2023-01-13T14:16:12.210Z", + createdAt: "2023-01-13T14:16:12.210Z", }, Log: { - _id: '', + _id: "", user: { - _id: '', - email: 'johndoe@gmail.com', - firstName: 'John', - lastName: 'Doe' + _id: "", + email: "johndoe@gmail.com", + firstName: "John", + lastName: "Doe", }, - workspace: '', + workspace: "", actionNames: [ - 'addSecrets' + "addSecrets", ], actions: [ { - name: 'addSecrets', - user: '', - workspace: '', + name: "addSecrets", + user: "", + workspace: "", payload: [ { - oldSecretVersion: '', - newSecretVersion: '' - } - ] - } + oldSecretVersion: "", + newSecretVersion: "", + }, + ], + }, ], - channel: 'cli', - ipAddress: '192.168.0.1', - updatedAt: '2023-01-13T14:16:12.210Z', - createdAt: '2023-01-13T14:16:12.210Z' + channel: "cli", + ipAddress: "192.168.0.1", + updatedAt: "2023-01-13T14:16:12.210Z", + createdAt: "2023-01-13T14:16:12.210Z", }, SecretSnapshot: { - workspace: '', + workspace: "", version: 1, secretVersions: [ { - _id: '' - } - ] + _id: "", + }, + ], }, SecretVersion: { - _id: '', - secret: '', + _id: "", + secret: "", version: 1, - workspace: '', - type: 'shared', - user: '', - environment: 'dev', - isDeleted: '', - secretKeyCiphertext: '', - secretKeyIV: '', - secretKeyTag: '', - secretValueCiphertext: '', - secretValueIV: '', - secretValueTag: '', + workspace: "", + type: "shared", + user: "", + environment: "dev", + isDeleted: "", + secretKeyCiphertext: "", + secretKeyIV: "", + secretKeyTag: "", + secretValueCiphertext: "", + secretValueIV: "", + secretValueTag: "", }, ServiceTokenData: { - _id: '', - name: '', - workspace: '', - environment: '', + _id: "", + name: "", + workspace: "", + environment: "", user: { - _id: '', - firstName: '', - lastName: '' + _id: "", + firstName: "", + lastName: "", }, - expiresAt: '2023-01-13T14:16:12.210Z', - encryptedKey: '', - iv: '', - tag: '', - updatedAt: '2023-01-13T14:16:12.210Z', - createdAt: '2023-01-13T14:16:12.210Z' - } - } + expiresAt: "2023-01-13T14:16:12.210Z", + encryptedKey: "", + iv: "", + tag: "", + updatedAt: "2023-01-13T14:16:12.210Z", + createdAt: "2023-01-13T14:16:12.210Z", + }, + }, }; - const outputJSONFile = '../spec.json'; - const outputYAMLFile = '../docs/spec.yaml'; - const endpointsFiles = ['../src/index.ts']; + const outputJSONFile = "../spec.json"; + const outputYAMLFile = "../docs/spec.yaml"; + const endpointsFiles = ["../src/index.ts"]; const spec = await swaggerAutogen(outputJSONFile, endpointsFiles, doc); await fs.writeFile(outputYAMLFile, yaml.dump(spec.data)); diff --git a/backend/tests/helper/helper.ts b/backend/tests/helper/helper.ts index d72a101c1..510f5f353 100644 --- a/backend/tests/helper/helper.ts +++ b/backend/tests/helper/helper.ts @@ -4,15 +4,15 @@ import axiosInstance from "../../src/config/request"; import { Secret } from "../../src/models"; import { testUserEmail, testUserPassword } from "../../src/utils/addDevelopmentUser"; // eslint-disable-next-line @typescript-eslint/no-var-requires -const crypto = require('crypto') +const crypto = require("crypto") // eslint-disable-next-line @typescript-eslint/no-var-requires -const jsrp = require('jsrp'); +const jsrp = require("jsrp"); // eslint-disable-next-line @typescript-eslint/no-var-requires -const axios = require('axios'); +const axios = require("axios"); import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser"; import { - encryptSymmetric128BitHexKeyUTF8 -} from '../../src/utils/crypto'; + encryptSymmetric128BitHexKeyUTF8, +} from "../../src/utils/crypto"; interface TokenData { token: string; @@ -37,11 +37,11 @@ export const getJWTFromTestUser = (): Promise => { // POST: /login1 const reqBody = { email: EMAIL, - clientPublicKey + clientPublicKey, } - const loginOneRes = await axiosInstance.post('http://localhost:4000/api/v1/auth/login1', reqBody); + const loginOneRes = await axiosInstance.post("http://localhost:4000/api/v1/auth/login1", reqBody); const serverPublicKey = loginOneRes.data.serverPublicKey; const salt = loginOneRes.data.salt; @@ -53,10 +53,10 @@ export const getJWTFromTestUser = (): Promise => { // POST: /login2 const reqBody2 = { email: EMAIL, - clientProof + clientProof, } - const response2 = await axiosInstance.post('http://localhost:4000/api/v1/auth/login2', reqBody2); + const response2 = await axiosInstance.post("http://localhost:4000/api/v1/auth/login2", reqBody2); resolve(response2.data) }) @@ -65,25 +65,25 @@ export const getJWTFromTestUser = (): Promise => { export const getServiceTokenFromTestUser = async () => { const loggedInUserDetails = await getJWTFromTestUser() - const randomBytes = crypto.randomBytes(16).toString('hex'); + const randomBytes = crypto.randomBytes(16).toString("hex"); const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ plaintext: plainTextWorkspaceKey, key: randomBytes, }); - const newServiceToken = await axiosInstance.post('http://localhost:4000/api/v2/service-token/', { - 'name': "test service token", - 'workspaceId': testWorkspaceId, - 'environment': "dev", - 'encryptedKey': ciphertext, - 'iv': iv, - 'tag': tag, - 'expiresIn': Date.now() + 90000, - 'permissions': ["read"] + const newServiceToken = await axiosInstance.post("http://localhost:4000/api/v2/service-token/", { + "name": "test service token", + "workspaceId": testWorkspaceId, + "environment": "dev", + "encryptedKey": ciphertext, + "iv": iv, + "tag": tag, + "expiresIn": Date.now() + 90000, + "permissions": ["read"], }, { headers: { - 'Authorization': `Bearer ${loggedInUserDetails.token}` - } + "Authorization": `Bearer ${loggedInUserDetails.token}`, + }, }); return `${newServiceToken.data.serviceToken}.${randomBytes}` diff --git a/backend/tests/integration-tests/routes/v2/service-tokens.ts b/backend/tests/integration-tests/routes/v2/service-tokens.ts index b011db36f..15d776bdd 100644 --- a/backend/tests/integration-tests/routes/v2/service-tokens.ts +++ b/backend/tests/integration-tests/routes/v2/service-tokens.ts @@ -1,6 +1,6 @@ -import request from 'supertest' -import main from '../../../../src/index' -import { getServiceTokenFromTestUser } from '../../../helper/helper'; +import request from "supertest" +import main from "../../../../src/index" +import { getServiceTokenFromTestUser } from "../../../helper/helper"; let server: any; beforeAll(async () => { @@ -20,18 +20,18 @@ describe("GET /api/v2/service-token", () => { // get the service token details const serviceTokenDetails = await request(server) .get("/api/v2/service-token") - .set('Authorization', `Bearer ${serviceToken}`) + .set("Authorization", `Bearer ${serviceToken}`) expect(serviceTokenDetails.body).toMatchObject({ _id: expect.any(String), - name: 'test service token', - workspace: '63cefb15c8d3175601cfa989', - environment: 'dev', + name: "test service token", + workspace: "63cefb15c8d3175601cfa989", + environment: "dev", user: { - _id: '63cefa6ec8d3175601cfa980', - email: 'test@localhost.local', - firstName: 'Jake', - lastName: 'Moni', + _id: "63cefa6ec8d3175601cfa980", + email: "test@localhost.local", + firstName: "Jake", + lastName: "Moni", isMfaEnabled: false, mfaMethods: expect.any(Array), devices: [ @@ -49,7 +49,7 @@ describe("GET /api/v2/service-token", () => { encryptedKey: expect.any(String), iv: expect.any(String), tag: expect.any(String), - permissions: ['read'], + permissions: ["read"], createdAt: expect.any(String), updatedAt: expect.any(String), }); diff --git a/backend/tests/setupTests.ts b/backend/tests/setupTests.ts index 903347a2a..ef30ce85a 100644 --- a/backend/tests/setupTests.ts +++ b/backend/tests/setupTests.ts @@ -1,7 +1,7 @@ -import { Server } from 'http'; -import main from '../src'; -import { describe, expect, it, beforeAll, afterAll } from '@jest/globals'; -import request from 'supertest'; +import { Server } from "http"; +import main from "../src"; +import { afterAll, beforeAll, describe, expect, it } from "@jest/globals"; +import request from "supertest"; let server: Server; @@ -13,9 +13,9 @@ afterAll(async () => { server.close(); }); -describe('Healthcheck endpoint', () => { - it('GET /healthcheck should return OK', async () => { - const res = await request(server).get('/healthcheck'); +describe("Healthcheck endpoint", () => { + it("GET /healthcheck should return OK", async () => { + const res = await request(server).get("/healthcheck"); expect(res.status).toEqual(200); }); }); diff --git a/backend/tests/unit-tests/utils/crypto.test.ts b/backend/tests/unit-tests/utils/crypto.test.ts index d50b3057c..f9f2aba01 100644 --- a/backend/tests/unit-tests/utils/crypto.test.ts +++ b/backend/tests/unit-tests/utils/crypto.test.ts @@ -1,78 +1,78 @@ -import { describe, test, expect } from '@jest/globals'; +import { describe, expect, test } from "@jest/globals"; import { decryptAsymmetric, encryptAsymmetric, -} from '../../../src/utils/crypto'; +} from "../../../src/utils/crypto"; -describe('Crypto', () => { - describe('encryptAsymmetric', () => { - describe('given all valid publicKey, privateKey and plaintext', () => { - const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; - const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; - const plaintext = 'secret-message'; +describe("Crypto", () => { + describe("encryptAsymmetric", () => { + describe("given all valid publicKey, privateKey and plaintext", () => { + const publicKey = "6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw="; + const privateKey = "Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0="; + const plaintext = "secret-message"; - test('should encrypt plain text', () => { + test("should encrypt plain text", () => { const result = encryptAsymmetric({ plaintext, publicKey, privateKey }); expect(result.ciphertext).toBeDefined(); expect(result.nonce).toBeDefined(); }); }); - describe('given empty/undefined publicKey', () => { + describe("given empty/undefined publicKey", () => { let publicKey: string; - const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; - const plaintext = 'secret-message'; + const privateKey = "Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0="; + const plaintext = "secret-message"; - test('should throw error if publicKey is undefined', () => { + test("should throw error if publicKey is undefined", () => { expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('invalid encoding'); + }).toThrowError("invalid encoding"); }); - test('should throw error if publicKey is empty string', () => { - publicKey = ''; + test("should throw error if publicKey is empty string", () => { + publicKey = ""; expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('bad public key size'); + }).toThrowError("bad public key size"); }); }); - describe('given empty/undefined privateKey', () => { - const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; + describe("given empty/undefined privateKey", () => { + const publicKey = "6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw="; let privateKey: string; - const plaintext = 'secret-message'; + const plaintext = "secret-message"; - test('should throw error if privateKey is undefined', () => { + test("should throw error if privateKey is undefined", () => { expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('invalid encoding'); + }).toThrowError("invalid encoding"); }); - test('should throw error if privateKey is empty string', () => { - privateKey = ''; + test("should throw error if privateKey is empty string", () => { + privateKey = ""; expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('bad secret key size'); + }).toThrowError("bad secret key size"); }); }); - describe('given undefined/invalid plaint text', () => { - const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; - const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; + describe("given undefined/invalid plaint text", () => { + const publicKey = "6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw="; + const privateKey = "Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0="; let plaintext: string; - test('should throw error if plaintext is undefined', () => { + test("should throw error if plaintext is undefined", () => { expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('expected string'); + }).toThrowError("expected string"); }); - test('should encrypt plaintext containing special characters', () => { - plaintext = '131@#$%235!@#&*(&123sadfkjadjf'; + test("should encrypt plaintext containing special characters", () => { + plaintext = "131@#$%235!@#&*(&123sadfkjadjf"; const result = encryptAsymmetric({ plaintext, publicKey, - privateKey + privateKey, }); expect(result.ciphertext).toBeDefined(); expect(result.nonce).toBeDefined(); @@ -80,17 +80,17 @@ describe('Crypto', () => { }); }); - describe('decryptAsymmetric', () => { - describe('given all valid publicKey, privateKey and plaintext', () => { - const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; - const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; - const plaintext = 'secret-message'; + describe("decryptAsymmetric", () => { + describe("given all valid publicKey, privateKey and plaintext", () => { + const publicKey = "6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw="; + const privateKey = "Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0="; + const plaintext = "secret-message"; - test('should decrypt the encrypted plaintext', () => { + test("should decrypt the encrypted plaintext", () => { const encryptedResult = encryptAsymmetric({ plaintext, publicKey, - privateKey + privateKey, }); const ciphertext = encryptedResult.ciphertext; const nonce = encryptedResult.nonce; @@ -99,7 +99,7 @@ describe('Crypto', () => { ciphertext, nonce, publicKey, - privateKey + privateKey, }); expect(decryptedResult).toBeDefined(); @@ -107,18 +107,18 @@ describe('Crypto', () => { }); }); - describe('given ciphertext or nonce is modified before decrypt', () => { - const publicKey = '6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw='; - const privateKey = 'Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0='; - const plaintext = 'secret-message'; + describe("given ciphertext or nonce is modified before decrypt", () => { + const publicKey = "6U5m6S5jlyazJ+R4z7Yf/Ah4th4JwKxDN8Wn7+upvzw="; + const privateKey = "Z8W53YV+2ddjJCrFwzptjK96y2QsQI9oXuvfcx+qxz0="; + const plaintext = "secret-message"; - test('should throw error if ciphertext is modified', () => { + test("should throw error if ciphertext is modified", () => { const encryptedResult = encryptAsymmetric({ plaintext, publicKey, - privateKey + privateKey, }); - const ciphertext = '=12adfJ@#52af1231=123'; // modified + const ciphertext = "=12adfJ@#52af1231=123"; // modified const nonce = encryptedResult.nonce; expect(() => { @@ -126,28 +126,28 @@ describe('Crypto', () => { ciphertext, nonce, publicKey, - privateKey + privateKey, }); - }).toThrowError('invalid encoding'); + }).toThrowError("invalid encoding"); }); - test('should throw error if nonce is modified', () => { + test("should throw error if nonce is modified", () => { const encryptedResult = encryptAsymmetric({ plaintext, publicKey, - privateKey + privateKey, }); const ciphertext = encryptedResult.ciphertext; - const nonce = '=12adfJ@#52af1231=123'; // modified + const nonce = "=12adfJ@#52af1231=123"; // modified expect(() => { decryptAsymmetric({ ciphertext, nonce, publicKey, - privateKey + privateKey, }); - }).toThrowError('invalid encoding'); + }).toThrowError("invalid encoding"); }); }); }); diff --git a/backend/tests/unit-tests/utils/posthog.test.ts b/backend/tests/unit-tests/utils/posthog.test.ts index 9f202385c..5c05684f4 100644 --- a/backend/tests/unit-tests/utils/posthog.test.ts +++ b/backend/tests/unit-tests/utils/posthog.test.ts @@ -1,29 +1,29 @@ -import { describe, test, expect } from '@jest/globals'; -import { getChannelFromUserAgent } from '../../../src/utils/posthog'; +import { describe, expect, test } from "@jest/globals"; +import { getChannelFromUserAgent } from "../../../src/utils/posthog"; -describe('posthog getChannelFromUserAgent', () => { +describe("posthog getChannelFromUserAgent", () => { test("should return 'web' when userAgent includes 'mozilla'", () => { const userAgent = - 'Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.115 Mobile Safari/537.36'; + "Mozilla/5.0 (Linux; Android 10) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/111.0.5563.115 Mobile Safari/537.36"; const channel = getChannelFromUserAgent(userAgent); - expect(channel).toBe('web'); + expect(channel).toBe("web"); }); test("should return 'cli'", () => { - const userAgent = 'cli'; + const userAgent = "cli"; const channel = getChannelFromUserAgent(userAgent); - expect(channel).toBe('cli'); + expect(channel).toBe("cli"); }); test("should return 'k8-operator'", () => { - const userAgent = 'k8-operator'; + const userAgent = "k8-operator"; const channel = getChannelFromUserAgent(userAgent); - expect(channel).toBe('k8-operator'); + expect(channel).toBe("k8-operator"); }); - test('should return undefined if no userAgent', () => { + test("should return undefined if no userAgent", () => { const userAgent = undefined; const channel = getChannelFromUserAgent(userAgent); - expect(channel).toBe('other'); + expect(channel).toBe("other"); }); }); diff --git a/cli/packages/api/api.go b/cli/packages/api/api.go index 5f8bc7aae..a94be0ab0 100644 --- a/cli/packages/api/api.go +++ b/cli/packages/api/api.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "net/http" "github.com/Infisical/infisical-merge/packages/config" "github.com/go-resty/resty/v2" @@ -10,63 +11,6 @@ import ( const USER_AGENT = "cli" -func CallBatchModifySecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchModifySecretsByWorkspaceAndEnvRequest) error { - endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL) - response, err := httpClient. - R(). - SetBody(request). - SetHeader("User-Agent", USER_AGENT). - Patch(endpoint) - - if err != nil { - return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return fmt.Errorf("CallBatchModifySecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) - } - - return nil -} - -func CallBatchCreateSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchCreateSecretsByWorkspaceAndEnvRequest) error { - endpoint := fmt.Sprintf("%v/v2/secrets/", config.INFISICAL_URL) - response, err := httpClient. - R(). - SetBody(request). - SetHeader("User-Agent", USER_AGENT). - Post(endpoint) - - if err != nil { - return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return fmt.Errorf("CallBatchCreateSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) - } - - return nil -} - -func CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient *resty.Client, request BatchDeleteSecretsBySecretIdsRequest) error { - endpoint := fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL) - response, err := httpClient. - R(). - SetBody(request). - SetHeader("User-Agent", USER_AGENT). - Delete(endpoint) - - if err != nil { - return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return fmt.Errorf("CallBatchDeleteSecretsByWorkspaceAndEnv: Unsuccessful response: [response=%s]", response) - } - - return nil -} - func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) { endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", config.INFISICAL_URL, request.WorkspaceId) var result GetEncryptedWorkspaceKeyResponse @@ -106,28 +50,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta return tokenDetailsResponse, nil } -func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) { - var secretsResponse GetEncryptedSecretsV2Response - response, err := httpClient. - R(). - SetResult(&secretsResponse). - SetHeader("User-Agent", USER_AGENT). - SetQueryParam("environment", request.Environment). - SetQueryParam("workspaceId", request.WorkspaceId). - SetQueryParam("tagSlugs", request.TagSlugs). - Get(fmt.Sprintf("%v/v2/secrets", config.INFISICAL_URL)) - - if err != nil { - return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err) - } - - if response.IsError() { - return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response) - } - - return secretsResponse, nil -} - func CallLogin1V2(httpClient *resty.Client, request GetLoginOneV2Request) (GetLoginOneV2Response, error) { var loginOneV2Response GetLoginOneV2Response response, err := httpClient. @@ -159,6 +81,22 @@ func CallVerifyMfaToken(httpClient *resty.Client, request VerifyMfaTokenRequest) SetBody(request). Post(fmt.Sprintf("%v/v2/auth/mfa/verify", config.INFISICAL_URL)) + cookies := response.Cookies() + // Find a cookie by name + cookieName := "jid" + var refreshToken *http.Cookie + for _, cookie := range cookies { + if cookie.Name == cookieName { + refreshToken = cookie + break + } + } + + // When MFA is enabled + if refreshToken != nil { + verifyMfaTokenResponse.RefreshToken = refreshToken.Value + } + if err != nil { return nil, nil, fmt.Errorf("CallVerifyMfaToken: Unable to complete api request [err=%s]", err) } @@ -179,6 +117,22 @@ func CallLogin2V2(httpClient *resty.Client, request GetLoginTwoV2Request) (GetLo SetBody(request). Post(fmt.Sprintf("%v/v2/auth/login2", config.INFISICAL_URL)) + cookies := response.Cookies() + // Find a cookie by name + cookieName := "jid" + var refreshToken *http.Cookie + for _, cookie := range cookies { + if cookie.Name == cookieName { + refreshToken = cookie + break + } + } + + // When MFA is enabled + if refreshToken != nil { + loginTwoV2Response.RefreshToken = refreshToken.Value + } + if err != nil { return GetLoginTwoV2Response{}, fmt.Errorf("CallLogin2V2: Unable to complete api request [err=%s]", err) } @@ -247,3 +201,133 @@ func CallGetAccessibleEnvironments(httpClient *resty.Client, request GetAccessib return accessibleEnvironmentsResponse, nil } + +func CallGetNewAccessTokenWithRefreshToken(httpClient *resty.Client, refreshToken string) (GetNewAccessTokenWithRefreshTokenResponse, error) { + var newAccessToken GetNewAccessTokenWithRefreshTokenResponse + response, err := httpClient. + R(). + SetResult(&newAccessToken). + SetHeader("User-Agent", USER_AGENT). + SetCookie(&http.Cookie{ + Name: "jid", + Value: refreshToken, + }). + Post(fmt.Sprintf("%v/v1/auth/token", config.INFISICAL_URL)) + + if err != nil { + return GetNewAccessTokenWithRefreshTokenResponse{}, err + } + + if response.IsError() { + return GetNewAccessTokenWithRefreshTokenResponse{}, fmt.Errorf("CallGetNewAccessTokenWithRefreshToken: Unsuccessful response: [response=%v]", response) + } + + return newAccessToken, nil +} + +func CallGetSecretsV3(httpClient *resty.Client, request GetEncryptedSecretsV3Request) (GetEncryptedSecretsV3Response, error) { + var secretsResponse GetEncryptedSecretsV3Response + + httpRequest := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetQueryParam("environment", request.Environment). + SetQueryParam("workspaceId", request.WorkspaceId) + + if request.SecretPath != "" { + httpRequest.SetQueryParam("secretPath", request.SecretPath) + } + + response, err := httpRequest.Get(fmt.Sprintf("%v/v3/secrets", config.INFISICAL_URL)) + + if err != nil { + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return GetEncryptedSecretsV3Response{}, fmt.Errorf("CallGetSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return secretsResponse, nil +} + +func CallCreateSecretsV3(httpClient *resty.Client, request CreateSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallCreateSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallCreateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} + +func CallDeleteSecretsV3(httpClient *resty.Client, request DeleteSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Delete(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallDeleteSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallDeleteSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} + +func CallUpdateSecretsV3(httpClient *resty.Client, request UpdateSecretByNameV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Patch(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallUpdateSecretsV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallUpdateSecretsV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} + +func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV3Request) error { + var secretsResponse GetEncryptedSecretsV3Response + response, err := httpClient. + R(). + SetResult(&secretsResponse). + SetHeader("User-Agent", USER_AGENT). + SetBody(request). + Post(fmt.Sprintf("%v/v3/secrets/%s", config.INFISICAL_URL, request.SecretName)) + + if err != nil { + return fmt.Errorf("CallGetSingleSecretByNameV3: Unable to complete api request [err=%s]", err) + } + + if response.IsError() { + return fmt.Errorf("CallGetSingleSecretByNameV3: Unsuccessful response. Please make sure your secret path, workspace and environment name are all correct [response=%s]", response) + } + + return nil +} diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index 71354c84c..7de7a962b 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -143,24 +143,7 @@ type Secret struct { SecretCommentHash string `json:"secretCommentHash,omitempty"` Type string `json:"type,omitempty"` ID string `json:"id,omitempty"` -} - -type BatchCreateSecretsByWorkspaceAndEnvRequest struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - Secrets []Secret `json:"secrets"` -} - -type BatchModifySecretsByWorkspaceAndEnvRequest struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - Secrets []Secret `json:"secrets"` -} - -type BatchDeleteSecretsBySecretIdsRequest struct { - EnvironmentName string `json:"environmentName"` - WorkspaceId string `json:"workspaceId"` - SecretIds []string `json:"secretIds"` + PlainTextKey string `json:"plainTextKey"` } type GetEncryptedWorkspaceKeyRequest struct { @@ -194,41 +177,6 @@ type GetSecretsByWorkspaceIdAndEnvironmentRequest struct { WorkspaceId string `json:"workspaceId"` } -type GetEncryptedSecretsV2Request struct { - Environment string `json:"environment"` - WorkspaceId string `json:"workspaceId"` - TagSlugs string `json:"tagSlugs"` -} - -type GetEncryptedSecretsV2Response struct { - Secrets []struct { - ID string `json:"_id"` - Version int `json:"version"` - Workspace string `json:"workspace"` - Type string `json:"type"` - Environment string `json:"environment"` - SecretKeyCiphertext string `json:"secretKeyCiphertext"` - SecretKeyIV string `json:"secretKeyIV"` - SecretKeyTag string `json:"secretKeyTag"` - SecretValueCiphertext string `json:"secretValueCiphertext"` - SecretValueIV string `json:"secretValueIV"` - SecretValueTag string `json:"secretValueTag"` - SecretCommentCiphertext string `json:"secretCommentCiphertext"` - SecretCommentIV string `json:"secretCommentIV"` - SecretCommentTag string `json:"secretCommentTag"` - V int `json:"__v"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - User string `json:"user,omitempty"` - Tags []struct { - ID string `json:"_id"` - Name string `json:"name"` - Slug string `json:"slug"` - Workspace string `json:"workspace"` - } `json:"tags"` - } `json:"secrets"` -} - type GetServiceTokenDetailsResponse struct { ID string `json:"_id"` Name string `json:"name"` @@ -281,6 +229,7 @@ type GetLoginTwoV2Response struct { ProtectedKey string `json:"protectedKey"` ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyTag string `json:"protectedKeyTag"` + RefreshToken string `json:"RefreshToken"` } type VerifyMfaTokenRequest struct { @@ -298,6 +247,7 @@ type VerifyMfaTokenResponse struct { ProtectedKey string `json:"protectedKey"` ProtectedKeyIV string `json:"protectedKeyIV"` ProtectedKeyTag string `json:"protectedKeyTag"` + RefreshToken string `json:"refreshToken"` } type VerifyMfaTokenErrorResponse struct { @@ -314,3 +264,113 @@ type VerifyMfaTokenErrorResponse struct { Application string `json:"application"` Extra []interface{} `json:"extra"` } + +type GetNewAccessTokenWithRefreshTokenResponse struct { + Token string `json:"token"` +} + +type GetEncryptedSecretsV3Request struct { + Environment string `json:"environment"` + WorkspaceId string `json:"workspaceId"` + SecretPath string `json:"secretPath"` +} + +type GetEncryptedSecretsV3Response struct { + Secrets []struct { + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Tags []struct { + ID string `json:"_id"` + Name string `json:"name"` + Slug string `json:"slug"` + Workspace string `json:"workspace"` + } `json:"tags"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + Algorithm string `json:"algorithm"` + KeyEncoding string `json:"keyEncoding"` + Folder string `json:"folder"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"secrets"` +} + +type CreateSecretV3Request struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + SecretPath string `json:"secretPath"` +} + +type DeleteSecretV3Request struct { + SecretName string `json:"secretName"` + WorkspaceId string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` +} + +type UpdateSecretByNameV3Request struct { + SecretName string `json:"secretName"` + WorkspaceID string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` +} + +type GetSingleSecretByNameV3Request struct { + SecretName string `json:"secretName"` + WorkspaceId string `json:"workspaceId"` + Environment string `json:"environment"` + Type string `json:"type"` + SecretPath string `json:"secretPath"` +} + +type GetSingleSecretByNameSecretResponse struct { + Secrets []struct { + ID string `json:"_id"` + Version int `json:"version"` + Workspace string `json:"workspace"` + Type string `json:"type"` + Environment string `json:"environment"` + SecretKeyCiphertext string `json:"secretKeyCiphertext"` + SecretKeyIV string `json:"secretKeyIV"` + SecretKeyTag string `json:"secretKeyTag"` + SecretValueCiphertext string `json:"secretValueCiphertext"` + SecretValueIV string `json:"secretValueIV"` + SecretValueTag string `json:"secretValueTag"` + SecretCommentCiphertext string `json:"secretCommentCiphertext"` + SecretCommentIV string `json:"secretCommentIV"` + SecretCommentTag string `json:"secretCommentTag"` + Algorithm string `json:"algorithm"` + KeyEncoding string `json:"keyEncoding"` + Folder string `json:"folder"` + V int `json:"__v"` + CreatedAt time.Time `json:"createdAt"` + UpdatedAt time.Time `json:"updatedAt"` + } `json:"secrets"` +} diff --git a/cli/packages/cmd/login.go b/cli/packages/cmd/login.go index 56998cbdb..c629130a4 100644 --- a/cli/packages/cmd/login.go +++ b/cli/packages/cmd/login.go @@ -448,7 +448,7 @@ func getFreshUserCredentials(email string, password string) (*api.GetLoginOneV2R }) if err != nil { - util.HandleError(err) + return nil, nil, err } // **** Login 2 diff --git a/cli/packages/cmd/run.go b/cli/packages/cmd/run.go index afce399d5..8b82faa65 100644 --- a/cli/packages/cmd/run.go +++ b/cli/packages/cmd/run.go @@ -82,7 +82,12 @@ var runCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) if err != nil { util.HandleError(err, "Could not fetch secrets", "If you are using a service token to fetch secrets, please ensure it is valid") @@ -184,6 +189,7 @@ func init() { runCmd.Flags().Bool("secret-overriding", true, "Prioritizes personal secrets, if any, with the same name over shared secrets") runCmd.Flags().StringP("command", "c", "", "chained commands to execute (e.g. \"npm install && npm run dev; echo ...\")") runCmd.Flags().StringP("tags", "t", "", "filter secrets by tag slugs ") + runCmd.Flags().String("path", "/", "get secrets within a folder path") } // Will execute a single command and pass in the given secrets into the process diff --git a/cli/packages/cmd/secrets.go b/cli/packages/cmd/secrets.go index 26835eff1..0d6181a56 100644 --- a/cli/packages/cmd/secrets.go +++ b/cli/packages/cmd/secrets.go @@ -44,6 +44,11 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + shouldExpandSecrets, err := cmd.Flags().GetBool("expand") if err != nil { util.HandleError(err) @@ -54,7 +59,7 @@ var secretsCmd = &cobra.Command{ util.HandleError(err, "Unable to parse flag") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs}) + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, InfisicalToken: infisicalToken, TagSlugs: tagSlugs, SecretsPath: secretsPath}) if err != nil { util.HandleError(err) } @@ -103,6 +108,11 @@ var secretsSetCmd = &cobra.Command{ } } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + workspaceFile, err := util.GetWorkSpaceFromFile() if err != nil { util.HandleError(err, "Unable to get your local config details") @@ -140,7 +150,7 @@ var secretsSetCmd = &cobra.Command{ plainTextEncryptionKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) // pull current secrets - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) + secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName, SecretsPath: secretsPath}) if err != nil { util.HandleError(err, "unable to retrieve secrets") } @@ -191,6 +201,8 @@ var secretsSetCmd = &cobra.Command{ SecretValueIV: base64.StdEncoding.EncodeToString(encryptedValue.Nonce), SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, + PlainTextKey: key, + Type: existingSecret.Type, } // Only add to modifications if the value is different @@ -222,6 +234,7 @@ var secretsSetCmd = &cobra.Command{ SecretValueTag: base64.StdEncoding.EncodeToString(encryptedValue.AuthTag), SecretValueHash: hashedValue, Type: util.SECRET_TYPE_SHARED, + PlainTextKey: key, } secretsToCreate = append(secretsToCreate, encryptedSecretDetails) secretOperations = append(secretOperations, SecretSetOperation{ @@ -232,30 +245,43 @@ var secretsSetCmd = &cobra.Command{ } } - if len(secretsToCreate) > 0 { - batchCreateRequest := api.BatchCreateSecretsByWorkspaceAndEnvRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - Environment: environmentName, - Secrets: secretsToCreate, + for _, secret := range secretsToCreate { + createSecretRequest := api.CreateSecretV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secret.PlainTextKey, + SecretKeyCiphertext: secret.SecretKeyCiphertext, + SecretKeyIV: secret.SecretKeyIV, + SecretKeyTag: secret.SecretKeyTag, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, } - err = api.CallBatchCreateSecretsByWorkspaceAndEnv(httpClient, batchCreateRequest) + err = api.CallCreateSecretsV3(httpClient, createSecretRequest) if err != nil { util.HandleError(err, "Unable to process new secret creations") return } } - if len(secretsToModify) > 0 { - batchModifyRequest := api.BatchModifySecretsByWorkspaceAndEnvRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - Environment: environmentName, - Secrets: secretsToModify, + for _, secret := range secretsToModify { + updateSecretRequest := api.UpdateSecretByNameV3Request{ + WorkspaceID: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secret.PlainTextKey, + SecretValueCiphertext: secret.SecretValueCiphertext, + SecretValueIV: secret.SecretValueIV, + SecretValueTag: secret.SecretValueTag, + Type: secret.Type, + SecretPath: secretsPath, } - err = api.CallBatchModifySecretsByWorkspaceAndEnv(httpClient, batchModifyRequest) + err = api.CallUpdateSecretsV3(httpClient, updateSecretRequest) if err != nil { - util.HandleError(err, "Unable to process the modifications to your secrets") + util.HandleError(err, "Unable to process secret update request") return } } @@ -288,6 +314,16 @@ var secretsDeleteCmd = &cobra.Command{ } } + secretsPath, err := cmd.Flags().GetString("path") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + + secretType, err := cmd.Flags().GetString("type") + if err != nil { + util.HandleError(err, "Unable to parse flag") + } + loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails() if err != nil { util.HandleError(err, "Unable to authenticate") @@ -298,46 +334,28 @@ var secretsDeleteCmd = &cobra.Command{ util.HandleError(err, "Unable to get local project details") } - secrets, err := util.GetAllEnvironmentVariables(models.GetAllSecretsParameters{Environment: environmentName}) - if err != nil { - util.HandleError(err, "Unable to fetch secrets") - } - - secretByKey := getSecretsByKeys(secrets) - validSecretIdsToDelete := []string{} - invalidSecretNamesThatDoNotExist := []string{} - - for _, secretKeyFromArg := range args { - if value, ok := secretByKey[strings.ToUpper(secretKeyFromArg)]; ok { - validSecretIdsToDelete = append(validSecretIdsToDelete, value.ID) - } else { - invalidSecretNamesThatDoNotExist = append(invalidSecretNamesThatDoNotExist, secretKeyFromArg) + for _, secretName := range args { + request := api.DeleteSecretV3Request{ + WorkspaceId: workspaceFile.WorkspaceId, + Environment: environmentName, + SecretName: secretName, + Type: secretType, + SecretPath: secretsPath, } - } - if len(invalidSecretNamesThatDoNotExist) != 0 { - message := fmt.Sprintf("secret name(s) [%v] does not exist in your project. To see which secrets exist run [infisical secrets]", strings.Join(invalidSecretNamesThatDoNotExist, ", ")) - util.PrintErrorMessageAndExit(message) - } + httpClient := resty.New(). + SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). + SetHeader("Accept", "application/json") - request := api.BatchDeleteSecretsBySecretIdsRequest{ - WorkspaceId: workspaceFile.WorkspaceId, - EnvironmentName: environmentName, - SecretIds: validSecretIdsToDelete, - } - - httpClient := resty.New(). - SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken). - SetHeader("Accept", "application/json") - - err = api.CallBatchDeleteSecretsByWorkspaceAndEnv(httpClient, request) - if err != nil { - util.HandleError(err, "Unable to complete your batch delete request") + err = api.CallDeleteSecretsV3(httpClient, request) + if err != nil { + util.HandleError(err, "Unable to complete your delete request") + } } fmt.Printf("secret name(s) [%v] have been deleted from your project \n", strings.Join(args, ", ")) - Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(secrets)).Set("version", util.CLI_VERSION)) + Telemetry.CaptureEvent("cli-command:secrets delete", posthog.NewProperties().Set("secretCount", len(args)).Set("version", util.CLI_VERSION)) }, } @@ -611,11 +629,15 @@ func init() { secretsCmd.AddCommand(secretsGetCmd) secretsCmd.AddCommand(secretsSetCmd) + secretsSetCmd.Flags().String("path", "/", "get secrets within a folder path") + secretsSetCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { util.RequireLogin() util.RequireLocalWorkspaceFile() } + secretsDeleteCmd.Flags().String("type", "personal", "the type of secret to delete: personal or shared (default: personal)") + secretsDeleteCmd.Flags().String("path", "/", "get secrets within a folder path") secretsCmd.AddCommand(secretsDeleteCmd) secretsDeleteCmd.PersistentPreRun = func(cmd *cobra.Command, args []string) { util.RequireLogin() @@ -626,5 +648,6 @@ func init() { secretsCmd.PersistentFlags().String("env", "dev", "Used to select the environment name on which actions should be taken on") secretsCmd.Flags().Bool("expand", true, "Parse shell parameter expansions in your secrets") secretsCmd.PersistentFlags().StringP("tags", "t", "", "filter secrets by tag slugs") + secretsCmd.Flags().String("path", "/", "get secrets within a folder path") rootCmd.AddCommand(secretsCmd) } diff --git a/cli/packages/models/cli.go b/cli/packages/models/cli.go index b9b0ab7b5..18a64750f 100644 --- a/cli/packages/models/cli.go +++ b/cli/packages/models/cli.go @@ -5,9 +5,10 @@ import ( ) type UserCredentials struct { - Email string `json:"email"` - PrivateKey string `json:"privateKey"` - JTWToken string `json:"JTWToken"` + Email string `json:"email"` + PrivateKey string `json:"privateKey"` + JTWToken string `json:"JTWToken"` + RefreshToken string `json:"RefreshToken"` } // The file struct for Infisical config file @@ -63,4 +64,5 @@ type GetAllSecretsParameters struct { InfisicalToken string TagSlugs string WorkspaceId string + SecretsPath string } diff --git a/cli/packages/util/credentials.go b/cli/packages/util/credentials.go index 9147a717a..6b203c2e3 100644 --- a/cli/packages/util/credentials.go +++ b/cli/packages/util/credentials.go @@ -9,6 +9,7 @@ import ( "github.com/Infisical/infisical-merge/packages/config" "github.com/Infisical/infisical-merge/packages/models" "github.com/go-resty/resty/v2" + "github.com/rs/zerolog/log" ) type LoggedInUserDetails struct { @@ -96,6 +97,20 @@ func GetCurrentLoggedInUserDetails() (LoggedInUserDetails, error) { } isAuthenticated := api.CallIsAuthenticated(httpClient) + + if !isAuthenticated { + accessTokenResponse, _ := api.CallGetNewAccessTokenWithRefreshToken(httpClient, userCreds.RefreshToken) + if accessTokenResponse.Token != "" { + isAuthenticated = true + userCreds.JTWToken = accessTokenResponse.Token + } + } + + err = StoreUserCredsInKeyRing(&userCreds) + if err != nil { + log.Debug().Msg("unable to store your user credentials with new access token") + } + if !isAuthenticated { return LoggedInUserDetails{ IsUserLoggedIn: true, // was logged in diff --git a/cli/packages/util/helper.go b/cli/packages/util/helper.go index 8527c4059..d96ac3899 100644 --- a/cli/packages/util/helper.go +++ b/cli/packages/util/helper.go @@ -74,23 +74,12 @@ func ConfigContainsEmail(users []models.LoggedInUser, email string) bool { } func RequireLogin() { - currentUserDetails, err := GetCurrentLoggedInUserDetails() + // get the config file that stores the current logged in user email + configFile, _ := GetConfigFile() - if err != nil { - HandleError(err, "unable to retrieve your login details") - } - - if !currentUserDetails.IsUserLoggedIn { + if configFile.LoggedInUserEmail == "" { PrintErrorMessageAndExit("You must be logged in to run this command. To login, run [infisical login]") } - - if currentUserDetails.LoginExpired { - PrintErrorMessageAndExit("Your login expired, please login in again. To login, run [infisical login]") - } - - if currentUserDetails.UserCredentials.Email == "" && currentUserDetails.UserCredentials.JTWToken == "" && currentUserDetails.UserCredentials.PrivateKey == "" { - PrintErrorMessageAndExit("One or more of your login details is empty. Please try logging in again via by running [infisical login]") - } } func RequireServiceToken() { diff --git a/cli/packages/util/secrets.go b/cli/packages/util/secrets.go index a713abddb..eef691fdc 100644 --- a/cli/packages/util/secrets.go +++ b/cli/packages/util/secrets.go @@ -34,7 +34,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl return nil, api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err) } - encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ + encryptedSecrets, err := api.CallGetSecretsV3(httpClient, api.GetEncryptedSecretsV3Request{ WorkspaceId: serviceTokenDetails.Workspace, Environment: serviceTokenDetails.Environment, }) @@ -61,7 +61,7 @@ func GetPlainTextSecretsViaServiceToken(fullServiceToken string) ([]models.Singl return plainTextSecrets, serviceTokenDetails, nil } -func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, workspaceId string, environmentName string, tagSlugs string, secretsPath string) ([]models.SingleEnvironmentVariable, error) { httpClient := resty.New() httpClient.SetAuthToken(JTWToken). SetHeader("Accept", "application/json") @@ -102,11 +102,17 @@ func GetPlainTextSecretsViaJTW(JTWToken string, receiversPrivateKey string, work plainTextWorkspaceKey := crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey) - encryptedSecrets, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{ + getSecretsRequest := api.GetEncryptedSecretsV3Request{ WorkspaceId: workspaceId, Environment: environmentName, - TagSlugs: tagSlugs, - }) + // TagSlugs: tagSlugs, + } + + if secretsPath != "" { + getSecretsRequest.SecretPath = secretsPath + } + + encryptedSecrets, err := api.CallGetSecretsV3(httpClient, getSecretsRequest) if err != nil { return nil, err @@ -162,7 +168,7 @@ func GetAllEnvironmentVariables(params models.GetAllSecretsParameters) ([]models return nil, fmt.Errorf("unable to validate environment name because [err=%s]", err) } - secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs) + secretsToReturn, errorToReturn = GetPlainTextSecretsViaJTW(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceFile.WorkspaceId, params.Environment, params.TagSlugs, params.SecretsPath) log.Debug().Msgf("GetAllEnvironmentVariables: Trying to fetch secrets JTW token [err=%s]", errorToReturn) backupSecretsEncryptionKey := []byte(loggedInUserDetails.UserCredentials.PrivateKey)[0:32] @@ -333,7 +339,7 @@ func OverrideSecrets(secrets []models.SingleEnvironmentVariable, secretType stri return secretsToReturn } -func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV2Response) ([]models.SingleEnvironmentVariable, error) { +func GetPlainTextSecrets(key []byte, encryptedSecrets api.GetEncryptedSecretsV3Response) ([]models.SingleEnvironmentVariable, error) { plainTextSecrets := []models.SingleEnvironmentVariable{} for _, secret := range encryptedSecrets.Secrets { // Decrypt key diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index baee98940..5fa4fbc30 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -3,49 +3,29 @@ title: "Authentication" description: "How to authenticate with the Infisical Public API" --- -## Essentials +The Public API accepts multiple modes of authentication being via [Infisical Token](/documentation/platform/token) or API Key. -The Public API accepts multiple modes of authentication being via API Key, Service Account credentials, or [Infisical Token](/documentation/platform/token). - -- API Key: Provides full access to all endpoints representing the user. -- Service Account: Provides scoped access to an organization and select projects representing a machine such as a VM or application client. - [Infisical Token](/documentation/platform/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment. +- API Key: Provides full access to all endpoints representing the user without ability to encrypt/decrypt secrets for **E2EE** endpoints. - - -The API key mode uses an API key to authenticate with the API. + + + The Infisical Token mode uses an Infisical Token to authenticate with the API. -To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. + To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `. -You can obtain an API key in User Settings > API Keys + You can obtain an Infisical Token in Project Settings > Service Tokens. -![API key dashboard](../../images/api-key-dashboard.png) -![API key in personal settings](../../images/api-key-settings.png) - - -The Service Account mode uses an Access Key to authenticate with the API and a Public Key and Private Key to perform any cryptographic operations. + ![token add](../../images/project-token-add.png) + + + The API key mode uses an API key to authenticate with the API. -To authenticate requests with Infisical using the Access Key, you must include it in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `. + To authenticate requests with Infisical using the API Key, you must include an API key in the `X-API-KEY` header of HTTP requests made to the platform. -You can create a Service Account in Organization Settings > Service Accounts + You can obtain an API key in User Settings > API Keys - - - -The Infisical Token mode uses an Infisical Token to authenticate with the API. - -To authenticate requests with Infisical using the Infisical Token, you must include your Infisical Token in the `Authorization` header of HTTP requests made to the platform with the value `Bearer `. - -You can obtain an Infisical Token in Project Settings > Service Tokens. - -![token add](../../images/project-token-add.png) - - - -## Use Cases - -Depending on your use case, it may make sense to use one or another authentication mode: - -- API Key (not recommended): Use if you need full access to the Public API without needing to access any secrets endpoints (because API keys can't encrypt/decrypt secrets). -- Service Account (recommeded): Use if you need access to multiple projects and environments in an organization; service accounts can generate short-lived access tokens, making them useful for some complex setups. -- Service Token (recommeded): Use if you need short-lived, scoped CRUD access to the secrets of a specific project and environment. + ![API key dashboard](../../images/api-key-dashboard.png) + ![API key in personal settings](../../images/api-key-settings.png) + + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/create-secret.mdx b/docs/api-reference/overview/examples/create-secret.mdx deleted file mode 100644 index 4ea787ff0..000000000 --- a/docs/api-reference/overview/examples/create-secret.mdx +++ /dev/null @@ -1,233 +0,0 @@ ---- -title: "Create secret" -description: "How to add a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. Decrypt the (encrypted) project key with the key from your Infisical Token. -3. Encrypt your secret with the project key -4. [Send (encrypted) secret to Infisical](/api-reference/endpoints/secrets/create) - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); -const nacl = require('tweetnacl'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const createSecrets = async () => { - const serviceToken = ''; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared'; // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'some_value'; - const secretComment = 'some_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) secret to Infisical - await axios.post( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -createSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def create_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared or "personal" - secret_key = "some_key" - secret_value = "some_value" - secret_comment = "some_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) secret to Infisical - requests.post( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -create_secrets() - -``` - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/delete-secret.mdx b/docs/api-reference/overview/examples/delete-secret.mdx deleted file mode 100644 index ff2ec687c..000000000 --- a/docs/api-reference/overview/examples/delete-secret.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: "Delete secret" -description: "How to delete a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create either an [API Key](/api-reference/overview/authentication) or [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Example - - - -```js -const axios = require('axios'); -const BASE_URL = 'https://app.infisical.com'; - -const deleteSecrets = async () => { - const serviceToken = 'your_service_token'; - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key' - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Delete secret from Infisical - await axios.delete( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - }, - } - ); -}; - -deleteSecrets(); -``` - - - -```Python -import requests - -BASE_URL = "https://app.infisical.com" - - -def delete_secrets(): - service_token = "" - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Delete secret from Infisical - requests.delete( - f"{BASE_URL}/api/v2/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -delete_secrets() - -``` - - - - If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header. - - diff --git a/docs/api-reference/overview/examples/e2ee-disabled.mdx b/docs/api-reference/overview/examples/e2ee-disabled.mdx new file mode 100644 index 000000000..9864a1ff5 --- /dev/null +++ b/docs/api-reference/overview/examples/e2ee-disabled.mdx @@ -0,0 +1,176 @@ +--- +title: "E2EE Disabled" +--- + +Using Infisical's API to read/write secrets with E2EE disabled allows you to create, update, and retrieve secrets +in plaintext. Effectively, this means each such secret operation only requires 1 HTTP call. + + + + Retrieve all secrets for an Infisical project and environment. + + + + ```bash + curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw?environment=environment&workspaceId=workspaceId' \ + --header 'Authorization: Bearer serviceToken' + + ``` + + + + + The ID of the workspace + + + The environment slug + + + Path to secrets in workspace + + + + Create a secret in Infisical. + + + + ```bash + curl --location --request POST 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ + --header 'Authorization: Bearer serviceToken' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "workspaceId": "workspaceId", + "environment": "environment", + "type": "shared", + "secretValue": "secretValue", + "secretPath": "/" + }' + ``` + + + + + Name of secret to create + + + The ID of the workspace + + + The environment slug + + + Value of secret + + + Comment of secret + + + Path to secret in workspace + + + The type of the secret. Valid options are โ€œsharedโ€ or โ€œpersonalโ€ + + + + Retrieve a secret from Infisical. + + + + ```bash + curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \ + --header 'Authorization: Bearer serviceToken' + ``` + + + + + Name of secret to retrieve + + + The ID of the workspace + + + The environment slug + + + Path to secrets in workspace + + + The type of the secret. Valid options are โ€œsharedโ€ or โ€œpersonalโ€ + + + + Update an existing secret in Infisical. + + + + ```bash + curl --location --request PATCH 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ + --header 'Authorization: Bearer serviceToken' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "workspaceId": "workspaceId", + "environment": "environment", + "type": "shared", + "secretValue": "secretValue", + "secretPath": "/" + }' + ``` + + + + + Name of secret to update + + + The ID of the workspace + + + The environment slug + + + Value of secret + + + Path to secret in workspace. + + + The type of the secret. Valid options are โ€œsharedโ€ or โ€œpersonalโ€ + + + + Delete a secret in Infisical. + + + + ```bash + curl --location --request DELETE 'https://app.infisical.com/api/v3/secrets/raw/secretName' \ + --header 'Authorization: Bearer serviceToken' \ + --header 'Content-Type: application/json' \ + --data-raw '{ + "workspaceId": "workspaceId", + "environment": "environment", + "type": "shared", + "secretPath": "/" + }' + ``` + + + + + Name of secret to update + + + The ID of the workspace + + + The environment slug + + + Path to secret in workspace. + + + The type of the secret. Valid options are โ€œsharedโ€ or โ€œpersonalโ€ + + + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/e2ee-enabled.mdx b/docs/api-reference/overview/examples/e2ee-enabled.mdx new file mode 100644 index 000000000..0cbeaabcf --- /dev/null +++ b/docs/api-reference/overview/examples/e2ee-enabled.mdx @@ -0,0 +1,858 @@ +--- +title: "E2EE Enabled" +--- + +Using Infisical's API to read/write secrets with E2EE enabled allows you to create, update, and retrieve secrets +but requires you to perform client-side encryption/decryption operations. For this reason, we recommend using one of the available +SDKs instead. + + + + + + Retrieve all secrets for an Infisical project and environment. +```js +const crypto = require('crypto'); +const axios = require('axios'); + +const BASE_URL = 'https://app.infisical.com'; +const ALGORITHM = 'aes-256-gcm'; + +const decrypt = ({ ciphertext, iv, tag, secret}) => { + const decipher = crypto.createDecipheriv( + ALGORITHM, + secret, + Buffer.from(iv, 'base64') + ); + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); + + return cleartext; +} + +const getSecrets = async () => { + const serviceToken = 'your_service_token'; + const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Get secrets for your project and environment + const { data } = await axios.get( + `${BASE_URL}/api/v3/secrets?${new URLSearchParams({ + environment: serviceTokenData.environment, + workspaceId: serviceTokenData.workspace + })}`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + const encryptedSecrets = data.secrets; + + // 3. Decrypt the (encrypted) project key with the key from your Infisical Token + const projectKey = decrypt({ + ciphertext: serviceTokenData.encryptedKey, + iv: serviceTokenData.iv, + tag: serviceTokenData.tag, + secret: serviceTokenSecret + }); + + // 4. Decrypt the (encrypted) secrets + const secrets = encryptedSecrets.map((secret) => { + const secretKey = decrypt({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + secret: projectKey + }); + + const secretValue = decrypt({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + secret: projectKey + }); + + return ({ + secretKey, + secretValue + }); + }); + + console.log('secrets: ', secrets); +} + +getSecrets(); + +``` + + + +```Python +import requests +import base64 +from Cryptodome.Cipher import AES + + +BASE_URL = "http://app.infisical.com" + + +def decrypt(ciphertext, iv, tag, secret): + secret = bytes(secret, "utf-8") + iv = base64.standard_b64decode(iv) + tag = base64.standard_b64decode(tag) + ciphertext = base64.standard_b64decode(ciphertext) + + cipher = AES.new(secret, AES.MODE_GCM, iv) + cipher.update(tag) + cleartext = cipher.decrypt(ciphertext).decode("utf-8") + return cleartext + + +def get_secrets(): + service_token = "your_service_token" + service_token_secret = service_token[service_token.rindex(".") + 1 :] + + # 1. Get your Infisical Token data + service_token_data = requests.get( + f"{BASE_URL}/api/v2/service-token", + headers={"Authorization": f"Bearer {service_token}"}, + ).json() + + # 2. Get secrets for your project and environment + data = requests.get( + f"{BASE_URL}/api/v3/secrets", + params={ + "environment": service_token_data["environment"], + "workspaceId": service_token_data["workspace"], + }, + headers={"Authorization": f"Bearer {service_token}"}, + ).json() + + encrypted_secrets = data["secrets"] + + # 3. Decrypt the (encrypted) project key with the key from your Infisical Token + project_key = decrypt( + ciphertext=service_token_data["encryptedKey"], + iv=service_token_data["iv"], + tag=service_token_data["tag"], + secret=service_token_secret, + ) + + # 4. Decrypt the (encrypted) secrets + secrets = [] + for secret in encrypted_secrets: + secret_key = decrypt( + ciphertext=secret["secretKeyCiphertext"], + iv=secret["secretKeyIV"], + tag=secret["secretKeyTag"], + secret=project_key, + ) + + secret_value = decrypt( + ciphertext=secret["secretValueCiphertext"], + iv=secret["secretValueIV"], + tag=secret["secretValueTag"], + secret=project_key, + ) + + secrets.append( + { + "secret_key": secret_key, + "secret_value": secret_value, + } + ) + + print("secrets:", secrets) + + +get_secrets() + +``` + + + + + + +Create a secret in Infisical. +```js +const crypto = require('crypto'); +const axios = require('axios'); +const nacl = require('tweetnacl'); + +const BASE_URL = 'https://app.infisical.com'; +const ALGORITHM = 'aes-256-gcm'; +const BLOCK_SIZE_BYTES = 16; + +const encrypt = ({ text, secret }) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; +} + +const decrypt = ({ ciphertext, iv, tag, secret}) => { + const decipher = crypto.createDecipheriv( + ALGORITHM, + secret, + Buffer.from(iv, 'base64') + ); + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); + + return cleartext; +} + +const createSecrets = async () => { + const serviceToken = ''; + const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); + + const secretType = 'shared'; // 'shared' or 'personal' + const secretKey = 'some_key'; + const secretValue = 'some_value'; + const secretComment = 'some_comment'; + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Decrypt the (encrypted) project key with the key from your Infisical Token + const projectKey = decrypt({ + ciphertext: serviceTokenData.encryptedKey, + iv: serviceTokenData.iv, + tag: serviceTokenData.tag, + secret: serviceTokenSecret + }); + + // 3. Encrypt your secret with the project key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encrypt({ + text: secretKey, + secret: projectKey + }); + + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encrypt({ + text: secretValue, + secret: projectKey + }); + + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encrypt({ + text: secretComment, + secret: projectKey + }); + + // 4. Send (encrypted) secret to Infisical + await axios.post( + `${BASE_URL}/api/v3/secrets/${secretKey}`, + { + workspaceId: serviceTokenData.workspace, + environment: serviceTokenData.environment, + type: secretType, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + }, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); +} + +createSecrets(); +``` + + + +```Python +import base64 +import requests +from Cryptodome.Cipher import AES +from Cryptodome.Random import get_random_bytes + + +BASE_URL = "https://app.infisical.com" +BLOCK_SIZE_BYTES = 16 + + +def encrypt(text, secret): + iv = get_random_bytes(BLOCK_SIZE_BYTES) + secret = bytes(secret, "utf-8") + cipher = AES.new(secret, AES.MODE_GCM, iv) + ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) + return { + "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), + "tag": base64.standard_b64encode(tag).decode("utf-8"), + "iv": base64.standard_b64encode(iv).decode("utf-8"), + } + + +def decrypt(ciphertext, iv, tag, secret): + secret = bytes(secret, "utf-8") + iv = base64.standard_b64decode(iv) + tag = base64.standard_b64decode(tag) + ciphertext = base64.standard_b64decode(ciphertext) + + cipher = AES.new(secret, AES.MODE_GCM, iv) + cipher.update(tag) + cleartext = cipher.decrypt(ciphertext).decode("utf-8") + return cleartext + + +def create_secrets(): + service_token = "your_service_token" + service_token_secret = service_token[service_token.rindex(".") + 1 :] + + secret_type = "shared" # "shared or "personal" + secret_key = "some_key" + secret_value = "some_value" + secret_comment = "some_comment" + + # 1. Get your Infisical Token data + service_token_data = requests.get( + f"{BASE_URL}/api/v2/service-token", + headers={"Authorization": f"Bearer {service_token}"}, + ).json() + + # 2. Decrypt the (encrypted) project key with the key from your Infisical Token + project_key = decrypt( + ciphertext=service_token_data["encryptedKey"], + iv=service_token_data["iv"], + tag=service_token_data["tag"], + secret=service_token_secret, + ) + + # 3. Encrypt your secret with the project key + encrypted_key_data = encrypt(text=secret_key, secret=project_key) + encrypted_value_data = encrypt(text=secret_value, secret=project_key) + encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) + + # 4. Send (encrypted) secret to Infisical + requests.post( + f"{BASE_URL}/api/v3/secrets/{secret_key}", + json={ + "workspaceId": service_token_data["workspace"], + "environment": service_token_data["environment"], + "type": secret_type, + "secretKeyCiphertext": encrypted_key_data["ciphertext"], + "secretKeyIV": encrypted_key_data["iv"], + "secretKeyTag": encrypted_key_data["tag"], + "secretValueCiphertext": encrypted_value_data["ciphertext"], + "secretValueIV": encrypted_value_data["iv"], + "secretValueTag": encrypted_value_data["tag"], + "secretCommentCiphertext": encrypted_comment_data["ciphertext"], + "secretCommentIV": encrypted_comment_data["iv"], + "secretCommentTag": encrypted_comment_data["tag"] + }, + headers={"Authorization": f"Bearer {service_token}"}, + ) + + +create_secrets() + +``` + + + + + + + Retrieve a secret from Infisical. +```js +const crypto = require('crypto'); +const axios = require('axios'); + +const BASE_URL = 'https://app.infisical.com'; +const ALGORITHM = 'aes-256-gcm'; + +const decrypt = ({ ciphertext, iv, tag, secret}) => { + const decipher = crypto.createDecipheriv( + ALGORITHM, + secret, + Buffer.from(iv, 'base64') + ); + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); + + return cleartext; +} + +const getSecret = async () => { + const serviceToken = 'your_service_token'; + const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); + + const secretType = 'shared' // 'shared' or 'personal' + const secretKey = 'some_key'; + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Get the secret from your project and environment + const { data } = await axios.get( + `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({ + environment: serviceTokenData.environment, + workspaceId: serviceTokenData.workspace, + type: secretType // optional, defaults to 'shared' + })}`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + const encryptedSecret = data.secret; + + // 3. Decrypt the (encrypted) project key with the key from your Infisical Token + const projectKey = decrypt({ + ciphertext: serviceTokenData.encryptedKey, + iv: serviceTokenData.iv, + tag: serviceTokenData.tag, + secret: serviceTokenSecret + }); + + // 4. Decrypt the (encrypted) secret value + + const secretValue = decrypt({ + ciphertext: encryptedSecret.secretValueCiphertext, + iv: encryptedSecret.secretValueIV, + tag: encryptedSecret.secretValueTag, + secret: projectKey + }); + + console.log('secret: ', ({ + secretKey, + secretValue + })); +} + +getSecret(); + +``` + + + +```Python +import requests +import base64 +from Cryptodome.Cipher import AES + + +BASE_URL = "http://app.infisical.com" + + +def decrypt(ciphertext, iv, tag, secret): + secret = bytes(secret, "utf-8") + iv = base64.standard_b64decode(iv) + tag = base64.standard_b64decode(tag) + ciphertext = base64.standard_b64decode(ciphertext) + + cipher = AES.new(secret, AES.MODE_GCM, iv) + cipher.update(tag) + cleartext = cipher.decrypt(ciphertext).decode("utf-8") + return cleartext + + +def get_secret(): + service_token = "your_service_token" + service_token_secret = service_token[service_token.rindex(".") + 1 :] + + secret_type = "shared" # "shared" or "personal" + secret_key = "some_key" + + # 1. Get your Infisical Token data + service_token_data = requests.get( + f"{BASE_URL}/api/v2/service-token", + headers={"Authorization": f"Bearer {service_token}"}, + ).json() + + # 2. Get secret from your project and environment + data = requests.get( + f"{BASE_URL}/api/v3/secrets/{secret_key}", + params={ + "environment": service_token_data["environment"], + "workspaceId": service_token_data["workspace"], + "type": secret_type # optional, defaults to "shared" + }, + headers={"Authorization": f"Bearer {service_token}"}, + ).json() + + encrypted_secret = data["secret"] + + # 3. Decrypt the (encrypted) project key with the key from your Infisical Token + project_key = decrypt( + ciphertext=service_token_data["encryptedKey"], + iv=service_token_data["iv"], + tag=service_token_data["tag"], + secret=service_token_secret, + ) + + # 4. Decrypt the (encrypted) secret value + secret_value = decrypt( + ciphertext=encrypted_secret["secretValueCiphertext"], + iv=encrypted_secret["secretValueIV"], + tag=encrypted_secret["secretValueTag"], + secret=project_key, + ) + + print("secret: ", { + "secret_key": secret_key, + "secret_value": secret_value + }) + + +get_secret() + +``` + + + + + + +Update an existing secret in Infisical. +```js +const crypto = require('crypto'); +const axios = require('axios'); + +const BASE_URL = 'https://app.infisical.com'; +const ALGORITHM = 'aes-256-gcm'; +const BLOCK_SIZE_BYTES = 16; + +const encrypt = ({ text, secret }) => { + const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); + const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); + + let ciphertext = cipher.update(text, 'utf8', 'base64'); + ciphertext += cipher.final('base64'); + return { + ciphertext, + iv: iv.toString('base64'), + tag: cipher.getAuthTag().toString('base64') + }; +} + +const decrypt = ({ ciphertext, iv, tag, secret}) => { + const decipher = crypto.createDecipheriv( + ALGORITHM, + secret, + Buffer.from(iv, 'base64') + ); + decipher.setAuthTag(Buffer.from(tag, 'base64')); + + let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); + cleartext += decipher.final('utf8'); + + return cleartext; +} + +const updateSecrets = async () => { + const serviceToken = 'your_service_token'; + const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); + + const secretType = 'shared' // 'shared' or 'personal' + const secretKey = 'some_key'; + const secretValue = 'updated_value'; + const secretComment = 'updated_comment'; + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Decrypt the (encrypted) project key with the key from your Infisical Token + const projectKey = decrypt({ + ciphertext: serviceTokenData.encryptedKey, + iv: serviceTokenData.iv, + tag: serviceTokenData.tag, + secret: serviceTokenSecret + }); + + // 3. Encrypt your updated secret with the project key + const { + ciphertext: secretKeyCiphertext, + iv: secretKeyIV, + tag: secretKeyTag + } = encrypt({ + text: secretKey, + secret: projectKey + }); + + const { + ciphertext: secretValueCiphertext, + iv: secretValueIV, + tag: secretValueTag + } = encrypt({ + text: secretValue, + secret: projectKey + }); + + const { + ciphertext: secretCommentCiphertext, + iv: secretCommentIV, + tag: secretCommentTag + } = encrypt({ + text: secretComment, + secret: projectKey + }); + + // 4. Send (encrypted) updated secret to Infisical + await axios.patch( + `${BASE_URL}/api/v3/secrets/${secretKey}`, + { + workspaceId: serviceTokenData.workspace, + environment: serviceTokenData.environment, + type: secretType, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag + }, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); +} + +updateSecrets(); +``` + + + +```Python +import base64 +import requests +from Cryptodome.Cipher import AES +from Cryptodome.Random import get_random_bytes + + +BASE_URL = "https://app.infisical.com" +BLOCK_SIZE_BYTES = 16 + + +def encrypt(text, secret): + iv = get_random_bytes(BLOCK_SIZE_BYTES) + secret = bytes(secret, "utf-8") + cipher = AES.new(secret, AES.MODE_GCM, iv) + ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) + return { + "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), + "tag": base64.standard_b64encode(tag).decode("utf-8"), + "iv": base64.standard_b64encode(iv).decode("utf-8"), + } + + +def decrypt(ciphertext, iv, tag, secret): + secret = bytes(secret, "utf-8") + iv = base64.standard_b64decode(iv) + tag = base64.standard_b64decode(tag) + ciphertext = base64.standard_b64decode(ciphertext) + + cipher = AES.new(secret, AES.MODE_GCM, iv) + cipher.update(tag) + cleartext = cipher.decrypt(ciphertext).decode("utf-8") + return cleartext + + +def update_secret(): + service_token = "your_service_token" + service_token_secret = service_token[service_token.rindex(".") + 1 :] + + secret_type = "shared" # "shared" or "personal" + secret_key = "some_key" + secret_value = "updated_value" + secret_comment = "updated_comment" + + # 1. Get your Infisical Token data + service_token_data = requests.get( + f"{BASE_URL}/api/v2/service-token", + headers={"Authorization": f"Bearer {service_token}"}, + ).json() + + # 2. Decrypt the (encrypted) project key with the key from your Infisical Token + project_key = decrypt( + ciphertext=service_token_data["encryptedKey"], + iv=service_token_data["iv"], + tag=service_token_data["tag"], + secret=service_token_secret, + ) + + # 3. Encrypt your updated secret with the project key + encrypted_key_data = encrypt(text=secret_key, secret=project_key) + encrypted_value_data = encrypt(text=secret_value, secret=project_key) + encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) + + # 4. Send (encrypted) updated secret to Infisical + requests.patch( + f"{BASE_URL}/api/v3/secrets/{secret_key}", + json={ + "workspaceId": service_token_data["workspace"], + "environment": service_token_data["environment"], + "type": secret_type, + "secretKeyCiphertext": encrypted_key_data["ciphertext"], + "secretKeyIV": encrypted_key_data["iv"], + "secretKeyTag": encrypted_key_data["tag"], + "secretValueCiphertext": encrypted_value_data["ciphertext"], + "secretValueIV": encrypted_value_data["iv"], + "secretValueTag": encrypted_value_data["tag"], + "secretCommentCiphertext": encrypted_comment_data["ciphertext"], + "secretCommentIV": encrypted_comment_data["iv"], + "secretCommentTag": encrypted_comment_data["tag"] + }, + headers={"Authorization": f"Bearer {service_token}"}, + ) + + +update_secret() + +``` + + + + + + + Delete a secret in Infisical. +```js +const axios = require('axios'); +const BASE_URL = 'https://app.infisical.com'; + +const deleteSecrets = async () => { + const serviceToken = 'your_service_token'; + const secretType = 'shared' // 'shared' or 'personal' + const secretKey = 'some_key' + + // 1. Get your Infisical Token data + const { data: serviceTokenData } = await axios.get( + `${BASE_URL}/api/v2/service-token`, + { + headers: { + Authorization: `Bearer ${serviceToken}` + } + } + ); + + // 2. Delete secret from Infisical + await axios.delete( + `${BASE_URL}/api/v3/secrets/${secretKey}`, + { + workspaceId: serviceTokenData.workspace, + environment: serviceTokenData.environment, + type: secretType + }, + { + headers: { + Authorization: `Bearer ${serviceToken}` + }, + } + ); +}; + +deleteSecrets(); +``` + + + +```Python +import requests + +BASE_URL = "https://app.infisical.com" + + +def delete_secrets(): + service_token = "" + secret_type = "shared" # "shared" or "personal" + secret_key = "some_key" + + # 1. Get your Infisical Token data + service_token_data = requests.get( + f"{BASE_URL}/api/v2/service-token", + headers={"Authorization": f"Bearer {service_token}"}, + ).json() + + # 2. Delete secret from Infisical + requests.delete( + f"{BASE_URL}/api/v2/secrets/{secret_key}", + json={ + "workspaceId": service_token_data["workspace"], + "environment": service_token_data["environment"], + "type": secret_type + }, + headers={"Authorization": f"Bearer {service_token}"}, + ) + + +delete_secrets() + +``` + + + + If using an `API_KEY` to authenticate with the Infisical API, then you should include it in the `X_API_KEY` header. + + + + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/note.mdx b/docs/api-reference/overview/examples/note.mdx new file mode 100644 index 000000000..8491dfaae --- /dev/null +++ b/docs/api-reference/overview/examples/note.mdx @@ -0,0 +1,54 @@ +--- +title: "Note on E2EE" +--- + +Each project in Infisical can have **End-to-End Encryption (E2EE)** enabled or disabled. + +By default, all projects have **E2EE** enabled which means the server is not able to decrypt any values because all secret encryption/decryption operations occur on the client-side; this can be (optionally) disabled. However, this has limitations around functionality and ease-of-use: + +- You cannot make HTTP calls to Infisical to read/write secrets in plaintext. +- You cannot leverage non-E2EE features like native integrations and in-platform automations like dynamic secrets and secret rotation. + + + + Example read/write secrets without client-side encryption/decryption + + + Example read/write secrets with client-side encryption/decryption + + + +## FAQ + + + + We recommend starting with having **E2EE** enabled and disabling it if: + + - You're self-hosting Infisical, so having your instance of Infisical be able to read your secrets isn't an issue. + - You want an easier way to read/write secrets with Infisical. + - You need more power out of non-E2EE features such as secret rotation, dynamic secrets, etc. + + + + You can enable/disable E2EE for your project in Infisical in the Project Settings. + + + It is secure and in fact how most vendors in our industry are able to offer features like secret rotation. In this mode, secrets are encrypted at rest by + a series of keys, secured ultimately by a top-level `ROOT_ENCRYPTION_KEY` located on the server. + + If you're concerned about Infisical Cloud's ability to read your secrets, then you may wish to + use it with **E2EE** enabled or self-host Infisical on your own infrastructure and disable E2EE there. + + As an organization, we do not read any customer secrets without explicit permission; access to the `ROOT_ENCRYPTION_KEY` is restricted to one individual in the organization. + + \ No newline at end of file diff --git a/docs/api-reference/overview/examples/retrieve-secret.mdx b/docs/api-reference/overview/examples/retrieve-secret.mdx deleted file mode 100644 index a65876253..000000000 --- a/docs/api-reference/overview/examples/retrieve-secret.mdx +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: "Retrieve secret" -description: "How to get a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. [Get the secret from your project and environment](/api-reference/endpoints/secrets/read-one). -3. Decrypt the (encrypted) project key with the key from your Infisical Token. -4. Decrypt the (encrypted) secret - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecret = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get the secret from your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets/${secretKey}?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace, - type: secretType // optional, defaults to 'shared' - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecret = data.secret; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secret value - - const secretValue = decrypt({ - ciphertext: encryptedSecret.secretValueCiphertext, - iv: encryptedSecret.secretValueIV, - tag: encryptedSecret.secretValueTag, - secret: projectKey - }); - - console.log('secret: ', ({ - secretKey, - secretValue - })); -} - -getSecret(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secret from your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - "type": secret_type # optional, defaults to "shared" - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secret = data["secret"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secret value - secret_value = decrypt( - ciphertext=encrypted_secret["secretValueCiphertext"], - iv=encrypted_secret["secretValueIV"], - tag=encrypted_secret["secretValueTag"], - secret=project_key, - ) - - print("secret: ", { - "secret_key": secret_key, - "secret_value": secret_value - }) - - -get_secret() - -``` - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/retrieve-secrets.mdx b/docs/api-reference/overview/examples/retrieve-secrets.mdx deleted file mode 100644 index f237822aa..000000000 --- a/docs/api-reference/overview/examples/retrieve-secrets.mdx +++ /dev/null @@ -1,195 +0,0 @@ ---- -title: "Retrieve secrets" -description: "How to get all secrets using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. [Get secrets for your project and environment](/api-reference/endpoints/secrets/read). -3. Decrypt the (encrypted) project key with the key from your Infisical Token. -4. Decrypt the (encrypted) secrets - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const getSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Get secrets for your project and environment - const { data } = await axios.get( - `${BASE_URL}/api/v3/secrets?${new URLSearchParams({ - environment: serviceTokenData.environment, - workspaceId: serviceTokenData.workspace - })}`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - const encryptedSecrets = data.secrets; - - // 3. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 4. Decrypt the (encrypted) secrets - const secrets = encryptedSecrets.map((secret) => { - const secretKey = decrypt({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - secret: projectKey - }); - - const secretValue = decrypt({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - secret: projectKey - }); - - return ({ - secretKey, - secretValue - }); - }); - - console.log('secrets: ', secrets); -} - -getSecrets(); - -``` - - - -```Python -import requests -import base64 -from Cryptodome.Cipher import AES - - -BASE_URL = "http://app.infisical.com" - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def get_secrets(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Get secrets for your project and environment - data = requests.get( - f"{BASE_URL}/api/v3/secrets", - params={ - "environment": service_token_data["environment"], - "workspaceId": service_token_data["workspace"], - }, - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - encrypted_secrets = data["secrets"] - - # 3. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 4. Decrypt the (encrypted) secrets - secrets = [] - for secret in encrypted_secrets: - secret_key = decrypt( - ciphertext=secret["secretKeyCiphertext"], - iv=secret["secretKeyIV"], - tag=secret["secretKeyTag"], - secret=project_key, - ) - - secret_value = decrypt( - ciphertext=secret["secretValueCiphertext"], - iv=secret["secretValueIV"], - tag=secret["secretValueTag"], - secret=project_key, - ) - - secrets.append( - { - "secret_key": secret_key, - "secret_value": secret_value, - } - ) - - print("secrets:", secrets) - - -get_secrets() - -``` - - \ No newline at end of file diff --git a/docs/api-reference/overview/examples/update-secret.mdx b/docs/api-reference/overview/examples/update-secret.mdx deleted file mode 100644 index 49255aa79..000000000 --- a/docs/api-reference/overview/examples/update-secret.mdx +++ /dev/null @@ -1,229 +0,0 @@ ---- -title: "Update secret" -description: "How to update a secret using an Infisical Token scoped to a project and environment" ---- - -Prerequisites: - -- Set up and add envars to [Infisical Cloud](https://app.infisical.com). -- Create an [Infisical Token](/documentation/platform/token) for your project and environment with write access enabled. -- Grasp a basic understanding of the system and its underlying cryptography [here](/api-reference/overview/introduction). -- [Ensure that your project is blind-indexed](../blind-indices). - -## Flow - -1. [Get your Infisical Token data](/api-reference/endpoints/service-tokens/get) including a (encrypted) project key. -2. Decrypt the (encrypted) project key with the key from your Infisical Token. -3. Encrypt your updated secret with the project key -4. [Send (encrypted) updated secret to Infical](/api-reference/endpoints/secrets/update) - -## Example - - - -```js -const crypto = require('crypto'); -const axios = require('axios'); - -const BASE_URL = 'https://app.infisical.com'; -const ALGORITHM = 'aes-256-gcm'; -const BLOCK_SIZE_BYTES = 16; - -const encrypt = ({ text, secret }) => { - const iv = crypto.randomBytes(BLOCK_SIZE_BYTES); - const cipher = crypto.createCipheriv(ALGORITHM, secret, iv); - - let ciphertext = cipher.update(text, 'utf8', 'base64'); - ciphertext += cipher.final('base64'); - return { - ciphertext, - iv: iv.toString('base64'), - tag: cipher.getAuthTag().toString('base64') - }; -} - -const decrypt = ({ ciphertext, iv, tag, secret}) => { - const decipher = crypto.createDecipheriv( - ALGORITHM, - secret, - Buffer.from(iv, 'base64') - ); - decipher.setAuthTag(Buffer.from(tag, 'base64')); - - let cleartext = decipher.update(ciphertext, 'base64', 'utf8'); - cleartext += decipher.final('utf8'); - - return cleartext; -} - -const updateSecrets = async () => { - const serviceToken = 'your_service_token'; - const serviceTokenSecret = serviceToken.substring(serviceToken.lastIndexOf('.') + 1); - - const secretType = 'shared' // 'shared' or 'personal' - const secretKey = 'some_key'; - const secretValue = 'updated_value'; - const secretComment = 'updated_comment'; - - // 1. Get your Infisical Token data - const { data: serviceTokenData } = await axios.get( - `${BASE_URL}/api/v2/service-token`, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); - - // 2. Decrypt the (encrypted) project key with the key from your Infisical Token - const projectKey = decrypt({ - ciphertext: serviceTokenData.encryptedKey, - iv: serviceTokenData.iv, - tag: serviceTokenData.tag, - secret: serviceTokenSecret - }); - - // 3. Encrypt your updated secret with the project key - const { - ciphertext: secretKeyCiphertext, - iv: secretKeyIV, - tag: secretKeyTag - } = encrypt({ - text: secretKey, - secret: projectKey - }); - - const { - ciphertext: secretValueCiphertext, - iv: secretValueIV, - tag: secretValueTag - } = encrypt({ - text: secretValue, - secret: projectKey - }); - - const { - ciphertext: secretCommentCiphertext, - iv: secretCommentIV, - tag: secretCommentTag - } = encrypt({ - text: secretComment, - secret: projectKey - }); - - // 4. Send (encrypted) updated secret to Infisical - await axios.patch( - `${BASE_URL}/api/v3/secrets/${secretKey}`, - { - workspaceId: serviceTokenData.workspace, - environment: serviceTokenData.environment, - type: secretType, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - }, - { - headers: { - Authorization: `Bearer ${serviceToken}` - } - } - ); -} - -updateSecrets(); -``` - - - -```Python -import base64 -import requests -from Cryptodome.Cipher import AES -from Cryptodome.Random import get_random_bytes - - -BASE_URL = "https://app.infisical.com" -BLOCK_SIZE_BYTES = 16 - - -def encrypt(text, secret): - iv = get_random_bytes(BLOCK_SIZE_BYTES) - secret = bytes(secret, "utf-8") - cipher = AES.new(secret, AES.MODE_GCM, iv) - ciphertext, tag = cipher.encrypt_and_digest(text.encode("utf-8")) - return { - "ciphertext": base64.standard_b64encode(ciphertext).decode("utf-8"), - "tag": base64.standard_b64encode(tag).decode("utf-8"), - "iv": base64.standard_b64encode(iv).decode("utf-8"), - } - - -def decrypt(ciphertext, iv, tag, secret): - secret = bytes(secret, "utf-8") - iv = base64.standard_b64decode(iv) - tag = base64.standard_b64decode(tag) - ciphertext = base64.standard_b64decode(ciphertext) - - cipher = AES.new(secret, AES.MODE_GCM, iv) - cipher.update(tag) - cleartext = cipher.decrypt(ciphertext).decode("utf-8") - return cleartext - - -def update_secret(): - service_token = "your_service_token" - service_token_secret = service_token[service_token.rindex(".") + 1 :] - - secret_type = "shared" # "shared" or "personal" - secret_key = "some_key" - secret_value = "updated_value" - secret_comment = "updated_comment" - - # 1. Get your Infisical Token data - service_token_data = requests.get( - f"{BASE_URL}/api/v2/service-token", - headers={"Authorization": f"Bearer {service_token}"}, - ).json() - - # 2. Decrypt the (encrypted) project key with the key from your Infisical Token - project_key = decrypt( - ciphertext=service_token_data["encryptedKey"], - iv=service_token_data["iv"], - tag=service_token_data["tag"], - secret=service_token_secret, - ) - - # 3. Encrypt your updated secret with the project key - encrypted_key_data = encrypt(text=secret_key, secret=project_key) - encrypted_value_data = encrypt(text=secret_value, secret=project_key) - encrypted_comment_data = encrypt(text=secret_comment, secret=project_key) - - # 4. Send (encrypted) updated secret to Infisical - requests.patch( - f"{BASE_URL}/api/v3/secrets/{secret_key}", - json={ - "workspaceId": service_token_data["workspace"], - "environment": service_token_data["environment"], - "type": secret_type, - "secretKeyCiphertext": encrypted_key_data["ciphertext"], - "secretKeyIV": encrypted_key_data["iv"], - "secretKeyTag": encrypted_key_data["tag"], - "secretValueCiphertext": encrypted_value_data["ciphertext"], - "secretValueIV": encrypted_value_data["iv"], - "secretValueTag": encrypted_value_data["tag"], - "secretCommentCiphertext": encrypted_comment_data["ciphertext"], - "secretCommentIV": encrypted_comment_data["iv"], - "secretCommentTag": encrypted_comment_data["tag"] - }, - headers={"Authorization": f"Bearer {service_token}"}, - ) - - -update_secret() - -``` - - \ No newline at end of file diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 5199256e5..546e0b9a2 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -8,31 +8,6 @@ rotating credentials, or for integrating secret management into a larger system. With the Public API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more. - - We highly recommend using one of the available SDKs when working with the Infisical API. - - If you decide to make your own requests using the API reference instead, be prepared for a steeper learning curve and more manual work. - - In April 2023, we added the capability for users to query for secrets by name to improve the user experience of Infisical. If your project was created prior to April 2023, please read and follow the section on [blind indices](./blind-indices) and how to enable them for better usage of Infisical. - - -## Concepts - -Using Infisical's API to manage secrets requires a basic understanding of the system and its underlying cryptography detailed [here](/security/overview). A few key points: - -- Each user has a public/private key pair that is stored with the platform; private keys are encrypted locally by protected keys that are encrypted by keys derived from Argon2id applied to the user's password before being sent off to the server during the account signup process. -- Each (encrypted) secret belongs to a project and environment. -- Each project has an (encrypted) project key used to encrypt the secrets within that project; Infisical stores copies of the project key, for each member of that project, encrypted under each member's public key. -- Secrets are encrypted symmetrically by your copy of the project key belonging to the project containing. -- Infisical Tokens contain a symmetric key that can be used to decrypt a copy of a project key from the [call to get the Infisical Token data](/api-reference/endpoints/service-tokens/get). -- Infisical uses AES256-GCM and [TweetNaCl.js](https://tweetnacl.js.org/#/) for symmetric and asymmetric encryption/decryption operations. - - - Infisical's system requires that secrets be encrypted/decrypted on the - client-side to maintain E2EE. We strongly recommend you read up on the system - prior to using the Infisical API. The (opt-in) ability to retrieve secrets - back in decrypted format if you choose to share secrets with Infisical is on - our roadmap. - + \ No newline at end of file diff --git a/docs/documentation/getting-started/api.mdx b/docs/documentation/getting-started/api.mdx new file mode 100644 index 000000000..279982911 --- /dev/null +++ b/docs/documentation/getting-started/api.mdx @@ -0,0 +1,59 @@ +--- +title: "REST API" +--- + +Infisical's Public (REST) API is the most flexible, platform-agnostic way to read/write secrets for your application. + +Prerequisites: + +- Have a project with secrets ready in [Infisical Cloud](https://app.infisical.com). +- Create an [Infisical Token](/documentation/platform/token) scoped to an environment in your project in Infisical. + +To keep it simple, we're going to fetch secrets from the API with **End-to-End Encryption (E2EE)** disabled. + + + It's possible to use the API with **E2EE** enabled but this means learning about how encryption works with Infisical and performing client-side encryption/decryption operations yourself. + yourself. + + If **E2EE** is a must for your team, we recommend either using one of the [Infisical SDKs](/documentation/getting-started/sdks) or checking out the [examples for E2EE](/api-reference/overview/examples/e2ee-disabled). + + +## Configuration + +Head to your Project Settings, where you created your service token, and un-check the **E2EE** setting. + +## Retrieve Secret + +Retrieve a secret from the project and environment in Infisical scoped to your service token by making a HTTP request with the following format/details: + +```bash +curl --location --request GET 'https://app.infisical.com/api/v3/secrets/raw/secretName?workspaceId=workspaceId&environment=environment' \ + --header 'Authorization: Bearer serviceToken' +``` + + + Name of secret to retrieve + + + The ID of the workspace + + + The environment slug + + + Path to secrets in workspace + + + The type of the secret. Valid options are โ€œsharedโ€ or โ€œpersonalโ€ + + +Depending on your application requirements, you may wish to use Infisical's API in different ways such as by retaining **E2EE** +or fetching multiple secrets at once instead of one at a time. + +Whatever the case, we recommend glossing over the [API Examples](/api-reference/overview/examples/note) +to gain a deeper understanding of how you to best leverage the Infisical API for your use-case. + +See also: + +- Explore the [API Examples](/api-reference/overview/examples/note) +- [API Reference](/api-reference/overview/introduction) \ No newline at end of file diff --git a/docs/documentation/getting-started/introduction.mdx b/docs/documentation/getting-started/introduction.mdx index 2db9f5284..46a2f48e3 100644 --- a/docs/documentation/getting-started/introduction.mdx +++ b/docs/documentation/getting-started/introduction.mdx @@ -42,6 +42,14 @@ Start syncing environment variables with [Infisical Cloud](https://app.infisical > Fetch and save secrets as native Kubernetes secrets + + Fetch secrets via HTTP request + ## Resources diff --git a/docs/documentation/platform/folder.mdx b/docs/documentation/platform/folder.mdx new file mode 100644 index 000000000..716cb4e46 --- /dev/null +++ b/docs/documentation/platform/folder.mdx @@ -0,0 +1,46 @@ +--- +title: "Folder" +description: "How Infisical structures secrets into folders" +--- + +Folders can be used to group secrets into multiple levels, which can help organize secrets in monorepos or microservice-based architectures. For example, you could create a folder for each environment, such as production, staging, and development. + +Within each environment folder, you could create subfolders for different types of secrets, such as database credentials, API keys, and SSH keys. This can help to keep your secrets organized and easy to find. + +## Dashboard + +![dashboard with folders](../../images/dashboard-folders.png) + +Only alphabets, numbers, and dashes are allowed in folder names. You can create a folder for each environment from the dashboard. + +![dashboard add folders](../../images/dashboard-add-folder.png) + +To create a nested folder or access the secrets of a folder, click on an existing folder to open it. You will then be able to modify the secrets of that folder and create new folders inside it. + +## Dashboard Secret Overview + +The overview screen provides a comprehensive view of all your secrets and folders, organized by environment. + +![dashboard secret overview with folders](../../images/dashboard-folder-overview.png) + +When you click on a folder, the overview will be updated to show only the secrets and folders in that folder. This makes it easy to find the information you need, no matter how deeply nested it is. + +## Integrations + +You can easily scope injected secrets to a folder during integrations by providing the secret path option. + +![integrations scoped with folders](../../images/integration-folders.png) + +For more information on integrations, [refer infisical integration](/integrations/overview) + +## Service Tokens + +You can scope the secrets that can be read and written using an Infisical token by providing the secret path option when creating the token. + +![folder scoped service token](../../images/project-folder-token.png) + +For more information, [refer infisical token section.](./token) + +## Point-In-Time Recovery + +For more information on how PIT recovery works on folders, [please refer to this section.](./pit-recovery) diff --git a/docs/documentation/platform/pit-recovery.mdx b/docs/documentation/platform/pit-recovery.mdx index eeba72d60..f741de8bb 100644 --- a/docs/documentation/platform/pit-recovery.mdx +++ b/docs/documentation/platform/pit-recovery.mdx @@ -22,3 +22,9 @@ Environment variables can be rolled back to any point in time via the "Rollback Rolling back environment variables to a past snapshot creates a new commit and snapshot at the top of the stack and updates secret versions. + +## Folders + +Any folder operation, such as creating, updating, or deleting a folder, will create a new commit. + +When you roll back the contents of a folder, the folder will be restored to its latest snapshot. The nested folders will also be restored to their respective latest versions. diff --git a/docs/documentation/platform/token.mdx b/docs/documentation/platform/token.mdx index caeb1641d..1fecb2fef 100644 --- a/docs/documentation/platform/token.mdx +++ b/docs/documentation/platform/token.mdx @@ -10,7 +10,7 @@ An Infisical Token is useful for: It's also useful for CI/CD environments and integrations such as [Docker](/integrations/platforms/docker) and [Docker Compose](/integrations/platforms/docker-compose). -To generate the the token, head over to your project settings as shown below. +To generate the the token, head over to your project settings as shown below. On creating a service token you can scope it to a path to limit the access. ![token add](../../images/project-token-add.png) diff --git a/docs/images/dashboard-add-folder.png b/docs/images/dashboard-add-folder.png new file mode 100644 index 000000000..6040dcc3f Binary files /dev/null and b/docs/images/dashboard-add-folder.png differ diff --git a/docs/images/dashboard-folder-overview.png b/docs/images/dashboard-folder-overview.png new file mode 100644 index 000000000..265f732fe Binary files /dev/null and b/docs/images/dashboard-folder-overview.png differ diff --git a/docs/images/dashboard-folders.png b/docs/images/dashboard-folders.png new file mode 100644 index 000000000..d3e92482f Binary files /dev/null and b/docs/images/dashboard-folders.png differ diff --git a/docs/images/integration-folders.png b/docs/images/integration-folders.png new file mode 100644 index 000000000..ea3af8b01 Binary files /dev/null and b/docs/images/integration-folders.png differ diff --git a/docs/images/integrations-cloudflare-auth.png b/docs/images/integrations-cloudflare-auth.png new file mode 100644 index 000000000..c714a21cc Binary files /dev/null and b/docs/images/integrations-cloudflare-auth.png differ diff --git a/docs/images/integrations-cloudflare-create.png b/docs/images/integrations-cloudflare-create.png new file mode 100644 index 000000000..d70533719 Binary files /dev/null and b/docs/images/integrations-cloudflare-create.png differ diff --git a/docs/images/integrations-cloudflare-credentials-1.png b/docs/images/integrations-cloudflare-credentials-1.png new file mode 100644 index 000000000..4035cfb92 Binary files /dev/null and b/docs/images/integrations-cloudflare-credentials-1.png differ diff --git a/docs/images/integrations-cloudflare-credentials-2.png b/docs/images/integrations-cloudflare-credentials-2.png new file mode 100644 index 000000000..c3794bdc9 Binary files /dev/null and b/docs/images/integrations-cloudflare-credentials-2.png differ diff --git a/docs/images/integrations-cloudflare-credentials-3.png b/docs/images/integrations-cloudflare-credentials-3.png new file mode 100644 index 000000000..0777fa1f7 Binary files /dev/null and b/docs/images/integrations-cloudflare-credentials-3.png differ diff --git a/docs/images/integrations-cloudflare-credentials-4.png b/docs/images/integrations-cloudflare-credentials-4.png new file mode 100644 index 000000000..957305ec7 Binary files /dev/null and b/docs/images/integrations-cloudflare-credentials-4.png differ diff --git a/docs/images/integrations-cloudflare.png b/docs/images/integrations-cloudflare.png new file mode 100644 index 000000000..69aefe6a4 Binary files /dev/null and b/docs/images/integrations-cloudflare.png differ diff --git a/docs/images/project-folder-token.png b/docs/images/project-folder-token.png new file mode 100644 index 000000000..4fda56d27 Binary files /dev/null and b/docs/images/project-folder-token.png differ diff --git a/docs/images/project-token-add.png b/docs/images/project-token-add.png index cd17b3027..f282399e6 100644 Binary files a/docs/images/project-token-add.png and b/docs/images/project-token-add.png differ diff --git a/docs/images/project-token-added.png b/docs/images/project-token-added.png index 302a86280..fb47f3525 100644 Binary files a/docs/images/project-token-added.png and b/docs/images/project-token-added.png differ diff --git a/docs/integrations/cloud/checkly.mdx b/docs/integrations/cloud/checkly.mdx index 54b2e7ac5..90c0850d3 100644 --- a/docs/integrations/cloud/checkly.mdx +++ b/docs/integrations/cloud/checkly.mdx @@ -31,7 +31,7 @@ Press on the Checkly tile and input your Checkly API Key to grant Infisical acce ## Start integration -Select which Infisical environment secrets you want to sync to Checkly press create integration to start syncing secrets. +Select which Infisical environment secrets you want to sync to Checkly and press create integration to start syncing secrets. ![integrations checkly](../../images/integrations-checkly-create.png) -![integrations checkly](../../images/integrations-checkly.png) \ No newline at end of file +![integrations checkly](../../images/integrations-checkly.png) diff --git a/docs/integrations/cloud/cloudflare-pages.mdx b/docs/integrations/cloud/cloudflare-pages.mdx new file mode 100644 index 000000000..4a95a006a --- /dev/null +++ b/docs/integrations/cloud/cloudflare-pages.mdx @@ -0,0 +1,44 @@ +--- +title: "Cloudflare Pages" +description: "How to sync secrets from Infisical to Cloudflare Pages" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Authorize Infisical for Cloudflare Pages + +Obtain a Cloudflare [API token](https://dash.cloudflare.com/profile/api-tokens) and [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/): + +1. Create a new [API token](https://dash.cloudflare.com/profile/api-tokens) in My Profile > API Tokens + +![integrations cloudflare credentials 1](../../images/integrations-cloudflare-credentials-1.png) +![integrations cloudflare credentials 2](../../images/integrations-cloudflare-credentials-2.png) +![integrations cloudflare credentials 3](../../images/integrations-cloudflare-credentials-3.png) + +2. Copy your [Account ID](https://developers.cloudflare.com/fundamentals/get-started/basic-tasks/find-account-and-zone-ids/) from Account > Workers & Pages > Overview + +![integrations cloudflare credentials 4](../../images/integrations-cloudflare-credentials-4.png) + +Press on the Cloudflare Pages tile and input your Cloudflare API token and account ID to grant Infisical access to your Cloudflare Pages. + +![integrations cloudflare authorization](../../images/integrations-cloudflare-auth.png) + + + If this is your project's first cloud integration, then you'll have to grant + Infisical access to your project's environment variables. Although this step + breaks E2EE, it's necessary for Infisical to sync the environment variables to + the cloud platform. + + +## Start integration + +Select which Infisical environment secrets you want to sync to Cloudflare and press create integration to start syncing secrets. + +![integrations cloudflare](../../images/integrations-cloudflare-create.png) +![integrations cloudflare](../../images/integrations-cloudflare.png) diff --git a/docs/integrations/frameworks/terraform.mdx b/docs/integrations/frameworks/terraform.mdx index 7161ec586..0826a833e 100644 --- a/docs/integrations/frameworks/terraform.mdx +++ b/docs/integrations/frameworks/terraform.mdx @@ -1,34 +1,91 @@ --- title: "Terraform" -description: "How to use Infisical to inject environment variables and secrets into terraform." +description: "Fetch Secrets From Infisical With Terraform" --- -Prerequisites: +This guide provides step-by-step guidance on how to fetch secrets from Infisical using Terraform. -- Set up and add envars to [Infisical Cloud](https://app.infisical.com) -- [Install the CLI](/cli/overview) +## Prerequisites -## Initialize Infisical for your [Terraform](https://www.terraform.io/) project +- Basic understanding of Terraform +- Install [Terraform](https://www.terraform.io/downloads.html) -```bash -# navigate to the root of your of your project -cd /path/to/project +## Steps -# then initialize Infisical -infisical init +### 1. Define Required Providers + +Specify `infisical` in the `required_providers` block within the `terraform` block of your configuration file. If you would like to use a specific version of the provider, uncomment and replace `` with the version of the Infisical provider that you want to use. + +```hcl main.tf +terraform { + required_providers { + infisical = { + # version = + source = "infisical/infisical" + } + } +} ``` -## Run terraform as usual but with Infisical +### 2. Configure the Infisical Provider -```bash -infisical run -- +Set up the Infisical provider by specifying the `host` and `service_token`. Replace `<>` in `service_token` with your actual token. The `host` is only required if you are using a self-hosted instance of Infisical. -# Example -infisical run -- terraform plan +```hcl main.tf +provider "infisical" { + host = "https://app.infisical.com" # Only required if using self hosted instance of Infisical, default is https://app.infisical.com + service_token = "<>" # Get token https://infisical.com/docs/documentation/platform/token +} ``` - - To inject any arbitrary variable to terraform, you have - to prefix them with `TF_VAR`. Read more about that - [here](https://developer.hashicorp.com/terraform/cli/config/environment-variables#tf_var_name). - + + It is recommended to use Terraform variables to pass your service token dynamically to avoid hard coding it + + +### 3. Fetch Infisical Secrets + +Use the `infisical_secrets` data source to fetch your secrets. This is defined with an empty block `{}` as the provider automatically fetches all secrets associated with your service token. + +```hcl main.tf +data "infisical_secrets" "my-secrets" {} +``` + +### 4. Define Outputs + +As an example, we are going to output your fetched secrets. Replace `SECRET-NAME` with the actual name of your secret. + +For a single secret: + +```hcl main.tf +output "single-secret" { + value = data.infisical_secrets.my-secrets.secrets["SECRET-NAME"] +} +``` + +For all secrets: + +```hcl +output "all-secrets" { + value = data.infisical_secrets.my-secrets.secrets +} +``` + +### 5. Run Terraform + +Once your configuration is complete, initialize your Terraform working directory: + +```bash +$ terraform init +``` + +Then, run the plan command to view the fetched secrets: + +```bash +$ terraform plan +``` + +Terraform will now fetch your secrets from Infisical and display them as output according to your configuration. + +## Conclusion + +You have now successfully set up and used the Infisical provider with Terraform to fetch secrets. For more information, visit the [Infisical documentation](https://registry.terraform.io/providers/Infisical/infisical/latest/docs). diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 79db492ea..352863eda 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -7,43 +7,44 @@ Integrations allow environment variables to be synced from Infisical into your l Missing an integration? [Throw in a request](https://github.com/Infisical/infisical/issues). -| Integration | Type | Status | -| -------------------------------------------------------------- | --------- | ----------- | -| [Docker](/integrations/platforms/docker) | Platform | Available | -| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available | -| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available | -| [Terraform](/integrations/frameworks/terraform) | Infrastructure as code | Available | -| [PM2](/integrations/platforms/pm2) | Platform | Available | -| [Heroku](/integrations/cloud/heroku) | Cloud | Available | -| [Vercel](/integrations/cloud/vercel) | Cloud | Available | -| [Netlify](/integrations/cloud/netlify) | Cloud | Available | -| [Render](/integrations/cloud/render) | Cloud | Available | -| [Railway](/integrations/cloud/railway) | Cloud | Available | -| [Fly.io](/integrations/cloud/flyio) | Cloud | Available | -| [Supabase](/integrations/cloud/supabase) | Cloud | Available | -| [Checkly](/integrations/cloud/checkly) | Cloud | Available | -| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available | -| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | -| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | -| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | -| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | -| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available | -| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available | -| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available | -| [React](/integrations/frameworks/react) | Framework | Available | -| [Vue](/integrations/frameworks/vue) | Framework | Available | -| [Express](/integrations/frameworks/express) | Framework | Available | -| [Next.js](/integrations/frameworks/nextjs) | Framework | Available | -| [NestJS](/integrations/frameworks/nestjs) | Framework | Available | -| [SvelteKit](/integrations/frameworks/sveltekit) | Framework | Available | -| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available | -| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available | -| [Remix](/integrations/frameworks/remix) | Framework | Available | -| [Vite](/integrations/frameworks/vite) | Framework | Available | -| [Fiber](/integrations/frameworks/fiber) | Framework | Available | -| [Django](/integrations/frameworks/django) | Framework | Available | -| [Flask](/integrations/frameworks/flask) | Framework | Available | -| [Laravel](/integrations/frameworks/laravel) | Framework | Available | -| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | -| GCP Secret Manager | Cloud | Coming soon | -| Jenkins | CI/CD | Coming soon | +| Integration | Type | Status | +| -------------------------------------------------------------- | ---------------------- | ----------- | +| [Docker](/integrations/platforms/docker) | Platform | Available | +| [Docker-Compose](/integrations/platforms/docker-compose) | Platform | Available | +| [Kubernetes](/integrations/platforms/kubernetes) | Platform | Available | +| [Terraform](/integrations/frameworks/terraform) | Infrastructure as code | Available | +| [PM2](/integrations/platforms/pm2) | Platform | Available | +| [Heroku](/integrations/cloud/heroku) | Cloud | Available | +| [Vercel](/integrations/cloud/vercel) | Cloud | Available | +| [Netlify](/integrations/cloud/netlify) | Cloud | Available | +| [Render](/integrations/cloud/render) | Cloud | Available | +| [Railway](/integrations/cloud/railway) | Cloud | Available | +| [Fly.io](/integrations/cloud/flyio) | Cloud | Available | +| [Supabase](/integrations/cloud/supabase) | Cloud | Available | +| [Cloudflare Pages](/integrations/cloud/cloudflare-pages) | Cloud | Available | +| [Checkly](/integrations/cloud/checkly) | Cloud | Available | +| [HashiCorp Vault](/integrations/cloud/hashicorp-vault) | Cloud | Available | +| [AWS Parameter Store](/integrations/cloud/aws-parameter-store) | Cloud | Available | +| [AWS Secret Manager](/integrations/cloud/aws-secret-manager) | Cloud | Available | +| [Azure Key Vault](/integrations/cloud/azure-key-vault) | Cloud | Available | +| [GitHub Actions](/integrations/cicd/githubactions) | CI/CD | Available | +| [GitLab](/integrations/cicd/gitlab) | CI/CD | Available | +| [CircleCI](/integrations/cicd/circleci) | CI/CD | Available | +| [Travis CI](/integrations/cicd/travisci) | CI/CD | Available | +| [React](/integrations/frameworks/react) | Framework | Available | +| [Vue](/integrations/frameworks/vue) | Framework | Available | +| [Express](/integrations/frameworks/express) | Framework | Available | +| [Next.js](/integrations/frameworks/nextjs) | Framework | Available | +| [NestJS](/integrations/frameworks/nestjs) | Framework | Available | +| [SvelteKit](/integrations/frameworks/sveltekit) | Framework | Available | +| [Nuxt](/integrations/frameworks/nuxt) | Framework | Available | +| [Gatsby](/integrations/frameworks/gatsby) | Framework | Available | +| [Remix](/integrations/frameworks/remix) | Framework | Available | +| [Vite](/integrations/frameworks/vite) | Framework | Available | +| [Fiber](/integrations/frameworks/fiber) | Framework | Available | +| [Django](/integrations/frameworks/django) | Framework | Available | +| [Flask](/integrations/frameworks/flask) | Framework | Available | +| [Laravel](/integrations/frameworks/laravel) | Framework | Available | +| [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | +| GCP Secret Manager | Cloud | Coming soon | +| Jenkins | CI/CD | Coming soon | diff --git a/docs/mint.json b/docs/mint.json index 553db5172..d687af107 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -22,10 +22,7 @@ } }, "api": { - "baseUrl": [ - "https://app.infisical.com", - "http://localhost:8080" - ], + "baseUrl": ["https://app.infisical.com", "http://localhost:8080"], "auth": { "method": "key", "name": "X-API-KEY" @@ -92,7 +89,8 @@ "documentation/getting-started/sdks", "documentation/getting-started/cli", "documentation/getting-started/docker", - "documentation/getting-started/kubernetes" + "documentation/getting-started/kubernetes", + "documentation/getting-started/api" ] }, { @@ -111,6 +109,7 @@ "pages": [ "documentation/platform/organization", "documentation/platform/project", + "documentation/platform/folder", "documentation/platform/pit-recovery", "documentation/platform/secret-versioning", "documentation/platform/audit-logs", @@ -197,6 +196,7 @@ "integrations/cloud/railway", "integrations/cloud/flyio", "integrations/cloud/supabase", + "integrations/cloud/cloudflare-pages", "integrations/cloud/checkly", "integrations/cloud/hashicorp-vault", "integrations/cloud/azure-key-vault", @@ -226,9 +226,7 @@ }, { "group": "Overview", - "pages": [ - "sdks/overview" - ] + "pages": ["sdks/overview"] }, { "group": "SDKs", @@ -247,17 +245,15 @@ "pages": [ "api-reference/overview/introduction", "api-reference/overview/authentication", - "api-reference/overview/blind-indices", { "group": "Examples", "pages": [ - "api-reference/overview/examples/retrieve-secrets", - "api-reference/overview/examples/create-secret", - "api-reference/overview/examples/retrieve-secret", - "api-reference/overview/examples/update-secret", - "api-reference/overview/examples/delete-secret" + "api-reference/overview/examples/note", + "api-reference/overview/examples/e2ee-disabled", + "api-reference/overview/examples/e2ee-enabled" ] - } + }, + "api-reference/overview/blind-indices" ] }, { @@ -305,9 +301,7 @@ }, { "group": "Service Tokens", - "pages": [ - "api-reference/endpoints/service-tokens/get" - ] + "pages": ["api-reference/endpoints/service-tokens/get"] } ] }, @@ -321,9 +315,7 @@ }, { "group": "Overview", - "pages": [ - "changelog/overview" - ] + "pages": ["changelog/overview"] }, { "group": "Contributing", diff --git a/docs/spec.yaml b/docs/spec.yaml index 89f01e8da..799c7f6f4 100644 --- a/docs/spec.yaml +++ b/docs/spec.yaml @@ -47,8 +47,6 @@ paths: description: Secret versions '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v1/secret/{secretId}/secret-versions/rollback: post: summary: Roll back secret to a version. @@ -74,8 +72,6 @@ paths: description: Secret rolled back to '400': description: Bad Request - security: - - apiKeyAuth: [] requestBody: required: true content: @@ -138,8 +134,6 @@ paths: description: Project secret snapshots '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v1/workspace/{workspaceId}/secret-snapshots/count: get: description: '' @@ -184,8 +178,6 @@ paths: description: Secrets rolled back to '400': description: Bad Request - security: - - apiKeyAuth: [] requestBody: required: true content: @@ -255,8 +247,6 @@ paths: description: Project logs '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v1/action/{actionId}: get: description: '' @@ -1677,8 +1667,6 @@ paths: description: Current user on request '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v2/users/me/mfa: patch: description: '' @@ -1716,8 +1704,6 @@ paths: description: Organizations that user is part of '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v2/organizations/{organizationId}/memberships: get: summary: Return organization memberships @@ -1744,8 +1730,6 @@ paths: description: Memberships of organization '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v2/organizations/{organizationId}/memberships/{membershipId}: patch: summary: Update organization membership @@ -1776,8 +1760,6 @@ paths: description: Updated organization membership '400': description: Bad Request - security: - - apiKeyAuth: [] requestBody: required: true content: @@ -1819,8 +1801,6 @@ paths: description: Deleted organization membership '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v2/organizations/{organizationId}/workspaces: get: summary: Return projects in organization that user is part of @@ -1845,8 +1825,6 @@ paths: items: $ref: '#/components/schemas/Project' description: Projects of organization - security: - - apiKeyAuth: [] /api/v2/organizations/{organizationId}/service-accounts: get: description: '' @@ -2057,8 +2035,6 @@ paths: description: Encrypted project key for the given project '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/service-token-data: get: description: '' @@ -2099,8 +2075,6 @@ paths: description: Memberships of project '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/memberships/{membershipId}: patch: summary: Update project membership @@ -2131,8 +2105,6 @@ paths: description: Updated membership '400': description: Bad Request - security: - - apiKeyAuth: [] requestBody: required: true content: @@ -2172,8 +2144,6 @@ paths: description: Deleted membership '400': description: Bad Request - security: - - apiKeyAuth: [] /api/v2/workspace/{workspaceId}/auto-capitalization: patch: description: '' @@ -2407,8 +2377,6 @@ paths: description: >- Newly-created secrets for the given project and environment - security: - - apiKeyAuth: [] requestBody: required: true content: @@ -2462,8 +2430,6 @@ paths: items: $ref: '#/components/schemas/Secret' description: Secrets for the given project and environment - security: - - apiKeyAuth: [] patch: summary: Update secret(s) description: Update secret(s) @@ -2481,8 +2447,6 @@ paths: items: $ref: '#/components/schemas/Secret' description: Updated secrets - security: - - apiKeyAuth: [] requestBody: required: true content: @@ -2514,8 +2478,6 @@ paths: items: $ref: '#/components/schemas/Secret' description: Deleted secrets - security: - - apiKeyAuth: [] requestBody: required: true content: diff --git a/frontend/.eslintrc.js b/frontend/.eslintrc.js index 155e6d6e8..e9cec0e24 100644 --- a/frontend/.eslintrc.js +++ b/frontend/.eslintrc.js @@ -10,66 +10,68 @@ module.exports = { es2021: true }, extends: [ - 'airbnb', - 'airbnb-typescript', - 'airbnb/hooks', - 'plugin:react/recommended', - 'prettier', - 'plugin:storybook/recommended' + "airbnb", + "airbnb-typescript", + "airbnb/hooks", + "plugin:react/recommended", + "prettier", + "plugin:storybook/recommended" ], parserOptions: { - ecmaVersion: 'latest', - sourceType: 'module', - project: './tsconfig.json', + ecmaVersion: "latest", + sourceType: "module", + project: "./tsconfig.json", ecmaFeatures: { jsx: true }, tsconfigRootDir: __dirname }, - plugins: ['react', 'prettier', 'simple-import-sort', 'import'], + plugins: ["react", "prettier", "simple-import-sort", "import"], rules: { - 'react/react-in-jsx-scope': 'off', - 'import/prefer-default-export': 'off', - 'react-hooks/exhaustive-deps': 'off', - '@typescript-eslint/ban-ts-comment': 'warn', - 'react/jsx-props-no-spreading': 'off', // switched off for component building + quotes: ["error", "double", { avoidEscape: true }], + "comma-dangle": ["error", "only-multiline"], + "react/react-in-jsx-scope": "off", + "import/prefer-default-export": "off", + "react-hooks/exhaustive-deps": "off", + "@typescript-eslint/ban-ts-comment": "warn", + "react/jsx-props-no-spreading": "off", // switched off for component building // TODO: This rule will be switched ON after complete revamp of frontend - '@typescript-eslint/no-explicit-any': 'off', - 'no-console': 'off', - 'arrow-body-style': 'off', - 'no-underscore-dangle': [ - 'error', + "@typescript-eslint/no-explicit-any": "off", + "no-console": "off", + "arrow-body-style": "off", + "no-underscore-dangle": [ + "error", { - allow: ['_id'] + allow: ["_id"] } ], - 'jsx-a11y/anchor-is-valid': 'off', + "jsx-a11y/anchor-is-valid": "off", // all those tags must be converted to label or a p component // - 'react/require-default-props': 'off', - 'react/jsx-filename-extension': [ + "react/require-default-props": "off", + "react/jsx-filename-extension": [ 1, { - extensions: ['.tsx', '.ts'] + extensions: [".tsx", ".ts"] } ], // TODO: turn this rule ON after migration. everything should use arrow functions - 'react/function-component-definition': [ + "react/function-component-definition": [ 0, { - namedComponents: 'arrow-function' + namedComponents: "arrow-function" } ], - 'react/no-unknown-property': [ - 'error', + "react/no-unknown-property": [ + "error", { - ignore: ['jsx'] + ignore: ["jsx"] } ], - '@typescript-eslint/no-non-null-assertion': 'off', - 'simple-import-sort/exports': 'warn', - 'simple-import-sort/imports': [ - 'warn', + "@typescript-eslint/no-non-null-assertion": "off", + "simple-import-sort/exports": "warn", + "simple-import-sort/imports": [ + "warn", { groups: [ // Node.js builtins. You could also generate this regex if you use a `.js` config. @@ -77,26 +79,26 @@ module.exports = { // Note that if you use the `node:` prefix for Node.js builtins, // you can avoid this complexity: You can simply use "^node:". [ - '^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)' + "^(assert|buffer|child_process|cluster|console|constants|crypto|dgram|dns|domain|events|fs|http|https|module|net|os|path|punycode|querystring|readline|repl|stream|string_decoder|sys|timers|tls|tty|url|util|vm|zlib|freelist|v8|process|async_hooks|http2|perf_hooks)(/.*|$)" ], // Packages `react` related packages - ['^react', '^next', '^@?\\w'], - ['^@app'], + ["^react", "^next", "^@?\\w"], + ["^@app"], // Internal packages. - ['^~(/.*|$)'], + ["^~(/.*|$)"], // Relative imports - ['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'], + ["^\\.\\.(?!/?$)", "^\\.\\./?$", "^\\./(?=.*/)(?!/?$)", "^\\.(?!/?$)", "^\\./?$"], // Style imports. - ['^.+\\.?(css|scss)$'] + ["^.+\\.?(css|scss)$"] ] } ] }, - ignorePatterns: ['next.config.js'], + ignorePatterns: ["next.config.js"], settings: { - 'import/resolver': { + "import/resolver": { typescript: { - project: ['./tsconfig.json'] + project: ["./tsconfig.json"] } } } diff --git a/frontend/.prettierrc b/frontend/.prettierrc index da5d26a53..0b8ef54d2 100644 --- a/frontend/.prettierrc +++ b/frontend/.prettierrc @@ -1,5 +1,5 @@ { - "singleQuote": true, + "singleQuote": false, "printWidth": 100, "trailingComma": "none", "tabWidth": 2, diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 520f0fb7f..b33d63b4f 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -28,6 +28,8 @@ ARG POSTHOG_HOST ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST ARG POSTHOG_API_KEY ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY +ARG INTERCOM_ID +ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID # Build RUN npm run build @@ -46,6 +48,9 @@ VOLUME /app/.next/cache/images ARG POSTHOG_API_KEY ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \ BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY +ARG INTERCOM_ID +ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \ + BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID COPY --chown=nextjs:nodejs --chmod=555 scripts ./scripts COPY --from=builder /app/public ./public diff --git a/frontend/next.config.js b/frontend/next.config.js index 0fc0bec3e..b133818bd 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -7,14 +7,14 @@ const path = require('path'); const ContentSecurityPolicy = ` default-src 'self'; - script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com 'unsafe-inline' 'unsafe-eval'; + script-src 'self' https://app.posthog.com https://js.stripe.com https://api.stripe.com https://widget.intercom.io https://js.intercomcdn.com 'unsafe-inline' 'unsafe-eval'; style-src 'self' https://rsms.me 'unsafe-inline'; child-src https://api.stripe.com; - frame-src https://js.stripe.com/ https://api.stripe.com; - connect-src 'self' https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com http://localhost:*; - img-src 'self' https://*.stripe.com https://i.ytimg.com/ data:; - media-src; - font-src 'self' https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com; + frame-src https://js.stripe.com/ https://api.stripe.com https://www.youtube.com/; + connect-src 'self' wss://nexus-websocket-a.intercom.io https://api-iam.intercom.io https://api.heroku.com/ https://id.heroku.com/oauth/authorize https://id.heroku.com/oauth/token https://checkout.stripe.com https://app.posthog.com https://api.stripe.com http://localhost:*; + img-src 'self' https://static.intercomassets.com https://js.intercomcdn.com https://downloads.intercomcdn.com https://*.stripe.com https://i.ytimg.com/ data:; + media-src https://js.intercomcdn.com; + font-src 'self' https://fonts.intercomcdn.com/ https://maxcdn.bootstrapcdn.com https://rsms.me https://fonts.gstatic.com; `; // You can choose which headers to add to the list diff --git a/frontend/package.json b/frontend/package.json index 9d5071736..efe833849 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,7 +6,8 @@ "build": "next build", "start": "next start", "start:docker": "next build && next start", - "lint": "eslint --fix --ext js,ts,tsx ./src", + "lint": "eslint --ext js,ts,tsx ./src", + "lint-and-fix": "eslint --fix --ext js,ts,tsx ./src", "type-check": "tsc --project tsconfig.json", "storybook": "storybook dev -p 6006 -s ./public", "build-storybook": "storybook build" diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index e873f1a72..480061e1a 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -18,7 +18,8 @@ const integrationSlugNameMapping: Mapping = { 'travisci': 'TravisCI', 'supabase': 'Supabase', 'checkly': 'Checkly', - 'hashicorp-vault': 'Vault' + 'hashicorp-vault': 'Vault', + 'cloudflare-pages': 'Cloudflare Pages' } const envMapping: Mapping = { diff --git a/frontend/public/images/integrations/Cloudflare.png b/frontend/public/images/integrations/Cloudflare.png new file mode 100644 index 000000000..24db8c681 Binary files /dev/null and b/frontend/public/images/integrations/Cloudflare.png differ diff --git a/frontend/public/images/integrations/Terraform.png b/frontend/public/images/integrations/Terraform.png new file mode 100644 index 000000000..166edd32a Binary files /dev/null and b/frontend/public/images/integrations/Terraform.png differ diff --git a/frontend/public/json/frameworkIntegrations.json b/frontend/public/json/frameworkIntegrations.json index 32863bc07..1a6ff94f2 100644 --- a/frontend/public/json/frameworkIntegrations.json +++ b/frontend/public/json/frameworkIntegrations.json @@ -17,6 +17,12 @@ "image": "Kubernetes", "docsLink": "https://infisical.com/docs/integrations/platforms/kubernetes" }, + { + "name": "Terraform", + "slug": "terraform", + "image": "Terraform", + "docsLink": "https://infisical.com/docs/integrations/frameworks/terraform" + }, { "name": "React", "slug": "react", diff --git a/frontend/scripts/start.sh b/frontend/scripts/start.sh index fea97eb6b..1db867e15 100644 --- a/frontend/scripts/start.sh +++ b/frontend/scripts/start.sh @@ -2,6 +2,8 @@ scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_POSTHOG_API_KEY" "$NEXT_PUBLIC_POSTHOG_API_KEY" +scripts/replace-variable.sh "$BAKED_NEXT_PUBLIC_INTERCOM_ID" "$NEXT_PUBLIC_INTERCOM_ID" + if [ "$TELEMETRY_ENABLED" != "false" ]; then echo "Telemetry is enabled" scripts/set-telemetry.sh true diff --git a/frontend/src/components/RouteGuard.tsx b/frontend/src/components/RouteGuard.tsx index 3b755341e..5c874541a 100644 --- a/frontend/src/components/RouteGuard.tsx +++ b/frontend/src/components/RouteGuard.tsx @@ -1,8 +1,8 @@ -import { ReactNode, useEffect, useState } from 'react'; -import { useRouter } from 'next/router'; +import { ReactNode, useEffect, useState } from "react"; +import { useRouter } from "next/router"; -import { publicPaths } from '@app/const'; -import checkAuth from '@app/pages/api/auth/CheckAuth'; +import { publicPaths } from "@app/const"; +import checkAuth from "@app/pages/api/auth/CheckAuth"; // #TODO: finish spinner only when the data loads fully // #TODO: Redirect somewhere if the page does not exist @@ -21,7 +21,7 @@ export default function RouteGuard({ children }: Prop): JSX.Element { */ async function authCheck(url: string) { // Make sure that we don't redirect when the user is on the following pages. - const path = `/${url.split('?')[0].split('/')[1]}`; + const path = `/${url.split("?")[0].split("/")[1]}`; // Check if the user is authenticated const response = await checkAuth(); @@ -30,15 +30,15 @@ export default function RouteGuard({ children }: Prop): JSX.Element { if (!publicPaths.includes(path)) { try { if (response.status !== 200) { - router.push('/login'); - console.log('Unauthorized to access.'); + router.push("/login"); + console.log("Unauthorized to access."); setAuthorized(false); } else { setAuthorized(true); - console.log('Authorized to access.'); + console.log("Authorized to access."); } } catch (error) { - console.log('Error (probably the authCheck route is stuck again...):', error); + console.log("Error (probably the authCheck route is stuck again...):", error); } } } @@ -53,16 +53,16 @@ export default function RouteGuard({ children }: Prop): JSX.Element { // #TODO: add the loading page when not yet authorized. const hideContent = () => setAuthorized(false); // const onError = () => setAuthorized(true) - router.events.on('routeChangeStart', hideContent); + router.events.on("routeChangeStart", hideContent); // router.events.on("routeChangeError", onError); // on route change complete - run auth check - router.events.on('routeChangeComplete', authCheck); + router.events.on("routeChangeComplete", authCheck); // unsubscribe from events in useEffect return function return () => { - router.events.off('routeChangeStart', hideContent); - router.events.off('routeChangeComplete', authCheck); + router.events.off("routeChangeStart", hideContent); + router.events.off("routeChangeComplete", authCheck); // router.events.off("routeChangeError", onError); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index b6f3e4341..706f79515 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -1,16 +1,16 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ /* eslint-disable no-undef */ -import posthog from 'posthog-js'; +import posthog from "posthog-js"; -import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config'; +import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from "../utilities/config"; export const initPostHog = () => { // @ts-ignore console.log("Hi there ๐Ÿ‘‹") try { - if (typeof window !== 'undefined') { + if (typeof window !== "undefined") { // @ts-ignore - if (ENV === 'production' && TELEMETRY_CAPTURING_ENABLED) { + if (ENV === "production" && TELEMETRY_CAPTURING_ENABLED) { posthog.init(POSTHOG_API_KEY, { api_host: POSTHOG_HOST }); diff --git a/frontend/src/components/basic/Error.tsx b/frontend/src/components/basic/Error.tsx index ab79fb24d..bf892e03d 100644 --- a/frontend/src/components/basic/Error.tsx +++ b/frontend/src/components/basic/Error.tsx @@ -1,5 +1,5 @@ -import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; const Error = ({ text }: { text: string }): JSX.Element => { return ( diff --git a/frontend/src/components/basic/EventFilter.tsx b/frontend/src/components/basic/EventFilter.tsx index 7c3a9dc25..46b2200f6 100644 --- a/frontend/src/components/basic/EventFilter.tsx +++ b/frontend/src/components/basic/EventFilter.tsx @@ -1,5 +1,5 @@ -import React, { Fragment } from 'react'; -import { useTranslation } from 'react-i18next'; +import React, { Fragment } from "react"; +import { useTranslation } from "react-i18next"; import { faAngleDown, faEye, @@ -7,9 +7,9 @@ import { faShuffle, faTrash, faX -} from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Listbox, Transition } from '@headlessui/react'; +} from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Listbox, Transition } from "@headlessui/react"; interface ListBoxProps { selected: string; @@ -18,19 +18,19 @@ interface ListBoxProps { const eventOptions = [ { - name: 'addSecrets', + name: "addSecrets", icon: faPlus }, { - name: 'readSecrets', + name: "readSecrets", icon: faEye }, { - name: 'updateSecrets', + name: "updateSecrets", icon: faShuffle }, { - name: 'deleteSecrets', + name: "deleteSecrets", icon: faTrash } ]; @@ -48,13 +48,13 @@ const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => {
- {selected !== '' ? ( + {selected !== "" ? (

{t(`activity.event.${selected}`)}

) : ( -

{String(t('common.select-event'))}

+

{String(t("common.select-event"))}

)} - {selected !== '' ? ( - select('')} /> + {selected !== "" ? ( + select("")} /> ) : ( )} @@ -70,15 +70,15 @@ const EventFilter = ({ selected, select }: ListBoxProps): JSX.Element => { {({ selected: isSelected }) => ( - {' '} + {" "} {t(`activity.event.${event.name}`)} )} diff --git a/frontend/src/components/basic/InputField.tsx b/frontend/src/components/basic/InputField.tsx index df6dd1424..0bc01defc 100644 --- a/frontend/src/components/basic/InputField.tsx +++ b/frontend/src/components/basic/InputField.tsx @@ -1,8 +1,8 @@ -import { memo, useState } from 'react'; -import { faCircle, faEye, faEyeSlash } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { memo, useState } from "react"; +import { faCircle, faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; -import guidGenerator from '../utilities/randomId'; +import guidGenerator from "../utilities/randomId"; interface InputFieldProps { isStatic?: boolean; @@ -34,7 +34,7 @@ const InputField = ({ placeholder, isStatic, text -}: InputFieldProps & Pick) => { +}: InputFieldProps & Pick) => { const [passwordVisible, setPasswordVisible] = useState(false); if (isStatic === true) { @@ -64,28 +64,28 @@ const InputField = ({
onChangeHandler(e.target.value)} - type={passwordVisible === false ? type : 'text'} + type={passwordVisible === false ? type : "text"} placeholder={placeholder} value={value} required={isRequired} className={`${ blurred - ? 'text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400' - : '' + ? "text-bunker-800 group-hover:text-gray-400 focus:text-gray-400 active:text-gray-400" + : "" } ${ - error ? 'focus:ring-red/50' : 'focus:ring-primary/50' + error ? "focus:ring-red/50" : "focus:ring-primary/50" } relative peer bg-mineshaft-900 rounded-md text-gray-400 text-md p-2 w-full min-w-16 outline-none focus:ring-4 duration-200`} name={name} spellCheck="false" autoComplete={autoComplete} id={id} /> - {label?.includes('Password') && ( + {label?.includes("Password") && (
diff --git a/frontend/src/components/basic/dialog/AddApiKeyDialog.tsx b/frontend/src/components/basic/dialog/AddApiKeyDialog.tsx index 5c2e26246..eeb0ca6b1 100644 --- a/frontend/src/components/basic/dialog/AddApiKeyDialog.tsx +++ b/frontend/src/components/basic/dialog/AddApiKeyDialog.tsx @@ -1,21 +1,21 @@ -import { Fragment, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { faCheck, faCopy } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { Dialog, Transition } from '@headlessui/react'; +import { Fragment, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { faCheck, faCopy } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Dialog, Transition } from "@headlessui/react"; -import addAPIKey from '@app/pages/api/apiKey/addAPIKey'; +import addAPIKey from "@app/pages/api/apiKey/addAPIKey"; -import Button from '../buttons/Button'; -import InputField from '../InputField'; -import ListBox from '../Listbox'; +import Button from "../buttons/Button"; +import InputField from "../InputField"; +import ListBox from "../Listbox"; const expiryMapping = { - '1 day': 86400, - '7 days': 604800, - '1 month': 2592000, - '6 months': 15552000, - '12 months': 31104000 + "1 day": 86400, + "7 days": 604800, + "1 month": 2592000, + "6 months": 15552000, + "12 months": 31104000 }; type Props = { @@ -28,9 +28,9 @@ type Props = { // TODO: convert to TS const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => { - const [apiKey, setApiKey] = useState(''); - const [apiKeyName, setApiKeyName] = useState(''); - const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState('1 day'); + const [apiKey, setApiKey] = useState(""); + const [apiKeyName, setApiKeyName] = useState(""); + const [apiKeyExpiresIn, setApiKeyExpiresIn] = useState("1 day"); const [apiKeyCopied, setApiKeyCopied] = useState(false); const { t } = useTranslation(); @@ -46,7 +46,7 @@ const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => function copyToClipboard() { // Get the text field - const copyText = document.getElementById('apiKey') as HTMLInputElement; + const copyText = document.getElementById("apiKey") as HTMLInputElement; // Select the text field copyText.select(); @@ -63,8 +63,8 @@ const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => const closeAddApiKeyModal = () => { closeModal(); - setApiKeyName(''); - setApiKey(''); + setApiKeyName(""); + setApiKey(""); }; return ( @@ -94,24 +94,24 @@ const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => leaveFrom="opacity-100 scale-100" leaveTo="opacity-0 scale-95" > - {apiKey === '' ? ( + {apiKey === "" ? ( - {t('section.api-key.add-dialog.title')} + {t("section.api-key.add-dialog.title")}

- {t('section.api-key.add-dialog.description')} + {t("section.api-key.add-dialog.description")}

@@ -133,10 +133,10 @@ const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) =>
@@ -147,12 +147,12 @@ const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => as="h3" className="z-50 text-lg font-medium leading-6 text-gray-400" > - {t('section.api-key.add-dialog.copy-service-token')} + {t("section.api-key.add-dialog.copy-service-token")}

- {t('section.api-key.add-dialog.copy-service-token-description')} + {t("section.api-key.add-dialog.copy-service-token-description")}

@@ -181,7 +181,7 @@ const AddApiKeyDialog = ({ isOpen, closeModal, apiKeys, setApiKeys }: Props) => )} - {t('common.click-to-copy')} + {t("common.click-to-copy")} diff --git a/frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx b/frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx index 63535f2be..e3e8acaa1 100644 --- a/frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx +++ b/frontend/src/components/basic/dialog/AddIncidentContactDialog.tsx @@ -1,11 +1,11 @@ -import { Fragment, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { Dialog, Transition } from '@headlessui/react'; +import { Fragment, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Dialog, Transition } from "@headlessui/react"; -import addIncidentContact from '@app/pages/api/organization/addIncidentContact'; +import addIncidentContact from "@app/pages/api/organization/addIncidentContact"; -import Button from '../buttons/Button'; -import InputField from '../InputField'; +import Button from "../buttons/Button"; +import InputField from "../InputField"; type Props = { isOpen: boolean; @@ -20,7 +20,7 @@ const AddIncidentContactDialog = ({ incidentContacts, setIncidentContacts }: Props) => { - const [incidentContactEmail, setIncidentContactEmail] = useState(''); + const [incidentContactEmail, setIncidentContactEmail] = useState(""); const { t } = useTranslation(); const submit = () => { @@ -29,7 +29,7 @@ const AddIncidentContactDialog = ({ ? incidentContacts.concat([incidentContactEmail]) : [incidentContactEmail] ); - addIncidentContact(localStorage.getItem('orgData.id') as string, incidentContactEmail); + addIncidentContact(localStorage.getItem("orgData.id") as string, incidentContactEmail); closeModal(); }; return ( @@ -61,16 +61,16 @@ const AddIncidentContactDialog = ({ > - {t('section.incident.add-dialog.title')} + {t("section.incident.add-dialog.title")}

- {t('section.incident.add-dialog.description')} + {t("section.incident.add-dialog.description")}

diff --git a/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx b/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx index aceeb9db9..2a25c8be6 100644 --- a/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx +++ b/frontend/src/components/basic/dialog/AddProjectMemberDialog.tsx @@ -1,10 +1,10 @@ -import { Fragment } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { useRouter } from 'next/router'; -import { Dialog, Transition } from '@headlessui/react'; +import { Fragment } from "react"; +import { Trans, useTranslation } from "react-i18next"; +import { useRouter } from "next/router"; +import { Dialog, Transition } from "@headlessui/react"; -import Button from '../buttons/Button'; -import ListBox from '../Listbox'; +import Button from "../buttons/Button"; +import ListBox from "../Listbox"; type Props = { isOpen: boolean; @@ -59,21 +59,21 @@ const AddProjectMemberDialog = ({ as="h3" className="z-50 text-lg font-medium leading-6 text-gray-400" > - {t('section.members.add-dialog.add-member-to-project')} + {t("section.members.add-dialog.add-member-to-project")} ) : ( - {t('section.members.add-dialog.already-all-invited')} + {t("section.members.add-dialog.already-all-invited")} )}
{data?.length > 0 ? (

- {t('section.members.add-dialog.user-will-email')} + {t("section.members.add-dialog.user-will-email")}

) : (

- {t('section.members.add-dialog.add-user-org-first')} + {t("section.members.add-dialog.add-user-org-first")}

)}
@@ -121,7 +121,7 @@ const AddProjectMemberDialog = ({
@@ -129,7 +129,7 @@ const AddProjectMemberDialog = ({
@@ -211,12 +211,12 @@ const AddServiceTokenDialog = ({ as="h3" className="z-50 text-lg font-medium leading-6 text-gray-400" > - {t('section.token.add-dialog.copy-service-token')} + {t("section.token.add-dialog.copy-service-token")}

- {t('section.token.add-dialog.copy-service-token-description')} + {t("section.token.add-dialog.copy-service-token-description")}

@@ -244,7 +244,7 @@ const AddServiceTokenDialog = ({ )} - {t('common.click-to-copy')} + {t("common.click-to-copy")} diff --git a/frontend/src/components/basic/dialog/AddUpdateEnvironmentDialog.tsx b/frontend/src/components/basic/dialog/AddUpdateEnvironmentDialog.tsx index 70e9b0f47..de5337ae6 100644 --- a/frontend/src/components/basic/dialog/AddUpdateEnvironmentDialog.tsx +++ b/frontend/src/components/basic/dialog/AddUpdateEnvironmentDialog.tsx @@ -1,8 +1,8 @@ -import { FormEventHandler, Fragment, useEffect, useState } from 'react'; -import { Dialog, Transition } from '@headlessui/react'; +import { FormEventHandler, Fragment, useEffect, useState } from "react"; +import { Dialog, Transition } from "@headlessui/react"; -import Button from '../buttons/Button'; -import InputField from '../InputField'; +import Button from "../buttons/Button"; +import InputField from "../InputField"; type FormFields = { name: string; slug: string }; @@ -31,14 +31,14 @@ export const AddUpdateEnvironmentDialog = ({ isEditMode, }: Props) => { const [formInput, setFormInput] = useState({ - name: '', - slug: '', + name: "", + slug: "", }); // This use effect can be removed when the unmount is happening from outside the component // When unmount happens outside state gets unmounted also useEffect(() => { - setFormInput(initialValues || { name: '', slug: '' }); + setFormInput(initialValues || { name: "", slug: "" }); }, [isOpen]); // REFACTOR: Move to react-hook-form with yup for better form management @@ -92,14 +92,14 @@ export const AddUpdateEnvironmentDialog = ({ className='text-lg font-medium leading-6 text-gray-400' > {isEditMode - ? 'Update environment' - : 'Create a new environment'} + ? "Update environment" + : "Create a new environment"}
onInputChange('name', val)} + onChangeHandler={(val) => onInputChange("name", val)} type='varName' value={formInput.name} placeholder='' @@ -111,7 +111,7 @@ export const AddUpdateEnvironmentDialog = ({
onInputChange('slug', val)} + onChangeHandler={(val) => onInputChange("slug", val)} type='varName' value={formInput.slug} placeholder='' @@ -128,8 +128,8 @@ export const AddUpdateEnvironmentDialog = ({ onButtonPressed={() => null} type='submit' color='mineshaft' - text={isEditMode ? 'Update' : 'Create'} - active={formInput.name !== '' && formInput.slug !== ''} + text={isEditMode ? "Update" : "Create"} + active={formInput.name !== "" && formInput.slug !== ""} size='md' />
diff --git a/frontend/src/components/basic/dialog/AddUserDialog.tsx b/frontend/src/components/basic/dialog/AddUserDialog.tsx index 68ce24696..b36c31d6c 100644 --- a/frontend/src/components/basic/dialog/AddUserDialog.tsx +++ b/frontend/src/components/basic/dialog/AddUserDialog.tsx @@ -1,8 +1,8 @@ -import { Fragment } from 'react'; -import { Dialog, Transition } from '@headlessui/react'; +import { Fragment } from "react"; +import { Dialog, Transition } from "@headlessui/react"; -import Button from '../buttons/Button'; -import InputField from '../InputField'; +import Button from "../buttons/Button"; +import InputField from "../InputField"; type Props = { isOpen: boolean; diff --git a/frontend/src/components/basic/dialog/DeleteActionModal.tsx b/frontend/src/components/basic/dialog/DeleteActionModal.tsx index 05fd66a71..712946679 100644 --- a/frontend/src/components/basic/dialog/DeleteActionModal.tsx +++ b/frontend/src/components/basic/dialog/DeleteActionModal.tsx @@ -1,7 +1,7 @@ -import { Fragment, useEffect, useState } from 'react'; -import { Dialog, Transition } from '@headlessui/react'; +import { Fragment, useEffect, useState } from "react"; +import { Dialog, Transition } from "@headlessui/react"; -import InputField from '../InputField'; +import InputField from "../InputField"; // REFACTOR: Move all these modals into one reusable one type Props = { diff --git a/frontend/src/components/basic/dialog/DeleteEnvVar.tsx b/frontend/src/components/basic/dialog/DeleteEnvVar.tsx index 6daed29fd..d2bcf5505 100644 --- a/frontend/src/components/basic/dialog/DeleteEnvVar.tsx +++ b/frontend/src/components/basic/dialog/DeleteEnvVar.tsx @@ -47,11 +47,11 @@ export const DeleteEnvVar = ({ isOpen, onClose, onSubmit }: Props) => { > - {t('dashboard:sidebar.delete-key-dialog.title')} + {t("dashboard:sidebar.delete-key-dialog.title")}

- {t('dashboard:sidebar.delete-key-dialog.confirm-delete-message')} + {t("dashboard:sidebar.delete-key-dialog.confirm-delete-message")}

diff --git a/frontend/src/components/basic/dialog/DeleteUserDialog.tsx b/frontend/src/components/basic/dialog/DeleteUserDialog.tsx index 883e241c0..9b6ecf012 100644 --- a/frontend/src/components/basic/dialog/DeleteUserDialog.tsx +++ b/frontend/src/components/basic/dialog/DeleteUserDialog.tsx @@ -1,5 +1,5 @@ -import { Fragment } from 'react'; -import { Dialog, Transition } from '@headlessui/react'; +import { Fragment } from "react"; +import { Dialog, Transition } from "@headlessui/react"; // #TODO: USE THIS. Currently it's not. Kinda complicated to set up because of state. diff --git a/frontend/src/components/basic/dialog/UpgradePlan.tsx b/frontend/src/components/basic/dialog/UpgradePlan.tsx index 95993c6a6..2573036eb 100644 --- a/frontend/src/components/basic/dialog/UpgradePlan.tsx +++ b/frontend/src/components/basic/dialog/UpgradePlan.tsx @@ -1,6 +1,6 @@ -import { Fragment } from 'react'; -import { useRouter } from 'next/router'; -import { Dialog, Transition } from '@headlessui/react'; +import { Fragment } from "react"; +import { useRouter } from "next/router"; +import { Dialog, Transition } from "@headlessui/react"; // REFACTOR: Move all these modals into one reusable one type Props = { diff --git a/frontend/src/components/basic/popups/BottomRightPopup.tsx b/frontend/src/components/basic/popups/BottomRightPopup.tsx index 58d8a498e..9a2938ad6 100644 --- a/frontend/src/components/basic/popups/BottomRightPopup.tsx +++ b/frontend/src/components/basic/popups/BottomRightPopup.tsx @@ -1,5 +1,5 @@ -import { faXmark } from '@fortawesome/free-solid-svg-icons'; -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { faXmark } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; interface PopupProps { buttonText: string; diff --git a/frontend/src/components/basic/table/ApiKeyTable.tsx b/frontend/src/components/basic/table/ApiKeyTable.tsx index 800ea7356..1522df00d 100644 --- a/frontend/src/components/basic/table/ApiKeyTable.tsx +++ b/frontend/src/components/basic/table/ApiKeyTable.tsx @@ -1,10 +1,10 @@ -import { faX } from '@fortawesome/free-solid-svg-icons'; +import { faX } from "@fortawesome/free-solid-svg-icons"; -import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; -import deleteAPIKey from '../../../pages/api/apiKey/deleteAPIKey'; -import guidGenerator from '../../utilities/randomId'; -import Button from '../buttons/Button'; +import deleteAPIKey from "../../../pages/api/apiKey/deleteAPIKey"; +import guidGenerator from "../../utilities/randomId"; +import Button from "../buttons/Button"; interface TokenProps { _id: string; @@ -58,7 +58,7 @@ const ApiKeyTable = ({ data, setApiKeys }: ServiceTokensProps) => { setApiKeys(data.filter((token) => token._id !== row._id)); createNotification({ text: `'${row.name}' API key has been revoked.`, - type: 'error' + type: "error" }); }} color="red" diff --git a/frontend/src/components/basic/table/EnvironmentsTable.tsx b/frontend/src/components/basic/table/EnvironmentsTable.tsx index 1a3ffe906..b9c94534a 100644 --- a/frontend/src/components/basic/table/EnvironmentsTable.tsx +++ b/frontend/src/components/basic/table/EnvironmentsTable.tsx @@ -1,13 +1,13 @@ -import { useEffect, useState } from 'react'; -import { faPencil, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; -import { plans } from 'public/data/frequentConstants'; +import { useEffect, useState } from "react"; +import { faPencil, faPlus, faX } from "@fortawesome/free-solid-svg-icons"; +import { plans } from "public/data/frequentConstants"; -import { usePopUp } from '../../../hooks/usePopUp'; -import getOrganizationSubscriptions from '../../../pages/api/organization/GetOrgSubscription'; -import Button from '../buttons/Button'; -import { AddUpdateEnvironmentDialog } from '../dialog/AddUpdateEnvironmentDialog'; -import DeleteActionModal from '../dialog/DeleteActionModal'; -import UpgradePlanModal from '../dialog/UpgradePlan'; +import { usePopUp } from "../../../hooks/usePopUp"; +import getOrganizationSubscriptions from "../../../pages/api/organization/GetOrgSubscription"; +import Button from "../buttons/Button"; +import { AddUpdateEnvironmentDialog } from "../dialog/AddUpdateEnvironmentDialog"; +import DeleteActionModal from "../dialog/DeleteActionModal"; +import UpgradePlanModal from "../dialog/UpgradePlan"; type Env = { name: string; slug: string }; @@ -20,17 +20,17 @@ type Props = { const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: Props) => { const { popUp, handlePopUpOpen, handlePopUpClose } = usePopUp([ - 'createUpdateEnv', - 'deleteEnv', - 'upgradePlan' + "createUpdateEnv", + "deleteEnv", + "upgradePlan" ] as const); - const [plan, setPlan] = useState(''); + const [plan, setPlan] = useState(""); const host = window.location.origin; useEffect(() => { // on initial load - run auth check (async () => { - const orgId = localStorage.getItem('orgData.id') as string; + const orgId = localStorage.getItem("orgData.id") as string; const subscriptions = await getOrganizationSubscriptions({ orgId }); @@ -44,7 +44,7 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: const onEnvCreateCB = async (env: Env) => { try { await onCreateEnv(env); - handlePopUpClose('createUpdateEnv'); + handlePopUpClose("createUpdateEnv"); } catch (error) { console.error(error); } @@ -52,8 +52,8 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: const onEnvUpdateCB = async (env: Env) => { try { - await onUpdateEnv((popUp.createUpdateEnv?.data as Pick)?.slug, env); - handlePopUpClose('createUpdateEnv'); + await onUpdateEnv((popUp.createUpdateEnv?.data as Pick)?.slug, env); + handlePopUpClose("createUpdateEnv"); } catch (error) { console.error(error); } @@ -61,8 +61,8 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }: const onEnvDeleteCB = async () => { try { - await onDeleteEnv((popUp.deleteEnv?.data as Pick)?.slug); - handlePopUpClose('deleteEnv'); + await onDeleteEnv((popUp.deleteEnv?.data as Pick)?.slug); + handlePopUpClose("deleteEnv"); } catch (error) { console.error(error); } @@ -85,10 +85,10 @@ const EnvironmentTable = ({ data = [], onCreateEnv, onDeleteEnv, onUpdateEnv }:
diff --git a/frontend/src/components/basic/table/ProjectUsersTable.tsx b/frontend/src/components/basic/table/ProjectUsersTable.tsx index 0f6df98d3..a94d2b313 100644 --- a/frontend/src/components/basic/table/ProjectUsersTable.tsx +++ b/frontend/src/components/basic/table/ProjectUsersTable.tsx @@ -1,22 +1,22 @@ -import { useEffect, useState } from 'react'; -import { useRouter } from 'next/router'; -import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from '@fortawesome/free-solid-svg-icons'; -import { plans } from 'public/data/frequentConstants'; +import { useEffect, useState } from "react"; +import { useRouter } from "next/router"; +import { faEye, faEyeSlash, faPenToSquare, faPlus, faX } from "@fortawesome/free-solid-svg-icons"; +import { plans } from "public/data/frequentConstants"; -import { useNotificationContext } from '@app/components/context/Notifications/NotificationProvider'; -import { Select, SelectItem } from '@app/components/v2'; -import updateUserProjectPermission from '@app/ee/api/memberships/UpdateUserProjectPermission'; -import getOrganizationSubscriptions from '@app/pages/api/organization/GetOrgSubscription'; -import changeUserRoleInWorkspace from '@app/pages/api/workspace/changeUserRoleInWorkspace'; -import deleteUserFromWorkspace from '@app/pages/api/workspace/deleteUserFromWorkspace'; -import getLatestFileKey from '@app/pages/api/workspace/getLatestFileKey'; -import getProjectInfo from '@app/pages/api/workspace/getProjectInfo'; -import uploadKeys from '@app/pages/api/workspace/uploadKeys'; +import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider"; +import { Select, SelectItem } from "@app/components/v2"; +import updateUserProjectPermission from "@app/ee/api/memberships/UpdateUserProjectPermission"; +import getOrganizationSubscriptions from "@app/pages/api/organization/GetOrgSubscription"; +import changeUserRoleInWorkspace from "@app/pages/api/workspace/changeUserRoleInWorkspace"; +import deleteUserFromWorkspace from "@app/pages/api/workspace/deleteUserFromWorkspace"; +import getLatestFileKey from "@app/pages/api/workspace/getLatestFileKey"; +import getProjectInfo from "@app/pages/api/workspace/getProjectInfo"; +import uploadKeys from "@app/pages/api/workspace/uploadKeys"; -import { decryptAssymmetric, encryptAssymmetric } from '../../utilities/cryptography/crypto'; -import guidGenerator from '../../utilities/randomId'; -import Button from '../buttons/Button'; -import UpgradePlanModal from '../dialog/UpgradePlan'; +import { decryptAssymmetric, encryptAssymmetric } from "../../utilities/cryptography/crypto"; +import guidGenerator from "../../utilities/randomId"; +import Button from "../buttons/Button"; +import UpgradePlanModal from "../dialog/UpgradePlan"; // const roles = ['admin', 'user']; // TODO: Set type for this @@ -45,8 +45,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa ); const host = window.location.origin; const router = useRouter(); - const [myRole, setMyRole] = useState('member'); - const [currentPlan, setCurrentPlan] = useState(''); + const [myRole, setMyRole] = useState("member"); + const [currentPlan, setCurrentPlan] = useState(""); const [workspaceEnvs, setWorkspaceEnvs] = useState([]); const [isUpgradeModalOpen, setIsUpgradeModalOpen] = useState(false); const { createNotification } = useNotificationContext(); @@ -87,8 +87,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa ...userData.slice(index + 1, userData?.length) ]); createNotification({ - text: `Successfully changed user role.`, - type: 'success' + text: "Successfully changed user role.", + type: "success" }); }; @@ -99,28 +99,28 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa slug: string ) => { let denials: { ability: string; environmentSlug: string }[]; - if (val === 'Read Only') { + if (val === "Read Only") { denials = [ { - ability: 'write', + ability: "write", environmentSlug: slug } ]; - } else if (val === 'No Access') { + } else if (val === "No Access") { denials = [ { - ability: 'write', + ability: "write", environmentSlug: slug }, { - ability: 'read', + ability: "read", environmentSlug: slug } ]; - } else if (val === 'Add Only') { + } else if (val === "Add Only") { denials = [ { - ability: 'read', + ability: "read", environmentSlug: slug } ]; @@ -128,7 +128,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa denials = []; } - if (currentPlan !== plans.professional && host === 'https://app.infisical.com' && workspaceId !== '63ea8121b6e2b0543ba79616') { + if (currentPlan !== plans.professional && host === "https://app.infisical.com" && workspaceId !== "63ea8121b6e2b0543ba79616") { setIsUpgradeModalOpen(true); } else { const allDenials = userData[index].deniedPermissions @@ -156,8 +156,8 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa ...userData.slice(index + 1, userData?.length) ]); createNotification({ - text: `Successfully changed user permissions.`, - type: 'success' + text: "Successfully changed user permissions.", + type: "success" }); } }; @@ -168,7 +168,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa const result = await getProjectInfo({ projectId: workspaceId }); setWorkspaceEnvs(result.environments); - const orgId = localStorage.getItem('orgData.id') as string; + const orgId = localStorage.getItem("orgData.id") as string; const subscriptions = await getOrganizationSubscriptions({ orgId }); @@ -181,7 +181,7 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa const grantAccess = async (id: string, publicKey: string) => { const result = await getLatestFileKey({ workspaceId }); - const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string; + const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; // assymmetrically decrypt symmetric key with local private key const key = decryptAssymmetric({ @@ -261,13 +261,13 @@ const ProjectUsersTable = ({ userData, changeData, myUser, filter, isUserListLoa // open={isOpen} onValueChange={(e) => handleRoleUpdate(index, e)} value={row.role} - isDisabled={myRole !== 'admin' || myUser === row.email} + isDisabled={myRole !== "admin" || myUser === row.email} // onOpenChange={(open) => setIsOpen(open)} > Admin Member - {row.status === 'completed' && myUser !== row.email && ( + {row.status === "completed" && myUser !== row.email && (
)} - {row.status === 'completed' && myUser !== row.email && ( + {row.status === "completed" && myUser !== row.email && (