diff --git a/.env.example b/.env.example index 79fca5686..df316753e 100644 --- a/.env.example +++ b/.env.example @@ -31,12 +31,12 @@ MONGO_PASSWORD=example SITE_URL=http://localhost:8080 # Mail/SMTP -SMTP_HOST= # required -SMTP_USERNAME= # required -SMTP_PASSWORD= # required +SMTP_HOST= +SMTP_USERNAME= +SMTP_PASSWORD= SMTP_PORT=587 SMTP_SECURE=false -SMTP_FROM_ADDRESS= # required +SMTP_FROM_ADDRESS= SMTP_FROM_NAME=Infisical # Integration @@ -66,4 +66,4 @@ STRIPE_WEBHOOK_SECRET= STRIPE_PRODUCT_STARTER= STRIPE_PRODUCT_TEAM= STRIPE_PRODUCT_PRO= -NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= \ No newline at end of file +NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 45816f5cd..fd27d42c1 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,6 +1,6 @@ # Description ๐Ÿ“ฃ -*Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.* + ## Type โœจ @@ -11,7 +11,7 @@ # Tests ๐Ÿ› ๏ธ -*Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible* + ```sh # Here's some code block to paste some code snippets diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b4f9546ae..0c06bf342 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -9,6 +9,12 @@ jobs: steps: - 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 @@ -45,8 +51,8 @@ jobs: token: ${{ secrets.DEPOT_PROJECT_TOKEN }} push: true context: backend - tags: infisical/backend:${{ steps.commit.outputs.short }}, - infisical/backend:latest + tags: infisical/backend:${{ steps.commit.outputs.short }}, + infisical/backend:latest platforms: linux/amd64,linux/arm64 frontend-image: @@ -94,8 +100,8 @@ jobs: push: true token: ${{ secrets.DEPOT_PROJECT_TOKEN }} context: frontend - tags: infisical/frontend:${{ steps.commit.outputs.short }}, - infisical/frontend:latest + tags: infisical/frontend:${{ steps.commit.outputs.short }}, + infisical/frontend:latest platforms: linux/amd64,linux/arm64 build-args: | POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} @@ -122,7 +128,7 @@ jobs: token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} - name: Save DigitalOcean kubeconfig with short-lived credentials run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179 - - name: switch to gamma namespace + - name: switch to gamma namespace run: kubectl config set-context --current --namespace=gamma - name: test kubectl run: kubectl get ingress @@ -135,4 +141,4 @@ jobs: exit 1 else echo "Helm upgrade was successful" - fi \ No newline at end of file + fi diff --git a/README.md b/README.md index 449b8cce6..32a3ce732 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ git commit activity - Cloudsmith downloads + Cloudsmith downloads Slack community channel @@ -366,7 +366,7 @@ Not sure where to get started? You can: - + ## ๐ŸŒŽ Translations diff --git a/backend/package-lock.json b/backend/package-lock.json index 653fb9259..b6874270d 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -9,15 +9,15 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.281.0", + "@aws-sdk/client-secrets-manager": "^3.294.0", "@godaddy/terminus": "^4.11.2", "@octokit/rest": "^19.0.5", - "@sentry/node": "^7.40.0", "@sentry/tracing": "^7.41.0", + "@sentry/node": "^7.41.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "await-to-js": "^3.0.0", - "aws-sdk": "^2.1324.0", + "aws-sdk": "^2.1338.0", "axios": "^1.1.3", "axios-retry": "^3.4.0", "bcrypt": "^5.1.0", @@ -38,9 +38,9 @@ "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", - "mongoose": "^6.10.1", + "mongoose": "^6.10.3", "nodemailer": "^6.8.0", - "posthog-node": "^2.5.4", + "posthog-node": "^2.6.0", "query-string": "^7.1.3", "request-ip": "^3.3.0", "rimraf": "^3.0.2", @@ -62,7 +62,7 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", - "@types/jest": "^29.2.4", + "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", @@ -177,6 +177,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.272.0.tgz", "integrity": "sha512-s2TV3phapcTwZNr4qLxbfuQuE9ZMP4RoJdkvRRCkKdm6jslsWLJf2Zlcxti/23hOlINUMYv2iXE2pftIgWGdpg==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -232,44 +233,44 @@ } }, "node_modules/@aws-sdk/client-secrets-manager": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.281.0.tgz", - "integrity": "sha512-vL+LygNDoCja/47pFwSnI3tCWfy5SGHlz6C1whcszmfl81gXyuHNlAK8RXVRiKhnD11zmCsHJhZyYSfTz5RErQ==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.294.0.tgz", + "integrity": "sha512-L3fcQiHqOPkc3odrWCVRrhC838p8mFLZxRpQPHz3YCxhSbeclhPjqHNyoljkHmoaqzo+e0owqkM3kYyfereGcg==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.281.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/credential-provider-node": "3.281.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-signing": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/client-sts": "3.294.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/credential-provider-node": "3.294.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-signing": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "tslib": "^2.3.1", "uuid": "^8.3.2" }, @@ -277,42 +278,54 @@ "node": ">=14.0.0" } }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/abort-controller": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.292.0.tgz", + "integrity": "sha512-lf+OPptL01kvryIJy7+dvFux5KbJ6OTwLPPEekVKZ2AfEvwcVtOZWFUhyw3PJCBTVncjKB1Kjl3V/eTS3YuPXQ==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.281.0.tgz", - "integrity": "sha512-3RvO5zClQhu37w9VMLoHPGk58S3y8Spb7XX8rW51bm5TUglYQskQ0X2VLEUW/7ZGx/peokHws9Z9+w5yGq5sdA==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.294.0.tgz", + "integrity": "sha512-+FuxQTi5WvnaXM5JbNLkBIzQ3An4gA0ox61N1u+3xled+nywKb1yQ7WmRpyMG5bLbkmnj3aqoo5/uskFc4c4EA==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "tslib": "^2.3.1" }, "engines": { @@ -320,41 +333,41 @@ } }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sso-oidc": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.281.0.tgz", - "integrity": "sha512-P6zf9pDuxApVoCYStAg7L8BU9AcWI8PxfLSX4r2WnmcQropxzPJ3op1j9nvbwwBDMFWephijVY4AVp8MqPcPyg==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.294.0.tgz", + "integrity": "sha512-/ZfDud76MdSPJ/TxjV2xLE30XbBQDZwKQ32axwoK1eziPvrAIUBYVgpBwj+m0quhoiQhBKkg3aFl6j39AF2thw==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "tslib": "^2.3.1" }, "engines": { @@ -362,44 +375,44 @@ } }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/client-sts": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.281.0.tgz", - "integrity": "sha512-w8QomyhCVEArRcXgOkjbofiS/PLEKWRAyYBovjMS1cGhns2ZYJXFgHNgr3VGE54TghUc5dR1CqKuBKKM4ThrgA==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.294.0.tgz", + "integrity": "sha512-AefqwhFjTDzelZuSYhriJbiI+GQwf2yKiKAnCt0gRj6rswewStM63Gtlhfb01sFPp+ZiqPcyQ47LqUaHp1mz/g==", "dependencies": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/credential-provider-node": "3.281.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-sdk-sts": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-signing": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/credential-provider-node": "3.294.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-sdk-sts": "3.292.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-signing": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "fast-xml-parser": "4.1.2", "tslib": "^2.3.1" }, @@ -407,19 +420,62 @@ "node": ">=14.0.0" } }, - "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.281.0.tgz", - "integrity": "sha512-H99nhMhHImQKgNhHKYc6usTS6UK8KzCcVGpILLVTuP97YlrYAMFAVstA3Xk6mZ28JAbHVXvI6vJjkMNOzCSKCA==", + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/config-resolver": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.292.0.tgz", + "integrity": "sha512-cB3twnNR7vYvlt2jvw8VlA1+iv/tVzl+/S39MKqw2tepU+AbJAM0EHwb/dkf1OKSmlrnANXhshx80MHF9zL4mA==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.272.0", - "@aws-sdk/credential-provider-imds": "3.272.0", - "@aws-sdk/credential-provider-process": "3.272.0", - "@aws-sdk/credential-provider-sso": "3.281.0", - "@aws-sdk/credential-provider-web-identity": "3.272.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-config-provider": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.292.0.tgz", + "integrity": "sha512-YbafSG0ZEKE2969CJWVtUhh3hfOeLPecFVoXOtegCyAJgY5Ghtu4TsVhL4DgiGAgOC30ojAmUVQEXzd7xJF5xA==", + "dependencies": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-imds": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.292.0.tgz", + "integrity": "sha512-W/peOgDSRYulgzFpUhvgi1pCm6piBz6xrVN17N4QOy+3NHBXRVMVzYk6ct2qpLPgJUSEZkcpP+Gds+bBm8ed1A==", + "dependencies": { + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.294.0.tgz", + "integrity": "sha512-pdTPbaAb5bWA+DnuKoL2TpXeNDp6Ejpv/OYt+bw2gdzl9w5r/ZCtUTTbW+Vvejr4WL5s3c1bY96kwdqCn7iLqA==", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.292.0", + "@aws-sdk/credential-provider-imds": "3.292.0", + "@aws-sdk/credential-provider-process": "3.292.0", + "@aws-sdk/credential-provider-sso": "3.294.0", + "@aws-sdk/credential-provider-web-identity": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", "tslib": "^2.3.1" }, "engines": { @@ -427,19 +483,33 @@ } }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.281.0.tgz", - "integrity": "sha512-jhddd+lJp8G8hBJ+6glmXjfWJT3nxiE1aliH3fBC4RR3D+1kRXc99Xg6mbUb8bm+GrVZ4gzfiqSgg+ByKjd7xA==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.294.0.tgz", + "integrity": "sha512-zUL1Qhb4BsQIZCs/TPpG4oIYH/9YsGiS+Se1tasSGjTOLfBy7jhOZ0QIdpEeyAx/EP8blOBredM9xWfEXgiHVA==", "dependencies": { - "@aws-sdk/credential-provider-env": "3.272.0", - "@aws-sdk/credential-provider-imds": "3.272.0", - "@aws-sdk/credential-provider-ini": "3.281.0", - "@aws-sdk/credential-provider-process": "3.272.0", - "@aws-sdk/credential-provider-sso": "3.281.0", - "@aws-sdk/credential-provider-web-identity": "3.272.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/credential-provider-env": "3.292.0", + "@aws-sdk/credential-provider-imds": "3.292.0", + "@aws-sdk/credential-provider-ini": "3.294.0", + "@aws-sdk/credential-provider-process": "3.292.0", + "@aws-sdk/credential-provider-sso": "3.294.0", + "@aws-sdk/credential-provider-web-identity": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.292.0.tgz", + "integrity": "sha512-CFVXuMuUvg/a4tknzRikEDwZBnKlHs1LZCpTXIGjBdUTdosoi4WNzDLzGp93ZRTtcgFz+4wirz2f7P3lC0NrQw==", + "dependencies": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", "tslib": "^2.3.1" }, "engines": { @@ -447,15 +517,357 @@ } }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.281.0.tgz", - "integrity": "sha512-IqJnpXuLpJYoSCf/Rt66/CPVTjfkam3z9+ZvlQJV+VbK+vGj276qEtTmSN3XPZZgF1XbWptvkzIWDszLhHiZmg==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.294.0.tgz", + "integrity": "sha512-UxrcAA/0l7j9+3tolYcG5M61D/IE1Bjd/9H87H1i2A2BrwUUBhW1Dp/vvROEDrrywlMDG3CDF3T/7ADtTak+sg==", "dependencies": { - "@aws-sdk/client-sso": "3.281.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/token-providers": "3.281.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/client-sso": "3.294.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/token-providers": "3.294.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.292.0.tgz", + "integrity": "sha512-4DbtIEM9gGVfqYlMdYXg3XY+vBhemjB1zXIequottW8loLYM8Vuz4/uGxxKNze6evVVzowsA0wKrYclE1aj/Rg==", + "dependencies": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/fetch-http-handler": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.292.0.tgz", + "integrity": "sha512-zh3bhUJbL8RSa39ZKDcy+AghtUkIP8LwcNlwRIoxMQh3Row4D1s4fCq0KZCx98NJBEXoiTLyTQlZxxI//BOb1Q==", + "dependencies": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/querystring-builder": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/hash-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.292.0.tgz", + "integrity": "sha512-1yLxmIsvE+eK36JXEgEIouTITdykQLVhsA5Oai//Lar6Ddgu1sFpLDbdkMtKbrh4I0jLN9RacNCkeVQjZPTCCQ==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-buffer-from": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/invalid-dependency": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.292.0.tgz", + "integrity": "sha512-39OUV78CD3TmEbjhpt+V+Fk4wAGWhixqHxDSN8+4WL0uB4Fl7k5m3Z9hNY78AttHQSl2twR7WtLztnXPAFsriw==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/is-array-buffer": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.292.0.tgz", + "integrity": "sha512-kW/G5T/fzI0sJH5foZG6XJiNCevXqKLxV50qIT4B1pMuw7regd4ALIy0HwSqj1nnn9mSbRWBfmby0jWCJsMcwg==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-content-length": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.292.0.tgz", + "integrity": "sha512-2gMWzQus5mj14menolpPDbYBeaOYcj7KNFZOjTjjI3iQ0KqyetG6XasirNrcJ/8QX1BRmpTol8Xjp2Ue3Gbzwg==", + "dependencies": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-endpoint": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.292.0.tgz", + "integrity": "sha512-cPMkiSxpZGG6tYlW4OS+ucS6r43f9ddX9kcUoemJCY10MOuogdPjulCAjE0HTs2PLKSOrrG4CTP4Q4wWDrH4Bw==", + "dependencies": { + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-config-provider": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-host-header": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.292.0.tgz", + "integrity": "sha512-mHuCWe3Yg2S5YZ7mB7sKU6C97XspfqrimWjMW9pfV2usAvLA3R0HrB03jpR5vpZ3P4q7HB6wK3S6CjYMGGRNag==", + "dependencies": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-logger": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.292.0.tgz", + "integrity": "sha512-yZNY1XYmG3NG+uonET7jzKXNiwu61xm/ZZ6i/l51SusuaYN+qQtTAhOFsieQqTehF9kP4FzbsWgPDwD8ZZX9lw==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.292.0.tgz", + "integrity": "sha512-kA3VZpPko0Zqd7CYPTKAxhjEv0HJqFu2054L04dde1JLr43ro+2MTdX7vsHzeAFUVRphqatFFofCumvXmU6Mig==", + "dependencies": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-retry": { + "version": "3.293.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.293.0.tgz", + "integrity": "sha512-7tiaz2GzRecNHaZ6YnF+Nrtk3au8qF6oiipf11R7MJiqJ0fkMLnz/iRrlakDziS9qF/a9v+3yxb4W4NHK3f4Tw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/service-error-classification": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "@aws-sdk/util-retry": "3.292.0", + "tslib": "^2.3.1", + "uuid": "^8.3.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-sdk-sts": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.292.0.tgz", + "integrity": "sha512-GN5ZHEqXZqDi+HkVbaXRX9HaW/vA5rikYpWKYsmxTUZ7fB7ijvEO3co3lleJv2C+iGYRtUIHC4wYNB5xgoTCxg==", + "dependencies": { + "@aws-sdk/middleware-signing": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-serde": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.292.0.tgz", + "integrity": "sha512-6hN9mTQwSvV8EcGvtXbS/MpK7WMCokUku5Wu7X24UwCNMVkoRHLIkYcxHcvBTwttuOU0d8hph1/lIX4dkLwkQw==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-signing": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.292.0.tgz", + "integrity": "sha512-GVfoSjDjEQ4TaO6x9MffyP3uRV+2KcS5FtexLCYOM9pJcnE9tqq9FJOrZ1xl1g+YjUVKxo4x8lu3tpEtIb17qg==", + "dependencies": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-stack": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.292.0.tgz", + "integrity": "sha512-WdQpRkuMysrEwrkByCM1qCn2PPpFGGQ2iXqaFha5RzCdZDlxJni9cVNb6HzWUcgjLEYVTXCmOR9Wxm3CNW44Qg==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.293.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.293.0.tgz", + "integrity": "sha512-gZ7/e6XwpKk9mvgA78q4Ffc796jTn02TUKx2qMDnkLVbeJXBNN2jnvYEKq8v70+o7fd/ALRudg8gBDmkkhM/Hw==", + "dependencies": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/node-config-provider": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.292.0.tgz", + "integrity": "sha512-S3NnC9dQ5GIbJYSDIldZb4zdpCOEua1tM7bjYL3VS5uqCEM93kIi/o/UkIUveMp/eqTS2LJa5HjNIz5Te6je0A==", + "dependencies": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/node-http-handler": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.292.0.tgz", + "integrity": "sha512-L/E3UDSwXLXjt1XWWh0RBD55F+aZI1AEdPwdES9i1PjnZLyuxuDhEDptVibNN56+I9/4Q3SbmuVRVlOD0uzBag==", + "dependencies": { + "@aws-sdk/abort-controller": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/querystring-builder": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/property-provider": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.292.0.tgz", + "integrity": "sha512-dHArSvsiqhno/g55N815gXmAMrmN8DP7OeFNqJ4wJG42xsF2PFN3DAsjIuHuXMwu+7A3R1LHqIpvv0hA9KeoJQ==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/protocol-http": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.292.0.tgz", + "integrity": "sha512-NLi4fq3k41aXIh1I97yX0JTy+3p6aW1NdwFwdMa674z86QNfb4SfRQRZBQe9wEnAZ/eWHVnlKIuII+U1URk/Kg==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/querystring-builder": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.292.0.tgz", + "integrity": "sha512-XElIFJaReIm24eEvBtV2dOtZvcm3gXsGu/ftG8MLJKbKXFKpAP1q+K6En0Bs7/T88voKghKdKpKT+eZUWgTqlg==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-uri-escape": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/querystring-parser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.292.0.tgz", + "integrity": "sha512-iTYpYo7a8X9RxiPbjjewIpm6XQPx2EOcF1dWCPRII9EFlmZ4bwnX+PDI36fIo9oVs8TIKXmwNGODU9nsg7CSAw==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/service-error-classification": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.292.0.tgz", + "integrity": "sha512-X1k3sixCeC45XSNHBe+kRBQBwPDyTFtFITb8O5Qw4dS9XWGhrUJT4CX0qE5aj8qP3F9U5nRizs9c2mBVVP0Caw==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/shared-ini-file-loader": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.292.0.tgz", + "integrity": "sha512-Av2TTYg1Jig2kbkD56ybiqZJB6vVrYjv1W5UQwY/q3nA/T2mcrgQ20ByCOt5Bv9VvY7FSgC+znj+L4a7RLGmBg==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/signature-v4": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.292.0.tgz", + "integrity": "sha512-+rw47VY5mvBecn13tDQTl1ipGWg5tE63faWgmZe68HoBL87ZiDzsd7bUKOvjfW21iMgWlwAppkaNNQayYRb2zg==", + "dependencies": { + "@aws-sdk/is-array-buffer": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-hex-encoding": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "@aws-sdk/util-uri-escape": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/smithy-client": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.292.0.tgz", + "integrity": "sha512-S8PKzjPkZ6SXYZuZiU787dMsvQ0d/LFEhw2OI4Oe2An9Fc2IwJ2FYukyHoQJOV2tV0DiuMebPo7eMyQyjKElvA==", + "dependencies": { + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/types": "3.292.0", "tslib": "^2.3.1" }, "engines": { @@ -463,14 +875,219 @@ } }, "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/token-providers": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.281.0.tgz", - "integrity": "sha512-36Vg/F6Edm7qdjcTeNVON+sK2edgHhmhTtAEjWcuUk5AX/Et+Ate/A2N8HD3nxwlAcgidfnBC9SHYJatbhcEnQ==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.294.0.tgz", + "integrity": "sha512-6nwO04LtC5f4AsUvGZXyjaswuEK4Rr2VsuANpMKrPCgunRfI58a8YXLniudOSXN6e7CFJ6M3uo/h5YXqtnzGug==", "dependencies": { - "@aws-sdk/client-sso-oidc": "3.281.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/client-sso-oidc": "3.294.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/types": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.292.0.tgz", + "integrity": "sha512-1teYAY2M73UXZxMAxqZxVS2qwXjQh0OWtt7qyLfha0TtIk/fZ1hRwFgxbDCHUFcdNBSOSbKH/ESor90KROXLCQ==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/url-parser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.292.0.tgz", + "integrity": "sha512-NZeAuZCk1x6TIiWuRfbOU6wHPBhf0ly2qOHzWut4BCH+b4RrDmFF8EmXcH1auEfGhE7yRyR6XqIN0t3S+hYACA==", + "dependencies": { + "@aws-sdk/querystring-parser": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-base64": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.292.0.tgz", + "integrity": "sha512-zjNCwNdy617yFvEjZorepNWXB2sQCVfsShCwFy/kIQ5iW5tT2jQKaqc0K77diU9atkooxw9p1W9m9sOgrkOFNw==", + "dependencies": { + "@aws-sdk/util-buffer-from": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-body-length-browser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.292.0.tgz", + "integrity": "sha512-Wd/BM+JsMiKvKs/bN3z6TredVEHh2pKudGfg3CSjTRpqFpOG903KDfyHBD42yg5PuCHoHoewJvTPKwgn7/vhaw==", + "dependencies": { + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-body-length-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.292.0.tgz", + "integrity": "sha512-BBgipZ2P6RhogWE/qj0oqpdlyd3iSBYmb+aD/TBXwB2lA/X8A99GxweBd/kp06AmcJRoMS9WIXgbWkiiBlRlSA==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-buffer-from": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.292.0.tgz", + "integrity": "sha512-RxNZjLoXNxHconH9TYsk5RaEBjSgTtozHeyIdacaHPj5vlQKi4hgL2hIfKeeNiAfQEVjaUFF29lv81xpNMzVMQ==", + "dependencies": { + "@aws-sdk/is-array-buffer": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-config-provider": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.292.0.tgz", + "integrity": "sha512-t3noYll6bPRSxeeNNEkC5czVjAiTPcsq00OwfJ2xyUqmquhLEfLwoJKmrT1uP7DjIEXdUtfoIQ2jWiIVm/oO5A==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-defaults-mode-browser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.292.0.tgz", + "integrity": "sha512-7+zVUlMGfa8/KT++9humHo6IDxTnxMCmWUj5jVNlkpk6h7Ecmppf7aXotviyVIA43lhtz0p2AErs0N0ekEUK+w==", + "dependencies": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-defaults-mode-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.292.0.tgz", + "integrity": "sha512-SSIw85eF4BVs0fOJRyshT+R3b/UmBPhiVKCUZm2rq6+lIGkDPiSwQU3d/80AhXtiL5SFT/IzAKKgQd8qMa7q3A==", + "dependencies": { + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/credential-provider-imds": "3.292.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-endpoints": { + "version": "3.293.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.293.0.tgz", + "integrity": "sha512-R/99aNV49Refpv5guiUjEUrZYlvnfaNBniB+/ZtMO3ixxUopapssCrUivuJrmhccmrYaTCZw7dRzIWjU1jJhKg==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-hex-encoding": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.292.0.tgz", + "integrity": "sha512-qBd5KFIUywQ3qSSbj814S2srk0vfv8A6QMI+Obs1y2LHZFdQN5zViptI4UhXhKOHe+NnrHWxSuLC/LMH6q3SmA==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-middleware": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.292.0.tgz", + "integrity": "sha512-KjhS7flfoBKDxbiBZjLjMvEizXgjfQb7GQEItgzGoI9rfGCmZtvqCcqQQoIlxb8bIzGRggAUHtBGWnlLbpb+GQ==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-retry": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.292.0.tgz", + "integrity": "sha512-JEHyF7MpVeRF5uR4LDYgpOKcFpOPiAj8TqN46SVOQQcL1K+V7cSr7O7N7J6MwJaN9XOzAcBadeIupMm7/BFbgw==", + "dependencies": { + "@aws-sdk/service-error-classification": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-uri-escape": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.292.0.tgz", + "integrity": "sha512-hOQtUMQ4VcQ9iwKz50AoCp1XBD5gJ9nly/gJZccAM7zSA5mOO8RRKkbdonqquVHxrO0CnYgiFeCh3V35GFecUw==", + "dependencies": { + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.292.0.tgz", + "integrity": "sha512-dld+lpC3QdmTQHdBWJ0WFDkXDSrJgfz03q6mQ8+7H+BC12ZhT0I0g9iuvUjolqy7QR00OxOy47Y9FVhq8EC0Gg==", + "dependencies": { + "@aws-sdk/types": "3.292.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.292.0.tgz", + "integrity": "sha512-f+NfIMal5E61MDc5WGhUEoicr7b1eNNhA+GgVdSB/Hg5fYhEZvFK9RZizH5rrtsLjjgcr9nPYSR7/nDKCJLumw==", + "dependencies": { + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/client-secrets-manager/node_modules/@aws-sdk/util-utf8": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8/-/util-utf8-3.292.0.tgz", + "integrity": "sha512-FPkj+Z59/DQWvoVu2wFaRncc3KVwe/pgK3MfVb0Lx+Ibey5KUx+sNpJmYcVYHUAe/Nv/JeIpOtYuC96IXOnI6w==", + "dependencies": { + "@aws-sdk/util-buffer-from": "3.292.0", "tslib": "^2.3.1" }, "engines": { @@ -614,6 +1231,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.272.0.tgz", "integrity": "sha512-Dr4CffRVNsOp3LRNdpvcH6XuSgXOSLblWliCy/5I86cNl567KVMxujVx6uPrdTXYs2h1rt3MNl6jQGnAiJeTbw==", + "optional": true, "dependencies": { "@aws-sdk/signature-v4": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -644,6 +1262,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.272.0.tgz", "integrity": "sha512-QI65NbLnKLYHyTYhXaaUrq6eVsCCrMUb05WDA7+TJkWkjXesovpjc8vUKgFiLSxmgKmb2uOhHNcDyObKMrYQFw==", + "optional": true, "dependencies": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -657,6 +1276,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.272.0.tgz", "integrity": "sha512-wwAfVY1jTFQEfxVfdYD5r5ieYGl+0g4nhekVxNMqE8E1JeRDd18OqiwAflzpgBIqxfqvCUkf+vl5JYyacMkNAQ==", + "optional": true, "dependencies": { "@aws-sdk/node-config-provider": "3.272.0", "@aws-sdk/property-provider": "3.272.0", @@ -713,6 +1333,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.272.0.tgz", "integrity": "sha512-hiCAjWWm2PeBFp5cjkxqyam/XADjiS+e7GzwC34TbZn3LisS0uoweLojj9tD11NnnUhyhbLteUvu5+rotOLwrg==", + "optional": true, "dependencies": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/shared-ini-file-loader": "3.272.0", @@ -744,6 +1365,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.272.0.tgz", "integrity": "sha512-ImrHMkcgneGa/HadHAQXPwOrX26sAKuB8qlMxZF/ZCM2B55u8deY+ZVkVuraeKb7YsahMGehPFOfRAF6mvFI5Q==", + "optional": true, "dependencies": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -783,6 +1405,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.272.0.tgz", "integrity": "sha512-1Qhm9e0RbS1Xf4CZqUbQyUMkDLd7GrsRXWIvm9b86/vgeV8/WnjO3CMue9D51nYgcyQORhYXv6uVjAYCWbUExA==", + "optional": true, "dependencies": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/querystring-builder": "3.272.0", @@ -795,6 +1418,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.272.0.tgz", "integrity": "sha512-40dwND+iAm3VtPHPZu7/+CIdVJFk2s0cWZt1lOiMPMSXycSYJ45wMk7Lly3uoqRx0uWfFK5iT2OCv+fJi5jTng==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "@aws-sdk/util-buffer-from": "3.208.0", @@ -809,6 +1433,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.272.0.tgz", "integrity": "sha512-ysW6wbjl1Y78txHUQ/Tldj2Rg1BI7rpMO9B9xAF6yAX3mQ7t6SUPQG/ewOGvH2208NBIl3qP5e/hDf0Q6r/1iw==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -818,6 +1443,7 @@ "version": "3.201.0", "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.201.0.tgz", "integrity": "sha512-UPez5qLh3dNgt0DYnPD/q0mVJY84rA17QE26hVNOW3fAji8W2wrwrxdacWOxyXvlxWsVRcKmr+lay1MDqpAMfg==", + "optional": true, "dependencies": { "tslib": "^2.3.1" }, @@ -829,6 +1455,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.272.0.tgz", "integrity": "sha512-sAbDZSTNmLX+UTGwlUHJBWy0QGQkiClpHwVFXACon+aG0ySLNeRKEVYs6NCPYldw4cj6hveLUn50cX44ukHErw==", + "optional": true, "dependencies": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -842,6 +1469,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.272.0.tgz", "integrity": "sha512-Dk3JVjj7SxxoUKv3xGiOeBksvPtFhTDrVW75XJ98Ymv8gJH5L1sq4hIeJAHRKogGiRFq2J73mnZSlM9FVXEylg==", + "optional": true, "dependencies": { "@aws-sdk/middleware-serde": "3.272.0", "@aws-sdk/protocol-http": "3.272.0", @@ -860,6 +1488,7 @@ "version": "3.278.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.278.0.tgz", "integrity": "sha512-oTkF3exy89KE8NgSeXFwD+0H0GRKL2qUw92t3caEj7+4KzU/0m3t7NtKlq2NLRtTJhZ/izYRpV536oogLzGm3g==", + "optional": true, "dependencies": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -873,6 +1502,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.272.0.tgz", "integrity": "sha512-u2SQ0hWrFwxbxxYMG5uMEgf01pQY5jauK/LYWgGIvuCmFgiyRQQP3oN7kkmsxnS9MWmNmhbyQguX2NY02s5e9w==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -885,6 +1515,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.272.0.tgz", "integrity": "sha512-Gp/eKWeUWVNiiBdmUM2qLkBv+VLSJKoWAO+aKmyxxwjjmWhE0FrfA1NQ1a3g+NGMhRbAfQdaYswRAKsul70ISg==", + "optional": true, "dependencies": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -898,6 +1529,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.272.0.tgz", "integrity": "sha512-pCGvHM7C76VbO/dFerH+Vwf7tGv7j+e+eGrvhQ35mRghCtfIou/WMfTZlD1TNee93crrAQQVZKjtW3dMB3WCzg==", + "optional": true, "dependencies": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/service-error-classification": "3.272.0", @@ -915,6 +1547,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.272.0.tgz", "integrity": "sha512-VvYPg7LrDIjUOWueSzo2wBzcNG7dw+cmzV6zAKaLxf0RC5jeAP4hE0OzDiiZfDrjNghEzgq/V+0NO+LewqYL9Q==", + "optional": true, "dependencies": { "@aws-sdk/middleware-signing": "3.272.0", "@aws-sdk/property-provider": "3.272.0", @@ -931,6 +1564,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.272.0.tgz", "integrity": "sha512-kW1uOxgPSwtXPB5rm3QLdWomu42lkYpQL94tM1BjyFOWmBLO2lQhk5a7Dw6HkTozT9a+vxtscLChRa6KZe61Hw==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -943,6 +1577,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.272.0.tgz", "integrity": "sha512-4LChFK4VAR91X+dupqM8fQqYhFGE0G4Bf9rQlVTgGSbi2KUOmpqXzH0/WKE228nKuEhmH8+Qd2VPSAE2JcyAUA==", + "optional": true, "dependencies": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/protocol-http": "3.272.0", @@ -959,6 +1594,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.272.0.tgz", "integrity": "sha512-jhwhknnPBGhfXAGV5GXUWfEhDFoP/DN8MPCO2yC5OAxyp6oVJ8lTPLkZYMTW5VL0c0eG44dXpF4Ib01V+PlDrQ==", + "optional": true, "dependencies": { "tslib": "^2.3.1" }, @@ -970,6 +1606,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.272.0.tgz", "integrity": "sha512-Qy7/0fsDJxY5l0bEk7WKDfqb4Os/sCAgFR2zEvrhDtbkhYPf72ysvg/nRUTncmCbo8tOok4SJii2myk8KMfjjw==", + "optional": true, "dependencies": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -983,6 +1620,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.272.0.tgz", "integrity": "sha512-YYCIBh9g1EQo7hm2l22HX5Yr9RoPQ2RCvhzKvF1n1e8t1QH4iObQrYUtqHG4khcm64Cft8C5MwZmgzHbya5Z6Q==", + "optional": true, "dependencies": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/shared-ini-file-loader": "3.272.0", @@ -997,6 +1635,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.272.0.tgz", "integrity": "sha512-VrW9PjhhngeyYp4yGYPe5S0vgZH6NwU3Po9xAgayUeE37Inr7LS1YteFMHdpgsUUeNXnh7d06CXqHo1XjtqOKA==", + "optional": true, "dependencies": { "@aws-sdk/abort-controller": "3.272.0", "@aws-sdk/protocol-http": "3.272.0", @@ -1012,6 +1651,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.272.0.tgz", "integrity": "sha512-V1pZTaH5eqpAt8O8CzbItHhOtzIfFuWymvwZFkAtwKuaHpnl7jjrTouV482zoq8AD/fF+VVSshwBKYA7bhidIw==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -1024,6 +1664,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.272.0.tgz", "integrity": "sha512-4JQ54v5Yn08jspNDeHo45CaSn1CvTJqS1Ywgr79eU6jBExtguOWv6LNtwVSBD9X37v88iqaxt8iu1Z3pZZAJeg==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -1036,6 +1677,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.272.0.tgz", "integrity": "sha512-ndo++7GkdCj5tBXE6rGcITpSpZS4PfyV38wntGYAlj9liL1omk3bLZRY6uzqqkJpVHqbg2fD7O2qHNItzZgqhw==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "@aws-sdk/util-uri-escape": "3.201.0", @@ -1049,6 +1691,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.272.0.tgz", "integrity": "sha512-5oS4/9n6N1LZW9tI3qq/0GnCuWoOXRgcHVB+AJLRBvDbEe+GI+C/xK1tKLsfpDNgsQJHc4IPQoIt4megyZ/1+A==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -1061,6 +1704,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.272.0.tgz", "integrity": "sha512-REoltM1LK9byyIufLqx9znhSolPcHQgVHIA2S0zu5sdt5qER4OubkLAXuo4MBbisUTmh8VOOvIyUb5ijZCXq1w==", + "optional": true, "engines": { "node": ">=14.0.0" } @@ -1069,6 +1713,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.272.0.tgz", "integrity": "sha512-lzFPohp5sy2XvwFjZIzLVCRpC0i5cwBiaXmFzXYQZJm6FSCszHO4ax+m9yrtlyVFF/2YPWl+/bzNthy4aJtseA==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -1081,6 +1726,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.272.0.tgz", "integrity": "sha512-pWxnHG1NqJWMwlhJ6NHNiUikOL00DHROmxah6krJPMPq4I3am2KY2Rs/8ouWhnEXKaHAv4EQhSALJ+7Mq5S4/A==", + "optional": true, "dependencies": { "@aws-sdk/is-array-buffer": "3.201.0", "@aws-sdk/types": "3.272.0", @@ -1098,6 +1744,7 @@ "version": "3.279.0", "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.279.0.tgz", "integrity": "sha512-ZcYWUQDGAYN6NXRpJuSn46PetrpPCA6TrDVwP9+3pERzTXZ66npXoG2XhHjNrOXy/Ted5A3OxKrM4/zLu9tK3A==", + "optional": true, "dependencies": { "@aws-sdk/middleware-stack": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -1138,6 +1785,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.272.0.tgz", "integrity": "sha512-vX/Tx02PlnQ/Kgtf5TnrNDHPNbY+amLZjW0Z1d9vzAvSZhQ4i9Y18yxoRDIaDTCNVRDjdhV8iuctW+05PB5JtQ==", + "optional": true, "dependencies": { "@aws-sdk/querystring-parser": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -1148,6 +1796,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.208.0.tgz", "integrity": "sha512-PQniZph5A6N7uuEOQi+1hnMz/FSOK/8kMFyFO+4DgA1dZ5pcKcn5wiFwHkcTb/BsgVqQa3Jx0VHNnvhlS8JyTg==", + "optional": true, "dependencies": { "@aws-sdk/util-buffer-from": "3.208.0", "tslib": "^2.3.1" @@ -1160,6 +1809,7 @@ "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz", "integrity": "sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg==", + "optional": true, "dependencies": { "tslib": "^2.3.1" } @@ -1168,6 +1818,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.208.0.tgz", "integrity": "sha512-3zj50e5g7t/MQf53SsuuSf0hEELzMtD8RX8C76f12OSRo2Bca4FLLYHe0TZbxcfQHom8/hOaeZEyTyMogMglqg==", + "optional": true, "dependencies": { "tslib": "^2.3.1" }, @@ -1179,6 +1830,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.208.0.tgz", "integrity": "sha512-7L0XUixNEFcLUGPeBF35enCvB9Xl+K6SQsmbrPk1P3mlV9mguWSDQqbOBwY1Ir0OVbD6H/ZOQU7hI/9RtRI0Zw==", + "optional": true, "dependencies": { "@aws-sdk/is-array-buffer": "3.201.0", "tslib": "^2.3.1" @@ -1191,6 +1843,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.208.0.tgz", "integrity": "sha512-DSRqwrERUsT34ug+anlMBIFooBEGwM8GejC7q00Y/9IPrQy50KnG5PW2NiTjuLKNi7pdEOlwTSEocJE15eDZIg==", + "optional": true, "dependencies": { "tslib": "^2.3.1" }, @@ -1202,6 +1855,7 @@ "version": "3.279.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.279.0.tgz", "integrity": "sha512-RnchYRrpapTT5Hu23LOfk6e8RMVq0kUzho6xA6TJj1a4uGxkcRMvgzPipCq1P5uHu0mrkQBg9pGPEVNOUs38/Q==", + "optional": true, "dependencies": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -1216,6 +1870,7 @@ "version": "3.279.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.279.0.tgz", "integrity": "sha512-A2NB10xReWC+GSnOivKGZ9rnljIZdEP8WMCQQEnA6DJNI19AUFF/O9QJ9y+cHGLKEms7jH86Y99wShdpzAK+Jw==", + "optional": true, "dependencies": { "@aws-sdk/config-resolver": "3.272.0", "@aws-sdk/credential-provider-imds": "3.272.0", @@ -1232,6 +1887,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.272.0.tgz", "integrity": "sha512-c4MPUaJt2G6gGpoiwIOqDfUa98c1J63RpYvf/spQEKOtC/tF5Gfqlxuq8FnAl5lHnrqj1B9ZXLLxFhHtDR0IiQ==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -1244,6 +1900,7 @@ "version": "3.201.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.201.0.tgz", "integrity": "sha512-7t1vR1pVxKx0motd3X9rI3m/xNp78p3sHtP5yo4NP4ARpxyJ0fokBomY8ScaH2D/B+U5o9ARxldJUdMqyBlJcA==", + "optional": true, "dependencies": { "tslib": "^2.3.1" }, @@ -1266,6 +1923,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.272.0.tgz", "integrity": "sha512-Abw8m30arbwxqmeMMha5J11ESpHUNmCeSqSzE8/C4B8jZQtHY4kq7f+upzcNIQ11lsd+uzBEzNG3+dDRi0XOJQ==", + "optional": true, "dependencies": { "tslib": "^2.3.1" }, @@ -1277,6 +1935,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.272.0.tgz", "integrity": "sha512-Ngha5414LR4gRHURVKC9ZYXsEJhMkm+SJ+44wlzOhavglfdcKKPUsibz5cKY1jpUV7oKECwaxHWpBB8r6h+hOg==", + "optional": true, "dependencies": { "@aws-sdk/service-error-classification": "3.272.0", "tslib": "^2.3.1" @@ -1289,6 +1948,7 @@ "version": "3.201.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.201.0.tgz", "integrity": "sha512-TeTWbGx4LU2c5rx0obHeDFeO9HvwYwQtMh1yniBz00pQb6Qt6YVOETVQikRZ+XRQwEyCg/dA375UplIpiy54mA==", + "optional": true, "dependencies": { "tslib": "^2.3.1" }, @@ -1300,6 +1960,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.272.0.tgz", "integrity": "sha512-Lp5QX5bH6uuwBlIdr7w7OAcAI50ttyskb++yUr9i+SPvj6RI2dsfIBaK4mDg1qUdM5LeUdvIyqwj3XHjFKAAvA==", + "optional": true, "dependencies": { "@aws-sdk/types": "3.272.0", "bowser": "^2.11.0", @@ -1310,6 +1971,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.272.0.tgz", "integrity": "sha512-ljK+R3l+Q1LIHrcR+Knhk0rmcSkfFadZ8V+crEGpABf/QUQRg7NkZMsoe814tfBO5F7tMxo8wwwSdaVNNHtoRA==", + "optional": true, "dependencies": { "@aws-sdk/node-config-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -1331,6 +1993,7 @@ "version": "3.254.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8/-/util-utf8-3.254.0.tgz", "integrity": "sha512-14Kso/eIt5/qfIBmhEL9L1IfyUqswjSTqO2mY7KOzUZ9SZbwn3rpxmtkhmATkRjD7XIlLKaxBkI7tU9Zjzj8Kw==", + "optional": true, "dependencies": { "@aws-sdk/util-buffer-from": "3.208.0", "tslib": "^2.3.1" @@ -3007,13 +3670,13 @@ "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==" }, "node_modules/@sentry/node": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz", - "integrity": "sha512-S2pcLBUsCg3+WYlyQbXKPey3H1yaZPt9fpJTPAoabbidYXweBAbtFjSCRwPFwCQMzWusWxlYEwWRT77C07pHVg==", + "version": "7.41.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.41.0.tgz", + "integrity": "sha512-JAz0TAVvq4z7dNrDBECLB5Fd4bGDGXSOR1f63iV/8fUNsZPqqdh8LXnctPXTpcTjsdlfmjUL1CC2RQ6yKYMtTQ==", "dependencies": { - "@sentry/core": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", + "@sentry/core": "7.41.0", + "@sentry/types": "7.41.0", + "@sentry/utils": "7.41.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", @@ -3023,39 +3686,6 @@ "node": ">=8" } }, - "node_modules/@sentry/node/node_modules/@sentry/core": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.40.0.tgz", - "integrity": "sha512-OPAobQG0GTY++r5LWUcOA1lS+2TY2Lmw/i5s4kL9WbY+f08dbLNEGNBObY7/V98OL4f7OG+nWaPFybgM7kqUTQ==", - "dependencies": { - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/node/node_modules/@sentry/types": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.40.0.tgz", - "integrity": "sha512-dIbqBenbmDx1F8pvfC11C88J83ecwumUhV+YOIxcmVd1fmlPF2hXWZ01+NTkTDkCu341sJx4wPQogByFy8FwGA==", - "engines": { - "node": ">=8" - } - }, - "node_modules/@sentry/node/node_modules/@sentry/utils": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.40.0.tgz", - "integrity": "sha512-ZdCbTpAXPiVVfvNJVftnDhsctOui71MDUhVIdLkgg4Cuic+WHGPRmmZ+H6uZdp7vRaeB+Uvnn5+t2iSAVo/mAA==", - "dependencies": { - "@sentry/types": "7.40.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/@sentry/node/node_modules/tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -3314,9 +3944,9 @@ } }, "node_modules/@types/jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz", - "integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -4081,9 +4711,9 @@ } }, "node_modules/aws-sdk": { - "version": "2.1324.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1324.0.tgz", - "integrity": "sha512-7T9Jn6qtzCANdqRcdhxZ9Fx31/U+h/VPFxEU3+sFEnC7WtGtRlgmsJOY2lIdFKRXkHYT3Jw5MqDyjnb/i1QqbA==", + "version": "2.1338.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1338.0.tgz", + "integrity": "sha512-apxv53ABuvi87UQHAUqRrJOaGNMiPXAe6bizzJhOnsaNqasg2KjDDit7QSCi6HlLNG44n1ApIvMtR/k+NnxU4Q==", "dependencies": { "buffer": "4.9.2", "events": "1.1.1", @@ -7634,9 +8264,9 @@ } }, "node_modules/mongoose": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.10.1.tgz", - "integrity": "sha512-QBXX27XXCEIi1zqimTBjlZsFFg2YzK9uQfuAIqG1AyIEeyKiiCaobD0Tst2ULwiX4PRB53VLRQvAqyu5TwvOZg==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.10.3.tgz", + "integrity": "sha512-fZ3pIlQn7lM632r1l4qiU58lKrJ+FufKVG8TNeRXSChAeu9alCl5KoQ9bLw4jnQNYevSq9o+sqZmFDHP+EVW3g==", "dependencies": { "bson": "^4.7.0", "kareem": "2.5.1", @@ -7647,7 +8277,7 @@ "sift": "16.0.1" }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" }, "funding": { "type": "opencollective", @@ -10740,9 +11370,9 @@ } }, "node_modules/posthog-node": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.4.tgz", - "integrity": "sha512-CdywlVh0CZU05/3MrBc0qY/zsLdU2X9XSz/yL1qMRhbyZhD8lrnuGlI69G2cpzZtli6S/nu64wcmULz/mFFA5w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.6.0.tgz", + "integrity": "sha512-/BiFw/jwdP0uJSRAIoYqLoBTjZ612xv74b1L/a3T/p1nJVL8e0OrHuxbJW56c6WVW/IKm9gBF/zhbqfaz0XgJQ==", "dependencies": { "axios": "^0.27.0" }, @@ -12581,6 +13211,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.272.0.tgz", "integrity": "sha512-s2TV3phapcTwZNr4qLxbfuQuE9ZMP4RoJdkvRRCkKdm6jslsWLJf2Zlcxti/23hOlINUMYv2iXE2pftIgWGdpg==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -12630,224 +13261,703 @@ } }, "@aws-sdk/client-secrets-manager": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.281.0.tgz", - "integrity": "sha512-vL+LygNDoCja/47pFwSnI3tCWfy5SGHlz6C1whcszmfl81gXyuHNlAK8RXVRiKhnD11zmCsHJhZyYSfTz5RErQ==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-secrets-manager/-/client-secrets-manager-3.294.0.tgz", + "integrity": "sha512-L3fcQiHqOPkc3odrWCVRrhC838p8mFLZxRpQPHz3YCxhSbeclhPjqHNyoljkHmoaqzo+e0owqkM3kYyfereGcg==", "requires": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/client-sts": "3.281.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/credential-provider-node": "3.281.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-signing": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/client-sts": "3.294.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/credential-provider-node": "3.294.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-signing": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "tslib": "^2.3.1", "uuid": "^8.3.2" }, "dependencies": { + "@aws-sdk/abort-controller": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/abort-controller/-/abort-controller-3.292.0.tgz", + "integrity": "sha512-lf+OPptL01kvryIJy7+dvFux5KbJ6OTwLPPEekVKZ2AfEvwcVtOZWFUhyw3PJCBTVncjKB1Kjl3V/eTS3YuPXQ==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, "@aws-sdk/client-sso": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.281.0.tgz", - "integrity": "sha512-3RvO5zClQhu37w9VMLoHPGk58S3y8Spb7XX8rW51bm5TUglYQskQ0X2VLEUW/7ZGx/peokHws9Z9+w5yGq5sdA==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.294.0.tgz", + "integrity": "sha512-+FuxQTi5WvnaXM5JbNLkBIzQ3An4gA0ox61N1u+3xled+nywKb1yQ7WmRpyMG5bLbkmnj3aqoo5/uskFc4c4EA==", "requires": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "tslib": "^2.3.1" } }, "@aws-sdk/client-sso-oidc": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.281.0.tgz", - "integrity": "sha512-P6zf9pDuxApVoCYStAg7L8BU9AcWI8PxfLSX4r2WnmcQropxzPJ3op1j9nvbwwBDMFWephijVY4AVp8MqPcPyg==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.294.0.tgz", + "integrity": "sha512-/ZfDud76MdSPJ/TxjV2xLE30XbBQDZwKQ32axwoK1eziPvrAIUBYVgpBwj+m0quhoiQhBKkg3aFl6j39AF2thw==", "requires": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "tslib": "^2.3.1" } }, "@aws-sdk/client-sts": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.281.0.tgz", - "integrity": "sha512-w8QomyhCVEArRcXgOkjbofiS/PLEKWRAyYBovjMS1cGhns2ZYJXFgHNgr3VGE54TghUc5dR1CqKuBKKM4ThrgA==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sts/-/client-sts-3.294.0.tgz", + "integrity": "sha512-AefqwhFjTDzelZuSYhriJbiI+GQwf2yKiKAnCt0gRj6rswewStM63Gtlhfb01sFPp+ZiqPcyQ47LqUaHp1mz/g==", "requires": { "@aws-crypto/sha256-browser": "3.0.0", "@aws-crypto/sha256-js": "3.0.0", - "@aws-sdk/config-resolver": "3.272.0", - "@aws-sdk/credential-provider-node": "3.281.0", - "@aws-sdk/fetch-http-handler": "3.272.0", - "@aws-sdk/hash-node": "3.272.0", - "@aws-sdk/invalid-dependency": "3.272.0", - "@aws-sdk/middleware-content-length": "3.272.0", - "@aws-sdk/middleware-endpoint": "3.272.0", - "@aws-sdk/middleware-host-header": "3.278.0", - "@aws-sdk/middleware-logger": "3.272.0", - "@aws-sdk/middleware-recursion-detection": "3.272.0", - "@aws-sdk/middleware-retry": "3.272.0", - "@aws-sdk/middleware-sdk-sts": "3.272.0", - "@aws-sdk/middleware-serde": "3.272.0", - "@aws-sdk/middleware-signing": "3.272.0", - "@aws-sdk/middleware-stack": "3.272.0", - "@aws-sdk/middleware-user-agent": "3.272.0", - "@aws-sdk/node-config-provider": "3.272.0", - "@aws-sdk/node-http-handler": "3.272.0", - "@aws-sdk/protocol-http": "3.272.0", - "@aws-sdk/smithy-client": "3.279.0", - "@aws-sdk/types": "3.272.0", - "@aws-sdk/url-parser": "3.272.0", - "@aws-sdk/util-base64": "3.208.0", - "@aws-sdk/util-body-length-browser": "3.188.0", - "@aws-sdk/util-body-length-node": "3.208.0", - "@aws-sdk/util-defaults-mode-browser": "3.279.0", - "@aws-sdk/util-defaults-mode-node": "3.279.0", - "@aws-sdk/util-endpoints": "3.272.0", - "@aws-sdk/util-retry": "3.272.0", - "@aws-sdk/util-user-agent-browser": "3.272.0", - "@aws-sdk/util-user-agent-node": "3.272.0", - "@aws-sdk/util-utf8": "3.254.0", + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/credential-provider-node": "3.294.0", + "@aws-sdk/fetch-http-handler": "3.292.0", + "@aws-sdk/hash-node": "3.292.0", + "@aws-sdk/invalid-dependency": "3.292.0", + "@aws-sdk/middleware-content-length": "3.292.0", + "@aws-sdk/middleware-endpoint": "3.292.0", + "@aws-sdk/middleware-host-header": "3.292.0", + "@aws-sdk/middleware-logger": "3.292.0", + "@aws-sdk/middleware-recursion-detection": "3.292.0", + "@aws-sdk/middleware-retry": "3.293.0", + "@aws-sdk/middleware-sdk-sts": "3.292.0", + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/middleware-signing": "3.292.0", + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/middleware-user-agent": "3.293.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/node-http-handler": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/smithy-client": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "@aws-sdk/util-body-length-browser": "3.292.0", + "@aws-sdk/util-body-length-node": "3.292.0", + "@aws-sdk/util-defaults-mode-browser": "3.292.0", + "@aws-sdk/util-defaults-mode-node": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "@aws-sdk/util-retry": "3.292.0", + "@aws-sdk/util-user-agent-browser": "3.292.0", + "@aws-sdk/util-user-agent-node": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", "fast-xml-parser": "4.1.2", "tslib": "^2.3.1" } }, - "@aws-sdk/credential-provider-ini": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.281.0.tgz", - "integrity": "sha512-H99nhMhHImQKgNhHKYc6usTS6UK8KzCcVGpILLVTuP97YlrYAMFAVstA3Xk6mZ28JAbHVXvI6vJjkMNOzCSKCA==", + "@aws-sdk/config-resolver": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.292.0.tgz", + "integrity": "sha512-cB3twnNR7vYvlt2jvw8VlA1+iv/tVzl+/S39MKqw2tepU+AbJAM0EHwb/dkf1OKSmlrnANXhshx80MHF9zL4mA==", "requires": { - "@aws-sdk/credential-provider-env": "3.272.0", - "@aws-sdk/credential-provider-imds": "3.272.0", - "@aws-sdk/credential-provider-process": "3.272.0", - "@aws-sdk/credential-provider-sso": "3.281.0", - "@aws-sdk/credential-provider-web-identity": "3.272.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-config-provider": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.292.0.tgz", + "integrity": "sha512-YbafSG0ZEKE2969CJWVtUhh3hfOeLPecFVoXOtegCyAJgY5Ghtu4TsVhL4DgiGAgOC30ojAmUVQEXzd7xJF5xA==", + "requires": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-imds": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.292.0.tgz", + "integrity": "sha512-W/peOgDSRYulgzFpUhvgi1pCm6piBz6xrVN17N4QOy+3NHBXRVMVzYk6ct2qpLPgJUSEZkcpP+Gds+bBm8ed1A==", + "requires": { + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.294.0.tgz", + "integrity": "sha512-pdTPbaAb5bWA+DnuKoL2TpXeNDp6Ejpv/OYt+bw2gdzl9w5r/ZCtUTTbW+Vvejr4WL5s3c1bY96kwdqCn7iLqA==", + "requires": { + "@aws-sdk/credential-provider-env": "3.292.0", + "@aws-sdk/credential-provider-imds": "3.292.0", + "@aws-sdk/credential-provider-process": "3.292.0", + "@aws-sdk/credential-provider-sso": "3.294.0", + "@aws-sdk/credential-provider-web-identity": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-node": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.281.0.tgz", - "integrity": "sha512-jhddd+lJp8G8hBJ+6glmXjfWJT3nxiE1aliH3fBC4RR3D+1kRXc99Xg6mbUb8bm+GrVZ4gzfiqSgg+ByKjd7xA==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.294.0.tgz", + "integrity": "sha512-zUL1Qhb4BsQIZCs/TPpG4oIYH/9YsGiS+Se1tasSGjTOLfBy7jhOZ0QIdpEeyAx/EP8blOBredM9xWfEXgiHVA==", "requires": { - "@aws-sdk/credential-provider-env": "3.272.0", - "@aws-sdk/credential-provider-imds": "3.272.0", - "@aws-sdk/credential-provider-ini": "3.281.0", - "@aws-sdk/credential-provider-process": "3.272.0", - "@aws-sdk/credential-provider-sso": "3.281.0", - "@aws-sdk/credential-provider-web-identity": "3.272.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/credential-provider-env": "3.292.0", + "@aws-sdk/credential-provider-imds": "3.292.0", + "@aws-sdk/credential-provider-ini": "3.294.0", + "@aws-sdk/credential-provider-process": "3.292.0", + "@aws-sdk/credential-provider-sso": "3.294.0", + "@aws-sdk/credential-provider-web-identity": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.292.0.tgz", + "integrity": "sha512-CFVXuMuUvg/a4tknzRikEDwZBnKlHs1LZCpTXIGjBdUTdosoi4WNzDLzGp93ZRTtcgFz+4wirz2f7P3lC0NrQw==", + "requires": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", "tslib": "^2.3.1" } }, "@aws-sdk/credential-provider-sso": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.281.0.tgz", - "integrity": "sha512-IqJnpXuLpJYoSCf/Rt66/CPVTjfkam3z9+ZvlQJV+VbK+vGj276qEtTmSN3XPZZgF1XbWptvkzIWDszLhHiZmg==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.294.0.tgz", + "integrity": "sha512-UxrcAA/0l7j9+3tolYcG5M61D/IE1Bjd/9H87H1i2A2BrwUUBhW1Dp/vvROEDrrywlMDG3CDF3T/7ADtTak+sg==", "requires": { - "@aws-sdk/client-sso": "3.281.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/token-providers": "3.281.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/client-sso": "3.294.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/token-providers": "3.294.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.292.0.tgz", + "integrity": "sha512-4DbtIEM9gGVfqYlMdYXg3XY+vBhemjB1zXIequottW8loLYM8Vuz4/uGxxKNze6evVVzowsA0wKrYclE1aj/Rg==", + "requires": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/fetch-http-handler": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.292.0.tgz", + "integrity": "sha512-zh3bhUJbL8RSa39ZKDcy+AghtUkIP8LwcNlwRIoxMQh3Row4D1s4fCq0KZCx98NJBEXoiTLyTQlZxxI//BOb1Q==", + "requires": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/querystring-builder": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-base64": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/hash-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.292.0.tgz", + "integrity": "sha512-1yLxmIsvE+eK36JXEgEIouTITdykQLVhsA5Oai//Lar6Ddgu1sFpLDbdkMtKbrh4I0jLN9RacNCkeVQjZPTCCQ==", + "requires": { + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-buffer-from": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/invalid-dependency": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.292.0.tgz", + "integrity": "sha512-39OUV78CD3TmEbjhpt+V+Fk4wAGWhixqHxDSN8+4WL0uB4Fl7k5m3Z9hNY78AttHQSl2twR7WtLztnXPAFsriw==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/is-array-buffer": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.292.0.tgz", + "integrity": "sha512-kW/G5T/fzI0sJH5foZG6XJiNCevXqKLxV50qIT4B1pMuw7regd4ALIy0HwSqj1nnn9mSbRWBfmby0jWCJsMcwg==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-content-length": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.292.0.tgz", + "integrity": "sha512-2gMWzQus5mj14menolpPDbYBeaOYcj7KNFZOjTjjI3iQ0KqyetG6XasirNrcJ/8QX1BRmpTol8Xjp2Ue3Gbzwg==", + "requires": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-endpoint": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.292.0.tgz", + "integrity": "sha512-cPMkiSxpZGG6tYlW4OS+ucS6r43f9ddX9kcUoemJCY10MOuogdPjulCAjE0HTs2PLKSOrrG4CTP4Q4wWDrH4Bw==", + "requires": { + "@aws-sdk/middleware-serde": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/url-parser": "3.292.0", + "@aws-sdk/util-config-provider": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-host-header": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.292.0.tgz", + "integrity": "sha512-mHuCWe3Yg2S5YZ7mB7sKU6C97XspfqrimWjMW9pfV2usAvLA3R0HrB03jpR5vpZ3P4q7HB6wK3S6CjYMGGRNag==", + "requires": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-logger": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.292.0.tgz", + "integrity": "sha512-yZNY1XYmG3NG+uonET7jzKXNiwu61xm/ZZ6i/l51SusuaYN+qQtTAhOFsieQqTehF9kP4FzbsWgPDwD8ZZX9lw==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-recursion-detection": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.292.0.tgz", + "integrity": "sha512-kA3VZpPko0Zqd7CYPTKAxhjEv0HJqFu2054L04dde1JLr43ro+2MTdX7vsHzeAFUVRphqatFFofCumvXmU6Mig==", + "requires": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-retry": { + "version": "3.293.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.293.0.tgz", + "integrity": "sha512-7tiaz2GzRecNHaZ6YnF+Nrtk3au8qF6oiipf11R7MJiqJ0fkMLnz/iRrlakDziS9qF/a9v+3yxb4W4NHK3f4Tw==", + "requires": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/service-error-classification": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "@aws-sdk/util-retry": "3.292.0", + "tslib": "^2.3.1", + "uuid": "^8.3.2" + } + }, + "@aws-sdk/middleware-sdk-sts": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.292.0.tgz", + "integrity": "sha512-GN5ZHEqXZqDi+HkVbaXRX9HaW/vA5rikYpWKYsmxTUZ7fB7ijvEO3co3lleJv2C+iGYRtUIHC4wYNB5xgoTCxg==", + "requires": { + "@aws-sdk/middleware-signing": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-serde": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.292.0.tgz", + "integrity": "sha512-6hN9mTQwSvV8EcGvtXbS/MpK7WMCokUku5Wu7X24UwCNMVkoRHLIkYcxHcvBTwttuOU0d8hph1/lIX4dkLwkQw==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-signing": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.292.0.tgz", + "integrity": "sha512-GVfoSjDjEQ4TaO6x9MffyP3uRV+2KcS5FtexLCYOM9pJcnE9tqq9FJOrZ1xl1g+YjUVKxo4x8lu3tpEtIb17qg==", + "requires": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/signature-v4": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-stack": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.292.0.tgz", + "integrity": "sha512-WdQpRkuMysrEwrkByCM1qCn2PPpFGGQ2iXqaFha5RzCdZDlxJni9cVNb6HzWUcgjLEYVTXCmOR9Wxm3CNW44Qg==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "3.293.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.293.0.tgz", + "integrity": "sha512-gZ7/e6XwpKk9mvgA78q4Ffc796jTn02TUKx2qMDnkLVbeJXBNN2jnvYEKq8v70+o7fd/ALRudg8gBDmkkhM/Hw==", + "requires": { + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-endpoints": "3.293.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/node-config-provider": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.292.0.tgz", + "integrity": "sha512-S3NnC9dQ5GIbJYSDIldZb4zdpCOEua1tM7bjYL3VS5uqCEM93kIi/o/UkIUveMp/eqTS2LJa5HjNIz5Te6je0A==", + "requires": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/node-http-handler": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.292.0.tgz", + "integrity": "sha512-L/E3UDSwXLXjt1XWWh0RBD55F+aZI1AEdPwdES9i1PjnZLyuxuDhEDptVibNN56+I9/4Q3SbmuVRVlOD0uzBag==", + "requires": { + "@aws-sdk/abort-controller": "3.292.0", + "@aws-sdk/protocol-http": "3.292.0", + "@aws-sdk/querystring-builder": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/property-provider": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.292.0.tgz", + "integrity": "sha512-dHArSvsiqhno/g55N815gXmAMrmN8DP7OeFNqJ4wJG42xsF2PFN3DAsjIuHuXMwu+7A3R1LHqIpvv0hA9KeoJQ==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/protocol-http": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.292.0.tgz", + "integrity": "sha512-NLi4fq3k41aXIh1I97yX0JTy+3p6aW1NdwFwdMa674z86QNfb4SfRQRZBQe9wEnAZ/eWHVnlKIuII+U1URk/Kg==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/querystring-builder": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.292.0.tgz", + "integrity": "sha512-XElIFJaReIm24eEvBtV2dOtZvcm3gXsGu/ftG8MLJKbKXFKpAP1q+K6En0Bs7/T88voKghKdKpKT+eZUWgTqlg==", + "requires": { + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-uri-escape": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/querystring-parser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.292.0.tgz", + "integrity": "sha512-iTYpYo7a8X9RxiPbjjewIpm6XQPx2EOcF1dWCPRII9EFlmZ4bwnX+PDI36fIo9oVs8TIKXmwNGODU9nsg7CSAw==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/service-error-classification": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.292.0.tgz", + "integrity": "sha512-X1k3sixCeC45XSNHBe+kRBQBwPDyTFtFITb8O5Qw4dS9XWGhrUJT4CX0qE5aj8qP3F9U5nRizs9c2mBVVP0Caw==" + }, + "@aws-sdk/shared-ini-file-loader": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.292.0.tgz", + "integrity": "sha512-Av2TTYg1Jig2kbkD56ybiqZJB6vVrYjv1W5UQwY/q3nA/T2mcrgQ20ByCOt5Bv9VvY7FSgC+znj+L4a7RLGmBg==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/signature-v4": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.292.0.tgz", + "integrity": "sha512-+rw47VY5mvBecn13tDQTl1ipGWg5tE63faWgmZe68HoBL87ZiDzsd7bUKOvjfW21iMgWlwAppkaNNQayYRb2zg==", + "requires": { + "@aws-sdk/is-array-buffer": "3.292.0", + "@aws-sdk/types": "3.292.0", + "@aws-sdk/util-hex-encoding": "3.292.0", + "@aws-sdk/util-middleware": "3.292.0", + "@aws-sdk/util-uri-escape": "3.292.0", + "@aws-sdk/util-utf8": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/smithy-client": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.292.0.tgz", + "integrity": "sha512-S8PKzjPkZ6SXYZuZiU787dMsvQ0d/LFEhw2OI4Oe2An9Fc2IwJ2FYukyHoQJOV2tV0DiuMebPo7eMyQyjKElvA==", + "requires": { + "@aws-sdk/middleware-stack": "3.292.0", + "@aws-sdk/types": "3.292.0", "tslib": "^2.3.1" } }, "@aws-sdk/token-providers": { - "version": "3.281.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.281.0.tgz", - "integrity": "sha512-36Vg/F6Edm7qdjcTeNVON+sK2edgHhmhTtAEjWcuUk5AX/Et+Ate/A2N8HD3nxwlAcgidfnBC9SHYJatbhcEnQ==", + "version": "3.294.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.294.0.tgz", + "integrity": "sha512-6nwO04LtC5f4AsUvGZXyjaswuEK4Rr2VsuANpMKrPCgunRfI58a8YXLniudOSXN6e7CFJ6M3uo/h5YXqtnzGug==", "requires": { - "@aws-sdk/client-sso-oidc": "3.281.0", - "@aws-sdk/property-provider": "3.272.0", - "@aws-sdk/shared-ini-file-loader": "3.272.0", - "@aws-sdk/types": "3.272.0", + "@aws-sdk/client-sso-oidc": "3.294.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/shared-ini-file-loader": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/types": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.292.0.tgz", + "integrity": "sha512-1teYAY2M73UXZxMAxqZxVS2qwXjQh0OWtt7qyLfha0TtIk/fZ1hRwFgxbDCHUFcdNBSOSbKH/ESor90KROXLCQ==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/url-parser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.292.0.tgz", + "integrity": "sha512-NZeAuZCk1x6TIiWuRfbOU6wHPBhf0ly2qOHzWut4BCH+b4RrDmFF8EmXcH1auEfGhE7yRyR6XqIN0t3S+hYACA==", + "requires": { + "@aws-sdk/querystring-parser": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-base64": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.292.0.tgz", + "integrity": "sha512-zjNCwNdy617yFvEjZorepNWXB2sQCVfsShCwFy/kIQ5iW5tT2jQKaqc0K77diU9atkooxw9p1W9m9sOgrkOFNw==", + "requires": { + "@aws-sdk/util-buffer-from": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-body-length-browser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.292.0.tgz", + "integrity": "sha512-Wd/BM+JsMiKvKs/bN3z6TredVEHh2pKudGfg3CSjTRpqFpOG903KDfyHBD42yg5PuCHoHoewJvTPKwgn7/vhaw==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-body-length-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.292.0.tgz", + "integrity": "sha512-BBgipZ2P6RhogWE/qj0oqpdlyd3iSBYmb+aD/TBXwB2lA/X8A99GxweBd/kp06AmcJRoMS9WIXgbWkiiBlRlSA==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-buffer-from": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.292.0.tgz", + "integrity": "sha512-RxNZjLoXNxHconH9TYsk5RaEBjSgTtozHeyIdacaHPj5vlQKi4hgL2hIfKeeNiAfQEVjaUFF29lv81xpNMzVMQ==", + "requires": { + "@aws-sdk/is-array-buffer": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-config-provider": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.292.0.tgz", + "integrity": "sha512-t3noYll6bPRSxeeNNEkC5czVjAiTPcsq00OwfJ2xyUqmquhLEfLwoJKmrT1uP7DjIEXdUtfoIQ2jWiIVm/oO5A==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-defaults-mode-browser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.292.0.tgz", + "integrity": "sha512-7+zVUlMGfa8/KT++9humHo6IDxTnxMCmWUj5jVNlkpk6h7Ecmppf7aXotviyVIA43lhtz0p2AErs0N0ekEUK+w==", + "requires": { + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-defaults-mode-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.292.0.tgz", + "integrity": "sha512-SSIw85eF4BVs0fOJRyshT+R3b/UmBPhiVKCUZm2rq6+lIGkDPiSwQU3d/80AhXtiL5SFT/IzAKKgQd8qMa7q3A==", + "requires": { + "@aws-sdk/config-resolver": "3.292.0", + "@aws-sdk/credential-provider-imds": "3.292.0", + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/property-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-endpoints": { + "version": "3.293.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.293.0.tgz", + "integrity": "sha512-R/99aNV49Refpv5guiUjEUrZYlvnfaNBniB+/ZtMO3ixxUopapssCrUivuJrmhccmrYaTCZw7dRzIWjU1jJhKg==", + "requires": { + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-hex-encoding": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.292.0.tgz", + "integrity": "sha512-qBd5KFIUywQ3qSSbj814S2srk0vfv8A6QMI+Obs1y2LHZFdQN5zViptI4UhXhKOHe+NnrHWxSuLC/LMH6q3SmA==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-middleware": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.292.0.tgz", + "integrity": "sha512-KjhS7flfoBKDxbiBZjLjMvEizXgjfQb7GQEItgzGoI9rfGCmZtvqCcqQQoIlxb8bIzGRggAUHtBGWnlLbpb+GQ==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-retry": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.292.0.tgz", + "integrity": "sha512-JEHyF7MpVeRF5uR4LDYgpOKcFpOPiAj8TqN46SVOQQcL1K+V7cSr7O7N7J6MwJaN9XOzAcBadeIupMm7/BFbgw==", + "requires": { + "@aws-sdk/service-error-classification": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-uri-escape": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.292.0.tgz", + "integrity": "sha512-hOQtUMQ4VcQ9iwKz50AoCp1XBD5gJ9nly/gJZccAM7zSA5mOO8RRKkbdonqquVHxrO0CnYgiFeCh3V35GFecUw==", + "requires": { + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.292.0.tgz", + "integrity": "sha512-dld+lpC3QdmTQHdBWJ0WFDkXDSrJgfz03q6mQ8+7H+BC12ZhT0I0g9iuvUjolqy7QR00OxOy47Y9FVhq8EC0Gg==", + "requires": { + "@aws-sdk/types": "3.292.0", + "bowser": "^2.11.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.292.0.tgz", + "integrity": "sha512-f+NfIMal5E61MDc5WGhUEoicr7b1eNNhA+GgVdSB/Hg5fYhEZvFK9RZizH5rrtsLjjgcr9nPYSR7/nDKCJLumw==", + "requires": { + "@aws-sdk/node-config-provider": "3.292.0", + "@aws-sdk/types": "3.292.0", + "tslib": "^2.3.1" + } + }, + "@aws-sdk/util-utf8": { + "version": "3.292.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8/-/util-utf8-3.292.0.tgz", + "integrity": "sha512-FPkj+Z59/DQWvoVu2wFaRncc3KVwe/pgK3MfVb0Lx+Ibey5KUx+sNpJmYcVYHUAe/Nv/JeIpOtYuC96IXOnI6w==", + "requires": { + "@aws-sdk/util-buffer-from": "3.292.0", "tslib": "^2.3.1" } } @@ -12981,6 +14091,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/config-resolver/-/config-resolver-3.272.0.tgz", "integrity": "sha512-Dr4CffRVNsOp3LRNdpvcH6XuSgXOSLblWliCy/5I86cNl567KVMxujVx6uPrdTXYs2h1rt3MNl6jQGnAiJeTbw==", + "optional": true, "requires": { "@aws-sdk/signature-v4": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13005,6 +14116,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.272.0.tgz", "integrity": "sha512-QI65NbLnKLYHyTYhXaaUrq6eVsCCrMUb05WDA7+TJkWkjXesovpjc8vUKgFiLSxmgKmb2uOhHNcDyObKMrYQFw==", + "optional": true, "requires": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13015,6 +14127,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-imds/-/credential-provider-imds-3.272.0.tgz", "integrity": "sha512-wwAfVY1jTFQEfxVfdYD5r5ieYGl+0g4nhekVxNMqE8E1JeRDd18OqiwAflzpgBIqxfqvCUkf+vl5JYyacMkNAQ==", + "optional": true, "requires": { "@aws-sdk/node-config-provider": "3.272.0", "@aws-sdk/property-provider": "3.272.0", @@ -13062,6 +14175,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.272.0.tgz", "integrity": "sha512-hiCAjWWm2PeBFp5cjkxqyam/XADjiS+e7GzwC34TbZn3LisS0uoweLojj9tD11NnnUhyhbLteUvu5+rotOLwrg==", + "optional": true, "requires": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/shared-ini-file-loader": "3.272.0", @@ -13087,6 +14201,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.272.0.tgz", "integrity": "sha512-ImrHMkcgneGa/HadHAQXPwOrX26sAKuB8qlMxZF/ZCM2B55u8deY+ZVkVuraeKb7YsahMGehPFOfRAF6mvFI5Q==", + "optional": true, "requires": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13120,6 +14235,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/fetch-http-handler/-/fetch-http-handler-3.272.0.tgz", "integrity": "sha512-1Qhm9e0RbS1Xf4CZqUbQyUMkDLd7GrsRXWIvm9b86/vgeV8/WnjO3CMue9D51nYgcyQORhYXv6uVjAYCWbUExA==", + "optional": true, "requires": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/querystring-builder": "3.272.0", @@ -13132,6 +14248,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/hash-node/-/hash-node-3.272.0.tgz", "integrity": "sha512-40dwND+iAm3VtPHPZu7/+CIdVJFk2s0cWZt1lOiMPMSXycSYJ45wMk7Lly3uoqRx0uWfFK5iT2OCv+fJi5jTng==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "@aws-sdk/util-buffer-from": "3.208.0", @@ -13143,6 +14260,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/invalid-dependency/-/invalid-dependency-3.272.0.tgz", "integrity": "sha512-ysW6wbjl1Y78txHUQ/Tldj2Rg1BI7rpMO9B9xAF6yAX3mQ7t6SUPQG/ewOGvH2208NBIl3qP5e/hDf0Q6r/1iw==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13152,6 +14270,7 @@ "version": "3.201.0", "resolved": "https://registry.npmjs.org/@aws-sdk/is-array-buffer/-/is-array-buffer-3.201.0.tgz", "integrity": "sha512-UPez5qLh3dNgt0DYnPD/q0mVJY84rA17QE26hVNOW3fAji8W2wrwrxdacWOxyXvlxWsVRcKmr+lay1MDqpAMfg==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13160,6 +14279,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-content-length/-/middleware-content-length-3.272.0.tgz", "integrity": "sha512-sAbDZSTNmLX+UTGwlUHJBWy0QGQkiClpHwVFXACon+aG0ySLNeRKEVYs6NCPYldw4cj6hveLUn50cX44ukHErw==", + "optional": true, "requires": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13170,6 +14290,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint/-/middleware-endpoint-3.272.0.tgz", "integrity": "sha512-Dk3JVjj7SxxoUKv3xGiOeBksvPtFhTDrVW75XJ98Ymv8gJH5L1sq4hIeJAHRKogGiRFq2J73mnZSlM9FVXEylg==", + "optional": true, "requires": { "@aws-sdk/middleware-serde": "3.272.0", "@aws-sdk/protocol-http": "3.272.0", @@ -13185,6 +14306,7 @@ "version": "3.278.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.278.0.tgz", "integrity": "sha512-oTkF3exy89KE8NgSeXFwD+0H0GRKL2qUw92t3caEj7+4KzU/0m3t7NtKlq2NLRtTJhZ/izYRpV536oogLzGm3g==", + "optional": true, "requires": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13195,6 +14317,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.272.0.tgz", "integrity": "sha512-u2SQ0hWrFwxbxxYMG5uMEgf01pQY5jauK/LYWgGIvuCmFgiyRQQP3oN7kkmsxnS9MWmNmhbyQguX2NY02s5e9w==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13204,6 +14327,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.272.0.tgz", "integrity": "sha512-Gp/eKWeUWVNiiBdmUM2qLkBv+VLSJKoWAO+aKmyxxwjjmWhE0FrfA1NQ1a3g+NGMhRbAfQdaYswRAKsul70ISg==", + "optional": true, "requires": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13214,6 +14338,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-retry/-/middleware-retry-3.272.0.tgz", "integrity": "sha512-pCGvHM7C76VbO/dFerH+Vwf7tGv7j+e+eGrvhQ35mRghCtfIou/WMfTZlD1TNee93crrAQQVZKjtW3dMB3WCzg==", + "optional": true, "requires": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/service-error-classification": "3.272.0", @@ -13228,6 +14353,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-sts/-/middleware-sdk-sts-3.272.0.tgz", "integrity": "sha512-VvYPg7LrDIjUOWueSzo2wBzcNG7dw+cmzV6zAKaLxf0RC5jeAP4hE0OzDiiZfDrjNghEzgq/V+0NO+LewqYL9Q==", + "optional": true, "requires": { "@aws-sdk/middleware-signing": "3.272.0", "@aws-sdk/property-provider": "3.272.0", @@ -13241,6 +14367,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-serde/-/middleware-serde-3.272.0.tgz", "integrity": "sha512-kW1uOxgPSwtXPB5rm3QLdWomu42lkYpQL94tM1BjyFOWmBLO2lQhk5a7Dw6HkTozT9a+vxtscLChRa6KZe61Hw==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13250,6 +14377,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-signing/-/middleware-signing-3.272.0.tgz", "integrity": "sha512-4LChFK4VAR91X+dupqM8fQqYhFGE0G4Bf9rQlVTgGSbi2KUOmpqXzH0/WKE228nKuEhmH8+Qd2VPSAE2JcyAUA==", + "optional": true, "requires": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/protocol-http": "3.272.0", @@ -13263,6 +14391,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-stack/-/middleware-stack-3.272.0.tgz", "integrity": "sha512-jhwhknnPBGhfXAGV5GXUWfEhDFoP/DN8MPCO2yC5OAxyp6oVJ8lTPLkZYMTW5VL0c0eG44dXpF4Ib01V+PlDrQ==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13271,6 +14400,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.272.0.tgz", "integrity": "sha512-Qy7/0fsDJxY5l0bEk7WKDfqb4Os/sCAgFR2zEvrhDtbkhYPf72ysvg/nRUTncmCbo8tOok4SJii2myk8KMfjjw==", + "optional": true, "requires": { "@aws-sdk/protocol-http": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13281,6 +14411,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/node-config-provider/-/node-config-provider-3.272.0.tgz", "integrity": "sha512-YYCIBh9g1EQo7hm2l22HX5Yr9RoPQ2RCvhzKvF1n1e8t1QH4iObQrYUtqHG4khcm64Cft8C5MwZmgzHbya5Z6Q==", + "optional": true, "requires": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/shared-ini-file-loader": "3.272.0", @@ -13292,6 +14423,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/node-http-handler/-/node-http-handler-3.272.0.tgz", "integrity": "sha512-VrW9PjhhngeyYp4yGYPe5S0vgZH6NwU3Po9xAgayUeE37Inr7LS1YteFMHdpgsUUeNXnh7d06CXqHo1XjtqOKA==", + "optional": true, "requires": { "@aws-sdk/abort-controller": "3.272.0", "@aws-sdk/protocol-http": "3.272.0", @@ -13304,6 +14436,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/property-provider/-/property-provider-3.272.0.tgz", "integrity": "sha512-V1pZTaH5eqpAt8O8CzbItHhOtzIfFuWymvwZFkAtwKuaHpnl7jjrTouV482zoq8AD/fF+VVSshwBKYA7bhidIw==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13313,6 +14446,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/protocol-http/-/protocol-http-3.272.0.tgz", "integrity": "sha512-4JQ54v5Yn08jspNDeHo45CaSn1CvTJqS1Ywgr79eU6jBExtguOWv6LNtwVSBD9X37v88iqaxt8iu1Z3pZZAJeg==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13322,6 +14456,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-builder/-/querystring-builder-3.272.0.tgz", "integrity": "sha512-ndo++7GkdCj5tBXE6rGcITpSpZS4PfyV38wntGYAlj9liL1omk3bLZRY6uzqqkJpVHqbg2fD7O2qHNItzZgqhw==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "@aws-sdk/util-uri-escape": "3.201.0", @@ -13332,6 +14467,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/querystring-parser/-/querystring-parser-3.272.0.tgz", "integrity": "sha512-5oS4/9n6N1LZW9tI3qq/0GnCuWoOXRgcHVB+AJLRBvDbEe+GI+C/xK1tKLsfpDNgsQJHc4IPQoIt4megyZ/1+A==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13340,12 +14476,14 @@ "@aws-sdk/service-error-classification": { "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.272.0.tgz", - "integrity": "sha512-REoltM1LK9byyIufLqx9znhSolPcHQgVHIA2S0zu5sdt5qER4OubkLAXuo4MBbisUTmh8VOOvIyUb5ijZCXq1w==" + "integrity": "sha512-REoltM1LK9byyIufLqx9znhSolPcHQgVHIA2S0zu5sdt5qER4OubkLAXuo4MBbisUTmh8VOOvIyUb5ijZCXq1w==", + "optional": true }, "@aws-sdk/shared-ini-file-loader": { "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/shared-ini-file-loader/-/shared-ini-file-loader-3.272.0.tgz", "integrity": "sha512-lzFPohp5sy2XvwFjZIzLVCRpC0i5cwBiaXmFzXYQZJm6FSCszHO4ax+m9yrtlyVFF/2YPWl+/bzNthy4aJtseA==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13355,6 +14493,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4/-/signature-v4-3.272.0.tgz", "integrity": "sha512-pWxnHG1NqJWMwlhJ6NHNiUikOL00DHROmxah6krJPMPq4I3am2KY2Rs/8ouWhnEXKaHAv4EQhSALJ+7Mq5S4/A==", + "optional": true, "requires": { "@aws-sdk/is-array-buffer": "3.201.0", "@aws-sdk/types": "3.272.0", @@ -13369,6 +14508,7 @@ "version": "3.279.0", "resolved": "https://registry.npmjs.org/@aws-sdk/smithy-client/-/smithy-client-3.279.0.tgz", "integrity": "sha512-ZcYWUQDGAYN6NXRpJuSn46PetrpPCA6TrDVwP9+3pERzTXZ66npXoG2XhHjNrOXy/Ted5A3OxKrM4/zLu9tK3A==", + "optional": true, "requires": { "@aws-sdk/middleware-stack": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13400,6 +14540,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/url-parser/-/url-parser-3.272.0.tgz", "integrity": "sha512-vX/Tx02PlnQ/Kgtf5TnrNDHPNbY+amLZjW0Z1d9vzAvSZhQ4i9Y18yxoRDIaDTCNVRDjdhV8iuctW+05PB5JtQ==", + "optional": true, "requires": { "@aws-sdk/querystring-parser": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13410,6 +14551,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-base64/-/util-base64-3.208.0.tgz", "integrity": "sha512-PQniZph5A6N7uuEOQi+1hnMz/FSOK/8kMFyFO+4DgA1dZ5pcKcn5wiFwHkcTb/BsgVqQa3Jx0VHNnvhlS8JyTg==", + "optional": true, "requires": { "@aws-sdk/util-buffer-from": "3.208.0", "tslib": "^2.3.1" @@ -13419,6 +14561,7 @@ "version": "3.188.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-browser/-/util-body-length-browser-3.188.0.tgz", "integrity": "sha512-8VpnwFWXhnZ/iRSl9mTf+VKOX9wDE8QtN4bj9pBfxwf90H1X7E8T6NkiZD3k+HubYf2J94e7DbeHs7fuCPW5Qg==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13427,6 +14570,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-body-length-node/-/util-body-length-node-3.208.0.tgz", "integrity": "sha512-3zj50e5g7t/MQf53SsuuSf0hEELzMtD8RX8C76f12OSRo2Bca4FLLYHe0TZbxcfQHom8/hOaeZEyTyMogMglqg==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13435,6 +14579,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-buffer-from/-/util-buffer-from-3.208.0.tgz", "integrity": "sha512-7L0XUixNEFcLUGPeBF35enCvB9Xl+K6SQsmbrPk1P3mlV9mguWSDQqbOBwY1Ir0OVbD6H/ZOQU7hI/9RtRI0Zw==", + "optional": true, "requires": { "@aws-sdk/is-array-buffer": "3.201.0", "tslib": "^2.3.1" @@ -13444,6 +14589,7 @@ "version": "3.208.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-config-provider/-/util-config-provider-3.208.0.tgz", "integrity": "sha512-DSRqwrERUsT34ug+anlMBIFooBEGwM8GejC7q00Y/9IPrQy50KnG5PW2NiTjuLKNi7pdEOlwTSEocJE15eDZIg==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13452,6 +14598,7 @@ "version": "3.279.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-browser/-/util-defaults-mode-browser-3.279.0.tgz", "integrity": "sha512-RnchYRrpapTT5Hu23LOfk6e8RMVq0kUzho6xA6TJj1a4uGxkcRMvgzPipCq1P5uHu0mrkQBg9pGPEVNOUs38/Q==", + "optional": true, "requires": { "@aws-sdk/property-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13463,6 +14610,7 @@ "version": "3.279.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-defaults-mode-node/-/util-defaults-mode-node-3.279.0.tgz", "integrity": "sha512-A2NB10xReWC+GSnOivKGZ9rnljIZdEP8WMCQQEnA6DJNI19AUFF/O9QJ9y+cHGLKEms7jH86Y99wShdpzAK+Jw==", + "optional": true, "requires": { "@aws-sdk/config-resolver": "3.272.0", "@aws-sdk/credential-provider-imds": "3.272.0", @@ -13476,6 +14624,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.272.0.tgz", "integrity": "sha512-c4MPUaJt2G6gGpoiwIOqDfUa98c1J63RpYvf/spQEKOtC/tF5Gfqlxuq8FnAl5lHnrqj1B9ZXLLxFhHtDR0IiQ==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "tslib": "^2.3.1" @@ -13485,6 +14634,7 @@ "version": "3.201.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-hex-encoding/-/util-hex-encoding-3.201.0.tgz", "integrity": "sha512-7t1vR1pVxKx0motd3X9rI3m/xNp78p3sHtP5yo4NP4ARpxyJ0fokBomY8ScaH2D/B+U5o9ARxldJUdMqyBlJcA==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13501,6 +14651,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-middleware/-/util-middleware-3.272.0.tgz", "integrity": "sha512-Abw8m30arbwxqmeMMha5J11ESpHUNmCeSqSzE8/C4B8jZQtHY4kq7f+upzcNIQ11lsd+uzBEzNG3+dDRi0XOJQ==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13509,6 +14660,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-retry/-/util-retry-3.272.0.tgz", "integrity": "sha512-Ngha5414LR4gRHURVKC9ZYXsEJhMkm+SJ+44wlzOhavglfdcKKPUsibz5cKY1jpUV7oKECwaxHWpBB8r6h+hOg==", + "optional": true, "requires": { "@aws-sdk/service-error-classification": "3.272.0", "tslib": "^2.3.1" @@ -13518,6 +14670,7 @@ "version": "3.201.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-uri-escape/-/util-uri-escape-3.201.0.tgz", "integrity": "sha512-TeTWbGx4LU2c5rx0obHeDFeO9HvwYwQtMh1yniBz00pQb6Qt6YVOETVQikRZ+XRQwEyCg/dA375UplIpiy54mA==", + "optional": true, "requires": { "tslib": "^2.3.1" } @@ -13526,6 +14679,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.272.0.tgz", "integrity": "sha512-Lp5QX5bH6uuwBlIdr7w7OAcAI50ttyskb++yUr9i+SPvj6RI2dsfIBaK4mDg1qUdM5LeUdvIyqwj3XHjFKAAvA==", + "optional": true, "requires": { "@aws-sdk/types": "3.272.0", "bowser": "^2.11.0", @@ -13536,6 +14690,7 @@ "version": "3.272.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.272.0.tgz", "integrity": "sha512-ljK+R3l+Q1LIHrcR+Knhk0rmcSkfFadZ8V+crEGpABf/QUQRg7NkZMsoe814tfBO5F7tMxo8wwwSdaVNNHtoRA==", + "optional": true, "requires": { "@aws-sdk/node-config-provider": "3.272.0", "@aws-sdk/types": "3.272.0", @@ -13546,6 +14701,7 @@ "version": "3.254.0", "resolved": "https://registry.npmjs.org/@aws-sdk/util-utf8/-/util-utf8-3.254.0.tgz", "integrity": "sha512-14Kso/eIt5/qfIBmhEL9L1IfyUqswjSTqO2mY7KOzUZ9SZbwn3rpxmtkhmATkRjD7XIlLKaxBkI7tU9Zjzj8Kw==", + "optional": true, "requires": { "@aws-sdk/util-buffer-from": "3.208.0", "tslib": "^2.3.1" @@ -14789,43 +15945,19 @@ } }, "@sentry/node": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.40.0.tgz", - "integrity": "sha512-S2pcLBUsCg3+WYlyQbXKPey3H1yaZPt9fpJTPAoabbidYXweBAbtFjSCRwPFwCQMzWusWxlYEwWRT77C07pHVg==", + "version": "7.41.0", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.41.0.tgz", + "integrity": "sha512-JAz0TAVvq4z7dNrDBECLB5Fd4bGDGXSOR1f63iV/8fUNsZPqqdh8LXnctPXTpcTjsdlfmjUL1CC2RQ6yKYMtTQ==", "requires": { - "@sentry/core": "7.40.0", - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", + "@sentry/core": "7.41.0", + "@sentry/types": "7.41.0", + "@sentry/utils": "7.41.0", "cookie": "^0.4.1", "https-proxy-agent": "^5.0.0", "lru_map": "^0.3.3", "tslib": "^1.9.3" }, "dependencies": { - "@sentry/core": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.40.0.tgz", - "integrity": "sha512-OPAobQG0GTY++r5LWUcOA1lS+2TY2Lmw/i5s4kL9WbY+f08dbLNEGNBObY7/V98OL4f7OG+nWaPFybgM7kqUTQ==", - "requires": { - "@sentry/types": "7.40.0", - "@sentry/utils": "7.40.0", - "tslib": "^1.9.3" - } - }, - "@sentry/types": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.40.0.tgz", - "integrity": "sha512-dIbqBenbmDx1F8pvfC11C88J83ecwumUhV+YOIxcmVd1fmlPF2hXWZ01+NTkTDkCu341sJx4wPQogByFy8FwGA==" - }, - "@sentry/utils": { - "version": "7.40.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.40.0.tgz", - "integrity": "sha512-ZdCbTpAXPiVVfvNJVftnDhsctOui71MDUhVIdLkgg4Cuic+WHGPRmmZ+H6uZdp7vRaeB+Uvnn5+t2iSAVo/mAA==", - "requires": { - "@sentry/types": "7.40.0", - "tslib": "^1.9.3" - } - }, "tslib": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", @@ -15081,9 +16213,9 @@ } }, "@types/jest": { - "version": "29.4.0", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.4.0.tgz", - "integrity": "sha512-VaywcGQ9tPorCX/Jkkni7RWGFfI11whqzs8dvxF41P17Z+z872thvEvlIbznjPJ02kl1HMX3LmLOonsj2n7HeQ==", + "version": "29.5.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.0.tgz", + "integrity": "sha512-3Emr5VOl/aoBwnWcH/EFQvlSAmjV+XtV9GGu5mwdYew5vhQh0IUZx/60x0TzHDu09Bi7HMx10t/namdJw5QIcg==", "dev": true, "requires": { "expect": "^29.0.0", @@ -15621,9 +16753,9 @@ "integrity": "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g==" }, "aws-sdk": { - "version": "2.1324.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1324.0.tgz", - "integrity": "sha512-7T9Jn6qtzCANdqRcdhxZ9Fx31/U+h/VPFxEU3+sFEnC7WtGtRlgmsJOY2lIdFKRXkHYT3Jw5MqDyjnb/i1QqbA==", + "version": "2.1338.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1338.0.tgz", + "integrity": "sha512-apxv53ABuvi87UQHAUqRrJOaGNMiPXAe6bizzJhOnsaNqasg2KjDDit7QSCi6HlLNG44n1ApIvMtR/k+NnxU4Q==", "requires": { "buffer": "4.9.2", "events": "1.1.1", @@ -18309,9 +19441,9 @@ } }, "mongoose": { - "version": "6.10.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.10.1.tgz", - "integrity": "sha512-QBXX27XXCEIi1zqimTBjlZsFFg2YzK9uQfuAIqG1AyIEeyKiiCaobD0Tst2ULwiX4PRB53VLRQvAqyu5TwvOZg==", + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-6.10.3.tgz", + "integrity": "sha512-fZ3pIlQn7lM632r1l4qiU58lKrJ+FufKVG8TNeRXSChAeu9alCl5KoQ9bLw4jnQNYevSq9o+sqZmFDHP+EVW3g==", "requires": { "bson": "^4.7.0", "kareem": "2.5.1", @@ -20501,9 +21633,9 @@ } }, "posthog-node": { - "version": "2.5.4", - "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.5.4.tgz", - "integrity": "sha512-CdywlVh0CZU05/3MrBc0qY/zsLdU2X9XSz/yL1qMRhbyZhD8lrnuGlI69G2cpzZtli6S/nu64wcmULz/mFFA5w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/posthog-node/-/posthog-node-2.6.0.tgz", + "integrity": "sha512-/BiFw/jwdP0uJSRAIoYqLoBTjZ612xv74b1L/a3T/p1nJVL8e0OrHuxbJW56c6WVW/IKm9gBF/zhbqfaz0XgJQ==", "requires": { "axios": "^0.27.0" }, diff --git a/backend/package.json b/backend/package.json index 85195acc6..8638cf444 100644 --- a/backend/package.json +++ b/backend/package.json @@ -1,14 +1,15 @@ { "dependencies": { - "@aws-sdk/client-secrets-manager": "^3.281.0", + "@aws-sdk/client-secrets-manager": "^3.294.0", "@godaddy/terminus": "^4.11.2", "@octokit/rest": "^19.0.5", "@sentry/tracing": "^7.41.0", "@sentry/node": "^7.40.0", + "@sentry/node": "^7.41.0", "@types/crypto-js": "^4.1.1", "@types/libsodium-wrappers": "^0.7.10", "await-to-js": "^3.0.0", - "aws-sdk": "^2.1324.0", + "aws-sdk": "^2.1338.0", "axios": "^1.1.3", "axios-retry": "^3.4.0", "bcrypt": "^5.1.0", @@ -29,9 +30,9 @@ "jsrp": "^0.2.4", "libsodium-wrappers": "^0.7.10", "lodash": "^4.17.21", - "mongoose": "^6.10.1", + "mongoose": "^6.10.3", "nodemailer": "^6.8.0", - "posthog-node": "^2.5.4", + "posthog-node": "^2.6.0", "query-string": "^7.1.3", "request-ip": "^3.3.0", "rimraf": "^3.0.2", @@ -57,7 +58,7 @@ "lint-and-fix": "eslint . --ext .ts --fix", "lint-staged": "lint-staged", "pretest": "docker compose -f test-resources/docker-compose.test.yml up -d", - "test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles", + "test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest", "test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json", "posttest": "docker compose -f test-resources/docker-compose.test.yml down" }, @@ -80,7 +81,7 @@ "@types/cookie-parser": "^1.4.3", "@types/cors": "^2.8.12", "@types/express": "^4.17.14", - "@types/jest": "^29.2.4", + "@types/jest": "^29.5.0", "@types/jsonwebtoken": "^8.5.9", "@types/lodash": "^4.14.191", "@types/node": "^18.11.3", diff --git a/backend/src/config/index.ts b/backend/src/config/index.ts index 33066f613..29f62cdc6 100644 --- a/backend/src/config/index.ts +++ b/backend/src/config/index.ts @@ -13,7 +13,7 @@ export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!; export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m'; export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!; export const getMongoURL = () => infisical.get('MONGO_URL')!; -export const getNodeEnv = () => infisical.get('NODE_ENV')!; +export const getNodeEnv = () => infisical.get('NODE_ENV')! || 'production'; export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true; export const getLokiHost = () => infisical.get('LOKI_HOST')!; export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!; @@ -48,4 +48,17 @@ export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!; export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!; export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true; export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!; -export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true \ No newline at end of file +export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true +export const getHttpsEnabled = () => { + if (getNodeEnv() != "production") { + // no https for anything other than prod + return false + } + + if (infisical.get('HTTPS_ENABLED') == undefined || infisical.get('HTTPS_ENABLED') == "") { + // default when no value present + return true + } + + return infisical.get('HTTPS_ENABLED') === 'true' && true +} \ No newline at end of file diff --git a/backend/src/controllers/v1/authController.ts b/backend/src/controllers/v1/authController.ts index 3c6ec523a..84404bc74 100644 --- a/backend/src/controllers/v1/authController.ts +++ b/backend/src/controllers/v1/authController.ts @@ -15,10 +15,10 @@ import { BadRequestError } from '../../utils/errors'; import { EELogService } from '../../ee/services'; import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { - getNodeEnv, getJwtRefreshSecret, getJwtAuthLifetime, - getJwtAuthSecret + getJwtAuthSecret, + getHttpsEnabled } from '../../config'; declare module 'jsonwebtoken' { @@ -126,21 +126,21 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getNodeEnv() === 'production' ? true : false + secure: getHttpsEnabled() }); const loginAction = await EELogService.createAction({ name: ACTION_LOGIN, userId: user._id }); - + loginAction && await EELogService.createLog({ userId: user._id, actions: [loginAction], channel: getChannelFromUserAgent(req.headers['user-agent']), ipAddress: req.ip }); - + // return (access) token in response return res.status(200).send({ token: tokens.token, @@ -182,14 +182,14 @@ export const logout = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getNodeEnv() === 'production' ? true : false + secure: getHttpsEnabled() as boolean }); const logoutAction = await EELogService.createAction({ name: ACTION_LOGOUT, userId: req.user._id }); - + logoutAction && await EELogService.createLog({ userId: req.user._id, actions: [logoutAction], diff --git a/backend/src/controllers/v1/integrationAuthController.ts b/backend/src/controllers/v1/integrationAuthController.ts index da8fc5570..12a39a389 100644 --- a/backend/src/controllers/v1/integrationAuthController.ts +++ b/backend/src/controllers/v1/integrationAuthController.ts @@ -12,6 +12,11 @@ import { getTeams, revokeAccess } from '../../integrations'; +import { + INTEGRATION_VERCEL_API_URL, + INTEGRATION_RAILWAY_API_URL +} from '../../variables'; +import request from '../../config/request'; /*** * Return integration authorization with id [integrationAuthId] @@ -188,22 +193,203 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => { * @returns */ export const getIntegrationAuthTeams = async (req: Request, res: Response) => { - let teams; - try { - teams = await getTeams({ - integrationAuth: req.integrationAuth, - accessToken: req.accessToken + 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 + */ +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 params = new URLSearchParams({ + projectId: appId, + ...(req.integrationAuth.teamId ? { + teamId: req.integrationAuth.teamId + } : {}) + }); + + let branches: string[] = []; + + if (appId && appId !== '') { + const { data }: { data: VercelBranch[] } = await request.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 + */ +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[] = []; + + 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 { + node { + id + name + isEphemeral + } + } + } + } + `; + + const variables = { + projectId: appId + } + + const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, { + query, + variables, + }, { + headers: { + 'Authorization': `Bearer ${req.accessToken}`, + 'Content-Type': 'application/json', + }, }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: "Failed to get integration authorization teams" + + environments = edges.map((e: RailwayEnvironment) => { + return ({ + name: e.node.name, + environmentId: e.node.id + }); }); } return res.status(200).send({ - teams + environments + }); +} + +/** + * Return list of Railway services for Railway project with id + * [appId] + * @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 = ` + query project($id: String!) { + project(id: $id) { + createdAt + deletedAt + id + description + expiredAt + isPublic + isTempProject + isUpdatable + name + prDeploys + teamId + updatedAt + upstreamUrl + services { + edges { + node { + id + name + } + } + } + } + } + `; + + if (appId && appId !== '') { + const variables = { + id: appId + } + + const { data: { data: { project: { services: { edges } } } } } = await request.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 }); } diff --git a/backend/src/controllers/v1/integrationController.ts b/backend/src/controllers/v1/integrationController.ts index 827ce21b9..6b633fc2c 100644 --- a/backend/src/controllers/v1/integrationController.ts +++ b/backend/src/controllers/v1/integrationController.ts @@ -24,6 +24,9 @@ export const createIntegration = async (req: Request, res: Response) => { isActive, sourceEnvironment, targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, owner, path, region @@ -39,12 +42,15 @@ export const createIntegration = async (req: Request, res: Response) => { app, appId, targetEnvironment, + targetEnvironmentId, + targetService, + targetServiceId, owner, path, region, integration: req.integrationAuth.integration, integrationAuth: new Types.ObjectId(integrationAuthId) - }).save(); + }).save(); if (integration) { // trigger event - push secrets diff --git a/backend/src/controllers/v1/secretController.ts b/backend/src/controllers/v1/secretController.ts index 1d1f8981c..4b377ec72 100644 --- a/backend/src/controllers/v1/secretController.ts +++ b/backend/src/controllers/v1/secretController.ts @@ -9,7 +9,7 @@ import { import { pushKeys } from '../../helpers/key'; import { eventPushSecrets } from '../../events'; import { EventService } from '../../services'; -import { getPostHogClient } from '../../services'; +import { TelemetryService } from '../../services'; interface PushSecret { ciphertextKey: string; @@ -38,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); let { secrets }: { secrets: PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -112,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => { let secrets; let key; try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; @@ -181,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { let secrets; let key; try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v2/apiKeyDataController.ts b/backend/src/controllers/v2/apiKeyDataController.ts index fd87f7306..e4450ae90 100644 --- a/backend/src/controllers/v2/apiKeyDataController.ts +++ b/backend/src/controllers/v2/apiKeyDataController.ts @@ -50,6 +50,7 @@ export const createAPIKeyData = async (req: Request, res: Response) => { apiKeyData = await new APIKeyData({ name, + lastUsed: new Date(), expiresAt, user: req.user._id, secretHash diff --git a/backend/src/controllers/v2/authController.ts b/backend/src/controllers/v2/authController.ts index 3a36d559c..92b4a159a 100644 --- a/backend/src/controllers/v2/authController.ts +++ b/backend/src/controllers/v2/authController.ts @@ -17,9 +17,9 @@ import { } from '../../variables'; import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { - getNodeEnv, getJwtMfaLifetime, - getJwtMfaSecret + getJwtMfaSecret, + getHttpsEnabled } from '../../config'; declare module 'jsonwebtoken' { @@ -163,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getNodeEnv() === 'production' ? true : false + secure: getHttpsEnabled() }); // case: user does not have MFA enablgged @@ -302,7 +302,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getNodeEnv() === 'production' ? true : false + secure: getHttpsEnabled() }); interface VerifyMfaTokenRes { diff --git a/backend/src/controllers/v2/environmentController.ts b/backend/src/controllers/v2/environmentController.ts index b82dca9fe..4985420fb 100644 --- a/backend/src/controllers/v2/environmentController.ts +++ b/backend/src/controllers/v2/environmentController.ts @@ -11,7 +11,7 @@ import { import { SecretVersion } from '../../ee/models'; import { BadRequestError } from '../../utils/errors'; import _ from 'lodash'; -import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; /** * Create new workspace environment named [environmentName] under workspace with id @@ -244,8 +244,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async ( throw BadRequestError() } relatedWorkspace.environments.forEach(environment => { - const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) - const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) + const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS }) + const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS }) if (isReadBlocked && isWriteBlocked) { return } else { diff --git a/backend/src/controllers/v2/index.ts b/backend/src/controllers/v2/index.ts index d266ace3f..db78fa503 100644 --- a/backend/src/controllers/v2/index.ts +++ b/backend/src/controllers/v2/index.ts @@ -7,6 +7,7 @@ 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'; @@ -20,6 +21,7 @@ export { apiKeyDataController, secretController, secretsController, + serviceAccountsController, environmentController, tagController } diff --git a/backend/src/controllers/v2/organizationsController.ts b/backend/src/controllers/v2/organizationsController.ts index d167e5129..613206ba3 100644 --- a/backend/src/controllers/v2/organizationsController.ts +++ b/backend/src/controllers/v2/organizationsController.ts @@ -1,9 +1,11 @@ import { Request, Response } from 'express'; import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { MembershipOrg, Membership, - Workspace + Workspace, + ServiceAccount } from '../../models'; import { deleteMembershipOrg } from '../../helpers/membershipOrg'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; @@ -260,37 +262,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) => } } */ - let workspaces; - try { - const { organizationId } = req.params; + const { organizationId } = req.params; - const workspacesSet = new Set( - ( - await Workspace.find( - { - organization: organizationId - }, - '_id' - ) - ).map((w) => w._id.toString()) - ); + const workspacesSet = new Set( + ( + await Workspace.find( + { + organization: organizationId + }, + '_id' + ) + ).map((w) => w._id.toString()) + ); - workspaces = ( - await Membership.find({ - user: req.user._id - }).populate('workspace') - ) - .filter((m) => workspacesSet.has(m.workspace._id.toString())) - .map((m) => m.workspace); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get organization workspaces' - }); - } - - return res.status(200).send({ + const workspaces = ( + await Membership.find({ + user: req.user._id + }).populate('workspace') + ) + .filter((m) => workspacesSet.has(m.workspace._id.toString())) + .map((m) => m.workspace); + +return res.status(200).send({ workspaces }); +} + +/** + * Return service accounts for organization with id [organizationId] + * @param req + * @param res + */ +export const getOrganizationServiceAccounts = async (req: Request, res: Response) => { + const { organizationId } = req.params; + + const serviceAccounts = await ServiceAccount.find({ + organization: new Types.ObjectId(organizationId) + }); + + return res.status(200).send({ + serviceAccounts + }); } \ No newline at end of file diff --git a/backend/src/controllers/v2/secretController.ts b/backend/src/controllers/v2/secretController.ts index cd91dca67..91328ea7b 100644 --- a/backend/src/controllers/v2/secretController.ts +++ b/backend/src/controllers/v2/secretController.ts @@ -7,7 +7,7 @@ const { ValidationError } = mongoose.Error; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { AnyBulkWriteOperation } from 'mongodb'; import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; -import { getPostHogClient } from '../../services'; +import { TelemetryService } from '../../services'; /** * Create secret for workspace with id [workspaceId] and environment [environment] @@ -15,7 +15,7 @@ import { getPostHogClient } from '../../services'; * @param res */ export const createSecret = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const secretToCreate: CreateSecretRequestBody = req.body.secret; const { workspaceId, environment } = req.params const sanitizedSecret: SanitizedSecretForCreate = { @@ -68,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => { * @param res */ export const createSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const { workspaceId, environment } = req.params const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] @@ -130,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretIdsToDelete: string[] = req.body.secretIds @@ -184,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => { * @param res */ export const deleteSecret = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); await Secret.findByIdAndDelete(req._secret._id) if (postHogClient) { @@ -213,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => { * @returns */ export const updateSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); 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()) @@ -281,7 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => { * @returns */ export const updateSecret = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { workspaceId, environmentName } = req.params const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; @@ -335,7 +335,7 @@ export const updateSecret = async (req: Request, res: Response) => { * @returns */ export const getSecrets = async (req: Request, res: Response) => { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { environment } = req.query; const { workspaceId } = req.params; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 66aecd423..82b16edd6 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -15,12 +15,12 @@ import { UnauthorizedRequestError, ValidationError } from '../../utils/errors'; import { EventService } from '../../services'; import { eventPushSecrets } from '../../events'; import { EESecretService, EELogService } from '../../ee/services'; -import { getPostHogClient } from '../../services'; +import { TelemetryService } from '../../services'; import { getChannelFromUserAgent } from '../../utils/posthog'; -import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; +import { PERMISSION_WRITE_SECRETS } from '../../variables'; import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; import Tag from '../../models/tag'; -import _ from 'lodash'; +import _, { eq } from 'lodash'; import { BatchSecretRequest, BatchSecret @@ -28,12 +28,13 @@ import { /** * Peform a batch of any specified CUD secret operations + * (used by dashboard) * @param req * @param res */ export const batchSecrets = async (req: Request, res: Response) => { const channel = getChannelFromUserAgent(req.headers['user-agent']); - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const { workspaceId, @@ -91,7 +92,9 @@ export const batchSecrets = async (req: Request, res: Response) => { const addAction = await EELogService.createAction({ name: ACTION_ADD_SECRETS, - userId: req.user._id, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId), secretIds: createdSecrets.map((n) => n._id) }) as IAction; @@ -328,14 +331,15 @@ export const createSecrets = async (req: Request, res: Response) => { } } */ - const postHogClient = getPostHogClient(); const channel = getChannelFromUserAgent(req.headers['user-agent']) const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body; - const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE) - if (!hasAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + if (req.user) { + const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, PERMISSION_WRITE_SECRETS) + if (!hasAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) + } } let listOfSecretsToCreate; @@ -378,7 +382,7 @@ export const createSecrets = async (req: Request, res: Response) => { version: 1, workspace: new Types.ObjectId(workspaceId), type, - user: type === SECRET_PERSONAL ? req.user : undefined, + user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined, environment, secretKeyCiphertext, secretKeyIV, @@ -391,7 +395,7 @@ export const createSecrets = async (req: Request, res: Response) => { secretCommentTag, tags }); - }) + }); const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject()); @@ -447,14 +451,18 @@ export const createSecrets = async (req: Request, res: Response) => { const addAction = await EELogService.createAction({ name: ACTION_ADD_SECRETS, - userId: req.user._id, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId), secretIds: newlyCreatedSecrets.map((n) => n._id) }); // (EE) create (audit) log addAction && await EELogService.createLog({ - userId: req.user._id.toString(), + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId), actions: [addAction], channel, @@ -466,10 +474,15 @@ export const createSecrets = async (req: Request, res: Response) => { workspaceId }); + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets added', - distinctId: req.user.email, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: listOfSecretsToCreate.length, environment, @@ -533,91 +546,120 @@ export const getSecrets = async (req: Request, res: Response) => { } */ - const postHogClient = getPostHogClient(); + const { tagSlugs } = req.query; + const workspaceId = req.query.workspaceId as string; + const environment = req.query.environment as string; - const { workspaceId, environment, tagSlugs } = req.query; + // secrets to return + let secrets: ISecret[] = []; + + // query tags table to get all tags ids for the tag names for the given workspace + let tagIds = []; const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : []; - let userId = "" // used for getting personal secrets for user - let userEmail = "" // used for posthog - if (req.user) { - userId = req.user._id; - userEmail = req.user.email; - } - - if (req.serviceTokenData) { - userId = req.serviceTokenData.user._id - userEmail = req.serviceTokenData.user.email; - } - - // none service token case as service tokens are already scoped to env and project - let hasWriteOnlyAccess - if (!req.serviceTokenData) { - hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment) - const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment) - if (hasNoAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } - } - let secrets: any - let secretQuery: any - if (tagNamesList != undefined && tagNamesList.length != 0) { - const workspaceFromDB = await Tag.find({ workspace: workspaceId }) - - const tagIds = _.map(tagNamesList, (tagName) => { + const workspaceFromDB = await Tag.find({ workspace: workspaceId }); + tagIds = _.map(tagNamesList, (tagName) => { const tag = _.find(workspaceFromDB, { slug: tagName }); return tag ? tag.id : null; }); + } - secretQuery = { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, - { user: { $exists: false } } - ], - tags: { $in: tagIds }, - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + if (req.user) { + // case: client authorization is via JWT + const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment) + const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment) + if (hasNoAccess) { + throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) } - } else { - secretQuery = { + + const secretQuery: any = { workspace: workspaceId, environment, $or: [ - { user: userId }, - { user: { $exists: false } } - ], - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } + { user: req.user._id }, // personal secrets for this user + { user: { $exists: false } } // shared secrets from workspace + ] + } + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + if (hasWriteOnlyAccess) { + // only return the secret keys and not the values since user does not have right to see values + secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags") + } else { + secrets = await Secret.find(secretQuery).populate("tags") } } - if (hasWriteOnlyAccess) { - secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag") - } else { - secrets = await Secret.find(secretQuery).populate("tags") + // case: client authorization is via service token + if (req.serviceTokenData) { + const userId = req.serviceTokenData.user._id + + const secretQuery: any = { + workspace: workspaceId, + environment, + $or: [ + { user: userId }, // personal secrets for this user + { user: { $exists: false } } // shared secrets from workspace + ] + } + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + // TODO check if service token has write only permission + + secrets = await Secret.find(secretQuery).populate("tags"); + } + + // case: client authorization is via service account + if (req.serviceAccount) { + const secretQuery: any = { + workspace: workspaceId, + environment, + user: { $exists: false } // shared secrets only from workspace + } + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + secrets = await Secret.find(secretQuery).populate("tags"); } const channel = getChannelFromUserAgent(req.headers['user-agent']) const readAction = await EELogService.createAction({ name: ACTION_READ_SECRETS, - userId: new Types.ObjectId(userId), + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId as string), secretIds: secrets.map((n: any) => n._id) }); readAction && await EELogService.createLog({ - userId: new Types.ObjectId(userId), + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(workspaceId as string), actions: [readAction], channel, ipAddress: req.ip }); + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets pulled', - distinctId: userEmail, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: secrets.length, environment, @@ -633,59 +675,6 @@ export const getSecrets = async (req: Request, res: Response) => { }); } - -export const getOnlySecretKeys = async (req: Request, res: Response) => { - const { workspaceId, environment } = req.query; - - let userId = "" // used for getting personal secrets for user - let userEmail = "" // used for posthog - if (req.user) { - userId = req.user._id; - userEmail = req.user.email; - } - - if (req.serviceTokenData) { - userId = req.serviceTokenData.user._id - userEmail = req.serviceTokenData.user.email; - } - - // none service token case as service tokens are already scoped - if (!req.serviceTokenData) { - const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ) - if (!hasAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } - } - - const [err, secretKeys] = await to(Secret.find( - { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, - { user: { $exists: false } } - ], - type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } - } - ) - .select("secretKeyIV secretKeyTag secretKeyCiphertext") - .then()) - - if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack }); - - // readAction && await EELogService.createLog({ - // userId: new Types.ObjectId(userId), - // workspaceId: new Types.ObjectId(workspaceId as string), - // actions: [readAction], - // channel, - // ipAddress: req.ip - // }); - - return res.status(200).send({ - secretKeys - }); -} - /** * Update secret(s) * @param req @@ -736,10 +725,8 @@ export const updateSecrets = async (req: Request, res: Response) => { } } */ - const postHogClient = getPostHogClient(); const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; - // TODO: move type interface PatchSecret { id: string; secretKeyCiphertext: string; @@ -865,14 +852,18 @@ export const updateSecrets = async (req: Request, res: Response) => { const updateAction = await EELogService.createAction({ name: ACTION_UPDATE_SECRETS, - userId: req.user._id, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(key), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) }); // (EE) create (audit) log updateAction && await EELogService.createLog({ - userId: req.user._id.toString(), + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(key), actions: [updateAction], channel, @@ -884,10 +875,15 @@ export const updateSecrets = async (req: Request, res: Response) => { workspaceId: key }) + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets modified', - distinctId: req.user.email, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: workspaceSecretObj[key].length, environment: workspaceSecretObj[key][0].environment, @@ -909,7 +905,7 @@ export const updateSecrets = async (req: Request, res: Response) => { } /** - * Delete secret(s) with id [workspaceId] and environment [environment] + * Delete secret(s) * @param req * @param res */ @@ -958,7 +954,11 @@ export const deleteSecrets = async (req: Request, res: Response) => { } } */ - const postHogClient = getPostHogClient(); + + return res.status(200).send({ + message: 'delete secrets!!' + }); + const channel = getChannelFromUserAgent(req.headers['user-agent']) const toDelete = req.secrets.map((s: any) => s._id); @@ -992,14 +992,18 @@ export const deleteSecrets = async (req: Request, res: Response) => { }); const deleteAction = await EELogService.createAction({ name: ACTION_DELETE_SECRETS, - userId: req.user._id, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(key), secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) }); // (EE) create (audit) log deleteAction && await EELogService.createLog({ - userId: req.user._id.toString(), + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, workspaceId: new Types.ObjectId(key), actions: [deleteAction], channel, @@ -1011,10 +1015,15 @@ export const deleteSecrets = async (req: Request, res: Response) => { workspaceId: key }) + const postHogClient = TelemetryService.getPostHogClient(); if (postHogClient) { postHogClient.capture({ event: 'secrets deleted', - distinctId: req.user.email, + distinctId: TelemetryService.getDistinctId({ + user: req.user, + serviceAccount: req.serviceAccount, + serviceTokenData: req.serviceTokenData + }), properties: { numberOfSecrets: workspaceSecretObj[key].length, environment: workspaceSecretObj[key][0].environment, diff --git a/backend/src/controllers/v2/serviceAccountsController.ts b/backend/src/controllers/v2/serviceAccountsController.ts new file mode 100644 index 000000000..7eaf73cea --- /dev/null +++ b/backend/src/controllers/v2/serviceAccountsController.ts @@ -0,0 +1,306 @@ +import { Request, Response } from 'express'; +import { Types } from 'mongoose'; +import crypto from 'crypto'; +import bcrypt from 'bcrypt'; +import { + ServiceAccount, + ServiceAccountKey, + ServiceAccountOrganizationPermission, + ServiceAccountWorkspacePermission +} from '../../models'; +import { + 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 + * @param req + * @param res + */ +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' }); + } + + return res.status(200).send({ + serviceAccount + }); +} + +/** + * Return service account with id [serviceAccountId] + * @param req + * @param res + */ +export const getServiceAccountById = async (req: Request, res: Response) => { + const { serviceAccountId } = req.params; + + const serviceAccount = await ServiceAccount.findById(serviceAccountId); + + if (!serviceAccount) { + throw ServiceAccountNotFoundError({ message: 'Failed to find service account' }); + } + + return res.status(200).send({ + serviceAccount + }); +} + +/** + * Create a new service account under organization with id [organizationId] + * that has access to workspaces [workspaces] + * @param req + * @param res + * @returns + */ +export const createServiceAccount = async (req: Request, res: Response) => { + const { + name, + organizationId, + publicKey, + expiresIn, + }: CreateServiceAccountDto = req.body; + + let expiresAt; + if (expiresIn) { + expiresAt = new Date(); + expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); + } + + const secret = crypto.randomBytes(16).toString('base64'); + const secretHash = await bcrypt.hash(secret, getSaltRounds()); + + // create service account + const serviceAccount = await new ServiceAccount({ + name, + organization: new Types.ObjectId(organizationId), + user: req.user, + publicKey, + lastUsed: new Date(), + expiresAt, + secretHash + }).save(); + + const serviceAccountObj = serviceAccount.toObject(); + + delete serviceAccountObj.secretHash; + + // provision default org-level permission for service account + await new ServiceAccountOrganizationPermission({ + serviceAccount: serviceAccount._id + }).save(); + + const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64'); + + return res.status(200).send({ + serviceAccountAccessKey: `sa.${secretId}.${secret}`, + serviceAccount: serviceAccountObj + }); +} + +/** + * Change name of service account with id [serviceAccountId] to [name] + * @param req + * @param res + * @returns + */ +export const changeServiceAccountName = async (req: Request, res: Response) => { + const { serviceAccountId } = req.params; + const { name } = req.body; + + const serviceAccount = await ServiceAccount.findOneAndUpdate( + { + _id: new Types.ObjectId(serviceAccountId) + }, + { + name + }, + { + new: true + } + ); + + return res.status(200).send({ + serviceAccount + }); +} + +/** + * Add a service account key to service account with id [serviceAccountId] + * for workspace with id [workspaceId] + * @param req + * @param res + * @returns + */ +export const addServiceAccountKey = async (req: Request, res: Response) => { + const { + workspaceId, + encryptedKey, + nonce + } = req.body; + + const serviceAccountKey = await new ServiceAccountKey({ + encryptedKey, + nonce, + sender: req.user._id, + serviceAccount: req.serviceAccount._d, + workspace: new Types.ObjectId(workspaceId) + }).save(); + + return serviceAccountKey; +} + +/** + * Return workspace-level permission for service account with id [serviceAccountId] + * @param req + * @param res + */ +export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => { + const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({ + serviceAccount: req.serviceAccount._id + }).populate('workspace'); + + return res.status(200).send({ + serviceAccountWorkspacePermissions + }); +} + +/** + * Add a workspace permission to service account with id [serviceAccountId] + * @param req + * @param res + */ +export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => { + const { serviceAccountId } = req.params; + const { + environment, + workspaceId, + read = false, + write = false, + encryptedKey, + 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' + }); + } + + const existingPermission = await ServiceAccountWorkspacePermission.findOne({ + serviceAccount: new Types.ObjectId(serviceAccountId), + workspace: new Types.ObjectId(workspaceId), + environment + }); + + 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 + }).save(); + + const existingServiceAccountKey = await ServiceAccountKey.findOne({ + serviceAccount: new Types.ObjectId(serviceAccountId), + workspace: new Types.ObjectId(workspaceId) + }); + + if (!existingServiceAccountKey) { + await new ServiceAccountKey({ + encryptedKey, + nonce, + sender: req.user._id, + serviceAccount: new Types.ObjectId(serviceAccountId), + workspace: new Types.ObjectId(workspaceId) + }).save(); + } + + return res.status(200).send({ + serviceAccountWorkspacePermission + }); +} + +/** + * Delete workspace permission from service account with id [serviceAccountId] + * @param req + * @param res + */ +export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => { + const { serviceAccountWorkspacePermissionId } = req.params; + const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findByIdAndDelete(serviceAccountWorkspacePermissionId); + + if (serviceAccountWorkspacePermission) { + const { serviceAccount, workspace } = serviceAccountWorkspacePermission; + const count = await ServiceAccountWorkspacePermission.countDocuments({ + serviceAccount, + workspace + }); + + if (count === 0) { + await ServiceAccountKey.findOneAndDelete({ + serviceAccount, + workspace + }); + } + } + + return res.status(200).send({ + serviceAccountWorkspacePermission + }); +} + +/** + * Delete service account with id [serviceAccountId] + * @param req + * @param res + * @returns + */ +export const deleteServiceAccount = async (req: Request, res: Response) => { + const { serviceAccountId } = req.params; + + const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId); + + if (serviceAccount) { + await ServiceAccountKey.deleteMany({ + serviceAccount: serviceAccount._id + }); + + await ServiceAccountOrganizationPermission.deleteMany({ + serviceAccount: new Types.ObjectId(serviceAccountId) + }); + + await ServiceAccountWorkspacePermission.deleteMany({ + serviceAccount: new Types.ObjectId(serviceAccountId) + }); + } + + return res.status(200).send({ + serviceAccount + }); +} + +/** + * Return service account keys for service account with id [serviceAccountId] + * @param req + * @param res + * @returns + */ +export const getServiceAccountKeys = async (req: Request, res: Response) => { + const workspaceId = req.query.workspaceId as string; + + const serviceAccountKeys = await ServiceAccountKey.find({ + serviceAccount: req.serviceAccount._id, + ...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {}) + }); + + return res.status(200).send({ + serviceAccountKeys + }); +} \ No newline at end of file diff --git a/backend/src/controllers/v2/serviceTokenDataController.ts b/backend/src/controllers/v2/serviceTokenDataController.ts index a4e06f8e4..328749472 100644 --- a/backend/src/controllers/v2/serviceTokenDataController.ts +++ b/backend/src/controllers/v2/serviceTokenDataController.ts @@ -3,10 +3,16 @@ 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 { ABILITY_READ } from '../../variables/organization'; +import { + PERMISSION_READ_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT +} from '../../variables'; import { getSaltRounds } from '../../config'; /** @@ -53,59 +59,60 @@ export const getServiceTokenData = async (req: Request, res: Response) => { * @returns */ export const createServiceTokenData = async (req: Request, res: Response) => { - let serviceToken, serviceTokenData; + let serviceTokenData; - try { - const { - name, - workspaceId, - environment, - encryptedKey, - iv, - tag, - expiresIn, - permissions - } = req.body; + const { + name, + workspaceId, + environment, + encryptedKey, + iv, + tag, + expiresIn, + permissions + } = req.body; - const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ) - if (!hasAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } + const secret = crypto.randomBytes(16).toString('hex'); + const secretHash = await bcrypt.hash(secret, getSaltRounds()); - const secret = crypto.randomBytes(16).toString('hex'); - const secretHash = await bcrypt.hash(secret, getSaltRounds()); - - const expiresAt = new Date(); + let expiresAt; + if (!!expiresIn) { + expiresAt = new Date() expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); - - serviceTokenData = await new ServiceTokenData({ - name, - workspace: workspaceId, - environment, - user: req.user._id, - expiresAt, - secretHash, - encryptedKey, - iv, - tag, - permissions - }).save(); - - // return service token data without sensitive data - serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); - - if (!serviceTokenData) throw new Error('Failed to find service token data'); - - serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to create service token data' - }); } + let user, serviceAccount; + + if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) { + user = req.authData.authPayload._id; + } + + if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) { + serviceAccount = req.authData.authPayload._id; + } + + serviceTokenData = await new ServiceTokenData({ + name, + workspace: workspaceId, + environment, + user, + serviceAccount, + lastUsed: new Date(), + expiresAt, + secretHash, + encryptedKey, + iv, + tag, + permissions + }).save(); + + // return service token data without sensitive data + serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); + + if (!serviceTokenData) throw new Error('Failed to find service token data'); + + const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; + return res.status(200).send({ serviceToken, serviceTokenData @@ -119,25 +126,11 @@ export const createServiceTokenData = async (req: Request, res: Response) => { * @returns */ export const deleteServiceTokenData = async (req: Request, res: Response) => { - let serviceTokenData; - try { - const { serviceTokenDataId } = req.params; + const { serviceTokenDataId } = req.params; - serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to delete service token data' - }); - } + const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); return res.status(200).send({ serviceTokenData }); -} - -function UnauthorizedRequestError(arg0: { message: string; }) { - throw new Error('Function not implemented.'); } \ No newline at end of file diff --git a/backend/src/controllers/v2/signupController.ts b/backend/src/controllers/v2/signupController.ts index aaf055b78..ff41aa017 100644 --- a/backend/src/controllers/v2/signupController.ts +++ b/backend/src/controllers/v2/signupController.ts @@ -8,7 +8,7 @@ import { import { issueAuthTokens } from '../../helpers/auth'; import { INVITED, ACCEPTED } from '../../variables'; import request from '../../config/request'; -import { getNodeEnv, getLoopsApiKey } from '../../config'; +import { getLoopsApiKey, getHttpsEnabled } from '../../config'; /** * Complete setting up user by adding their personal and auth information as part of the @@ -24,9 +24,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => { email, firstName, lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, + protectedKey, + protectedKeyIV, + protectedKeyTag, publicKey, encryptedPrivateKey, encryptedPrivateKeyIV, @@ -38,9 +38,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => { email: string; firstName: string; lastName: string; - protectedKey: string; - protectedKeyIV: string; - protectedKeyTag: string; + protectedKey: string; + protectedKeyIV: string; + protectedKeyTag: string; publicKey: string; encryptedPrivateKey: string; encryptedPrivateKeyIV: string; @@ -48,11 +48,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => { salt: string; verifier: string; organizationName: string; - } = req.body; + } = req.body; // get user user = await User.findOne({ email }); - + if (!user || (user && user?.publicKey)) { // case 1: user doesn't exist. // case 2: user has already completed account @@ -66,10 +66,10 @@ export const completeAccountSignup = async (req: Request, res: Response) => { userId: user._id.toString(), firstName, lastName, - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, publicKey, encryptedPrivateKey, encryptedPrivateKeyIV, @@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getNodeEnv() === 'production' ? true : false + secure: getHttpsEnabled() }); } catch (err) { Sentry.setUser(null); @@ -158,9 +158,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => { email, firstName, lastName, - protectedKey, - protectedKeyIV, - protectedKeyTag, + protectedKey, + protectedKeyIV, + protectedKeyTag, publicKey, encryptedPrivateKey, encryptedPrivateKeyIV, @@ -192,10 +192,10 @@ export const completeAccountInvite = async (req: Request, res: Response) => { userId: user._id.toString(), firstName, lastName, - encryptionVersion: 2, - protectedKey, - protectedKeyIV, - protectedKeyTag, + encryptionVersion: 2, + protectedKey, + protectedKeyIV, + protectedKeyTag, publicKey, encryptedPrivateKey, encryptedPrivateKeyIV, @@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => { httpOnly: true, path: '/', sameSite: 'strict', - secure: getNodeEnv() === 'production' ? true : false + secure: getHttpsEnabled() }); } catch (err) { Sentry.setUser(null); @@ -241,7 +241,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => { message: 'Failed to complete account setup' }); } - + return res.status(200).send({ message: 'Successfully set up account', user, diff --git a/backend/src/controllers/v2/workspaceController.ts b/backend/src/controllers/v2/workspaceController.ts index 650c70610..ec32dcbaf 100644 --- a/backend/src/controllers/v2/workspaceController.ts +++ b/backend/src/controllers/v2/workspaceController.ts @@ -19,7 +19,7 @@ import { reformatPullSecrets } from '../../helpers/secret'; import { pushKeys } from '../../helpers/key'; -import { getPostHogClient, EventService } from '../../services'; +import { TelemetryService, EventService } from '../../services'; import { eventPushSecrets } from '../../events'; interface V2PushSecret { @@ -48,7 +48,7 @@ interface V2PushSecret { export const pushWorkspaceSecrets = async (req: Request, res: Response) => { // upload (encrypted) secrets to workspace with id [workspaceId] try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body; const { workspaceId } = req.params; @@ -122,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => { let secrets; try { - const postHogClient = getPostHogClient(); + const postHogClient = TelemetryService.getPostHogClient(); const environment: string = req.query.environment as string; const channel: string = req.query.channel as string; const { workspaceId } = req.params; @@ -506,5 +506,4 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => { message: 'Successfully changed autoCapitalization setting', workspace }); -}; - +}; \ 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 fcf158ba8..35534d19c 100644 --- a/backend/src/ee/controllers/v1/membershipController.ts +++ b/backend/src/ee/controllers/v1/membershipController.ts @@ -2,7 +2,8 @@ import { Request, Response } from "express"; import { Membership, Workspace } from "../../../models"; import { IMembershipPermission } from "../../../models/membership"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; -import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization"; +import { ADMIN, MEMBER } from "../../../variables/organization"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables'; import { Builder } from "builder-pattern" import _ from "lodash"; @@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) => const { membershipId } = req.params; const { permissions } = req.body; const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => { - if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) { + if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) { throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" }) } diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index ea9bb7dab..5a18ae08e 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -418,7 +418,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { .skip(offset) .limit(limit) .populate('actions') - .populate('user'); + .populate('user serviceAccount serviceTokenData'); } catch (err) { Sentry.setUser({ email: req.user.email }); diff --git a/backend/src/ee/helpers/action.ts b/backend/src/ee/helpers/action.ts index eb94810f4..33111d4ba 100644 --- a/backend/src/ee/helpers/action.ts +++ b/backend/src/ee/helpers/action.ts @@ -24,11 +24,15 @@ import { const createActionUpdateSecret = async ({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId: Types.ObjectId; secretIds: Types.ObjectId[]; }) => { @@ -46,6 +50,8 @@ const createActionUpdateSecret = async ({ action = await new Action({ name, user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, workspace: workspaceId, payload: { secretVersions: latestSecretVersions @@ -72,11 +78,15 @@ const createActionUpdateSecret = async ({ const createActionSecret = async ({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId: Types.ObjectId; secretIds: Types.ObjectId[]; }) => { @@ -94,6 +104,8 @@ const createActionSecret = async ({ action = await new Action({ name, user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, workspace: workspaceId, payload: { secretVersions: latestSecretVersions @@ -110,29 +122,36 @@ const createActionSecret = async ({ } /** - * Create an (audit) action for user with id [userId] + * Create an (audit) action for client with id [userId], + * [serviceAccountId], or [serviceTokenDataId] * @param {Object} obj * @param {String} obj.name - name of action * @param {String} obj.userId - id of user associated with action * @returns */ -const createActionUser = ({ +const createActionClient = ({ name, - userId + userId, + serviceAccountId, + serviceTokenDataId }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; }) => { let action; try { action = new Action({ name, - user: userId + user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId }).save(); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); - throw new Error('Failed to create user action'); + throw new Error('Failed to create client action'); } return action; @@ -149,11 +168,15 @@ const createActionUser = ({ const createActionHelper = async ({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds, }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; secretIds?: Types.ObjectId[]; }) => { @@ -162,7 +185,7 @@ const createActionHelper = async ({ switch (name) { case ACTION_LOGIN: case ACTION_LOGOUT: - action = await createActionUser({ + action = await createActionClient({ name, userId }); diff --git a/backend/src/ee/helpers/checkMembershipPermissions.ts b/backend/src/ee/helpers/checkMembershipPermissions.ts index 50cd28917..c97a51619 100644 --- a/backend/src/ee/helpers/checkMembershipPermissions.ts +++ b/backend/src/ee/helpers/checkMembershipPermissions.ts @@ -1,8 +1,9 @@ +import { Types } from 'mongoose'; import _ from "lodash"; import { Membership } from "../../models"; -import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; +import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables'; -export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { +export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) if (!membershipForWorkspace) { return false @@ -18,15 +19,15 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi return true } -export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => { +export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) if (!membershipForWorkspace) { return false } const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; - const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); - const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); + const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); // case: you have write only if read is blocked and write is not if (isReadDisallowed && !isWriteDisallowed) { @@ -36,15 +37,15 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env return false } -export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => { +export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => { const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) if (!membershipForWorkspace) { return true } const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; - const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); - const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); + const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); + const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); if (isReadBlocked && isWriteDisallowed) { return true diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts index bdb0a2380..77d280ee1 100644 --- a/backend/src/ee/helpers/log.ts +++ b/backend/src/ee/helpers/log.ts @@ -16,12 +16,16 @@ import { */ const createLogHelper = async ({ userId, + serviceAccountId, + serviceTokenDataId, workspaceId, actions, channel, ipAddress }: { - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; actions: IAction[]; channel: string; @@ -31,6 +35,8 @@ const createLogHelper = async ({ try { log = await new Log({ user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, workspace: workspaceId ?? undefined, actionNames: actions.map((a) => a.name), actions, diff --git a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts index 5eae3721c..af4d1e21c 100644 --- a/backend/src/ee/middleware/requireSecretSnapshotAuth.ts +++ b/backend/src/ee/middleware/requireSecretSnapshotAuth.ts @@ -15,32 +15,28 @@ import { const requireSecretSnapshotAuth = ({ acceptedRoles, }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { secretSnapshotId } = req.params; - - const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId); - - if (!secretSnapshot) { - return next(SecretSnapshotNotFoundError({ - message: 'Failed to find secret snapshot' - })); - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: secretSnapshot.workspace.toString(), - acceptedRoles - }); - - req.secretSnapshot = secretSnapshot as any; - - next(); - } catch (err) { - return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' })); + const { secretSnapshotId } = req.params; + + const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId); + + if (!secretSnapshot) { + return next(SecretSnapshotNotFoundError({ + message: 'Failed to find secret snapshot' + })); } + + await validateMembership({ + userId: req.user._id, + workspaceId: secretSnapshot.workspace, + acceptedRoles + }); + + req.secretSnapshot = secretSnapshot as any; + + next(); } } diff --git a/backend/src/ee/models/action.ts b/backend/src/ee/models/action.ts index 7cdd7a355..055e144fd 100644 --- a/backend/src/ee/models/action.ts +++ b/backend/src/ee/models/action.ts @@ -11,6 +11,8 @@ import { export interface IAction { name: string; user?: Types.ObjectId, + serviceAccount?: Types.ObjectId, + serviceTokenData?: Types.ObjectId, workspace?: Types.ObjectId, payload?: { secretVersions?: Types.ObjectId[] @@ -33,8 +35,15 @@ const actionSchema = new Schema( }, user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: 'User' + }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount' + }, + serviceTokenData: { + type: Schema.Types.ObjectId, + ref: 'ServiceTokenData' }, workspace: { type: Schema.Types.ObjectId, diff --git a/backend/src/ee/models/log.ts b/backend/src/ee/models/log.ts index 47be2e58f..9ed552640 100644 --- a/backend/src/ee/models/log.ts +++ b/backend/src/ee/models/log.ts @@ -11,6 +11,8 @@ import { export interface ILog { _id: Types.ObjectId; user?: Types.ObjectId; + serviceAccount?: Types.ObjectId; + serviceTokenData?: Types.ObjectId; workspace?: Types.ObjectId; actionNames: string[]; actions: Types.ObjectId[]; @@ -24,6 +26,14 @@ const logSchema = new Schema( type: Schema.Types.ObjectId, ref: 'User' }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount' + }, + serviceTokenData: { + type: Schema.Types.ObjectId, + ref: 'ServiceTokenData' + }, workspace: { type: Schema.Types.ObjectId, ref: 'Workspace' diff --git a/backend/src/ee/routes/v1/secret.ts b/backend/src/ee/routes/v1/secret.ts index 8e93919a0..3e956a388 100644 --- a/backend/src/ee/routes/v1/secret.ts +++ b/backend/src/ee/routes/v1/secret.ts @@ -7,7 +7,12 @@ import { } from '../../../middleware'; import { query, param, body } from 'express-validator'; import { secretController } from '../../controllers/v1'; -import { ADMIN, MEMBER } from '../../../variables'; +import { + ADMIN, + MEMBER, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from '../../../variables'; router.get( '/:secretId/secret-versions', @@ -15,7 +20,8 @@ router.get( acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS] }), param('secretId').exists().trim(), query('offset').exists().isInt(), @@ -30,7 +36,8 @@ router.post( acceptedAuthModes: ['jwt', 'apiKey'] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] }), param('secretId').exists().trim(), body('version').exists().isInt(), diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index a799d073b..722bfb4a7 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -15,7 +15,8 @@ router.get( acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), query('offset').exists().isInt(), @@ -30,7 +31,8 @@ router.get( acceptedAuthModes: ['jwt'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -43,7 +45,8 @@ router.post( acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('version').exists().isInt(), @@ -57,7 +60,8 @@ router.get( acceptedAuthModes: ['jwt', 'apiKey'] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), query('offset').exists().isInt(), diff --git a/backend/src/ee/services/EELogService.ts b/backend/src/ee/services/EELogService.ts index bbe03e09e..81d26765f 100644 --- a/backend/src/ee/services/EELogService.ts +++ b/backend/src/ee/services/EELogService.ts @@ -26,12 +26,16 @@ class EELogService { */ static async createLog({ userId, + serviceAccountId, + serviceTokenDataId, workspaceId, actions, channel, ipAddress }: { - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; actions: IAction[]; channel: string; @@ -40,6 +44,8 @@ class EELogService { if (!EELicenseService.isLicenseValid) return null; return await createLogHelper({ userId, + serviceAccountId, + serviceTokenDataId, workspaceId, actions, channel, @@ -59,17 +65,23 @@ class EELogService { static async createAction({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }: { name: string; - userId: Types.ObjectId; + userId?: Types.ObjectId; + serviceAccountId?: Types.ObjectId; + serviceTokenDataId?: Types.ObjectId; workspaceId?: Types.ObjectId; secretIds?: Types.ObjectId[]; }) { return await createActionHelper({ name, userId, + serviceAccountId, + serviceTokenDataId, workspaceId, secretIds }); diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index a08dcf2cc..9cbe82e34 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,15 +1,18 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; import { IUser, User, ServiceTokenData, + ServiceAccount, APIKeyData } from '../models'; import { AccountNotFoundError, ServiceTokenDataNotFoundError, + ServiceAccountNotFoundError, APIKeyDataNotFoundError, UnauthorizedRequestError, BadRequestError @@ -20,6 +23,12 @@ import { getJwtRefreshLifetime, getJwtRefreshSecret } from '../config'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; /** * @@ -37,7 +46,7 @@ const validateAuthMode = ({ const apiKey = headers['x-api-key']; const authHeader = headers['authorization']; - let authTokenType, authTokenValue; + 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.' }); @@ -45,7 +54,7 @@ const validateAuthMode = ({ if (typeof apiKey === 'string') { // case: treat request authentication type as via X-API-KEY (i.e. API Key) - authTokenType = 'apiKey'; + authMode = AUTH_MODE_API_KEY; authTokenValue = apiKey; } @@ -61,20 +70,24 @@ const validateAuthMode = ({ switch (tokenValue.split('.', 1)[0]) { case 'st': - authTokenType = 'serviceToken'; + authMode = AUTH_MODE_SERVICE_TOKEN; + break; + case 'sa': + authMode = AUTH_MODE_SERVICE_ACCOUNT; break; default: - authTokenType = 'jwt'; + authMode = AUTH_MODE_JWT; } + authTokenValue = tokenValue; } - if (!authTokenType || !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(authTokenType)) 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 ({ - authTokenType, + authMode, authTokenValue }); } @@ -90,25 +103,17 @@ const getAuthUserPayload = async ({ }: { authTokenValue: string; }) => { - let user; - try { - const decodedToken = ( - jwt.verify(authTokenValue, getJwtAuthSecret()) - ); + const decodedToken = ( + jwt.verify(authTokenValue, getJwtAuthSecret()) + ); - user = await User.findOne({ - _id: decodedToken.userId - }).select('+publicKey'); + const user = await User.findOne({ + _id: decodedToken.userId + }).select('+publicKey'); - 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' }); - - } catch (err) { - throw UnauthorizedRequestError({ - message: 'Failed to authenticate JWT token' - }); - } + if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); return user; } @@ -124,45 +129,70 @@ const getAuthSTDPayload = async ({ }: { authTokenValue: string; }) => { - let serviceTokenData; - try { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); - // TODO: optimize double query - serviceTokenData = await ServiceTokenData - .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt'); + let serviceTokenData = await ServiceTokenData + .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt'); - if (!serviceTokenData) { - 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' - }); - } - - const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash); - if (!isMatch) throw UnauthorizedRequestError({ - message: 'Failed to authenticate service token' - }); - - serviceTokenData = await ServiceTokenData - .findById(TOKEN_IDENTIFIER) - .select('+encryptedKey +iv +tag') - .populate<{user: IUser}>('user'); - - if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); - - } catch (err) { + if (!serviceTokenData) { + 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 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' + }); + + serviceTokenData = await ServiceTokenData + .findOneAndUpdate({ + _id: new Types.ObjectId(TOKEN_IDENTIFIER) + }, { + lastUsed: new Date() + }, { + new: true + }) + .select('+encryptedKey +iv +tag').populate('user serviceAccount'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); + return serviceTokenData; } +/** + * Return service account access key payload + * @param {Object} obj + * @param {String} obj.authTokenValue - service account access token value + * @returns {ServiceAccount} serviceAccount + */ +const getAuthSAAKPayload = async ({ + authTokenValue +}: { + authTokenValue: string; +}) => { + 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'); + + if (!serviceAccount) { + 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' + }); + + return serviceAccount; +} + /** * Return API key data payload corresponding to API key [authTokenValue] * @param {Object} obj @@ -174,33 +204,44 @@ const getAuthAPIKeyPayload = async ({ }: { authTokenValue: string; }) => { - let user; - try { - const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); + const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3); - const apiKeyData = await APIKeyData - .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') - .populate('user', '+publicKey'); + let apiKeyData = await APIKeyData + .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') + .populate<{ user: IUser }>('user', '+publicKey'); - if (!apiKeyData) { - 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' - }); - } - - const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash); - if (!isMatch) throw UnauthorizedRequestError({ - message: 'Failed to authenticate API key' - }); - - user = apiKeyData.user; - } catch (err) { + if (!apiKeyData) { + 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 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' + }); + + apiKeyData = await APIKeyData.findOneAndUpdate({ + _id: new Types.ObjectId(TOKEN_IDENTIFIER) + }, { + lastUsed: new Date() + }, { + new: true + }); + + if (!apiKeyData) { + throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); + } + + const user = await User.findById(apiKeyData.user).select('+publicKey'); + + if (!user) { + throw AccountNotFoundError({ + message: 'Failed to find user' }); } @@ -216,30 +257,23 @@ const getAuthAPIKeyPayload = async ({ * @return {String} obj.refreshToken - issued refresh token */ const issueAuthTokens = async ({ userId }: { userId: string }) => { - let token: string; - let refreshToken: string; - try { - // issue tokens - token = createToken({ - payload: { - userId - }, - expiresIn: getJwtAuthLifetime(), - secret: getJwtAuthSecret() - }); - refreshToken = createToken({ - payload: { - userId - }, - expiresIn: getJwtRefreshLifetime(), - secret: getJwtRefreshSecret() - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to issue tokens'); - } + // issue tokens + const token = createToken({ + payload: { + userId + }, + expiresIn: getJwtAuthLifetime(), + secret: getJwtAuthSecret() + }); + + const refreshToken = createToken({ + payload: { + userId + }, + expiresIn: getJwtRefreshLifetime(), + secret: getJwtRefreshSecret() + }); return { token, @@ -253,19 +287,14 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => { * @param {String} obj.userId - id of user whose tokens are cleared. */ const clearTokens = async ({ userId }: { userId: string }): Promise => { - try { - // increment refreshVersion on user by 1 - User.findOneAndUpdate({ - _id: userId - }, { - $inc: { - refreshVersion: 1 - } - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - } + // increment refreshVersion on user by 1 + User.findOneAndUpdate({ + _id: userId + }, { + $inc: { + refreshVersion: 1 + } + }); }; /** @@ -285,21 +314,16 @@ const createToken = ({ expiresIn: string | number; secret: string; }) => { - try { - return jwt.sign(payload, secret, { - expiresIn - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create a token'); - } + return jwt.sign(payload, secret, { + expiresIn + }); }; export { validateAuthMode, getAuthUserPayload, getAuthSTDPayload, + getAuthSAAKPayload, getAuthAPIKeyPayload, createToken, issueAuthTokens, diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index 5cfbeebf5..1e242778d 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -1,10 +1,16 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Bot, BotKey, Secret, ISecret, - IUser + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData } from '../models'; import { generateKeyPair, @@ -12,8 +18,88 @@ import { decryptSymmetric, decryptAsymmetric } from '../utils/crypto'; -import { SECRET_SHARED } from '../variables'; +import { + SECRET_SHARED, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; import { getEncryptionKey } from '../config'; +import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors'; +import { + validateMembership +} from '../helpers/membership'; +import { + validateUserClientForWorkspace +} from '../helpers/user'; +import { + validateServiceAccountClientForWorkspace +} from '../helpers/serviceAccount'; + +/** + * Validate authenticated clients for bot with id [botId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.botId - id of bot to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + */ +const validateClientForBot = async ({ + authData, + botId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + botId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + const bot = await Bot.findById(botId); + + if (!bot) throw BotNotFoundError(); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: bot.workspace, + acceptedRoles + }); + + return bot; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: bot.workspace + }); + + return bot; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for bot' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: bot.workspace, + acceptedRoles + }); + + return bot; + } + + throw BotNotFoundError({ + message: 'Failed client authorization for bot' + }); +} /** * Create an inactive bot with name [name] for workspace with id [workspaceId] @@ -222,6 +308,7 @@ const decryptSymmetricHelper = async ({ } export { + validateClientForBot, createBot, getSecretsHelper, encryptSymmetricHelper, diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 27afbb4b1..fbbc2f807 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -1,17 +1,42 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Bot, Integration, IntegrationAuth, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData } from '../models'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { BotService } from '../services'; import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY, INTEGRATION_VERCEL, INTEGRATION_NETLIFY } from '../variables'; -import { UnauthorizedRequestError } from '../utils/errors'; +import { + UnauthorizedRequestError, + IntegrationAuthNotFoundError, + IntegrationNotFoundError +} from '../utils/errors'; import RequestError from '../utils/requestError'; +import { + validateClientForIntegrationAuth +} from '../helpers/integrationAuth'; +import { + validateUserClientForWorkspace +} from '../helpers/user'; +import { + validateServiceAccountClientForWorkspace +} from '../helpers/serviceAccount'; +import { IntegrationService } from '../services'; interface Update { workspace: string; @@ -20,6 +45,84 @@ interface Update { accountId?: string; } +/** + * Validate authenticated clients for integration with id [integrationId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.integrationId - id of integration to validate against + * @param {String} obj.environment - (optional) environment in workspace to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ + const validateClientForIntegration = async ({ + authData, + integrationId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + integrationId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + + const integration = await Integration.findById(integrationId); + if (!integration) throw IntegrationNotFoundError(); + + const integrationAuth = await IntegrationAuth + .findById(integration.integrationAuth) + .select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); + + if (!integrationAuth) throw IntegrationAuthNotFoundError(); + + const accessToken = (await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + })).accessToken; + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integration.workspace, + acceptedRoles + }); + + return ({ integration, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: integration.workspace + }); + + return ({ integration, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for integration' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integration.workspace, + acceptedRoles + }); + + return ({ integration, accessToken }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for integration' + }); +} + /** * Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration * named [integration] @@ -140,7 +243,7 @@ const syncIntegrationsHelper = async ({ // get integration auth access token const access = await getIntegrationAuthAccessHelper({ - integrationAuthId: integration.integrationAuth.toString() + integrationAuthId: integration.integrationAuth }); // sync secrets to integration @@ -167,7 +270,7 @@ const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ - const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { + const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { let refreshToken; try { @@ -204,7 +307,7 @@ const syncIntegrationsHelper = async ({ * @param {String} obj.integrationAuthId - id of integration auth * @returns {String} accessToken - decrypted access token */ -const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { +const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { let accessId; let accessToken; try { @@ -367,6 +470,7 @@ const setIntegrationAuthAccessHelper = async ({ } export { + validateClientForIntegration, handleOAuthExchangeHelper, syncIntegrationsHelper, getIntegrationAuthRefreshHelper, diff --git a/backend/src/helpers/integrationAuth.ts b/backend/src/helpers/integrationAuth.ts index e69de29bb..c169fb799 100644 --- a/backend/src/helpers/integrationAuth.ts +++ b/backend/src/helpers/integrationAuth.ts @@ -0,0 +1,108 @@ +import { Types } from 'mongoose'; +import { + IntegrationAuth, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData, + IWorkspace +} from '../models'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; +import { + IntegrationAuthNotFoundError, + UnauthorizedRequestError +} from '../utils/errors'; +import { IntegrationService } from '../services'; +import { validateUserClientForWorkspace } from '../helpers/user'; +import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; + +/** + * Validate authenticated clients for integration authorization with id [integrationAuthId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.integrationAuthId - id of integration authorization to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ + const validateClientForIntegrationAuth = async ({ + authData, + integrationAuthId, + acceptedRoles, + attachAccessToken +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + integrationAuthId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; + attachAccessToken?: boolean; +}) => { + + const integrationAuth = await IntegrationAuth + .findById(integrationAuthId) + .populate<{ workspace: IWorkspace }>('workspace') + .select( + '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' + ); + + if (!integrationAuth) throw IntegrationAuthNotFoundError(); + + let accessToken; + if (attachAccessToken) { + accessToken = (await IntegrationService.getIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id + })).accessToken; + } + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integrationAuth.workspace._id, + acceptedRoles + }); + + return ({ integrationAuth, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: integrationAuth.workspace._id + }); + + return ({ integrationAuth, accessToken }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for integration authorization' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: integrationAuth.workspace._id, + acceptedRoles + }); + + return ({ integrationAuth, accessToken }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for integration authorization' + }); +} + +export { + validateClientForIntegrationAuth +}; \ No newline at end of file diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 406162a8e..503ca9fc6 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -1,5 +1,106 @@ import * as Sentry from '@sentry/node'; -import { Membership, Key } from '../models'; +import { Types } from 'mongoose'; +import { + Membership, + Key, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData +} from '../models'; +import { + MembershipNotFoundError, + BadRequestError, + UnauthorizedRequestError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; +import { + validateUserClientForWorkspace +} from '../helpers/user'; +import { + validateServiceAccountClientForWorkspace +} from '../helpers/serviceAccount'; +import { + validateServiceTokenDataClientForWorkspace +} from '../helpers/serviceTokenData'; + +/** + * Validate authenticated clients for membership with id [membershipId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.membershipId - id of membership to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles + * @returns {Membership} - validated membership + */ +const validateClientForMembership = async ({ + authData, + membershipId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + membershipId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + + const membership = await Membership.findById(membershipId); + + if (!membership) throw MembershipNotFoundError({ + message: 'Failed to find membership' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: membership.workspace, + acceptedRoles + }); + + return membership; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: membership.workspace + }); + + return membership; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId: new Types.ObjectId(membership.workspace) + }); + + return membership; + } + + if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: membership.workspace, + acceptedRoles + }); + + return membership; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for membership' + }); +} /** * Validate that user with id [userId] is a member of workspace with id [workspaceId] @@ -14,28 +115,24 @@ const validateMembership = async ({ workspaceId, acceptedRoles, }: { - userId: string; - workspaceId: string; - acceptedRoles: string[]; + userId: Types.ObjectId; + workspaceId: Types.ObjectId; + acceptedRoles?: Array<'admin' | 'member'>; }) => { - let membership; - //TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors - try { - membership = await Membership.findOne({ - user: userId, - workspace: workspaceId - }).populate("workspace"); - - if (!membership) throw new Error('Failed to find membership'); - + const membership = await Membership.findOne({ + user: userId, + workspace: workspaceId + }).populate("workspace"); + + if (!membership) { + throw MembershipNotFoundError({ message: 'Failed to find workspace membership' }); + } + + if (acceptedRoles) { if (!acceptedRoles.includes(membership.role)) { - throw new Error('Failed to validate membership role'); + throw BadRequestError({ message: 'Failed authorization for membership role' }); } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to validate membership'); } return membership; @@ -133,6 +230,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { }; export { + validateClientForMembership, validateMembership, addMemberships, findMembership, diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index 4de4be82d..b34e5dd2f 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -1,40 +1,140 @@ import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; -import { MembershipOrg, Workspace, Membership, Key } from '../models'; +import { + MembershipOrg, + Workspace, + Membership, + Key, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData +} from '../models'; +import { + MembershipOrgNotFoundError, + BadRequestError, + UnauthorizedRequestError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for organization membership with id [membershipOrgId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against + * @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles + * @param {MembershipOrg} - validated organization membership + */ +const validateClientForMembershipOrg = async ({ + authData, + membershipOrgId, + acceptedRoles, + acceptedStatuses +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + membershipOrgId: Types.ObjectId; + 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 ' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateMembershipOrg({ + userId: authData.authPayload._id, + organizationId: membershipOrg.organization, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; + } + + 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' + }); + + return membershipOrg; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service account client authorization for organization membership' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateMembershipOrg({ + userId: authData.authPayload._id, + organizationId: membershipOrg.organization, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for organization membership' + }); +} /** * Validate that user with id [userId] is a member of organization with id [organizationId] * and has at least one of the roles in [acceptedRoles] - * + * @param {Object} obj + * @param {Types.ObjectId} obj.userId + * @param {Types.ObjectId} obj.organizationId + * @param {String[]} obj.acceptedRoles */ -const validateMembership = async ({ +const validateMembershipOrg = async ({ userId, organizationId, - acceptedRoles + acceptedRoles, + acceptedStatuses }: { - userId: string; - organizationId: string; - acceptedRoles: string[]; + userId: Types.ObjectId; + organizationId: Types.ObjectId; + acceptedRoles?: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses?: Array<'invited' | 'accepted'>; }) => { - let membership; - try { - membership = await MembershipOrg.findOne({ - user: new Types.ObjectId(userId), - organization: new Types.ObjectId(organizationId) - }); - - if (!membership) throw new Error('Failed to find organization membership'); - - if (!acceptedRoles.includes(membership.role)) { - throw new Error('Failed to validate organization membership role'); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to validate organization membership'); + const membershipOrg = await MembershipOrg.findOne({ + user: userId, + organization: organizationId + }); + + if (!membershipOrg) { + throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' }); } - return membership; + if (acceptedRoles) { + if (!acceptedRoles.includes(membershipOrg.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' }); + } + } + + return membershipOrg; } /** @@ -156,7 +256,8 @@ const deleteMembershipOrg = async ({ }; export { - validateMembership, + validateClientForMembershipOrg, + validateMembershipOrg, findMembershipOrg, addMembershipsOrg, deleteMembershipOrg diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index fb559df1b..9840c9075 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -1,14 +1,110 @@ import * as Sentry from '@sentry/node'; import Stripe from 'stripe'; import { Types } from 'mongoose'; -import { ACCEPTED } from '../variables'; +import { + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData +} from '../models'; import { Organization, MembershipOrg } from '../models'; +import { + ACCEPTED, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY, + OWNER +} from '../variables'; import { getStripeSecretKey, getStripeProductPro, getStripeProductTeam, getStripeProductStarter } from '../config'; +import { + UnauthorizedRequestError, + OrganizationNotFoundError +} from '../utils/errors'; +import { + validateUserClientForOrganization +} from '../helpers/user'; +import { + validateServiceAccountClientForOrganization +} from '../helpers/serviceAccount'; + +/** + * Validate accepted clients for organization with id [organizationId] + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.organizationId - id of organization to validate against + */ +const validateClientForOrganization = async ({ + authData, + organizationId, + acceptedRoles, + acceptedStatuses +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, + organizationId: Types.ObjectId; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; +}) => { + + const organization = await Organization.findById(organizationId); + + if (!organization) { + throw OrganizationNotFoundError({ + message: 'Failed to find organization' + }); + } + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses + }); + + return ({ organization, membershipOrg }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForOrganization({ + serviceAccount: authData.authPayload, + organization + }); + + return ({ organization }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for organization' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses + }); + + return ({ organization, membershipOrg }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for organization' + }); +} /** * Create an organization with name [name] @@ -172,6 +268,7 @@ const updateSubscriptionOrgQuantity = async ({ }; export { + validateClientForOrganization, createOrganization, initSubscriptionOrg, updateSubscriptionOrgQuantity diff --git a/backend/src/helpers/rateLimiter.ts b/backend/src/helpers/rateLimiter.ts index 432bd5f97..073ba11bc 100644 --- a/backend/src/helpers/rateLimiter.ts +++ b/backend/src/helpers/rateLimiter.ts @@ -15,7 +15,7 @@ const apiLimiter = rateLimit({ }); // 10 requests per minute -const authLimiter = rateLimit({ +const authLimit = rateLimit({ windowMs: 60 * 1000, max: 10, standardHeaders: true, @@ -36,8 +36,16 @@ const passwordLimiter = rateLimit({ } }); -export { - apiLimiter, - authLimiter, - passwordLimiter +const authLimiter = (req: any, res: any, next: any) => { + if (process.env.NODE_ENV === 'production') { + authLimit(req, res, next); + } else { + next(); + } +}; + +export { + apiLimiter, + authLimiter, + passwordLimiter }; diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index c0f57f3ef..7a7979357 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -21,60 +21,8 @@ import { ACTION_READ_SECRETS } from '../variables'; import _ from 'lodash'; -import { ABILITY_WRITE } from '../variables/organization'; import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; -/** - * Validate that user with id [userId] can modify secrets with ids [secretIds] - * @param {Object} obj - * @param {Object} obj.userId - id of user to validate - * @param {Object} obj.secretIds - secret ids - * @returns {Secret[]} secrets - */ -const validateSecrets = async ({ - userId, - secretIds -}: { - userId: string; - secretIds: string[]; -}) => { - let secrets; - try { - secrets = await Secret.find({ - _id: { - $in: secretIds.map((secretId: string) => new Types.ObjectId(secretId)) - } - }); - - if (secrets.length != secretIds.length) { - throw BadRequestError({ message: 'Unable to validate some secrets' }) - } - - const userMemberships = await Membership.find({ user: userId }) - 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())) { - const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; - const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE }); - - if (isDisallowed) { - throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' }); - } - } else { - throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' }); - } - }); - - } catch (err) { - throw BadRequestError({ message: 'Unable to validate secrets' }) - } - - return secrets; -} - interface V1PushSecret { ciphertextKey: string; ivKey: string; @@ -714,7 +662,6 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { }; export { - validateSecrets, v1PushSecrets, v2PushSecrets, pullSecrets, diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts new file mode 100644 index 000000000..7d3e54105 --- /dev/null +++ b/backend/src/helpers/secrets.ts @@ -0,0 +1,198 @@ +import { Types } from 'mongoose'; +import { + User, + IUser, + ServiceAccount, + IServiceAccount, + ServiceTokenData, + IServiceTokenData, + Secret, + ISecret +} from '../models'; +import { + validateMembership +} from '../helpers/membership'; +import { + validateUserClientForSecret, + validateUserClientForSecrets +} from '../helpers/user'; +import { + validateServiceTokenDataClientForSecrets, validateServiceTokenDataClientForWorkspace +} from '../helpers/serviceTokenData'; +import { + validateServiceAccountClientForSecrets, + validateServiceAccountClientForWorkspace +} from '../helpers/serviceAccount'; +import { + BadRequestError, + UnauthorizedRequestError, + SecretNotFoundError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for secrets with id [secretId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.secretId - id of secret to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +const validateClientForSecret = async ({ + authData, + secretId, + acceptedRoles, + requiredPermissions +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, + secretId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions: string[]; +}) => { + const secret = await Secret.findById(secretId); + + if (!secret) throw SecretNotFoundError({ + message: 'Failed to find secret' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment, + requiredPermissions + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId: secret.workspace, + environment: secret.environment + }); + + return secret; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForSecret({ + user: authData.authPayload, + secret, + acceptedRoles, + requiredPermissions + }); + + return secret; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for secret' + }); +} + +/** + * Validate authenticated clients for secrets with ids [secretIds] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against + * @param {String} obj.environment - (optional) environment in workspace to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +const validateClientForSecrets = async ({ + authData, + secretIds, + requiredPermissions +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, + secretIds: Types.ObjectId[]; + requiredPermissions: string[]; +}) => { + + let secrets: ISecret[] = []; + + secrets = await Secret.find({ + _id: { + $in: secretIds + } + }); + + if (secrets.length != secretIds.length) { + 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 + }); + + return secrets; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForSecrets({ + serviceAccount: authData.authPayload, + secrets, + requiredPermissions + }); + + return secrets; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForSecrets({ + serviceTokenData: authData.authPayload, + secrets, + requiredPermissions + }); + + return secrets; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForSecrets({ + user: authData.authPayload, + secrets, + requiredPermissions + }); + + return secrets; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for secrets resource' + }); +} + +export { + validateClientForSecret, + validateClientForSecrets +} \ No newline at end of file diff --git a/backend/src/helpers/serviceAccount.ts b/backend/src/helpers/serviceAccount.ts new file mode 100644 index 000000000..892767259 --- /dev/null +++ b/backend/src/helpers/serviceAccount.ts @@ -0,0 +1,271 @@ +import _ from 'lodash'; +import { Types } from 'mongoose'; +import { + User, + IUser, + ServiceAccount, + IServiceAccount, + ServiceTokenData, + IServiceTokenData, + ISecret, + IOrganization, + IServiceAccountWorkspacePermission, + ServiceAccountWorkspacePermission +} from '../models'; +import { + BadRequestError, + UnauthorizedRequestError, + ServiceAccountNotFoundError +} from '../utils/errors'; +import { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; +import { + validateUserClientForServiceAccount +} from '../helpers/user'; + +const validateClientForServiceAccount = async ({ + authData, + serviceAccountId, + requiredPermissions +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }, + serviceAccountId: Types.ObjectId; + requiredPermissions?: string[]; +}) => { + const serviceAccount = await ServiceAccount.findById(serviceAccountId); + + if (!serviceAccount) { + throw ServiceAccountNotFoundError({ + message: 'Failed to find service account' + }); + } + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + await validateUserClientForServiceAccount({ + user: authData.authPayload, + serviceAccount, + requiredPermissions + }); + + return serviceAccount; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForServiceAccount({ + serviceAccount: authData.authPayload, + targetServiceAccount: serviceAccount, + requiredPermissions + }); + + return serviceAccount; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for service account resource' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForServiceAccount({ + user: authData.authPayload, + serviceAccount, + requiredPermissions + }); + + return serviceAccount; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for service account resource' + }); +} + +/** + * Validate that service account (client) can access workspace + * with id [workspaceId] and its environment [environment] with required permissions + * [requiredPermissions] + * @param {Object} obj + * @param {ServiceAccount} obj.serviceAccount - service account client + * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against + * @param {String} environment - (optional) environment in workspace to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ + const validateServiceAccountClientForWorkspace = async ({ + serviceAccount, + workspaceId, + environment, + requiredPermissions +}: { + serviceAccount: IServiceAccount; + workspaceId: Types.ObjectId; + environment?: string; + requiredPermissions?: string[]; +}) => { + if (environment) { + // case: environment specified -> + // evaluate service account authorization for workspace + // in the context of a specific environment [environment] + const permission = await ServiceAccountWorkspacePermission.findOne({ + serviceAccount, + workspace: new Types.ObjectId(workspaceId), + environment + }); + + if (!permission) throw UnauthorizedRequestError({ + message: 'Failed service account authorization for the given workspace environment' + }); + + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + if (!permission.read) runningIsDisallowed = true; + break; + case PERMISSION_WRITE_SECRETS: + if (!permission.write) runningIsDisallowed = true; + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + + } else { + // case: no environment specified -> + // evaluate service account authorization for workspace + // without need of environment [environment] + + const permission = await ServiceAccountWorkspacePermission.findOne({ + serviceAccount, + workspace: new Types.ObjectId(workspaceId) + }); + + if (!permission) throw UnauthorizedRequestError({ + message: 'Failed service account authorization for the given workspace' + }); + } +} + +/** + * Validate that service account (client) can access secrets + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {ServiceAccount} obj.serviceAccount - service account client + * @param {Secret[]} secrets - secrets to validate against + * @param {string[]} requiredPermissions - required permissions as part of the endpoint + */ + const validateServiceAccountClientForSecrets = async ({ + serviceAccount, + secrets, + requiredPermissions +}: { + serviceAccount: IServiceAccount; + secrets: ISecret[]; + requiredPermissions?: string[]; +}) => { + + const permissions = await ServiceAccountWorkspacePermission.find({ + serviceAccount: serviceAccount._id + }); + + const permissionsObj = _.keyBy(permissions, (p) => { + return `${p.workspace.toString()}-${p.environment}` + }); + + secrets.forEach((secret: ISecret) => { + const permission = permissionsObj[`${secret.workspace.toString()}-${secret.environment}`]; + + if (!permission) throw BadRequestError({ + message: 'Failed to find any permission for the secret workspace and environment' + }); + + requiredPermissions?.forEach((requiredPermission: string) => { + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + if (!permission.read) runningIsDisallowed = true; + break; + case PERMISSION_WRITE_SECRETS: + if (!permission.write) runningIsDisallowed = true; + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + }); + }); +} + +/** + * Validate that service account (client) can access target service + * account [serviceAccount] with required permissions [requiredPermissions] + * @param {Object} obj + * @param {SerivceAccount} obj.serviceAccount - service account client + * @param {ServiceAccount} targetServiceAccount - target service account to validate against + * @param {string[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateServiceAccountClientForServiceAccount = ({ + serviceAccount, + targetServiceAccount, + requiredPermissions +}: { + serviceAccount: IServiceAccount; + targetServiceAccount: IServiceAccount; + requiredPermissions?: string[]; +}) => { + if (!serviceAccount.organization.equals(targetServiceAccount.organization)) { + throw UnauthorizedRequestError({ + message: 'Failed service account authorization for the given service account' + }); + } +} + +/** + * Validate that service account (client) can access organization [organization] + * @param {Object} obj + * @param {User} obj.user - service account client + * @param {Organization} obj.organization - organization to validate against + */ +const validateServiceAccountClientForOrganization = async ({ + serviceAccount, + organization +}: { + serviceAccount: IServiceAccount; + organization: IOrganization; +}) => { + if (!serviceAccount.organization.equals(organization._id)) { + throw UnauthorizedRequestError({ + message: 'Failed service account authorization for the given organization' + }); + } +} + +export { + validateClientForServiceAccount, + validateServiceAccountClientForWorkspace, + validateServiceAccountClientForSecrets, + validateServiceAccountClientForServiceAccount, + validateServiceAccountClientForOrganization +} \ No newline at end of file diff --git a/backend/src/helpers/serviceTokenData.ts b/backend/src/helpers/serviceTokenData.ts new file mode 100644 index 000000000..9a8bb1288 --- /dev/null +++ b/backend/src/helpers/serviceTokenData.ts @@ -0,0 +1,189 @@ +import { Types } from 'mongoose'; +import { + ISecret, + IServiceTokenData, + ServiceTokenData, + IUser, + User, + IServiceAccount, + ServiceAccount, +} from '../models'; +import { + UnauthorizedRequestError, + ServiceTokenDataNotFoundError +} from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; +import { validateUserClientForWorkspace } from '../helpers/user'; +import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; + +/** + * Validate authenticated clients for service token with id [serviceTokenId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + */ +const validateClientForServiceTokenData = async ({ + authData, + serviceTokenDataId, + acceptedRoles +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + serviceTokenDataId: Types.ObjectId; + acceptedRoles: Array<'admin' | 'member'>; +}) => { + const serviceTokenData = await ServiceTokenData + .findById(serviceTokenDataId) + .select('+encryptedKey +iv +tag') + .populate<{ user: IUser }>('user'); + + if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ + 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 + }); + + return serviceTokenData; + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: serviceTokenData.workspace + }); + + return serviceTokenData; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for service token data' + }); + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: serviceTokenData.workspace, + acceptedRoles + }); + + return serviceTokenData; + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for service token data' + }); +} + +/** + * Validate that service token (client) can access workspace + * with id [workspaceId] and its environment [environment] with required permissions + * [requiredPermissions] + * @param {Object} obj + * @param {ServiceTokenData} obj.serviceTokenData - service token client + * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against + * @param {String} environment - (optional) environment in workspace to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ + const validateServiceTokenDataClientForWorkspace = async ({ + serviceTokenData, + workspaceId, + environment, + requiredPermissions +}: { + serviceTokenData: IServiceTokenData; + workspaceId: Types.ObjectId; + environment?: string; + requiredPermissions?: string[]; +}) => { + + if (!serviceTokenData.workspace.equals(workspaceId)) { + // case: invalid workspaceId passed + throw UnauthorizedRequestError({ + message: 'Failed service token authorization for the given workspace' + }); + } + + if (environment) { + // case: environment is specified + + if (serviceTokenData.environment !== environment) { + // case: invalid environment passed + throw UnauthorizedRequestError({ + 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}` + }); + } + }); + } +} + +/** + * Validate that service token (client) can access secrets + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {ServiceTokenData} obj.serviceTokenData - service token client + * @param {Secret[]} secrets - secrets to validate against + * @param {string[]} requiredPermissions - required permissions as part of the endpoint + */ + const validateServiceTokenDataClientForSecrets = async ({ + serviceTokenData, + secrets, + requiredPermissions +}: { + serviceTokenData: IServiceTokenData; + secrets: ISecret[]; + requiredPermissions?: string[]; +}) => { + + secrets.forEach((secret: ISecret) => { + if (!serviceTokenData.workspace.equals(secret.workspace)) { + // case: invalid workspaceId passed + throw UnauthorizedRequestError({ + 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' + }); + } + + requiredPermissions?.forEach((permission) => { + if (!serviceTokenData.permissions.includes(permission)) { + throw UnauthorizedRequestError({ + message: `Failed service token authorization for the given workspace environment action: ${permission}` + }); + } + }); + }); +} + +export { + validateClientForServiceTokenData, + validateServiceTokenDataClientForWorkspace, + validateServiceTokenDataClientForSecrets +} \ No newline at end of file diff --git a/backend/src/helpers/telemetry.ts b/backend/src/helpers/telemetry.ts new file mode 100644 index 000000000..e69de29bb diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 932a4bd81..5a33f3d2e 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,6 +1,25 @@ import * as Sentry from '@sentry/node'; -import { IUser, User } from '../models'; +import { Types } from 'mongoose'; +import { + IUser, + ISecret, + IServiceAccount, + User, + Membership, + IOrganization, + Organization, +} from '../models'; import { sendMail } from './nodemailer'; +import { validateMembership } from './membership'; +import _ from 'lodash'; +import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; +import { + validateMembershipOrg +} from '../helpers/membershipOrg'; +import { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from '../variables'; /** * Initialize a user under email [email] @@ -146,4 +165,204 @@ const checkUserDevice = async ({ } } -export { setupAccount, completeAccount, checkUserDevice }; +/** + * Validate that user (client) can access workspace + * with id [workspaceId] and its environment [environment] with required permissions + * [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against + * @param {String} environment - (optional) environment in workspace to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateUserClientForWorkspace = async ({ + user, + workspaceId, + environment, + acceptedRoles, + requiredPermissions +}: { + user: IUser; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions?: string[]; +}) => { + + // validate user membership in workspace + const membership = await validateMembership({ + userId: user._id, + workspaceId, + acceptedRoles + }); + + let runningIsDisallowed = false; + requiredPermissions?.forEach((requiredPermission: string) => { + switch (requiredPermission) { + case PERMISSION_READ_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); + break; + case PERMISSION_WRITE_SECRETS: + runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); + break; + default: + break; + } + + if (runningIsDisallowed) { + throw UnauthorizedRequestError({ + message: `Failed permissions authorization for workspace environment action : ${requiredPermission}` + }); + } + }); + + return membership; +} + +/** + * Validate that user (client) can access secret [secret] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Secret[]} obj.secrets - secrets to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateUserClientForSecret = async ({ + user, + secret, + acceptedRoles, + requiredPermissions +}: { + user: IUser; + secret: ISecret; + acceptedRoles?: Array<'admin' | 'member'>; + requiredPermissions?: string[]; +}) => { + const membership = await validateMembership({ + userId: user._id, + workspaceId: secret.workspace, + acceptedRoles + }); + + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: 'You do not have the required permissions to perform this action' + }); + } + } +} + +/** + * Validate that user (client) can access secrets [secrets] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Secret[]} obj.secrets - secrets to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ + const validateUserClientForSecrets = async ({ + user, + secrets, + requiredPermissions +}: { + user: IUser; + secrets: ISecret[]; + requiredPermissions?: string[]; +}) => { + + // TODO: add acceptedRoles? + + const userMemberships = await Membership.find({ user: user._id }) + 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' + }); + } + + if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) { + const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions; + const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS }); + + if (isDisallowed) { + throw UnauthorizedRequestError({ + message: 'You do not have the required permissions to perform this action' + }); + } + } + }); +} + +/** + * Validate that user (client) can access service account [serviceAccount] + * with required permissions [requiredPermissions] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {ServiceAccount} obj.serviceAccount - service account to validate against + * @param {String[]} requiredPermissions - required permissions as part of the endpoint + */ +const validateUserClientForServiceAccount = async ({ + user, + serviceAccount, + requiredPermissions +}: { + user: IUser; + serviceAccount: IServiceAccount; + requiredPermissions?: string[]; +}) => { + if (!serviceAccount.user.equals(user._id)) { + // case: user who created service account is not the + // same user that is on the request + await validateMembershipOrg({ + userId: user._id, + organizationId: serviceAccount.organization, + acceptedRoles: [], + acceptedStatuses: [] + }); + } +} + +/** + * Validate that user (client) can access organization [organization] + * @param {Object} obj + * @param {User} obj.user - user client + * @param {Organization} obj.organization - organization to validate against + */ + const validateUserClientForOrganization = async ({ + user, + organization, + acceptedRoles, + acceptedStatuses +}: { + user: IUser; + organization: IOrganization; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; +}) => { + const membershipOrg = await validateMembershipOrg({ + userId: user._id, + organizationId: organization._id, + acceptedRoles, + acceptedStatuses + }); + + return membershipOrg; +} + +export { + setupAccount, + completeAccount, + checkUserDevice, + validateUserClientForWorkspace, + validateUserClientForSecrets, + validateUserClientForServiceAccount, + validateUserClientForOrganization, + validateUserClientForSecret +}; diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index b43252bf3..f3b27bf10 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,12 +1,115 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Workspace, Bot, Membership, Key, - Secret + Secret, + User, + IUser, + ServiceAccountWorkspacePermission, + ServiceAccount, + IServiceAccount, + ServiceTokenData, + IServiceTokenData, } from '../models'; import { createBot } from '../helpers/bot'; +import { validateUserClientForWorkspace } from '../helpers/user'; +import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount'; +import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData'; +import { validateMembership } from '../helpers/membership'; +import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; + +/** + * Validate authenticated clients for workspace with id [workspaceId] based + * on any known permissions. + * @param {Object} obj + * @param {Object} obj.authData - authenticated client details + * @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against + * @param {String} obj.environment - (optional) environment in workspace to validate against + * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles + * @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint + */ +const validateClientForWorkspace = async ({ + authData, + workspaceId, + environment, + acceptedRoles, + requiredPermissions +}: { + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + workspaceId: Types.ObjectId; + environment?: string; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions?: string[]; +}) => { + + const workspace = await Workspace.findById(workspaceId); + + if (!workspace) throw WorkspaceNotFoundError({ + message: 'Failed to find workspace' + }); + + if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { + const membership = await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId, + environment, + acceptedRoles, + requiredPermissions + }); + + return ({ membership }); + } + + if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId, + environment, + requiredPermissions + }); + + return {}; + } + + if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { + await validateServiceTokenDataClientForWorkspace({ + serviceTokenData: authData.authPayload, + workspaceId, + environment, + requiredPermissions + }); + + return {}; + } + + if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { + const membership = await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId, + environment, + acceptedRoles, + requiredPermissions + }); + + return ({ membership }); + } + + throw UnauthorizedRequestError({ + message: 'Failed client authorization for workspace' + }); +} /** * Create a workspace with name [name] in organization with id [organizationId] @@ -71,4 +174,8 @@ const deleteWorkspace = async ({ id }: { id: string }) => { } }; -export { createWorkspace, deleteWorkspace }; +export { + validateClientForWorkspace, + createWorkspace, + deleteWorkspace +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index 03534a927..912d39177 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -9,7 +9,7 @@ import * as Sentry from '@sentry/node'; import { DatabaseService } from './services'; import { setUpHealthEndpoint } from './services/health'; import { initSmtp } from './services/smtp'; -import { logTelemetryMessage } from './services'; +import { TelemetryService } from './services'; import { setTransporter } from './helpers/nodemailer'; import { createTestUserForDevelopment } from './utils/addDevelopmentUser'; // eslint-disable-next-line @typescript-eslint/no-var-requires @@ -56,6 +56,7 @@ import { secret as v2SecretRouter, // begin to phase out secrets as v2SecretsRouter, serviceTokenData as v2ServiceTokenDataRouter, + serviceAccounts as v2ServiceAccountsRouter, apiKeyData as v2APIKeyDataRouter, environment as v2EnvironmentRouter, tags as v2TagsRouter, @@ -79,7 +80,7 @@ const main = async () => { }); } - logTelemetryMessage(); + TelemetryService.logTelemetryMessage(); setTransporter(initSmtp()); await DatabaseService.initDatabase(getMongoURL()); @@ -150,6 +151,7 @@ const main = async () => { app.use('/api/v2/secret', v2SecretRouter); // deprecated app.use('/api/v2/secrets', v2SecretsRouter); app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route + app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new app.use('/api/v2/api-key', v2APIKeyDataRouter); // api docs @@ -170,7 +172,7 @@ const main = async () => { getLogger("backend-main").info(`Server started listening at port ${getPort()}`) }); - createTestUserForDevelopment(); + await createTestUserForDevelopment(); setUpHealthEndpoint(server); server.on('close', async () => { diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index f7eef6263..58568145d 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -12,17 +12,21 @@ import { 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 } from "../variables"; interface App { @@ -94,6 +98,11 @@ const getApps = async ({ accessToken, }); break; + case INTEGRATION_RAILWAY: + apps = await getAppsRailway({ + accessToken + }); + break; case INTEGRATION_FLYIO: apps = await getAppsFlyio({ accessToken, @@ -109,6 +118,11 @@ const getApps = async ({ accessToken, }) break; + case INTEGRATION_SUPABASE: + apps = await getAppsSupabase({ + accessToken + }); + break; } } catch (err) { Sentry.setUser(null); @@ -184,6 +198,7 @@ const getAppsVercel = async ({ apps = res.projects.map((a: any) => ({ name: a.name, + appId: a.id })); } catch (err) { Sentry.setUser(null); @@ -270,10 +285,13 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { apps = repos .filter((a: any) => a.permissions.admin === true) - .map((a: any) => ({ - name: a.name, - owner: a.owner.login, - })); + .map((a: any) => { + return ({ + appId: a.id, + name: a.name, + owner: a.owner.login, + }); + }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -319,6 +337,58 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { return apps; }; +/** + * Return list of projects for Railway integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Railway API + * @returns {Object[]} apps - names and ids of Railway services + * @returns {String} apps.name - name of Railway project + * @returns {String} apps.appId - id of Railway project + * +*/ +const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { + let apps: any[] = []; + try { + const query = ` + query GetProjects($userId: String, $teamId: String) { + projects(userId: $userId, teamId: $teamId) { + edges { + node { + id + name + } + } + } + } + `; + + const variables = {}; + + const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, { + query, + variables, + }, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Accept-Encoding': 'application/json' + }, + }); + + apps = edges.map((e: any) => ({ + name: e.node.name, + appId: e.node.id + })); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to get Railway services"); + } + + return apps; +} + /** * Return list of apps for Fly.io integration * @param {Object} obj @@ -545,4 +615,40 @@ const getAppsGitlab = async ({ return apps; } + +/** + * Return list of projects for Supabase integration + * @param {Object} obj + * @param {String} obj.accessToken - access token for Supabase API + * @returns {Object[]} apps - names of Supabase apps + * @returns {String} apps.name - name of Supabase app + */ +const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { + let apps: any; + try { + const { data } = await request.get( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + } + } + ); + + apps = data.map((a: any) => { + return { + name: a.name, + appId: a.id + }; + }); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to get Supabase projects'); + } + + return apps; +}; + export { getApps }; diff --git a/backend/src/integrations/sync.ts b/backend/src/integrations/sync.ts index 829b50531..285f31ca7 100644 --- a/backend/src/integrations/sync.ts +++ b/backend/src/integrations/sync.ts @@ -21,19 +21,24 @@ import { 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 } from "../variables"; import request from '../config/request'; +import axios from "axios"; /** * Sync/push [secrets] to [app] in integration named [integration] @@ -126,6 +131,13 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_RAILWAY: + await syncSecretsRailway({ + integration, + secrets, + accessToken + }); + break; case INTEGRATION_FLYIO: await syncSecretsFlyio({ integration, @@ -147,6 +159,13 @@ const syncSecrets = async ({ accessToken, }); break; + case INTEGRATION_SUPABASE: + await syncSecretsSupabase({ + integration, + secrets, + accessToken + }); + break; } } catch (err) { Sentry.setUser(null); @@ -608,6 +627,7 @@ const syncSecretsVercel = async ({ key: string; value: string; target: string[]; + gitBranch?: string; } try { @@ -621,46 +641,7 @@ const syncSecretsVercel = async ({ } : {}), }; - - // const res = ( - // await Promise.all( - // ( - // await request.get( - // `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, - // { - // params, - // headers: { - // Authorization: `Bearer ${accessToken}`, - // 'Accept-Encoding': 'application/json' - // } - // } - // )) - // .data - // .envs - // .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)) - // .map(async (secret: VercelSecret) => { - // if (secret.type === 'encrypted') { - // // case: secret is encrypted -> need to decrypt - // const decryptedSecret = (await request.get( - // `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`, - // { - // params, - // headers: { - // Authorization: `Bearer ${accessToken}`, - // 'Accept-Encoding': 'application/json' - // } - // } - // )).data; - - // return decryptedSecret; - // } - - // return secret; - // }))).reduce((obj: any, secret: any) => ({ - // ...obj, - // [secret.key]: secret - // }), {}); - + const vercelSecrets: VercelSecret[] = (await request.get( `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`, { @@ -673,7 +654,21 @@ const syncSecretsVercel = async ({ )) .data .envs - .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment)); + .filter((secret: VercelSecret) => { + if (!secret.target.includes(integration.targetEnvironment)) { + // case: secret does not have the same target environment + return false; + } + + if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) { + // case: secret on preview environment does not have same target git branch + return false; + } + + return true; + }); + + // return secret.target.includes(integration.targetEnvironment); const res: { [key: string]: VercelSecret } = {}; @@ -696,7 +691,7 @@ const syncSecretsVercel = async ({ res[vercelSecret.key] = vercelSecret; } } - + const updateSecrets: VercelSecret[] = []; const deleteSecrets: VercelSecret[] = []; const newSecrets: VercelSecret[] = []; @@ -710,6 +705,9 @@ const syncSecretsVercel = async ({ value: secrets[key], type: "encrypted", target: [integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) }); } }); @@ -726,7 +724,10 @@ const syncSecretsVercel = async ({ type: res[key].type, target: res[key].target.includes(integration.targetEnvironment) ? [...res[key].target] - : [...res[key].target, integration.targetEnvironment] + : [...res[key].target, integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) }); } } else { @@ -737,6 +738,9 @@ const syncSecretsVercel = async ({ value: res[key].value, type: "encrypted", // value doesn't matter target: [integration.targetEnvironment], + ...(integration.path ? { + gitBranch: integration.path + } : {}) }); } }); @@ -1060,7 +1064,7 @@ const syncSecretsGitHub = async ({ "GET /repos/{owner}/{repo}/actions/secrets/public-key", { owner: integration.owner, - repo: integration.app, + repo: integration.app } ) ).data; @@ -1167,6 +1171,58 @@ const syncSecretsRender = async ({ } }; +/** + * Sync/push [secrets] to Railway project with id [integration.appId] + * @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 - access token for Railway integration + */ +const syncSecretsRailway = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + + const query = ` + mutation UpsertVariables($input: VariableCollectionUpsertInput!) { + variableCollectionUpsert(input: $input) + } + `; + + const input = { + projectId: integration.appId, + environmentId: integration.targetEnvironmentId, + ...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}), + replace: true, + variables: secrets + }; + + await request.post(INTEGRATION_RAILWAY_API_URL, { + query, + variables: { + input, + }, + }, { + headers: { + 'Authorization': `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Accept-Encoding': 'application/json' + }, + }); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error("Failed to sync secrets to Railway"); + } +} + /** * Sync/push [secrets] to Fly.io app * @param {Object} obj @@ -1571,4 +1627,79 @@ const syncSecretsGitLab = async ({ } } +/** + * Sync/push [secrets] to Supabase with name [integration.app] + * @param {Object} obj + * @param {IIntegration} obj.integration - integration details + * @param {IIntegrationAuth} obj.integrationAuth - integration auth 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 - access token for Supabase integration + */ +const syncSecretsSupabase = async ({ + integration, + secrets, + accessToken +}: { + integration: IIntegration; + secrets: any; + accessToken: string; +}) => { + try { + const { data: getSecretsRes } = await request.get( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + } + } + ); + + // convert the secrets to [{}] format + const modifiedFormatForSecretInjection = Object.keys(secrets).map( + (key) => { + return { + name: key, + value: secrets[key] + }; + } + ); + + await request.post( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + modifiedFormatForSecretInjection, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Accept-Encoding': 'application/json' + } + } + ); + + const secretsToDelete: any = []; + getSecretsRes?.forEach((secretObj: any) => { + if (!(secretObj.name in secrets)) { + secretsToDelete.push(secretObj.name); + } + }); + + await request.delete( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + 'Accept-Encoding': 'application/json' + }, + data: secretsToDelete + } + ); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to sync secrets to Supabase'); + } +}; + + export { syncSecrets }; diff --git a/backend/src/interfaces/serviceAccounts/dto/AddServiceAccountPermissionDto.ts b/backend/src/interfaces/serviceAccounts/dto/AddServiceAccountPermissionDto.ts new file mode 100644 index 000000000..6d0954c00 --- /dev/null +++ b/backend/src/interfaces/serviceAccounts/dto/AddServiceAccountPermissionDto.ts @@ -0,0 +1,7 @@ +interface AddServiceAccountPermissionDto { + name: string; + workspaceId?: string; + environment?: string; +} + +export default AddServiceAccountPermissionDto; \ No newline at end of file diff --git a/backend/src/interfaces/serviceAccounts/dto/CreateServiceAccountDto.ts b/backend/src/interfaces/serviceAccounts/dto/CreateServiceAccountDto.ts new file mode 100644 index 000000000..dcab0f302 --- /dev/null +++ b/backend/src/interfaces/serviceAccounts/dto/CreateServiceAccountDto.ts @@ -0,0 +1,8 @@ +interface CreateServiceAccountDto { + organizationId: string; + name: string; + publicKey: string; + expiresIn: number; +} + +export default CreateServiceAccountDto; \ No newline at end of file diff --git a/backend/src/interfaces/serviceAccounts/dto/index.ts b/backend/src/interfaces/serviceAccounts/dto/index.ts new file mode 100644 index 000000000..52d8d8342 --- /dev/null +++ b/backend/src/interfaces/serviceAccounts/dto/index.ts @@ -0,0 +1,7 @@ +import CreateServiceAccountDto from './CreateServiceAccountDto'; +import AddServiceAccountPermissionDto from './AddServiceAccountPermissionDto'; + +export { + CreateServiceAccountDto, + AddServiceAccountPermissionDto +} \ No newline at end of file diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index 7e014103c..039b8612d 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -10,6 +10,8 @@ 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'; @@ -27,6 +29,8 @@ export { requireIntegrationAuthorizationAuth, requireServiceTokenAuth, requireServiceTokenDataAuth, + requireServiceAccountAuth, + requireServiceAccountWorkspacePermissionAuth, requireSecretAuth, requireSecretsAuth, validateRequest diff --git a/backend/src/middleware/requireAuth.ts b/backend/src/middleware/requireAuth.ts index f4921398a..16191d607 100644 --- a/backend/src/middleware/requireAuth.ts +++ b/backend/src/middleware/requireAuth.ts @@ -4,11 +4,23 @@ import { validateAuthMode, getAuthUserPayload, getAuthSTDPayload, - getAuthAPIKeyPayload + getAuthAPIKeyPayload, + getAuthSAAKPayload } from '../helpers/auth'; import { UnauthorizedRequestError } from '../utils/errors'; +import { + IUser, + IServiceAccount, + IServiceTokenData +} from '../models'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../variables'; declare module 'jsonwebtoken' { export interface UserIDJwtPayload extends jwt.JwtPayload { @@ -27,50 +39,58 @@ declare module 'jsonwebtoken' { * @returns */ const requireAuth = ({ - acceptedAuthModes = ['jwt'], - requiredServiceTokenPermissions = [] + acceptedAuthModes = [AUTH_MODE_JWT], }: { acceptedAuthModes: string[]; - requiredServiceTokenPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { + // validate auth token against accepted auth modes [acceptedAuthModes] // and return token type [authTokenType] and value [authTokenValue] - const { authTokenType, authTokenValue } = validateAuthMode({ + const { authMode, authTokenValue } = validateAuthMode({ headers: req.headers, acceptedAuthModes }); - // attach auth payloads - let serviceTokenData: any; - switch (authTokenType) { - case 'serviceToken': - serviceTokenData = await getAuthSTDPayload({ + let authPayload: IUser | IServiceAccount | IServiceTokenData; + switch (authMode) { + case AUTH_MODE_SERVICE_ACCOUNT: + authPayload = await getAuthSAAKPayload({ authTokenValue }); - - requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => { - if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) { - return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' })); - } - }); - - req.serviceTokenData = serviceTokenData; - req.user = serviceTokenData?.user; - + req.serviceAccount = authPayload; break; - case 'apiKey': - req.user = await getAuthAPIKeyPayload({ + case AUTH_MODE_SERVICE_TOKEN: + authPayload = await getAuthSTDPayload({ authTokenValue }); + req.serviceTokenData = authPayload; + break; + case AUTH_MODE_API_KEY: + authPayload = await getAuthAPIKeyPayload({ + authTokenValue + }); + req.user = authPayload; break; default: - req.user = await getAuthUserPayload({ + authPayload = await getAuthUserPayload({ authTokenValue }); + req.user = authPayload; break; } - + + req.requestData = { + ...req.params, + ...req.query, + ...req.body, + } + + req.authData = { + authMode, + authPayload // User, ServiceAccount, ServiceTokenData + } + return next(); } } diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts index 435b06a59..089f570c8 100644 --- a/backend/src/middleware/requireBotAuth.ts +++ b/backend/src/middleware/requireBotAuth.ts @@ -1,32 +1,28 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { Bot } from '../models'; import { validateMembership } from '../helpers/membership'; +import { validateClientForBot } from '../helpers/bot'; import { AccountNotFoundError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; const requireBotAuth = ({ acceptedRoles, - location = 'params' + locationBotId = 'params' }: { - acceptedRoles: string[]; - location?: req; + acceptedRoles: Array<'admin' | 'member'>; + locationBotId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - const bot = await Bot.findById(req[location].botId); + const { botId } = req[locationBotId]; - if (!bot) { - return next(AccountNotFoundError({message: 'Failed to locate Bot account'})) - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: bot.workspace.toString(), + req.bot = await validateClientForBot({ + authData: req.authData, + botId: new Types.ObjectId(botId), acceptedRoles }); - req.bot = bot; - next(); } } diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index b185b922b..bcde94f34 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -1,7 +1,9 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { Integration, IntegrationAuth } from '../models'; import { IntegrationService } from '../services'; import { validateMembership } from '../helpers/membership'; +import { validateClientForIntegration } from '../helpers/integration'; import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors'; /** @@ -13,42 +15,24 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err const requireIntegrationAuth = ({ acceptedRoles }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; }) => { return async (req: Request, res: Response, next: NextFunction) => { - // integration authorization middleware - const { integrationId } = req.params; - // validate integration accessibility - const integration = await Integration.findOne({ - _id: integrationId - }); - - if (!integration) { - return next(IntegrationNotFoundError({message: 'Failed to locate Integration'})) - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: integration.workspace.toString(), + const { integration, accessToken } = await validateClientForIntegration({ + authData: req.authData, + integrationId: new Types.ObjectId(integrationId), acceptedRoles }); - const integrationAuth = await IntegrationAuth.findOne({ - _id: integration.integrationAuth - }).select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); - - if (!integrationAuth) { - return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'})) + if (integration) { + req.integration = integration; + } + + if (accessToken) { + req.accessToken = accessToken; } - - req.integration = integration; - req.accessToken = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); return next(); }; diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index c712f4cca..af1eb1a53 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -1,7 +1,9 @@ import * as Sentry from '@sentry/node'; +import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; import { IntegrationAuth, IWorkspace } from '../models'; import { IntegrationService } from '../services'; +import { validateClientForIntegrationAuth } from '../helpers/integrationAuth'; import { validateMembership } from '../helpers/membership'; import { UnauthorizedRequestError } from '../utils/errors'; @@ -19,36 +21,26 @@ const requireIntegrationAuthorizationAuth = ({ attachAccessToken = true, location = 'params' }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; attachAccessToken?: boolean; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { integrationAuthId } = req[location]; - const integrationAuth = await IntegrationAuth.findOne({ - _id: integrationAuthId - }) - .populate<{ workspace: IWorkspace }>('workspace') - .select( - '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' - ); - if (!integrationAuth) { - return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'})) - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: integrationAuth.workspace._id.toString(), - acceptedRoles + const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({ + authData: req.authData, + integrationAuthId: new Types.ObjectId(integrationAuthId), + acceptedRoles, + attachAccessToken }); + + if (integrationAuth) { + req.integrationAuth = integrationAuth; + } - req.integrationAuth = integrationAuth; - if (attachAccessToken) { - const access = await IntegrationService.getIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString() - }); - req.accessToken = access.accessToken; + if (accessToken) { + req.accessToken = accessToken; } return next(); diff --git a/backend/src/middleware/requireMembershipAuth.ts b/backend/src/middleware/requireMembershipAuth.ts index f5e4fe8b1..851230371 100644 --- a/backend/src/middleware/requireMembershipAuth.ts +++ b/backend/src/middleware/requireMembershipAuth.ts @@ -1,9 +1,13 @@ +import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; import { UnauthorizedRequestError } from '../utils/errors'; import { Membership, } from '../models'; -import { validateMembership } from '../helpers/membership'; +import { + validateClientForMembership, + validateMembership +} from '../helpers/membership'; type req = 'params' | 'body' | 'query'; @@ -16,43 +20,25 @@ type req = 'params' | 'body' | 'query'; */ const requireMembershipAuth = ({ acceptedRoles, - location = 'params' + locationMembershipId = 'params' }: { - acceptedRoles: string[]; - location?: req; + acceptedRoles: Array<'admin' | 'member'>; + locationMembershipId: req }) => { return async ( req: Request, res: Response, next: NextFunction ) => { - try { - const { membershipId } = req[location]; - - const membership = await Membership.findById(membershipId); - - if (!membership) throw new Error('Failed to find target membership'); - - const userMembership = await Membership.findOne({ - workspace: membership.workspace - }); - - if (!userMembership) throw new Error('Failed to validate own membership') - - const targetMembership = await validateMembership({ - userId: req.user._id.toString(), - workspaceId: membership.workspace.toString(), - acceptedRoles - }); - - req.targetMembership = targetMembership; - - return next(); - } catch (err) { - return next(UnauthorizedRequestError({ - message: 'Unable to validate workspace membership' - })); - } + const { membershipId } = req[locationMembershipId]; + + req.targetMembership = await validateClientForMembership({ + authData: req.authData, + membershipId: new Types.ObjectId(membershipId), + acceptedRoles + }); + + return next(); } } diff --git a/backend/src/middleware/requireMembershipOrgAuth.ts b/backend/src/middleware/requireMembershipOrgAuth.ts index ec1a4d4f1..b34c9c5e2 100644 --- a/backend/src/middleware/requireMembershipOrgAuth.ts +++ b/backend/src/middleware/requireMembershipOrgAuth.ts @@ -1,11 +1,17 @@ +import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; import { UnauthorizedRequestError } from '../utils/errors'; import { MembershipOrg } from '../models'; -import { validateMembership } from '../helpers/membershipOrg'; +import { + validateClientForMembershipOrg, + validateMembershipOrg +} from '../helpers/membershipOrg'; +// TODO: transform + type req = 'params' | 'body' | 'query'; /** @@ -17,32 +23,24 @@ type req = 'params' | 'body' | 'query'; */ const requireMembershipOrgAuth = ({ acceptedRoles, - location = 'params' + acceptedStatuses, + locationMembershipOrgId = 'params' }: { - acceptedRoles: string[]; - location?: req; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; + locationMembershipOrgId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { membershipId } = req[location]; - const membershipOrg = await MembershipOrg.findById(membershipId); - - if (!membershipOrg) throw new Error('Failed to find target organization membership'); - - const targetMembership = await validateMembership({ - userId: req.user._id.toString(), - organizationId: membershipOrg.organization.toString(), - acceptedRoles - }); - - req.targetMembership = targetMembership; - - return next(); - } catch (err) { - return next(UnauthorizedRequestError({ - message: 'Unable to validate organization membership' - })); - } + const { membershipId } = req[locationMembershipOrgId]; + + req.membershipOrg = await validateClientForMembershipOrg({ + authData: req.authData, + membershipOrgId: new Types.ObjectId(membershipId), + acceptedRoles, + acceptedStatuses + }); + + return next(); } } diff --git a/backend/src/middleware/requireOrganizationAuth.ts b/backend/src/middleware/requireOrganizationAuth.ts index 04542b429..f6d8eb8ce 100644 --- a/backend/src/middleware/requireOrganizationAuth.ts +++ b/backend/src/middleware/requireOrganizationAuth.ts @@ -1,45 +1,46 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { IOrganization, MembershipOrg } from '../models'; import { UnauthorizedRequestError, ValidationError } from '../utils/errors'; +import { validateMembershipOrg } from '../helpers/membershipOrg'; +import { validateClientForOrganization } from '../helpers/organization'; + +type req = 'params' | 'body' | 'query'; /** * Validate if user on request is a member with proper roles for organization * on request params. * @param {Object} obj * @param {String[]} obj.acceptedRoles - accepted organization roles - * @param {String[]} obj.acceptedStatuses - accepted organization statuses + * @param {String[]} obj.accepteStatuses - accepted organization statuses */ const requireOrganizationAuth = ({ acceptedRoles, - acceptedStatuses + acceptedStatuses, + locationOrganizationId = 'params' }: { - acceptedRoles: string[]; - acceptedStatuses: string[]; + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; + locationOrganizationId?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { - // organization authorization middleware - - // validate organization membership - const membershipOrg = await MembershipOrg.findOne({ - user: req.user._id, - organization: req.params.organizationId - }).populate<{ organization: IOrganization }>('organization'); - - - if (!membershipOrg) { - return next(UnauthorizedRequestError({message: "You're not a member of this Organization."})) - } - //TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101 - if (!acceptedRoles.includes(membershipOrg.role)) { - return next(ValidationError({message: 'Failed to validate Organization Membership Role'})) + const { organizationId } = req[locationOrganizationId]; + + const { organization, membershipOrg } = await validateClientForOrganization({ + authData: req.authData, + organizationId: new Types.ObjectId(organizationId), + acceptedRoles, + acceptedStatuses + }); + + if (organization) { + req.organization = organization; } - if (!acceptedStatuses.includes(membershipOrg.status)) { - return next(ValidationError({message: 'Failed to validate Organization Membership Status'})) + if (membershipOrg) { + req.membershipOrg = membershipOrg; } - req.membershipOrg = membershipOrg; - return next(); }; }; diff --git a/backend/src/middleware/requireSecretAuth.ts b/backend/src/middleware/requireSecretAuth.ts index 36e47247e..1462d67b0 100644 --- a/backend/src/middleware/requireSecretAuth.ts +++ b/backend/src/middleware/requireSecretAuth.ts @@ -1,12 +1,17 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { UnauthorizedRequestError, SecretNotFoundError } from '../utils/errors'; import { Secret } from '../models'; import { validateMembership } from '../helpers/membership'; +import { + validateClientForSecret +} from '../helpers/secrets'; // note: used for old /v1/secret and /v2/secret routes. -// newer /v2/secrets routes use [requireSecretsAuth] middleware +// newer /v2/secrets routes use [requireSecretsAuth] middleware with the exception +// of some /ee endpoints /** * Validate if user on request has proper membership to modify secret. @@ -15,34 +20,25 @@ import { * @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing */ const requireSecretAuth = ({ - acceptedRoles + acceptedRoles, + requiredPermissions }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; + requiredPermissions: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { secretId } = req.params; - - const secret = await Secret.findById(secretId); - - if (!secret) { - return next(SecretNotFoundError({ - message: 'Failed to find secret' - })); - } - - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: secret.workspace.toString(), - acceptedRoles - }); - - req._secret = secret; + const { secretId } = req.params; + + const secret = await validateClientForSecret({ + authData: req.authData, + secretId: new Types.ObjectId(secretId), + acceptedRoles, + requiredPermissions + }); + + req._secret = secret; - next(); - } catch (err) { - return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret' })); - } + next(); } } diff --git a/backend/src/middleware/requireSecretsAuth.ts b/backend/src/middleware/requireSecretsAuth.ts index 2a6c36056..a076a3f1a 100644 --- a/backend/src/middleware/requireSecretsAuth.ts +++ b/backend/src/middleware/requireSecretsAuth.ts @@ -1,48 +1,35 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { UnauthorizedRequestError } from '../utils/errors'; import { Secret, Membership } from '../models'; -import { validateSecrets } from '../helpers/secret'; - -// TODO: make this work for delete route +import { validateClientForSecrets } from '../helpers/secrets'; const requireSecretsAuth = ({ - acceptedRoles + acceptedRoles, + requiredPermissions = [] }: { acceptedRoles: string[]; + requiredPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - let secrets; - try { - if (Array.isArray(req.body.secrets)) { - // case: validate multiple secrets - secrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds: req.body.secrets.map((s: any) => s.id) - }); - } else if (typeof req.body.secrets === 'object') { // change this to check for object - // case: validate 1 secret - secrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds: [req.body.secrets.id] - }); - } else if (Array.isArray(req.body.secretIds)) { - secrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds: req.body.secretIds - }); - } else if (typeof req.body.secretIds === 'string') { - // case: validate secretIds - secrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds: [req.body.secretIds] - }); - } - - req.secrets = secrets; - return next(); - } catch (err) { - return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret(s)' })); + let secretIds = []; + if (Array.isArray(req.body.secrets)) { + secretIds = req.body.secrets.map((s: any) => s.id); + } 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') { + secretIds = [req.body.secretIds]; } + + req.secrets = await validateClientForSecrets({ + authData: req.authData, + secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), + requiredPermissions + }); + + return next(); } } diff --git a/backend/src/middleware/requireServiceAccountAuth.ts b/backend/src/middleware/requireServiceAccountAuth.ts new file mode 100644 index 000000000..40861a737 --- /dev/null +++ b/backend/src/middleware/requireServiceAccountAuth.ts @@ -0,0 +1,40 @@ +import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; +import { ServiceAccount } from '../models'; +import { + ServiceAccountNotFoundError +} from '../utils/errors'; +import { + validateMembershipOrg +} from '../helpers/membershipOrg'; +import { + validateClientForServiceAccount +} from '../helpers/serviceAccount'; + +type req = 'params' | 'body' | 'query'; + +const requireServiceAccountAuth = ({ + acceptedRoles, + acceptedStatuses, + locationServiceAccountId = 'params', + requiredPermissions = [] +}: { + acceptedRoles: string[]; + acceptedStatuses: string[]; + locationServiceAccountId?: req; + requiredPermissions?: string[]; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const serviceAccountId = req[locationServiceAccountId].serviceAccountId; + + req.serviceAccount = await validateClientForServiceAccount({ + authData: req.authData, + serviceAccountId: new Types.ObjectId(serviceAccountId), + requiredPermissions + }); + + next(); + } +} + +export default requireServiceAccountAuth; \ No newline at end of file diff --git a/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts b/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts new file mode 100644 index 000000000..0ceb4f598 --- /dev/null +++ b/backend/src/middleware/requireServiceAccountWorkspacePermissionAuth.ts @@ -0,0 +1,52 @@ +import { Request, Response, NextFunction } from 'express'; +import { ServiceAccount, ServiceAccountWorkspacePermission } from '../models'; +import { + ServiceAccountNotFoundError +} from '../utils/errors'; +import { + validateMembershipOrg +} from '../helpers/membershipOrg'; + +type req = 'params' | 'body' | 'query'; + +const requireServiceAccountWorkspacePermissionAuth = ({ + acceptedRoles, + acceptedStatuses, + location = 'params' +}: { + acceptedRoles: Array<'owner' | 'admin' | 'member'>; + acceptedStatuses: Array<'invited' | 'accepted'>; + location?: req; +}) => { + return async (req: Request, res: Response, next: NextFunction) => { + const serviceAccountWorkspacePermissionId = req[location].serviceAccountWorkspacePermissionId; + const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findById(serviceAccountWorkspacePermissionId); + + if (!serviceAccountWorkspacePermission) { + 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' })); + } + + if (serviceAccount.user.toString() !== req.user.id.toString()) { + // case: creator of the service account is different from + // the user on the request -> apply middleware role/status validation + await validateMembershipOrg({ + userId: req.user._id, + organizationId: serviceAccount.organization, + acceptedRoles, + acceptedStatuses + }); + } + + req.serviceAccount = serviceAccount; + + next(); + } +} + +export default requireServiceAccountWorkspacePermissionAuth; \ No newline at end of file diff --git a/backend/src/middleware/requireServiceTokenDataAuth.ts b/backend/src/middleware/requireServiceTokenDataAuth.ts index 7c5ab4669..7715991ba 100644 --- a/backend/src/middleware/requireServiceTokenDataAuth.ts +++ b/backend/src/middleware/requireServiceTokenDataAuth.ts @@ -1,5 +1,7 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { ServiceToken, ServiceTokenData } from '../models'; +import { validateClientForServiceTokenData } from '../helpers/serviceTokenData'; import { validateMembership } from '../helpers/membership'; import { AccountNotFoundError, UnauthorizedRequestError } from '../utils/errors'; @@ -9,30 +11,17 @@ const requireServiceTokenDataAuth = ({ acceptedRoles, location = 'params' }: { - acceptedRoles: string[]; + acceptedRoles: Array<'admin' | 'member'>; location?: req; }) => { return async (req: Request, res: Response, next: NextFunction) => { const { serviceTokenDataId } = req[location]; - - const serviceTokenData = await ServiceTokenData - .findById(req[location].serviceTokenDataId) - .select('+encryptedKey +iv +tag').populate('user'); - - if (!serviceTokenData) { - return next(AccountNotFoundError({ message: 'Failed to locate service token data' })); - } - - if (req.user) { - // case: jwt auth - await validateMembership({ - userId: req.user._id.toString(), - workspaceId: serviceTokenData.workspace.toString(), - acceptedRoles - }); - } - - req.serviceTokenData = serviceTokenData; + + req.serviceTokenData = await validateClientForServiceTokenData({ + authData: req.authData, + serviceTokenDataId: new Types.ObjectId(serviceTokenDataId), + acceptedRoles + }); next(); } diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 56c24528c..5d094f972 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,5 +1,7 @@ import { Request, Response, NextFunction } from 'express'; +import { Types } from 'mongoose'; import { validateMembership } from '../helpers/membership'; +import { validateClientForWorkspace } from '../helpers/workspace'; import { UnauthorizedRequestError } from '../utils/errors'; type req = 'params' | 'body' | 'query'; @@ -13,38 +15,33 @@ type req = 'params' | 'body' | 'query'; */ const requireWorkspaceAuth = ({ acceptedRoles, - location = 'params' + locationWorkspaceId, + locationEnvironment = undefined, + requiredPermissions = [] }: { - acceptedRoles: string[]; - location?: req; + acceptedRoles: Array<'admin' | 'member'>; + locationWorkspaceId: req; + locationEnvironment?: req | undefined; + requiredPermissions?: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - try { - const { workspaceId } = req[location]; - - if (req.user) { - // case: jwt auth - const membership = await validateMembership({ - userId: req.user._id.toString(), - workspaceId, - acceptedRoles - }); - - req.membership = membership; - } - - if ( - req.serviceTokenData - && req.serviceTokenData.workspace.toString() !== workspaceId - && req.serviceTokenData.environment !== req.body.environment - ) { - next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) - } - - return next(); - } catch (err) { - return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'})) + const workspaceId = req[locationWorkspaceId]?.workspaceId; + const environment = locationEnvironment ? req[locationEnvironment]?.environment : undefined; + + // validate clients + const { membership } = await validateClientForWorkspace({ + authData: req.authData, + workspaceId: new Types.ObjectId(workspaceId), + environment, + acceptedRoles, + requiredPermissions + }); + + if (membership) { + req.membership = membership; } + + return next(); }; }; diff --git a/backend/src/models/apiKeyData.ts b/backend/src/models/apiKeyData.ts index af73b5f69..1b6831730 100644 --- a/backend/src/models/apiKeyData.ts +++ b/backend/src/models/apiKeyData.ts @@ -3,6 +3,7 @@ import { Schema, model, Types } from 'mongoose'; export interface IAPIKeyData { name: string; user: Types.ObjectId; + lastUsed: Date; expiresAt: Date; secretHash: string; } @@ -18,6 +19,9 @@ const apiKeyDataSchema = new Schema( ref: 'User', required: true }, + lastUsed: { + type: Date + }, expiresAt: { type: Date }, diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index ead32aae4..886f7fec6 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -10,6 +10,10 @@ import MembershipOrg, { IMembershipOrg } from './membershipOrg'; import Organization, { IOrganization } from './organization'; import Secret, { ISecret } from './secret'; 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, { IUser } from './user'; import UserAction, { IUserAction } from './userAction'; @@ -43,6 +47,14 @@ export { ISecret, ServiceToken, IServiceToken, + ServiceAccount, + IServiceAccount, + ServiceAccountKey, + IServiceAccountKey, + ServiceAccountOrganizationPermission, + IServiceAccountOrganizationPermission, + ServiceAccountWorkspacePermission, + IServiceAccountWorkspacePermission, TokenData, ITokenData, User, diff --git a/backend/src/models/integration.ts b/backend/src/models/integration.ts index 698846ea7..2a77c95e9 100644 --- a/backend/src/models/integration.ts +++ b/backend/src/models/integration.ts @@ -9,9 +9,11 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE } from "../variables"; export interface IIntegration { @@ -20,9 +22,12 @@ export interface IIntegration { environment: string; isActive: boolean; app: string; + appId: string; owner: string; targetEnvironment: string; - appId: string; + targetEnvironmentId: string; + targetService: string; + targetServiceId: string; path: string; region: string; integration: @@ -35,9 +40,11 @@ export interface IIntegration { | 'github' | 'gitlab' | 'render' + | 'railway' | 'flyio' | 'circleci' - | 'travisci'; + | 'travisci' + | 'supabase'; integrationAuth: Types.ObjectId; } @@ -71,6 +78,20 @@ const integrationSchema = new Schema( type: String, default: null, }, + targetEnvironmentId: { + type: String, + default: null + }, + targetService: { + // railway-specific service + type: String, + default: null + }, + targetServiceId: { + // railway-specific service + type: String, + default: null + }, owner: { // github-specific repo owner-login type: String, @@ -78,6 +99,7 @@ const integrationSchema = new Schema( }, path: { // aws-parameter-store-specific path + // (also) vercel preview-branch type: String, default: null }, @@ -98,9 +120,11 @@ const integrationSchema = new Schema( INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE ], required: true, }, diff --git a/backend/src/models/integrationAuth.ts b/backend/src/models/integrationAuth.ts index 43bef63ad..ead969fc7 100644 --- a/backend/src/models/integrationAuth.ts +++ b/backend/src/models/integrationAuth.ts @@ -1,4 +1,4 @@ -import { Schema, model, Types } from "mongoose"; +import { Schema, model, Types, Document } from "mongoose"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -9,15 +9,17 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE, } from "../variables"; -export interface IIntegrationAuth { +export interface IIntegrationAuth extends Document { _id: Types.ObjectId; workspace: Types.ObjectId; - integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'aws-parameter-store' | 'aws-secret-manager'; + integration: 'heroku' | 'vercel' | 'netlify' | 'github' | 'gitlab' | 'render' | 'railway' | 'flyio' | 'azure-key-vault' | 'circleci' | 'travisci' | 'supabase' | 'aws-parameter-store' | 'aws-secret-manager'; teamId: string; accountId: string; refreshCiphertext?: string; @@ -51,9 +53,11 @@ const integrationAuthSchema = new Schema( INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE ], required: true, }, diff --git a/backend/src/models/membershipOrg.ts b/backend/src/models/membershipOrg.ts index b5013acbb..540a4451b 100644 --- a/backend/src/models/membershipOrg.ts +++ b/backend/src/models/membershipOrg.ts @@ -1,7 +1,7 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types, Document } from 'mongoose'; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from '../variables'; -export interface IMembershipOrg { +export interface IMembershipOrg extends Document { _id: Types.ObjectId; user: Types.ObjectId; inviteEmail: string; diff --git a/backend/src/models/serviceAccount.ts b/backend/src/models/serviceAccount.ts new file mode 100644 index 000000000..9ff9dcb03 --- /dev/null +++ b/backend/src/models/serviceAccount.ts @@ -0,0 +1,53 @@ +import { Schema, model, Types, Document } from 'mongoose'; + +export interface IServiceAccount extends Document { + _id: Types.ObjectId; + name: string; + organization: Types.ObjectId; + user: Types.ObjectId; + publicKey: string; + lastUsed: Date; + expiresAt: Date; + secretHash: string; +} + +const serviceAccountSchema = new Schema( + { + name: { + type: String, + required: true + }, + organization: { + type: Schema.Types.ObjectId, + ref: 'Organization', + required: true + }, + user: { // user who created the service account + type: Schema.Types.ObjectId, + ref: 'User', + required: true + }, + publicKey: { + type: String, + required: true + }, + lastUsed: { + type: Date + }, + expiresAt: { + type: Date + }, + secretHash: { + type: String, + required: true, + select: false + } + }, + { + timestamps: true + } +); + +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 new file mode 100644 index 000000000..637ac188b --- /dev/null +++ b/backend/src/models/serviceAccountKey.ts @@ -0,0 +1,44 @@ +import { Schema, model, Types } from 'mongoose'; + +export interface IServiceAccountKey { + _id: Types.ObjectId; + encryptedKey: string; + nonce: string; + sender: Types.ObjectId; + serviceAccount: Types.ObjectId; + workspace: Types.ObjectId; +} + +const serviceAccountKeySchema = new Schema( + { + encryptedKey: { + type: String, + required: true + }, + nonce: { + type: String, + required: true + }, + sender: { + type: Schema.Types.ObjectId, + required: true + }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount', + required: true + }, + workspace: { + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + } + }, + { + timestamps: true + } +); + +const ServiceAccountKey = model('ServiceAccountKey', serviceAccountKeySchema); + +export default ServiceAccountKey; diff --git a/backend/src/models/serviceAccountOrganizationPermission.ts b/backend/src/models/serviceAccountOrganizationPermission.ts new file mode 100644 index 000000000..6454bc6a0 --- /dev/null +++ b/backend/src/models/serviceAccountOrganizationPermission.ts @@ -0,0 +1,23 @@ +import { Schema, model, Types, Document } from 'mongoose'; + +export interface IServiceAccountOrganizationPermission extends Document { + _id: Types.ObjectId; + serviceAccount: Types.ObjectId; +} + +const serviceAccountOrganizationPermissionSchema = new Schema( + { + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount', + required: true + } + }, + { + timestamps: true + } +); + +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 new file mode 100644 index 000000000..01e4c4ba6 --- /dev/null +++ b/backend/src/models/serviceAccountWorkspacePermission.ts @@ -0,0 +1,44 @@ +import { Schema, model, Types, Document } from 'mongoose'; + +export interface IServiceAccountWorkspacePermission extends Document { + _id: Types.ObjectId; + serviceAccount: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + read: boolean; + write: boolean; +} + +const serviceAccountWorkspacePermissionSchema = new Schema( + { + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount', + required: true + }, + workspace:{ + type: Schema.Types.ObjectId, + ref: 'Workspace', + required: true + }, + environment: { + type: String, + required: true + }, + read: { + type: Boolean, + default: false + }, + write: { + type: Boolean, + default: false + } + }, + { + timestamps: true + } +); + +const ServiceAccountWorkspacePermission = model('ServiceAccountWorkspacePermission', serviceAccountWorkspacePermissionSchema); + +export default ServiceAccountWorkspacePermission; \ No newline at end of file diff --git a/backend/src/models/serviceTokenData.ts b/backend/src/models/serviceTokenData.ts index 2467b0b82..86f0e44b3 100644 --- a/backend/src/models/serviceTokenData.ts +++ b/backend/src/models/serviceTokenData.ts @@ -1,10 +1,13 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types, Document } from 'mongoose'; -export interface IServiceTokenData { +export interface IServiceTokenData extends Document { + _id: Types.ObjectId; name: string; workspace: Types.ObjectId; environment: string; user: Types.ObjectId; + serviceAccount: Types.ObjectId; + lastUsed: Date; expiresAt: Date; secretHash: string; encryptedKey: string; @@ -24,14 +27,20 @@ const serviceTokenDataSchema = new Schema( ref: 'Workspace', required: true }, - environment: { // TODO: adapt to upcoming environment id + environment: { type: String, required: true }, user: { type: Schema.Types.ObjectId, - ref: 'User', - required: true + ref: 'User' + }, + serviceAccount: { + type: Schema.Types.ObjectId, + ref: 'ServiceAccount' + }, + lastUsed: { + type: Date }, expiresAt: { type: Date diff --git a/backend/src/routes/v1/auth.ts b/backend/src/routes/v1/auth.ts index 638e4501b..a3558f341 100644 --- a/backend/src/routes/v1/auth.ts +++ b/backend/src/routes/v1/auth.ts @@ -4,6 +4,7 @@ import { body } from 'express-validator'; 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); @@ -29,7 +30,7 @@ router.post( '/logout', authLimiter, requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), authController.logout ); @@ -37,7 +38,7 @@ router.post( router.post( '/checkAuth', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), authController.checkAuth ); diff --git a/backend/src/routes/v1/bot.ts b/backend/src/routes/v1/bot.ts index 4d3865562..83e126dc4 100644 --- a/backend/src/routes/v1/bot.ts +++ b/backend/src/routes/v1/bot.ts @@ -8,15 +8,16 @@ import { validateRequest } from '../../middleware'; import { botController } from '../../controllers/v1'; -import { ADMIN, MEMBER } from '../../variables'; +import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables'; router.get( '/:workspaceId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim().notEmpty(), validateRequest, @@ -26,7 +27,7 @@ router.get( router.patch( '/:botId/active', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireBotAuth({ acceptedRoles: [ADMIN, MEMBER] diff --git a/backend/src/routes/v1/integration.ts b/backend/src/routes/v1/integration.ts index 72d818b61..52930ce0c 100644 --- a/backend/src/routes/v1/integration.ts +++ b/backend/src/routes/v1/integration.ts @@ -6,14 +6,19 @@ import { requireIntegrationAuthorizationAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT, + AUTH_MODE_API_KEY +} from '../../variables'; import { body, param } from 'express-validator'; import { integrationController } from '../../controllers/v1'; router.post( // new: add new integration for integration auth '/', requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], @@ -25,6 +30,9 @@ router.post( // new: add new integration for integration auth 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(), @@ -35,7 +43,7 @@ router.post( // new: add new integration for integration auth router.patch( '/:integrationId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -54,7 +62,7 @@ router.patch( router.delete( '/:integrationId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireIntegrationAuth({ acceptedRoles: [ADMIN, MEMBER] diff --git a/backend/src/routes/v1/integrationAuth.ts b/backend/src/routes/v1/integrationAuth.ts index 2c1f5f045..fa5863029 100644 --- a/backend/src/routes/v1/integrationAuth.ts +++ b/backend/src/routes/v1/integrationAuth.ts @@ -7,13 +7,18 @@ import { requireIntegrationAuthorizationAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT, + AUTH_MODE_API_KEY +} from '../../variables'; import { integrationAuthController } from '../../controllers/v1'; router.get( '/integration-options', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), integrationAuthController.getIntegrationOptions ); @@ -21,7 +26,7 @@ router.get( router.get( '/:integrationAuthId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -34,11 +39,11 @@ router.get( router.post( '/oauth-token', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - location: 'body' + locationWorkspaceId: 'body' }), body('workspaceId').exists().trim().notEmpty(), body('code').exists().trim().notEmpty(), @@ -49,25 +54,25 @@ router.post( router.post( '/access-token', - requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - location: 'body' - }), body('workspaceId').exists().trim().notEmpty(), body('accessId').trim(), body('accessToken').exists().trim().notEmpty(), body('integration').exists().trim().notEmpty(), validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body' + }), integrationAuthController.saveIntegrationAccessToken ); router.get( '/:integrationAuthId/apps', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -81,7 +86,7 @@ router.get( router.get( '/:integrationAuthId/teams', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER] @@ -91,10 +96,53 @@ router.get( integrationAuthController.getIntegrationAuthTeams ); +router.get( + '/:integrationAuthId/vercel/branches', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER] + }), + param('integrationAuthId').exists().isString(), + query('appId').exists().isString(), + query('teamId').optional().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthVercelBranches +); + +router.get( + '/:integrationAuthId/railway/environments', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER] + }), + param('integrationAuthId').exists().isString(), + query('appId').exists().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthRailwayEnvironments +); + +router.get( + '/:integrationAuthId/railway/services', + requireAuth({ + acceptedAuthModes: ['jwt'] + }), + requireIntegrationAuthorizationAuth({ + acceptedRoles: [ADMIN, MEMBER] + }), + param('integrationAuthId').exists().isString(), + query('appId').exists().isString(), + validateRequest, + integrationAuthController.getIntegrationAuthRailwayServices +); + router.delete( '/:integrationAuthId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireIntegrationAuthorizationAuth({ acceptedRoles: [ADMIN, MEMBER], diff --git a/backend/src/routes/v1/inviteOrg.ts b/backend/src/routes/v1/inviteOrg.ts index d8e2d67cc..4762711fe 100644 --- a/backend/src/routes/v1/inviteOrg.ts +++ b/backend/src/routes/v1/inviteOrg.ts @@ -3,11 +3,12 @@ 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'; router.post( '/signup', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('inviteeEmail').exists().trim().notEmpty().isEmail(), body('organizationId').exists().trim().notEmpty(), diff --git a/backend/src/routes/v1/key.ts b/backend/src/routes/v1/key.ts index b66bd1276..be99c9c17 100644 --- a/backend/src/routes/v1/key.ts +++ b/backend/src/routes/v1/key.ts @@ -6,16 +6,17 @@ import { validateRequest } from '../../middleware'; import { body, param } from 'express-validator'; -import { ADMIN, MEMBER } from '../../variables'; +import { ADMIN, MEMBER, AUTH_MODE_JWT } from '../../variables'; import { keyController } from '../../controllers/v1'; router.post( '/:workspaceId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('key').exists(), @@ -26,10 +27,11 @@ router.post( router.get( '/:workspaceId/latest', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId'), validateRequest, diff --git a/backend/src/routes/v1/membership.ts b/backend/src/routes/v1/membership.ts index aaacada74..e830bd06d 100644 --- a/backend/src/routes/v1/membership.ts +++ b/backend/src/routes/v1/membership.ts @@ -4,13 +4,14 @@ 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', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), param('workspaceId').exists().trim(), validateRequest, @@ -20,7 +21,7 @@ router.get( // used for old CLI (deprecate) router.delete( '/:membershipId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), param('membershipId').exists().trim(), validateRequest, @@ -30,7 +31,7 @@ router.delete( router.post( '/:membershipId/change-role', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('role').exists().trim(), validateRequest, @@ -40,7 +41,7 @@ router.post( router.post( '/:membershipId/deny-permissions', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), param('membershipId').isMongoId().exists().trim(), body('permissions').isArray().exists(), diff --git a/backend/src/routes/v1/membershipOrg.ts b/backend/src/routes/v1/membershipOrg.ts index 255b836aa..2863c53fb 100644 --- a/backend/src/routes/v1/membershipOrg.ts +++ b/backend/src/routes/v1/membershipOrg.ts @@ -3,12 +3,13 @@ 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'; router.post( // TODO '/membershipOrg/:membershipOrgId/change-role', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), param('membershipOrgId'), validateRequest, @@ -18,7 +19,7 @@ router.post( router.delete( '/:membershipOrgId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), param('membershipOrgId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v1/organization.ts b/backend/src/routes/v1/organization.ts index 314f684ad..dded53f3a 100644 --- a/backend/src/routes/v1/organization.ts +++ b/backend/src/routes/v1/organization.ts @@ -6,13 +6,19 @@ import { requireOrganizationAuth, validateRequest } from '../../middleware'; -import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables'; +import { + OWNER, + ADMIN, + MEMBER, + ACCEPTED, + AUTH_MODE_JWT +} from '../../variables'; import { organizationController } from '../../controllers/v1'; router.get( // deprecated (moved to api/v2/users/me/organizations) '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), organizationController.getOrganizations ); @@ -20,7 +26,7 @@ router.get( // deprecated (moved to api/v2/users/me/organizations) router.post( // not used on frontend '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('organizationName').exists().trim().notEmpty(), validateRequest, @@ -30,7 +36,7 @@ router.post( // not used on frontend router.get( '/:organizationId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -44,7 +50,7 @@ router.get( router.get( // deprecated (moved to api/v2/organizations/:organizationId/memberships) '/:organizationId/users', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -58,7 +64,7 @@ router.get( // deprecated (moved to api/v2/organizations/:organizationId/members router.get( '/:organizationId/my-workspaces', // deprecated (moved to api/v2/organizations/:organizationId/workspaces) requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -72,7 +78,7 @@ router.get( router.patch( '/:organizationId/name', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -87,7 +93,7 @@ router.patch( router.get( '/:organizationId/incidentContactOrg', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -101,7 +107,7 @@ router.get( router.post( '/:organizationId/incidentContactOrg', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -116,7 +122,7 @@ router.post( router.delete( '/:organizationId/incidentContactOrg', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -131,7 +137,7 @@ router.delete( router.post( '/:organizationId/customer-portal-session', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -145,7 +151,7 @@ router.post( router.get( '/:organizationId/subscriptions', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -159,7 +165,7 @@ router.get( router.get( '/:organizationId/workspace-memberships', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], diff --git a/backend/src/routes/v1/password.ts b/backend/src/routes/v1/password.ts index bc353cf08..b04fa36af 100644 --- a/backend/src/routes/v1/password.ts +++ b/backend/src/routes/v1/password.ts @@ -4,11 +4,14 @@ 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'; router.post( '/srp1', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('clientPublicKey').exists().isString().trim().notEmpty(), validateRequest, @@ -19,7 +22,7 @@ router.post( '/change-password', passwordLimiter, requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('clientProof').exists().trim().notEmpty(), body('protectedKey').exists().isString().trim().notEmpty(), @@ -62,7 +65,7 @@ router.post( '/backup-private-key', passwordLimiter, requireAuth({ - acceptedAuthModes: ['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 diff --git a/backend/src/routes/v1/secret.ts b/backend/src/routes/v1/secret.ts index cce105500..e55dfaf43 100644 --- a/backend/src/routes/v1/secret.ts +++ b/backend/src/routes/v1/secret.ts @@ -8,15 +8,22 @@ import { } from '../../middleware'; import { body, query, param } from 'express-validator'; import { secretController } from '../../controllers/v1'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT +} from '../../variables'; + +// note to devs: these endpoints will be deprecated in favor of v2 router.post( '/:workspaceId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), body('secrets').exists(), body('keys').exists(), @@ -30,10 +37,11 @@ router.post( router.get( '/:workspaceId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), query('environment').exists().trim(), query('channel'), diff --git a/backend/src/routes/v1/serviceToken.ts b/backend/src/routes/v1/serviceToken.ts index 18487ac3e..2b75e7cbf 100644 --- a/backend/src/routes/v1/serviceToken.ts +++ b/backend/src/routes/v1/serviceToken.ts @@ -7,7 +7,11 @@ import { validateRequest } from '../../middleware'; import { body } from 'express-validator'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT +} from '../../variables'; import { serviceTokenController } from '../../controllers/v1'; // note: deprecate service-token routes in favor of service-token data routes/structure @@ -21,11 +25,11 @@ router.get( router.post( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - location: 'body' + locationWorkspaceId: 'body' }), body('name').exists().trim().notEmpty(), body('workspaceId').exists().trim().notEmpty(), diff --git a/backend/src/routes/v1/user.ts b/backend/src/routes/v1/user.ts index e73a6e0f2..b9d88dfb1 100644 --- a/backend/src/routes/v1/user.ts +++ b/backend/src/routes/v1/user.ts @@ -2,11 +2,14 @@ import express from 'express'; const router = express.Router(); import { requireAuth } from '../../middleware'; import { userController } from '../../controllers/v1'; +import { + AUTH_MODE_JWT +} from '../../variables'; router.get( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), userController.getUser ); diff --git a/backend/src/routes/v1/userAction.ts b/backend/src/routes/v1/userAction.ts index b14ab3471..c8d21f918 100644 --- a/backend/src/routes/v1/userAction.ts +++ b/backend/src/routes/v1/userAction.ts @@ -3,12 +3,13 @@ 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'; // note: [userAction] will be deprecated in /v2 in favor of [action] router.post( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('action'), validateRequest, @@ -18,7 +19,7 @@ router.post( router.get( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), query('action'), validateRequest, diff --git a/backend/src/routes/v1/workspace.ts b/backend/src/routes/v1/workspace.ts index 801462662..431a2e4f9 100644 --- a/backend/src/routes/v1/workspace.ts +++ b/backend/src/routes/v1/workspace.ts @@ -6,16 +6,21 @@ import { requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT +} from '../../variables'; import { workspaceController, membershipController } from '../../controllers/v1'; router.get( '/:workspaceId/keys', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -25,10 +30,11 @@ router.get( router.get( '/:workspaceId/users', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -38,7 +44,7 @@ router.get( router.get( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), workspaceController.getWorkspaces ); @@ -46,10 +52,11 @@ router.get( router.get( '/:workspaceId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -59,7 +66,7 @@ router.get( router.post( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('workspaceName').exists().trim().notEmpty(), body('organizationId').exists().trim().notEmpty(), @@ -70,10 +77,11 @@ router.post( router.delete( '/:workspaceId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN] + acceptedRoles: [ADMIN], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -83,10 +91,11 @@ router.delete( router.post( '/:workspaceId/name', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('name').exists().trim().notEmpty(), @@ -97,10 +106,11 @@ router.post( router.post( '/:workspaceId/invite-signup', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('email').exists().trim().notEmpty(), @@ -111,10 +121,11 @@ router.post( router.get( '/:workspaceId/integrations', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -124,10 +135,11 @@ router.get( router.get( '/:workspaceId/authorizations', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -137,10 +149,11 @@ router.get( router.get( '/:workspaceId/service-tokens', // deprecate requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v2/apiKeyData.ts b/backend/src/routes/v2/apiKeyData.ts index 07bbcbc44..939bdbe1f 100644 --- a/backend/src/routes/v2/apiKeyData.ts +++ b/backend/src/routes/v2/apiKeyData.ts @@ -1,16 +1,19 @@ import express from 'express'; const router = express.Router(); +import { param, body } from 'express-validator'; import { requireAuth, validateRequest } from '../../middleware'; -import { param, body } from 'express-validator'; import { apiKeyDataController } from '../../controllers/v2'; +import { + AUTH_MODE_JWT +} from '../../variables'; router.get( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), apiKeyDataController.getAPIKeyData ); @@ -18,7 +21,7 @@ router.get( router.post( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('name').exists().trim(), body('expiresIn'), // measured in ms @@ -29,7 +32,7 @@ router.post( router.delete( '/:apiKeyDataId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), param('apiKeyDataId').exists().trim(), validateRequest, diff --git a/backend/src/routes/v2/environment.ts b/backend/src/routes/v2/environment.ts index 11afa4f2e..0eb4b4a20 100644 --- a/backend/src/routes/v2/environment.ts +++ b/backend/src/routes/v2/environment.ts @@ -7,15 +7,20 @@ import { requireWorkspaceAuth, validateRequest, } from '../../middleware'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT +} from '../../variables'; router.post( '/:workspaceId/environments', requireAuth({ - acceptedAuthModes: ['jwt'], + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('environmentSlug').exists().trim(), @@ -27,10 +32,11 @@ router.post( router.put( '/:workspaceId/environments', requireAuth({ - acceptedAuthModes: ['jwt'], + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('environmentSlug').exists().trim(), @@ -43,10 +49,11 @@ router.put( router.delete( '/:workspaceId/environments', requireAuth({ - acceptedAuthModes: ['jwt'], + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('environmentSlug').exists().trim(), @@ -57,14 +64,15 @@ router.delete( router.get( '/:workspaceId/environments', requireAuth({ - acceptedAuthModes: ['jwt'], + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, environmentController.getAllAccessibleEnvironmentsOfWorkspace ); -export default router; +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/index.ts b/backend/src/routes/v2/index.ts index 538ee4800..c088771eb 100644 --- a/backend/src/routes/v2/index.ts +++ b/backend/src/routes/v2/index.ts @@ -6,6 +6,7 @@ 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" @@ -19,6 +20,7 @@ export { secret, secrets, serviceTokenData, + serviceAccounts, apiKeyData, environment, tags diff --git a/backend/src/routes/v2/organizations.ts b/backend/src/routes/v2/organizations.ts index e1488e8a7..eb2cef8eb 100644 --- a/backend/src/routes/v2/organizations.ts +++ b/backend/src/routes/v2/organizations.ts @@ -6,8 +6,15 @@ import { requireMembershipOrgAuth, validateRequest } from '../../middleware'; -import { body, param, query } from 'express-validator'; -import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables'; +import { body, param } from 'express-validator'; +import { + OWNER, + ADMIN, + MEMBER, + ACCEPTED, + AUTH_MODE_JWT, + AUTH_MODE_API_KEY +} from '../../variables'; import { organizationsController } from '../../controllers/v2'; // TODO: /POST to create membership @@ -17,7 +24,7 @@ router.get( param('organizationId').exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN, MEMBER], @@ -33,14 +40,15 @@ router.patch( body('role').exists().isString().trim().isIn([OWNER, ADMIN, MEMBER]), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], acceptedStatuses: [ACCEPTED] }), requireMembershipOrgAuth({ - acceptedRoles: [OWNER, ADMIN] + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] }), organizationsController.updateOrganizationMembership ); @@ -51,14 +59,15 @@ router.delete( param('membershipId').exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], acceptedStatuses: [ACCEPTED] }), requireMembershipOrgAuth({ - acceptedRoles: [OWNER, ADMIN] + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] }), organizationsController.deleteOrganizationMembership ); @@ -68,7 +77,7 @@ router.get( param('organizationId').exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireOrganizationAuth({ acceptedRoles: [OWNER, ADMIN], @@ -77,4 +86,18 @@ router.get( organizationsController.getOrganizationWorkspaces ); +router.get( + '/:organizationId/service-accounts', + param('organizationId').exists().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + organizationsController.getOrganizationServiceAccounts +); + export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/secret.ts b/backend/src/routes/v2/secret.ts index f1b61b47c..f2c825ba8 100644 --- a/backend/src/routes/v2/secret.ts +++ b/backend/src/routes/v2/secret.ts @@ -6,7 +6,14 @@ import { validateRequest } from '../../middleware'; import { body, param, query } from 'express-validator'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_TOKEN, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from '../../variables'; import { CreateSecretRequestBody, ModifySecretRequestBody } from '../../types/secret'; import { secretController } from '../../controllers/v2'; @@ -17,10 +24,11 @@ const router = express.Router(); router.post( '/batch-create/workspace/:workspaceId/environment/:environment', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().isMongoId().trim(), param('environment').exists().trim(), @@ -33,10 +41,11 @@ router.post( router.post( '/workspace/:workspaceId/environment/:environment', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().isMongoId().trim(), param('environment').exists().trim(), @@ -51,10 +60,11 @@ router.get( param('workspaceId').exists().trim(), query("environment").exists(), requireAuth({ - acceptedAuthModes: ['jwt', 'serviceToken'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), query('channel'), validateRequest, @@ -64,10 +74,11 @@ router.get( router.get( '/:secretId', requireAuth({ - acceptedAuthModes: ['jwt', 'serviceToken'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS] }), validateRequest, secretController.getSecret @@ -76,13 +87,14 @@ router.get( router.delete( '/batch/workspace/:workspaceId/environment/:environmentName', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), body('secretIds').exists().isArray().custom(array => array.length > 0), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), validateRequest, secretController.deleteSecrets @@ -91,10 +103,11 @@ router.delete( router.delete( '/:secretId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireSecretAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS] }), param('secretId').isMongoId(), validateRequest, @@ -104,29 +117,30 @@ router.delete( router.patch( '/batch-modify/workspace/:workspaceId/environment/:environmentName', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('secrets').exists().isArray().custom((secrets: ModifySecretRequestBody[]) => secrets.length > 0), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), validateRequest, secretController.updateSecrets ); - router.patch( '/workspace/:workspaceId/environment/:environmentName', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), body('secret').isObject(), param('workspaceId').exists().isMongoId().trim(), param('environmentName').exists().trim(), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), validateRequest, secretController.updateSecret diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 983a8e54f..57657b079 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -1,5 +1,6 @@ import express from 'express'; const router = express.Router(); +import { Types } from 'mongoose'; import { requireAuth, requireWorkspaceAuth, @@ -8,12 +9,18 @@ import { } from '../../middleware'; import { query, body } from 'express-validator'; import { secretsController } from '../../controllers/v2'; -import { validateSecrets } from '../../helpers/secret'; +import { validateClientForSecrets } from '../../helpers/secrets'; import { ADMIN, MEMBER, SECRET_PERSONAL, - SECRET_SHARED + SECRET_SHARED, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY } from '../../variables'; import { BatchSecretRequest @@ -22,12 +29,11 @@ import { router.post( '/batch', requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['read', 'write'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - location: 'body' + locationWorkspaceId: 'body' }), body('workspaceId').exists().isString().trim(), body('environment').exists().isString().trim(), @@ -40,12 +46,11 @@ router.post( .filter((secretId) => secretId !== undefined) if (secretIds.length > 0) { - const relevantSecrets = await validateSecrets({ - userId: req.user._id.toString(), - secretIds + req.secrets = await validateClientForSecrets({ + authData: req.authData, + secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), + requiredPermissions: [] }); - - req.secrets = relevantSecrets; } } return true; @@ -100,12 +105,13 @@ router.post( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['write'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - location: 'body' + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.createSecrets ); @@ -117,12 +123,13 @@ router.get( query('tagSlugs'), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['read'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - location: 'query' + locationWorkspaceId: 'query', + locationEnvironment: 'query', + requiredPermissions: [PERMISSION_READ_SECRETS] }), secretsController.getSecrets ); @@ -157,11 +164,11 @@ router.patch( }), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['write'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireSecretsAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.updateSecrets ); @@ -186,14 +193,13 @@ router.delete( .isEmpty(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey', 'serviceToken'], - requiredServiceTokenPermissions: ['write'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] }), requireSecretsAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), secretsController.deleteSecrets ); -export default router; - +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/serviceAccounts.ts b/backend/src/routes/v2/serviceAccounts.ts new file mode 100644 index 000000000..6f0db91b7 --- /dev/null +++ b/backend/src/routes/v2/serviceAccounts.ts @@ -0,0 +1,159 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, + requireOrganizationAuth, + requireWorkspaceAuth, + requireServiceAccountAuth, + requireServiceAccountWorkspacePermissionAuth, + validateRequest +} from '../../middleware'; +import { param, query, body } from 'express-validator'; +import { + OWNER, + ADMIN, + MEMBER, + ACCEPTED, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT +} from '../../variables'; +import { serviceAccountsController } from '../../controllers/v2'; + +router.get( // TODO: check + '/me', + requireAuth({ + acceptedAuthModes: [AUTH_MODE_SERVICE_ACCOUNT] + }), + serviceAccountsController.getCurrentServiceAccount +); + +router.get( + '/:serviceAccountId', + param('serviceAccountId').exists().isString().trim(), + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + 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 + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireOrganizationAuth({ + acceptedRoles: [OWNER, ADMIN, MEMBER], + acceptedStatuses: [ACCEPTED], + locationOrganizationId: 'body' + }), + serviceAccountsController.createServiceAccount +); + +router.patch( + '/:serviceAccountId/name', + param('serviceAccountId').exists().isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + serviceAccountsController.changeServiceAccountName +); + +router.delete( + '/:serviceAccountId', + param('serviceAccountId').exists().isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + serviceAccountsController.deleteServiceAccount +); + +router.get( + '/:serviceAccountId/permissions/workspace', + param('serviceAccountId').exists().isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + 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(), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body' + }), + serviceAccountsController.addServiceAccountWorkspacePermission +); + +router.delete( + '/:serviceAccountId/permissions/workspace/:serviceAccountWorkspacePermissionId', + param('serviceAccountId').exists().isString().trim(), + param('serviceAccountWorkspacePermissionId').exists().isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + requireServiceAccountWorkspacePermissionAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + serviceAccountsController.deleteServiceAccountWorkspacePermission +); + +router.get( + '/:serviceAccountId/keys', + query('workspaceId').optional().isString(), + requireAuth({ + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] + }), + requireServiceAccountAuth({ + acceptedRoles: [OWNER, ADMIN], + acceptedStatuses: [ACCEPTED] + }), + serviceAccountsController.getServiceAccountKeys +); + +export default router; \ No newline at end of file diff --git a/backend/src/routes/v2/serviceTokenData.ts b/backend/src/routes/v2/serviceTokenData.ts index 11e8b1c71..075d0d392 100644 --- a/backend/src/routes/v2/serviceTokenData.ts +++ b/backend/src/routes/v2/serviceTokenData.ts @@ -10,13 +10,17 @@ import { param, body } from 'express-validator'; import { ADMIN, MEMBER, + PERMISSION_WRITE_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN } from '../../variables'; import { serviceTokenDataController } from '../../controllers/v2'; router.get( '/', requireAuth({ - acceptedAuthModes: ['serviceToken'] + acceptedAuthModes: [AUTH_MODE_SERVICE_TOKEN] }), serviceTokenDataController.getServiceTokenData ); @@ -24,11 +28,13 @@ router.get( router.post( '/', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - location: 'body' + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS] }), body('name').exists().isString().trim(), body('workspaceId').exists().isString().trim(), @@ -53,7 +59,7 @@ router.post( router.delete( '/:serviceTokenDataId', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireServiceTokenDataAuth({ acceptedRoles: [ADMIN, MEMBER] diff --git a/backend/src/routes/v2/tags.ts b/backend/src/routes/v2/tags.ts index d78e1e0f1..c9a11c1bc 100644 --- a/backend/src/routes/v2/tags.ts +++ b/backend/src/routes/v2/tags.ts @@ -5,17 +5,22 @@ import { tagController } from '../../controllers/v2'; import { requireAuth, requireWorkspaceAuth, - validateRequest, + validateRequest } from '../../middleware'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT +} from '../../variables'; router.get( '/:workspaceId/tags', requireAuth({ - acceptedAuthModes: ['jwt'], + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -25,7 +30,7 @@ router.get( router.delete( '/tags/:tagId', requireAuth({ - acceptedAuthModes: ['jwt'], + acceptedAuthModes: [AUTH_MODE_JWT], }), param('tagId').exists().trim(), validateRequest, @@ -35,10 +40,11 @@ router.delete( router.post( '/:workspaceId/tags', requireAuth({ - acceptedAuthModes: ['jwt'], + acceptedAuthModes: [AUTH_MODE_JWT], }), requireWorkspaceAuth({ acceptedRoles: [MEMBER, ADMIN], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('name').exists().trim(), diff --git a/backend/src/routes/v2/users.ts b/backend/src/routes/v2/users.ts index bdf0978e1..63ae5eee9 100644 --- a/backend/src/routes/v2/users.ts +++ b/backend/src/routes/v2/users.ts @@ -6,11 +6,15 @@ import { } from '../../middleware'; import { body } from 'express-validator'; import { usersController } from '../../controllers/v2'; +import { + AUTH_MODE_JWT, + AUTH_MODE_API_KEY +} from '../../variables'; router.get( '/me', requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), usersController.getMe ); @@ -18,7 +22,7 @@ router.get( router.patch( '/me/mfa', requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), body('isMfaEnabled').exists().isBoolean(), validateRequest, @@ -28,7 +32,7 @@ router.patch( router.get( '/me/organizations', requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + 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 6183c9e66..e258636fb 100644 --- a/backend/src/routes/v2/workspace.ts +++ b/backend/src/routes/v2/workspace.ts @@ -7,16 +7,23 @@ import { requireWorkspaceAuth, validateRequest } from '../../middleware'; -import { ADMIN, MEMBER } from '../../variables'; +import { + ADMIN, + MEMBER, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from '../../variables'; import { workspaceController } from '../../controllers/v2'; router.post( '/:workspaceId/secrets', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), body('secrets').exists(), body('keys').exists(), @@ -30,10 +37,11 @@ router.post( router.get( '/:workspaceId/secrets', requireAuth({ - acceptedAuthModes: ['jwt', 'serviceToken'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_SERVICE_TOKEN] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), query('environment').exists().trim(), query('channel'), @@ -45,10 +53,11 @@ router.get( router.get( '/:workspaceId/encrypted-key', requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, @@ -58,27 +67,27 @@ router.get( router.get( '/:workspaceId/service-token-data', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), validateRequest, workspaceController.getWorkspaceServiceTokenData ); -// TODO: /POST to create membership and re-route inviting user to workspace there - router.get( // new - TODO: rewire dashboard to this route '/:workspaceId/memberships', param('workspaceId').exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), workspaceController.getWorkspaceMemberships ); @@ -90,13 +99,15 @@ router.patch( // TODO - rewire dashboard to this route body('role').exists().isString().trim().isIn([ADMIN, MEMBER]), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], + locationWorkspaceId: 'params' }), requireMembershipAuth({ - acceptedRoles: [ADMIN] + acceptedRoles: [ADMIN], + locationMembershipId: 'params' }), workspaceController.updateWorkspaceMembership ); @@ -107,13 +118,15 @@ router.delete( // TODO - rewire dashboard to this route param('membershipId').exists().trim(), validateRequest, requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] + acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] }), requireWorkspaceAuth({ acceptedRoles: [ADMIN], + locationWorkspaceId: 'params' }), requireMembershipAuth({ - acceptedRoles: [ADMIN] + acceptedRoles: [ADMIN], + locationMembershipId: 'params' }), workspaceController.deleteWorkspaceMembership ); @@ -121,10 +134,11 @@ router.delete( // TODO - rewire dashboard to this route router.patch( '/:workspaceId/auto-capitalization', requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: [AUTH_MODE_JWT] }), requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER] + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'params' }), param('workspaceId').exists().trim(), body('autoCapitalization').exists().trim().notEmpty(), diff --git a/backend/src/routes/v3/secrets.ts b/backend/src/routes/v3/secrets.ts new file mode 100644 index 000000000..b415139a9 --- /dev/null +++ b/backend/src/routes/v3/secrets.ts @@ -0,0 +1,8 @@ +import express from 'express'; +const router = express.Router(); +import { + requireAuth, validateRequest +} from '../../middleware'; +import { body } from 'express-validator'; + +export default router; \ No newline at end of file diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts index 7b3a20a6b..0fd634c33 100644 --- a/backend/src/services/IntegrationService.ts +++ b/backend/src/services/IntegrationService.ts @@ -1,3 +1,4 @@ +import { Types } from 'mongoose'; import { handleOAuthExchangeHelper, syncIntegrationsHelper, @@ -67,7 +68,7 @@ class IntegrationService { * @param {String} obj.integrationAuthId - id of integration auth * @param {String} refreshToken - decrypted refresh token */ - static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) { + static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: Types.ObjectId}) { return await getIntegrationAuthRefreshHelper({ integrationAuthId }); @@ -80,7 +81,7 @@ class IntegrationService { * @param {String} obj.integrationAuthId - id of integration auth * @param {String} accessToken - decrypted access token */ - static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) { + static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) { return await getIntegrationAuthAccessHelper({ integrationAuthId }); diff --git a/backend/src/services/PostHogClient.ts b/backend/src/services/PostHogClient.ts deleted file mode 100644 index 15ccf0919..000000000 --- a/backend/src/services/PostHogClient.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { PostHog } from 'posthog-node'; -import { getLogger } from '../utils/logger'; -import { - getNodeEnv, - getTelemetryEnabled, - getPostHogProjectApiKey, - getPostHogHost -} from '../config'; - -/** - * Logs telemetry enable/disable notice. - */ -const logTelemetryMessage = () => { - if(!getTelemetryEnabled()){ - getLogger("backend-main").info([ - "", - "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')) - } -} - -/** - * Return an instance of the PostHog client initialized. - * @returns - */ -const getPostHogClient = () => { - let postHogClient: any; - if (getNodeEnv() === 'production' && getTelemetryEnabled()) { - // case: enable opt-out telemetry in production - postHogClient = new PostHog(getPostHogProjectApiKey(), { - host: getPostHogHost() - }); - } - - return postHogClient; -} - -export { - logTelemetryMessage, - getPostHogClient -} - diff --git a/backend/src/services/TelemetryService.ts b/backend/src/services/TelemetryService.ts new file mode 100644 index 000000000..248ad3633 --- /dev/null +++ b/backend/src/services/TelemetryService.ts @@ -0,0 +1,89 @@ +import { PostHog } from 'posthog-node'; +import { getLogger } from '../utils/logger'; +import { + getNodeEnv, + getTelemetryEnabled, + getPostHogProjectApiKey, + getPostHogHost +} from '../config'; +import { + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData +} from '../models'; +import { + BadRequestError +} from '../utils/errors'; + +class Telemetry { + /** + * Logs telemetry enable/disable notice. + */ + static logTelemetryMessage = () => { + if(!getTelemetryEnabled()){ + getLogger("backend-main").info([ + "", + "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')) + } + } + + /** + * Return an instance of the PostHog client initialized. + * @returns + */ + static getPostHogClient = () => { + let postHogClient: any; + if (getNodeEnv() === 'production' && getTelemetryEnabled()) { + // case: enable opt-out telemetry in production + postHogClient = new PostHog(getPostHogProjectApiKey(), { + host: getPostHogHost() + }); + } + + return postHogClient; + } + + /** + * Return a distinct id for client to be used for logging telemetry + */ + static getDistinctId = ({ + user, + serviceAccount, + serviceTokenData + }: { + user?: IUser; + serviceAccount?: IServiceAccount; + serviceTokenData?: any; // TODO: fix (it's ServiceTokenData with user populated) + }) => { + let distinctId = ''; + + if (user) { + distinctId = user.email; + } + + if (serviceAccount) { + distinctId = `sa.${serviceAccount._id.toString()}`; + } + + if (serviceTokenData?.user && serviceTokenData?.user instanceof User) { + distinctId = serviceTokenData.user.email; + } else if (serviceTokenData?.serviceAccount && serviceTokenData?.serviceAccount instanceof ServiceAccount) { + distinctId = `sa.${serviceTokenData.serviceAccount._id.toString()}`; + } + + if (distinctId === '') { + throw BadRequestError({ + message: 'Failed to obtain distinct id for logging telemetry' + }); + } + + return distinctId; + } +} + +export default Telemetry; \ No newline at end of file diff --git a/backend/src/services/index.ts b/backend/src/services/index.ts index d98b70718..f93f45f58 100644 --- a/backend/src/services/index.ts +++ b/backend/src/services/index.ts @@ -1,13 +1,15 @@ import DatabaseService from './DatabaseService'; -import { logTelemetryMessage, getPostHogClient } from './PostHogClient'; +// 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'; export { - logTelemetryMessage, - getPostHogClient, + TelemetryService, + // logTelemetryMessage, + // getPostHogClient, DatabaseService, BotService, EventService, diff --git a/backend/src/services/smtp.ts b/backend/src/services/smtp.ts index 7a4ebf00b..b30a43447 100644 --- a/backend/src/services/smtp.ts +++ b/backend/src/services/smtp.ts @@ -66,7 +66,7 @@ export const initSmtp = () => { const transporter = nodemailer.createTransport(mailOpts); transporter .verify() - .then(() => { + .then((err) => { Sentry.setUser(null); Sentry.captureMessage('SMTP - Successfully connected'); }) diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index acee877bd..bff5e96c3 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -1,5 +1,10 @@ import * as express from 'express'; -import { ISecret } from '../../models'; +import { + IUser, + IServiceAccount, + IServiceTokenData, + ISecret +} from '../../models'; // TODO: fix (any) types declare global { @@ -19,10 +24,18 @@ declare global { secrets: any; secretSnapshot: any; serviceToken: any; + serviceAccount: any; accessToken: any; serviceTokenData: any; apiKeyData: any; query?: any; + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + requestData: { + [key: string]: string + }; } } } diff --git a/backend/src/utils/addDevelopmentUser.ts b/backend/src/utils/addDevelopmentUser.ts index 585740a6b..136d91a98 100644 --- a/backend/src/utils/addDevelopmentUser.ts +++ b/backend/src/utils/addDevelopmentUser.ts @@ -8,17 +8,18 @@ import { Key, Membership, MembershipOrg, Organization, User, Workspace } from ". import { Types } from 'mongoose'; import { getNodeEnv } from '../config'; -export const createTestUserForDevelopment = async () => { - if (getNodeEnv() === "development") { - const testUserEmail = "test@localhost.local" - const testUserPassword = "testInfisical1" - const testUserId = "63cefa6ec8d3175601cfa980" - const testWorkspaceId = "63cefb15c8d3175601cfa989" - const testOrgId = "63cefb15c8d3175601cfa985" - const testMembershipId = "63cefb159185d9aa3ef0cf35" - const testMembershipOrgId = "63cefb159185d9aa3ef0cf31" - const testWorkspaceKeyId = "63cf48f0225e6955acec5eff" +export const testUserEmail = "test@localhost.local" +export const testUserPassword = "testInfisical1" +export const testUserId = "63cefa6ec8d3175601cfa980" +export const testWorkspaceId = "63cefb15c8d3175601cfa989" +export const testOrgId = "63cefb15c8d3175601cfa985" +export const testMembershipId = "63cefb159185d9aa3ef0cf35" +export const testMembershipOrgId = "63cefb159185d9aa3ef0cf31" +export const testWorkspaceKeyId = "63cf48f0225e6955acec5eff" +export const plainTextWorkspaceKey = "543fef8224813a46230b0a50a46c5fb2" +export const createTestUserForDevelopment = async () => { + if (getNodeEnv() === "development" || getNodeEnv() === "test") { const testUser = { _id: testUserId, email: testUserEmail, diff --git a/backend/src/utils/errors.ts b/backend/src/utils/errors.ts index e9972edf5..987215091 100644 --- a/backend/src/utils/errors.ts +++ b/backend/src/utils/errors.ts @@ -73,6 +73,16 @@ export const ValidationError = (error?: Partial) => new Req 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', + context: error?.context, + stack: error?.stack +}); + //* ----->[INTEGRATION ERRORS]<----- export const IntegrationNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, @@ -93,6 +103,16 @@ export const WorkspaceNotFoundError = (error?: Partial) => 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', + context: error?.context, + stack: error?.stack +}); + //* ----->[ORGANIZATION ERRORS]<----- export const OrganizationNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, @@ -103,6 +123,16 @@ export const OrganizationNotFoundError = (error?: Partial) 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', + context: error?.context, + stack: error?.stack +}); + //* ----->[ACCOUNT ERRORS]<----- export const AccountNotFoundError = (error?: Partial) => new RequestError({ logLevel: error?.logLevel ?? LogLevel.ERROR, @@ -157,10 +187,38 @@ 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', + 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 +}); + +//* ----->[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', + context: error?.context, + 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', + context: error?.context, + 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', + context: error?.context, + stack: error?.stack +}) + //* ----->[MISC ERRORS]<----- diff --git a/backend/src/variables/authentication.ts b/backend/src/variables/authentication.ts new file mode 100644 index 000000000..2fe1f4fc6 --- /dev/null +++ b/backend/src/variables/authentication.ts @@ -0,0 +1,11 @@ +const AUTH_MODE_JWT = 'jwt'; +const AUTH_MODE_SERVICE_ACCOUNT = 'serviceAccount'; +const AUTH_MODE_SERVICE_TOKEN = 'serviceToken'; +const AUTH_MODE_API_KEY = 'apiKey'; // TODO: deprecate + +export { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts index b71044cba..979de21ab 100644 --- a/backend/src/variables/index.ts +++ b/backend/src/variables/index.ts @@ -15,9 +15,11 @@ import { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_AZURE_TOKEN_URL, @@ -31,9 +33,11 @@ import { 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, getIntegrationOptions } from "./integration"; import { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED } from "./organization"; @@ -63,6 +67,16 @@ import { TOKEN_EMAIL_ORG_INVITATION, TOKEN_EMAIL_PASSWORD_RESET } from './token'; +import { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} from './permission'; +import { + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY +} from './authentication'; export { OWNER, @@ -86,9 +100,11 @@ export { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_AZURE_TOKEN_URL, @@ -102,9 +118,11 @@ export { 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, EVENT_PUSH_SECRETS, EVENT_PULL_SECRETS, ACTION_LOGIN, @@ -113,6 +131,8 @@ export { ACTION_UPDATE_SECRETS, ACTION_DELETE_SECRETS, ACTION_READ_SECRETS, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, getIntegrationOptions, SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN, @@ -124,5 +144,9 @@ export { TOKEN_EMAIL_CONFIRMATION, TOKEN_EMAIL_MFA, TOKEN_EMAIL_ORG_INVITATION, - TOKEN_EMAIL_PASSWORD_RESET + TOKEN_EMAIL_PASSWORD_RESET, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY }; diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts index 52bfcc614..bbfd7f107 100644 --- a/backend/src/variables/integration.ts +++ b/backend/src/variables/integration.ts @@ -17,9 +17,11 @@ const INTEGRATION_NETLIFY = "netlify"; const INTEGRATION_GITHUB = "github"; const INTEGRATION_GITLAB = "gitlab"; const INTEGRATION_RENDER = "render"; +const INTEGRATION_RAILWAY = "railway"; const INTEGRATION_FLYIO = "flyio"; const INTEGRATION_CIRCLECI = "circleci"; const INTEGRATION_TRAVISCI = "travisci"; +const INTEGRATION_SUPABASE = 'supabase'; const INTEGRATION_SET = new Set([ INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -31,6 +33,7 @@ const INTEGRATION_SET = new Set([ INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE ]); // integration types @@ -52,9 +55,11 @@ const INTEGRATION_GITLAB_API_URL = "https://gitlab.com/api"; const INTEGRATION_VERCEL_API_URL = "https://api.vercel.com"; const INTEGRATION_NETLIFY_API_URL = "https://api.netlify.com"; const INTEGRATION_RENDER_API_URL = "https://api.render.com"; +const INTEGRATION_RAILWAY_API_URL = "https://backboard.railway.app/graphql/v2"; const INTEGRATION_FLYIO_API_URL = "https://api.fly.io/graphql"; const INTEGRATION_CIRCLECI_API_URL = "https://circleci.com/api"; const INTEGRATION_TRAVISCI_API_URL = "https://api.travis-ci.com"; +const INTEGRATION_SUPABASE_API_URL = 'https://api.supabase.com'; const getIntegrationOptions = () => { const INTEGRATION_OPTIONS = [ @@ -104,6 +109,15 @@ const getIntegrationOptions = () => { clientId: '', docsLink: '' }, + { + name: 'Railway', + slug: 'railway', + image: 'Railway.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, { name: 'Fly.io', slug: 'flyio', @@ -167,6 +181,15 @@ const getIntegrationOptions = () => { clientId: '', docsLink: '' }, + { + name: 'Supabase', + slug: 'supabase', + image: 'Supabase.png', + isAvailable: true, + type: 'pat', + clientId: '', + docsLink: '' + }, { name: 'Google Cloud Platform', slug: 'gcp', @@ -192,9 +215,11 @@ export { INTEGRATION_GITHUB, INTEGRATION_GITLAB, INTEGRATION_RENDER, + INTEGRATION_RAILWAY, INTEGRATION_FLYIO, INTEGRATION_CIRCLECI, INTEGRATION_TRAVISCI, + INTEGRATION_SUPABASE, INTEGRATION_SET, INTEGRATION_OAUTH2, INTEGRATION_AZURE_TOKEN_URL, @@ -208,8 +233,10 @@ export { 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, getIntegrationOptions }; diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts index 91af2ff25..80c7102c1 100644 --- a/backend/src/variables/organization.ts +++ b/backend/src/variables/organization.ts @@ -6,11 +6,7 @@ const MEMBER = "member"; // membership statuses const INVITED = "invited"; -// membership permissions ability -const ABILITY_READ = "read"; -const ABILITY_WRITE = "write"; - // -- organization const ACCEPTED = "accepted"; -export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED, ABILITY_READ, ABILITY_WRITE }; +export { OWNER, ADMIN, MEMBER, INVITED, ACCEPTED }; diff --git a/backend/src/variables/permission.ts b/backend/src/variables/permission.ts new file mode 100644 index 000000000..769344d7f --- /dev/null +++ b/backend/src/variables/permission.ts @@ -0,0 +1,7 @@ +const PERMISSION_READ_SECRETS = 'read'; +const PERMISSION_WRITE_SECRETS = 'write'; + +export { + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS +} \ No newline at end of file diff --git a/backend/test-resources/env-vars.js b/backend/test-resources/env-vars.js index a7542728f..6702649c1 100644 --- a/backend/test-resources/env-vars.js +++ b/backend/test-resources/env-vars.js @@ -3,3 +3,8 @@ process.env.MONGO_URL = 'mongodb://test:test1234@localhost:27018/?authSource=admin'; process.env.MONGO_USERNAME = 'test'; process.env.MONGO_PASSWORD = 'test1234'; +process.env.NODE_ENV = 'test'; +process.env.JWT_SIGNUP_SECRET= "38ea90fb7998b92176080f457d890392" +process.env.JWT_REFRESH_SECRET= "7764c7bbf3928ad501591a3e005eb364" +process.env.JWT_AUTH_SECRET= "5239fea3a4720c0e524f814a540e14a2" +process.env.JWT_SERVICE_SECRET= "8509fb8b90c9b53e9e61d1e35826dcb5" \ No newline at end of file diff --git a/backend/tests/data/batch-create-secrets-with-some-missing-params.json b/backend/tests/data/batch-create-secrets-with-some-missing-params.json new file mode 100644 index 000000000..40d07827c --- /dev/null +++ b/backend/tests/data/batch-create-secrets-with-some-missing-params.json @@ -0,0 +1,51 @@ +[ + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + } +] \ No newline at end of file diff --git a/backend/tests/data/batch-secrets-no-override.json b/backend/tests/data/batch-secrets-no-override.json new file mode 100644 index 000000000..7236c907a --- /dev/null +++ b/backend/tests/data/batch-secrets-no-override.json @@ -0,0 +1,56 @@ +[ + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "eaX9a2g=", + "secretKeyIV": "YJ4adgI/wEHifGdtT9reaA==", + "secretKeyTag": "dP73x3wrq7pqxzAHo+bfPA==", + "secretValueCiphertext": "cw==", + "secretValueIV": "7ksYWWZ3+9rzLG5NpEbEgg==", + "secretValueTag": "H0YQ8vrhiVJ0XSW4nBJdQA==", + "secretCommentCiphertext": "", + "secretCommentIV": "yXhMdLdA9q7Vaw4UUaeBYA==", + "secretCommentTag": "qMj7SHESM5Jn+C2qpbw2pA==" + } + } +] \ No newline at end of file diff --git a/backend/tests/data/batch-secrets-with-overrides.json b/backend/tests/data/batch-secrets-with-overrides.json new file mode 100644 index 000000000..6173289aa --- /dev/null +++ b/backend/tests/data/batch-secrets-with-overrides.json @@ -0,0 +1,38 @@ +[ + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "shared", + "environment": "dev", + "secretKeyCiphertext": "IVMtGWE=", + "secretKeyIV": "BDsG7/ylk7mT8MrIMn0e7w==", + "secretKeyTag": "1ujy08fctmZ1xTXMYr23UQ==", + "secretValueCiphertext": "I9psUg==", + "secretValueIV": "W+DJETpCerHkFv8AR9Fv4w==", + "secretValueTag": "yODOeN3HBr/usly4VSMt9w==", + "secretCommentCiphertext": "", + "secretCommentIV": "QET7oX2ZiuLDSzwrkeL2Ig==", + "secretCommentTag": "6P3xeA9eO+3Wp66ROHXgfg==" + } + }, + { + "method": "POST", + "secret": { + "workspace": "63cefb15c8d3175601cfa989", + "type": "personal", + "user": "63cefa6ec8d3175601cfa980", + "tags": [], + "environment": "dev", + "secretKeyCiphertext": "Q7lyRO8=", + "secretKeyIV": "yz8koc3d63ywJMiGXpCNSw==", + "secretKeyTag": "j2bMQ2d4sDZKA0OaKM5SXA==", + "secretValueCiphertext": "X4kaiShmtGZt", + "secretValueIV": "p/GdbksLVveNLsV3vz5GLA==", + "secretValueTag": "//dhRL+pagecavHJCtMPWg==", + "secretCommentCiphertext": "", + "secretCommentIV": "7eYJzuilvjQPutqrqbd2MQ==", + "secretCommentTag": "LpPv9K0Hhd5noE39Zu9U+w==" + } + } +] \ No newline at end of file diff --git a/backend/tests/helper/helper.ts b/backend/tests/helper/helper.ts new file mode 100644 index 000000000..c59c8d43b --- /dev/null +++ b/backend/tests/helper/helper.ts @@ -0,0 +1,96 @@ +// Helper functions for integration tests + +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') +// eslint-disable-next-line @typescript-eslint/no-var-requires +const jsrp = require('jsrp'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const axios = require('axios'); +import { plainTextWorkspaceKey, testWorkspaceId } from "../../src/utils/addDevelopmentUser"; +import { encryptSymmetric } from "../../src/utils/crypto"; + +interface TokenData { + token: string; + publicKey: string; + encryptedPrivateKey: string; + iv: string; + tag: string; +} + +export const getJWTFromTestUser = (): Promise => { + return new Promise((resolve, reject) => { + const client = new jsrp.client(); + const EMAIL = testUserEmail + const PASSWORD = testUserPassword + + client.init({ + username: EMAIL, + password: PASSWORD, + }, async () => { + const clientPublicKey = client.getPublicKey(); + + // POST: /login1 + const reqBody = { + email: EMAIL, + clientPublicKey + } + + + const loginOneRes = await axiosInstance.post('http://localhost:4000/api/v1/auth/login1', reqBody); + const serverPublicKey = loginOneRes.data.serverPublicKey; + const salt = loginOneRes.data.salt; + + client.setSalt(salt); + client.setServerPublicKey(serverPublicKey); + const clientSharedKey = client.getSharedKey(); // shared Key + const clientProof = client.getProof(); // called M1 + + // POST: /login2 + const reqBody2 = { + email: EMAIL, + clientProof + } + + const response2 = await axiosInstance.post('http://localhost:4000/api/v1/auth/login2', reqBody2); + + resolve(response2.data) + }) + }); +} + +export const getServiceTokenFromTestUser = async () => { + const loggedInUserDetails = await getJWTFromTestUser() + const randomBytes = crypto.randomBytes(16).toString('hex'); + const { ciphertext, iv, tag } = encryptSymmetric({ + 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"] + }, { + headers: { + 'Authorization': `Bearer ${loggedInUserDetails.token}` + } + }); + + return `${newServiceToken.data.serviceToken}.${randomBytes}` +} + +export const deleteAllSecrets = async () => { + await Secret.deleteMany() +} + +export const getAllSecrets = async () => { + return await Secret.find() +} \ No newline at end of file diff --git a/backend/tests/integration-tests/routes/v2/secrets.test.ts b/backend/tests/integration-tests/routes/v2/secrets.test.ts new file mode 100644 index 000000000..4df6056fe --- /dev/null +++ b/backend/tests/integration-tests/routes/v2/secrets.test.ts @@ -0,0 +1,408 @@ +import request from 'supertest' +import main from '../../../../src/index' +import { testWorkspaceId } from '../../../../src/utils/addDevelopmentUser'; +import { deleteAllSecrets, getAllSecrets, getJWTFromTestUser, getServiceTokenFromTestUser } from '../../../helper/helper'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const batchSecretRequestWithNoOverride = require('../../../data/batch-secrets-no-override.json'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const batchSecretRequestWithOverrides = require('../../../data/batch-secrets-with-overrides.json'); + +// eslint-disable-next-line @typescript-eslint/no-var-requires +const batchSecretRequestWithBadRequest = require('../../../data/batch-create-secrets-with-some-missing-params.json'); + +let server: any; +beforeAll(async () => { + server = await main; +}); + +afterAll(async () => { + server.close(); +}); + +describe("GET /api/v2/secrets", () => { + describe("Get secrets via JTW", () => { + test("should create secrets and read secrets via jwt", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithNoOverride + }) + + expect(createSecretsResponse.statusCode).toBe(200) + + + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${loginResponse.token}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) + + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(3) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); + + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); + + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); + + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); + + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); + + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); + + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); + + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); + + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); + + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } + }) + + test("Get secrets via jwt when personal overrides exist", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithOverrides + }) + + expect(createSecretsResponse.statusCode).toBe(200) + + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${loginResponse.token}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) + + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(2) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); + + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); + + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); + + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); + + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); + + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); + + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); + + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); + + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); + + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } + }) + }) + + describe("fetch secrets via service token", () => { + test("Get secrets via jwt when personal overrides exist", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithOverrides + }) + + expect(createSecretsResponse.statusCode).toBe(200) + + // now use the service token to fetch secrets + const serviceToken = await getServiceTokenFromTestUser() + + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${serviceToken}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) + + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(2) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); + + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); + + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); + + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); + + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); + + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); + + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); + + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); + + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); + + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } + }) + + test("should create secrets and read secrets via service token when no overrides", async () => { + try { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create secrets + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithNoOverride + }) + + expect(createSecretsResponse.statusCode).toBe(200) + + + // now use the service token to fetch secrets + const serviceToken = await getServiceTokenFromTestUser() + + const getSecrets = await request(server) + .get("/api/v2/secrets") + .set('Authorization', `Bearer ${serviceToken}`) + .query({ + workspaceId: testWorkspaceId, + environment: "dev" + }) + + expect(getSecrets.statusCode).toBe(200) + expect(getSecrets.body).toHaveProperty("secrets") + expect(getSecrets.body.secrets).toHaveLength(3) + expect(getSecrets.body.secrets).toBeInstanceOf(Array); + + getSecrets.body.secrets.forEach((secret: any) => { + expect(secret).toHaveProperty('_id'); + expect(secret._id).toBeTruthy(); + + expect(secret).toHaveProperty('version'); + expect(secret.version).toBeTruthy(); + + expect(secret).toHaveProperty('workspace'); + expect(secret.workspace).toBeTruthy(); + + expect(secret).toHaveProperty('type'); + expect(secret.type).toBeTruthy(); + + expect(secret).toHaveProperty('tags'); + expect(secret.tags).toHaveLength(0); + + expect(secret).toHaveProperty('environment'); + expect(secret.environment).toEqual("dev"); + + expect(secret).toHaveProperty('secretKeyCiphertext'); + expect(secret.secretKeyCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyIV'); + expect(secret.secretKeyIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretKeyTag'); + expect(secret.secretKeyTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueCiphertext'); + expect(secret.secretValueCiphertext).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueIV'); + expect(secret.secretValueIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretValueTag'); + expect(secret.secretValueTag).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentCiphertext'); + expect(secret.secretCommentCiphertext).toBeFalsy(); + + expect(secret).toHaveProperty('secretCommentIV'); + expect(secret.secretCommentIV).toBeTruthy(); + + expect(secret).toHaveProperty('secretCommentTag'); + expect(secret.secretCommentTag).toBeTruthy(); + + expect(secret).toHaveProperty('createdAt'); + expect(secret.createdAt).toBeTruthy(); + + expect(secret).toHaveProperty('updatedAt'); + expect(secret.updatedAt).toBeTruthy(); + }); + } finally { + // clean up + await deleteAllSecrets() + } + }) + }) + + describe("create secrets via JWT", () => { + test("Create secrets via jwt when some requests have missing required parameters", async () => { + // get login details + const loginResponse = await getJWTFromTestUser() + + // create creates + const createSecretsResponse = await request(server) + .post("/api/v2/secrets/batch") + .set('Authorization', `Bearer ${loginResponse.token}`) + .send({ + workspaceId: testWorkspaceId, + environment: "dev", + requests: batchSecretRequestWithBadRequest + }) + + const allSecretsInDB = await getAllSecrets() + + expect(createSecretsResponse.statusCode).toBe(500) // TODO should be set to 400 + expect(allSecretsInDB).toHaveLength(0) + }) + }) +}) \ No newline at end of file diff --git a/backend/tests/integration-tests/routes/v2/service-tokens.ts b/backend/tests/integration-tests/routes/v2/service-tokens.ts new file mode 100644 index 000000000..b011db36f --- /dev/null +++ b/backend/tests/integration-tests/routes/v2/service-tokens.ts @@ -0,0 +1,58 @@ +import request from 'supertest' +import main from '../../../../src/index' +import { getServiceTokenFromTestUser } from '../../../helper/helper'; +let server: any; + +beforeAll(async () => { + server = await main; +}); + +afterAll(async () => { + server.close(); +}); + +describe("GET /api/v2/service-token", () => { + describe("Get service token details", () => { + test("should respond create and get the details of a service token", async () => { + // generate a service token + const serviceToken = await getServiceTokenFromTestUser() + + // get the service token details + const serviceTokenDetails = await request(server) + .get("/api/v2/service-token") + .set('Authorization', `Bearer ${serviceToken}`) + + expect(serviceTokenDetails.body).toMatchObject({ + _id: expect.any(String), + name: 'test service token', + workspace: '63cefb15c8d3175601cfa989', + environment: 'dev', + user: { + _id: '63cefa6ec8d3175601cfa980', + email: 'test@localhost.local', + firstName: 'Jake', + lastName: 'Moni', + isMfaEnabled: false, + mfaMethods: expect.any(Array), + devices: [ + { + ip: expect.any(String), + userAgent: expect.any(String), + _id: expect.any(String), + }, + ], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }, + lastUsed: expect.any(String), + expiresAt: expect.any(String), + encryptedKey: expect.any(String), + iv: expect.any(String), + tag: expect.any(String), + permissions: ['read'], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }) + }) +}) \ No newline at end of file diff --git a/backend/tests/utils/crypto.test.ts b/backend/tests/unit-tests/utils/crypto.test.ts similarity index 99% rename from backend/tests/utils/crypto.test.ts rename to backend/tests/unit-tests/utils/crypto.test.ts index aea5f5f26..bbbfd0297 100644 --- a/backend/tests/utils/crypto.test.ts +++ b/backend/tests/unit-tests/utils/crypto.test.ts @@ -4,7 +4,7 @@ import { decryptSymmetric, encryptAsymmetric, encryptSymmetric -} from '../../src/utils/crypto'; +} from '../../../src/utils/crypto'; describe('Crypto', () => { describe('encryptAsymmetric', () => { diff --git a/backend/tests/utils/posthog.test.ts b/backend/tests/unit-tests/utils/posthog.test.ts similarity index 93% rename from backend/tests/utils/posthog.test.ts rename to backend/tests/unit-tests/utils/posthog.test.ts index a380060b0..9f202385c 100644 --- a/backend/tests/utils/posthog.test.ts +++ b/backend/tests/unit-tests/utils/posthog.test.ts @@ -1,5 +1,5 @@ import { describe, test, expect } from '@jest/globals'; -import { getChannelFromUserAgent } from '../../src/utils/posthog'; +import { getChannelFromUserAgent } from '../../../src/utils/posthog'; describe('posthog getChannelFromUserAgent', () => { test("should return 'web' when userAgent includes 'mozilla'", () => { diff --git a/cli/packages/api/model.go b/cli/packages/api/model.go index af8dbc5b4..71354c84c 100644 --- a/cli/packages/api/model.go +++ b/cli/packages/api/model.go @@ -230,19 +230,10 @@ type GetEncryptedSecretsV2Response struct { } type GetServiceTokenDetailsResponse struct { - ID string `json:"_id"` - Name string `json:"name"` - Workspace string `json:"workspace"` - Environment string `json:"environment"` - User struct { - ID string `json:"_id"` - Email string `json:"email"` - CreatedAt time.Time `json:"createdAt"` - UpdatedAt time.Time `json:"updatedAt"` - V int `json:"__v"` - FirstName string `json:"firstName"` - LastName string `json:"lastName"` - } `json:"user"` + ID string `json:"_id"` + Name string `json:"name"` + Workspace string `json:"workspace"` + Environment string `json:"environment"` ExpiresAt time.Time `json:"expiresAt"` EncryptedKey string `json:"encryptedKey"` Iv string `json:"iv"` diff --git a/cloudformation/ec2-deployment/infisical-ec2-deployment.template b/cloudformation/ec2-deployment/infisical-ec2-deployment.template index 36f0d9dce..2db93b779 100644 --- a/cloudformation/ec2-deployment/infisical-ec2-deployment.template +++ b/cloudformation/ec2-deployment/infisical-ec2-deployment.template @@ -96,6 +96,7 @@ Resources: echo "JWT_AUTH_SECRET=${!JWT_AUTH_SECRET}" >> .env echo "JWT_SERVICE_SECRET=${!JWT_SERVICE_SECRET}" >> .env echo "MONGO_URL=${!DOCUMENT_DB_CONNECTION_URL}" >> .env + echo "HTTPS_ENABLED=false" >> .env docker-compose up -d diff --git a/docs/api-reference/overview/authentication.mdx b/docs/api-reference/overview/authentication.mdx index 7df8f10e5..3e18f8ce4 100644 --- a/docs/api-reference/overview/authentication.mdx +++ b/docs/api-reference/overview/authentication.mdx @@ -1,25 +1,51 @@ --- title: "Authentication" +description: "How to authenticate with the Infisical Public API" --- -To authenticate requests with Infisical, you can either use an API Key or [Infisical Token](../../../getting-started/dashboard/token); certain endpoints will accept either one or both. -- API Key: This general-purpose authentication token provides user access to most endpoints in this reference. -- [Infisical Token](../../../getting-started/dashboard/token): This authentication token (also referred to as the service token) is scoped to a specific project and environment and used for CRUD secret operations. +## Essentials + +The Public API accepts multiple modes of authentication being via API Key, Service Account credentials, or [Infisical Token](../../../getting-started/dashboard/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](../../../getting-started/dashboard/token): Provides short-lived, scoped CRUD access to the secrets of a specific project and environment. +The API key mode uses an API key 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. You can obtain an API key in User Settings > API Keys ![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. + +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 `. + +You can create a Service Account in Organization Settings > Service Accounts + -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 st.`. + +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) - \ No newline at end of file + + +## 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. \ No newline at end of file diff --git a/docs/api-reference/overview/introduction.mdx b/docs/api-reference/overview/introduction.mdx index 3d748314b..585abc8f0 100644 --- a/docs/api-reference/overview/introduction.mdx +++ b/docs/api-reference/overview/introduction.mdx @@ -2,11 +2,17 @@ title: "Introduction" --- -Infisical's REST API provides users an alternative way to programmatically access and manage +Infisical's Public (REST) API provides users an alternative way to programmatically access and manage secrets via HTTPS requests. This can be useful for automating tasks, such as rotating credentials, or for integrating secret management into a larger system. -With the REST API, users can create, read, update, and delete secrets, as well as manage access control, query audit logs, and more. +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. + ## Concepts diff --git a/docs/cli/overview.mdx b/docs/cli/overview.mdx index 8934d8e07..53f75996a 100644 --- a/docs/cli/overview.mdx +++ b/docs/cli/overview.mdx @@ -109,15 +109,33 @@ infisical login The CLI is set to connect to Infisical Cloud by default, but if you're running your own instance of Infisical, you can direct the CLI to it using one of the methods provided below. -#### Export environment variable +#### Method 1: Export environment variable You can point the CLI to the self hosted Infisical instance by exporting the environment variable `INFISICAL_API_URL` in your terminal. -```bash -# Example -export INFISICAL_API_URL="https://your-self-hosted-infisical.com/api" -``` + + + ```bash + # Set backend host + export INFISICAL_API_URL="https://your-self-hosted-infisical.com/api" -#### Set manually on every command + # Remove backend host + unset INFISICAL_API_URL + ``` + + + ```bash + # Set backend host + setx INFISICAL_API_URL "https://your-self-hosted-infisical.com/api" + + # Remove backend host + setx INFISICAL_API_URL "" + + # NOTE: Once set or removed, please restart powershell for the change to take effect + ``` + + + +#### Method 2: Set manually on every command Another option to point the CLI to your self hosted Infisical instance is to set it via a flag on every command you run. ```bash diff --git a/docs/getting-started/dashboard/organization.mdx b/docs/getting-started/dashboard/organization.mdx index 63ec2cd49..161a79f14 100644 --- a/docs/getting-started/dashboard/organization.mdx +++ b/docs/getting-started/dashboard/organization.mdx @@ -24,6 +24,14 @@ To add a member to your organization, scroll down to the "Organization Members" projects by default. +## Service Accounts + +Service accounts represent machine identities such as VMs or application clients that can authenticate with Infisical. They can be provisioned read/write permissions for project(s) and environment(s). + +To add a service account to your organization, scroll down to the "Service Accounts" section and create a service account. Afterwards, you can press on the edit button beside the service account to provision it permissions. + +![organization service accounts](../../images/organization-service-accounts.png) + ## Incident contacts Incident contacts of an organization are alerted if anything abnormal is detected within the operations of an organization. diff --git a/docs/getting-started/dashboard/project.mdx b/docs/getting-started/dashboard/project.mdx index ce7df57e3..065835fb3 100644 --- a/docs/getting-started/dashboard/project.mdx +++ b/docs/getting-started/dashboard/project.mdx @@ -25,16 +25,16 @@ In most cases, environment variables belong to specific environments: developmen ![project environment](../../images/project-environment.png) -### Personal/Shared scoping +### Personal overrides -Every environment variable is classified as either personal or shared. +Every environment variable value can be overriden with a custom value. -- A personal environment variable is one created by a user of a project to be available for that user only. -- A shared environment variable is one created by a user of a project to be available for other users of the project. +- An overriden value can only be read and accesssed by the user that overrode the original shared value. +- A (default) shared value can be read and accesssed by other users in a project. -You can toggle the classification of an environment variable by pressing on its settings: +You can turn overrides on/off by toggling the override/branch icon: -![project variable toggle open](../../images/project-envar-toggle-open.png) +![project variable toggle open](../../images/project-envar-override.png) ### Search @@ -42,12 +42,6 @@ You can search for any environment variable by its key. ![project search](../../images/project-search.png) -### Sort - -You can sort environment variables alphabetically by their keys. - -![project sort](../../images/project-sort.png) - ### Hide/Un-hide You can hide or un-hide the values of your environment variables. By default, the values are hidden for your privacy. diff --git a/docs/getting-started/introduction.mdx b/docs/getting-started/introduction.mdx index cd5022083..d70d12e1b 100644 --- a/docs/getting-started/introduction.mdx +++ b/docs/getting-started/introduction.mdx @@ -3,7 +3,7 @@ title: "Introduction" description: "What is Infisical?" --- -Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret manager that enables teams to easily manage and sync their environment variables. +Infisical is an [open-source](https://opensource.com/resources/what-open-source), [end-to-end encrypted](https://en.wikipedia.org/wiki/End-to-end_encryption) secret management platform that enables teams to easily manage and sync their environment variables. Start syncing environment variables with [Infisical Cloud](https://app.infisical.com) or learn how to [host Infisical](/self-hosting/overview) yourself. diff --git a/docs/getting-started/quickstart.mdx b/docs/getting-started/quickstart.mdx index 256ae8f85..f19bd1120 100644 --- a/docs/getting-started/quickstart.mdx +++ b/docs/getting-started/quickstart.mdx @@ -1,6 +1,6 @@ --- title: "Quickstart" -description: "Start managing your developer secrets and configs with Infisical in 10 minutes." +description: "Start managing developer secrets and configs with Infisical in minutes." --- These examples demonstrate how to store and fetch environment variables from [Infisical Cloud](https://app.infisical.com) into your application. @@ -9,7 +9,7 @@ These examples demonstrate how to store and fetch environment variables from [In 1. Login or create an account at `app.infisical.com`. 2. Create a new project. -3. Populate your environment variables as in the image below. +3. Keep the default environment variables or populate them as in the image below. ![project quickstart](../images/project-quickstart.png) diff --git a/docs/images/dashboard-name-modal-organization.png b/docs/images/dashboard-name-modal-organization.png index 7fe84675e..59145d21b 100644 Binary files a/docs/images/dashboard-name-modal-organization.png and b/docs/images/dashboard-name-modal-organization.png differ diff --git a/docs/images/dashboard.png b/docs/images/dashboard.png index 961b53587..2188debfc 100644 Binary files a/docs/images/dashboard.png and b/docs/images/dashboard.png differ diff --git a/docs/images/integrations-railway-authorization.png b/docs/images/integrations-railway-authorization.png new file mode 100644 index 000000000..8608ea6d1 Binary files /dev/null and b/docs/images/integrations-railway-authorization.png differ diff --git a/docs/images/integrations-railway-create.png b/docs/images/integrations-railway-create.png new file mode 100644 index 000000000..548c06c5e Binary files /dev/null and b/docs/images/integrations-railway-create.png differ diff --git a/docs/images/integrations-railway-dashboard.png b/docs/images/integrations-railway-dashboard.png new file mode 100644 index 000000000..0d6b66754 Binary files /dev/null and b/docs/images/integrations-railway-dashboard.png differ diff --git a/docs/images/integrations-railway-token.png b/docs/images/integrations-railway-token.png new file mode 100644 index 000000000..d414289e9 Binary files /dev/null and b/docs/images/integrations-railway-token.png differ diff --git a/docs/images/integrations-railway.png b/docs/images/integrations-railway.png new file mode 100644 index 000000000..0bde672d2 Binary files /dev/null and b/docs/images/integrations-railway.png differ diff --git a/docs/images/integrations-supabase-authorization.png b/docs/images/integrations-supabase-authorization.png new file mode 100644 index 000000000..d6daab30c Binary files /dev/null and b/docs/images/integrations-supabase-authorization.png differ diff --git a/docs/images/integrations-supabase-create.png b/docs/images/integrations-supabase-create.png new file mode 100644 index 000000000..b6c2cd35d Binary files /dev/null and b/docs/images/integrations-supabase-create.png differ diff --git a/docs/images/integrations-supabase-dashboard.png b/docs/images/integrations-supabase-dashboard.png new file mode 100644 index 000000000..4908cfc5a Binary files /dev/null and b/docs/images/integrations-supabase-dashboard.png differ diff --git a/docs/images/integrations-supabase-token.png b/docs/images/integrations-supabase-token.png new file mode 100644 index 000000000..00a3c0b20 Binary files /dev/null and b/docs/images/integrations-supabase-token.png differ diff --git a/docs/images/integrations-supabase.png b/docs/images/integrations-supabase.png new file mode 100644 index 000000000..60808a232 Binary files /dev/null and b/docs/images/integrations-supabase.png differ diff --git a/docs/images/integrations.png b/docs/images/integrations.png index 44a8269dd..fc6593143 100644 Binary files a/docs/images/integrations.png and b/docs/images/integrations.png differ diff --git a/docs/images/organization-ic.png b/docs/images/organization-ic.png index d94f4061a..e45ad2d82 100644 Binary files a/docs/images/organization-ic.png and b/docs/images/organization-ic.png differ diff --git a/docs/images/organization-members.png b/docs/images/organization-members.png index 321808d51..90344b698 100644 Binary files a/docs/images/organization-members.png and b/docs/images/organization-members.png differ diff --git a/docs/images/organization-service-accounts.png b/docs/images/organization-service-accounts.png new file mode 100644 index 000000000..f48e848d3 Binary files /dev/null and b/docs/images/organization-service-accounts.png differ diff --git a/docs/images/organization.png b/docs/images/organization.png index 473c31db0..488d6c4cf 100644 Binary files a/docs/images/organization.png and b/docs/images/organization.png differ diff --git a/docs/images/pit-commits.png b/docs/images/pit-commits.png index 19cfa4976..22599311a 100644 Binary files a/docs/images/pit-commits.png and b/docs/images/pit-commits.png differ diff --git a/docs/images/pit-snapshot.png b/docs/images/pit-snapshot.png index 7e790e875..b7e913108 100644 Binary files a/docs/images/pit-snapshot.png and b/docs/images/pit-snapshot.png differ diff --git a/docs/images/pit-snapshots.png b/docs/images/pit-snapshots.png index f22231648..aa1cd52d3 100644 Binary files a/docs/images/pit-snapshots.png and b/docs/images/pit-snapshots.png differ diff --git a/docs/images/project-download.png b/docs/images/project-download.png index 8750b6cd3..00b02d9bf 100644 Binary files a/docs/images/project-download.png and b/docs/images/project-download.png differ diff --git a/docs/images/project-drag-drop.png b/docs/images/project-drag-drop.png index a283faec6..7ae0ed718 100644 Binary files a/docs/images/project-drag-drop.png and b/docs/images/project-drag-drop.png differ diff --git a/docs/images/project-envar-override.png b/docs/images/project-envar-override.png new file mode 100644 index 000000000..d9e01c8fd Binary files /dev/null and b/docs/images/project-envar-override.png differ diff --git a/docs/images/project-envar-toggle-open.png b/docs/images/project-envar-toggle-open.png deleted file mode 100644 index 297299454..000000000 Binary files a/docs/images/project-envar-toggle-open.png and /dev/null differ diff --git a/docs/images/project-environment.png b/docs/images/project-environment.png index 5b316511c..e2e0d2959 100644 Binary files a/docs/images/project-environment.png and b/docs/images/project-environment.png differ diff --git a/docs/images/project-hide.png b/docs/images/project-hide.png index 69fdb13f0..61aed0bee 100644 Binary files a/docs/images/project-hide.png and b/docs/images/project-hide.png differ diff --git a/docs/images/project-quickstart.png b/docs/images/project-quickstart.png index 6d8d7f664..9a9660d1d 100644 Binary files a/docs/images/project-quickstart.png and b/docs/images/project-quickstart.png differ diff --git a/docs/images/project-search.png b/docs/images/project-search.png index 7388b34ce..bafc0b2c3 100644 Binary files a/docs/images/project-search.png and b/docs/images/project-search.png differ diff --git a/docs/images/project-sort.png b/docs/images/project-sort.png deleted file mode 100644 index 134adc8d5..000000000 Binary files a/docs/images/project-sort.png and /dev/null differ diff --git a/docs/images/secret-versioning.png b/docs/images/secret-versioning.png index ec1734289..96dbfde41 100644 Binary files a/docs/images/secret-versioning.png and b/docs/images/secret-versioning.png differ diff --git a/docs/integrations/cicd/circleci.mdx b/docs/integrations/cicd/circleci.mdx index 7b9298f4b..58b8ae3ff 100644 --- a/docs/integrations/cicd/circleci.mdx +++ b/docs/integrations/cicd/circleci.mdx @@ -1,6 +1,6 @@ --- title: "CircleCI" -description: "How to automatically sync secrets from Infisical into your CircleCI project." +description: "How to sync secrets from Infisical to CircleCI" --- Prerequisites: diff --git a/docs/integrations/cicd/githubactions.mdx b/docs/integrations/cicd/githubactions.mdx index 8f93d8162..5ecf68d03 100644 --- a/docs/integrations/cicd/githubactions.mdx +++ b/docs/integrations/cicd/githubactions.mdx @@ -1,6 +1,6 @@ --- title: "GitHub Actions" -description: "How to automatically sync secrets from Infisical into your GitHub Actions." +description: "How to sync secrets from Infisical to GitHub Actions" --- diff --git a/docs/integrations/cicd/gitlab.mdx b/docs/integrations/cicd/gitlab.mdx index f52afe99f..8a5432272 100644 --- a/docs/integrations/cicd/gitlab.mdx +++ b/docs/integrations/cicd/gitlab.mdx @@ -1,6 +1,6 @@ --- title: "GitLab" -description: "How to automatically sync secrets from Infisical into GitLab." +description: "How to sync secrets from Infisical to GitLab" --- Prerequisites: diff --git a/docs/integrations/cicd/travisci.mdx b/docs/integrations/cicd/travisci.mdx index 5509ffc7a..79737a2d8 100644 --- a/docs/integrations/cicd/travisci.mdx +++ b/docs/integrations/cicd/travisci.mdx @@ -1,6 +1,6 @@ --- title: "Travis CI" -description: "How to automatically sync secrets from Infisical to your Travis CI repository." +description: "How to sync secrets from Infisical to Travis CI" --- Prerequisites: diff --git a/docs/integrations/cloud/aws-parameter-store.mdx b/docs/integrations/cloud/aws-parameter-store.mdx index 1bb6c40c9..63fd1e1c6 100644 --- a/docs/integrations/cloud/aws-parameter-store.mdx +++ b/docs/integrations/cloud/aws-parameter-store.mdx @@ -1,6 +1,6 @@ --- title: "AWS Parameter Store" -description: "How to automatically sync secrets from Infisical to your AWS Parameter Store." +description: "How to sync secrets from Infisical to AWS Parameter Store" --- Prerequisites: diff --git a/docs/integrations/cloud/aws-secret-manager.mdx b/docs/integrations/cloud/aws-secret-manager.mdx index 9b282c527..a798ae820 100644 --- a/docs/integrations/cloud/aws-secret-manager.mdx +++ b/docs/integrations/cloud/aws-secret-manager.mdx @@ -1,6 +1,6 @@ --- title: "AWS Secret Manager" -description: "How to automatically sync secrets from Infisical to your AWS Secret Manager." +description: "How to sync secrets from Infisical to AWS Secret Manager" --- Prerequisites: diff --git a/docs/integrations/cloud/azure-key-vault.mdx b/docs/integrations/cloud/azure-key-vault.mdx index 3b23f4d52..90c630666 100644 --- a/docs/integrations/cloud/azure-key-vault.mdx +++ b/docs/integrations/cloud/azure-key-vault.mdx @@ -1,6 +1,6 @@ --- title: "Azure Key Vault" -description: "How to automatically sync secrets from Infisical into your Azure Key Vault." +description: "How to sync secrets from Infisical to Azure Key Vault" --- Prerequisites: diff --git a/docs/integrations/cloud/flyio.mdx b/docs/integrations/cloud/flyio.mdx index 2a1dd7e2c..283c54176 100644 --- a/docs/integrations/cloud/flyio.mdx +++ b/docs/integrations/cloud/flyio.mdx @@ -1,6 +1,6 @@ --- title: "Fly.io" -description: "How to automatically sync secrets from Infisical into your Fly.io project." +description: "How to sync secrets from Infisical to Fly.io" --- Prerequisites: @@ -11,7 +11,7 @@ Prerequisites: ![integrations](../../images/integrations.png) -## Authorize Infisical for Fly.io +## Enter your Fly.io Access Token Obtain a Fly.io access token in Access Tokens diff --git a/docs/integrations/cloud/heroku.mdx b/docs/integrations/cloud/heroku.mdx index 9d0c26789..f3c58d27e 100644 --- a/docs/integrations/cloud/heroku.mdx +++ b/docs/integrations/cloud/heroku.mdx @@ -1,6 +1,6 @@ --- title: "Heroku" -description: "How to automatically sync secrets from Infisical into your Heroku project." +description: "How to sync secrets from Infisical to Heroku" --- Prerequisites: diff --git a/docs/integrations/cloud/netlify.mdx b/docs/integrations/cloud/netlify.mdx index b6dc36ba0..acd037417 100644 --- a/docs/integrations/cloud/netlify.mdx +++ b/docs/integrations/cloud/netlify.mdx @@ -1,6 +1,6 @@ --- title: "Netlify" -description: "How to automatically sync secrets from Infisical into your Netlify project." +description: "How to sync secrets from Infisical to Netlify" --- diff --git a/docs/integrations/cloud/railway.mdx b/docs/integrations/cloud/railway.mdx new file mode 100644 index 000000000..e19cfb626 --- /dev/null +++ b/docs/integrations/cloud/railway.mdx @@ -0,0 +1,54 @@ +--- +title: "Railway" +description: "How to sync secrets from Infisical to Railway" +--- + +Prerequisites: + +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Enter your Railway API Token + +Obtain a Railway API Token in your Railway [Account Settings > Tokens](https://railway.app/account/tokens). + +![integrations railway dashboard](../../images/integrations-railway-dashboard.png) +![integrations railway token](../../images/integrations-railway-token.png) + + + If this is your first time creating a Railway API token, then you'll be prompted to join + Railway's Private Boarding Beta program on the Railway Account Settings > Tokens page. + + Note that Railway project tokens will not work for this integration since they don't work with + Railway's Public API. + + +Press on the Railway tile and input your Railway API Key to grant Infisical access to your Railway account. + +![integrations railway authorization](../../images/integrations-railway-authorization.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 which Railway project and environment (and optionally service). Lastly, press create integration to start syncing secrets to Railway. + +![integrations create railway](../../images/integrations-railway-create.png) + + + Infisical integrates with both Railway's [shared variables](https://blog.railway.app/p/shared-variables-release) at the project environment level as well as service variables at the service level. + + To sync secrets to a specific service in a project, you can select a service from the Railway Service dropdown; otherwise, leaving it empty will sync secrets to the shared variables of that project. + + +![integrations railway](../../images/integrations-railway.png) + diff --git a/docs/integrations/cloud/render.mdx b/docs/integrations/cloud/render.mdx index 95890e92a..290e39012 100644 --- a/docs/integrations/cloud/render.mdx +++ b/docs/integrations/cloud/render.mdx @@ -1,6 +1,6 @@ --- title: "Render" -description: "How to automatically sync secrets from Infisical into your Render project." +description: "How to sync secrets from Infisical to Render" --- Prerequisites: diff --git a/docs/integrations/cloud/supabase.mdx b/docs/integrations/cloud/supabase.mdx new file mode 100644 index 000000000..14c787f75 --- /dev/null +++ b/docs/integrations/cloud/supabase.mdx @@ -0,0 +1,44 @@ +--- +title: "Supabase" +description: "How to sync secrets from Infisical to Supabase" +--- + + + The Supabase integration is useful if your Supabase project uses sensitive-information such as [environment variables in edge functions](https://supabase.com/docs/guides/functions/secrets). + + Synced envars can be accessed in edge functions using Deno's built-in handler: `Deno.env.get(MY_SECRET_NAME)`. + + +Prerequisites: + +- Have an account and project set up at [Supabase](https://supabase.com/) +- Set up and add envars to [Infisical Cloud](https://app.infisical.com) + +## Navigate to your project's integrations tab + +![integrations](../../images/integrations.png) + +## Enter your Supabase Access Token + +Obtain a Supabase Access Token in your Supabase [Account > Access Tokens](https://app.supabase.com/account/tokens). +![integrations supabase dashboard](../../images/integrations-supabase-dashboard.png) +![integrations supabase token](../../images/integrations-supabase-token.png) + +Press on the Supabase tile and input your Supabase Access Token to grant Infisical access to your Supabase account. + +![integrations supabase authorization](../../images/integrations-supabase-authorization.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 which Supabase project. Lastly, press create integration to start syncing secrets to Supabase. + +![integrations supabase create](../../images/integrations-supabase-create.png) + +![integrations supabase](../../images/integrations-supabase.png) \ No newline at end of file diff --git a/docs/integrations/cloud/vercel.mdx b/docs/integrations/cloud/vercel.mdx index af20c60cf..fb265ec98 100644 --- a/docs/integrations/cloud/vercel.mdx +++ b/docs/integrations/cloud/vercel.mdx @@ -1,6 +1,6 @@ --- title: "Vercel" -description: "How to automatically sync secrets from Infisical into your Vercel project." +description: "How to sync secrets from Infisical to Vercel" --- Prerequisites: diff --git a/docs/integrations/overview.mdx b/docs/integrations/overview.mdx index 15e5698c1..dcbdf44b8 100644 --- a/docs/integrations/overview.mdx +++ b/docs/integrations/overview.mdx @@ -18,7 +18,9 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [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 | | [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 | @@ -41,7 +43,5 @@ Missing an integration? Throw in a [request](https://github.com/Infisical/infisi | [Flask](/integrations/frameworks/flask) | Framework | Available | | [Laravel](/integrations/frameworks/laravel) | Framework | Available | | [Ruby on Rails](/integrations/frameworks/rails) | Framework | Available | -| GCP | Cloud | Coming soon | -| DigitalOcean | Cloud | Coming soon | -| GitHub Actions | CI/CD | Coming soon | +| GCP Secret Manager | Cloud | Coming soon | | Jenkins | CI/CD | Coming soon | diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 959a4fce2..5ea3967d9 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -1,6 +1,6 @@ --- title: 'Kubernetes' -description: "This page explains how to use Infisical to inject secrets into Kubernetes clusters." +description: "How to use Infisical to inject secrets into Kubernetes clusters." --- ![title](../../images/k8-diagram.png) diff --git a/docs/mint.json b/docs/mint.json index 48dc1805c..850809a4f 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -149,7 +149,9 @@ "integrations/cloud/vercel", "integrations/cloud/netlify", "integrations/cloud/render", + "integrations/cloud/railway", "integrations/cloud/flyio", + "integrations/cloud/supabase", "integrations/cloud/azure-key-vault", "integrations/cicd/githubactions", "integrations/cicd/gitlab", diff --git a/docs/self-hosting/overview.mdx b/docs/self-hosting/overview.mdx index f149f9663..8acdad974 100644 --- a/docs/self-hosting/overview.mdx +++ b/docs/self-hosting/overview.mdx @@ -17,7 +17,7 @@ Self-hosted Infisical allows you to maintain your sensitive information within y - 1 DocumentDB instance - Security groups - + @@ -28,6 +28,7 @@ Self-hosted Infisical allows you to maintain your sensitive information within y This deployment option is highly available + **Prerequisites** - You have understanding of [Kubernetes](https://kubernetes.io/) @@ -35,101 +36,7 @@ Self-hosted Infisical allows you to maintain your sensitive information within y - You have [kubectl](https://kubernetes.io/docs/reference/kubectl/kubectl/) installed and connected to your kubernetes cluster - #### 1. Fill our environment variables - - Before you can deploy the Helm chart, you must fill out the required environment variables. To do so, please copy the below file to a `.yaml` file. - Refer to the available [environment variables](/self-hosting/configuration/envars) to learn more - - - [View all available Helm chart values parameters](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical) - ```yaml - frontend: - enabled: true - name: frontend - podAnnotations: {} - deploymentAnnotations: {} - replicaCount: 2 - image: - repository: infisical/frontend - tag: "latest" - pullPolicy: IfNotPresent - kubeSecretRef: "" - service: - annotations: {} - type: ClusterIP - nodePort: "" - - frontendEnvironmentVariables: - SITE_URL: infisical.local - - backend: - enabled: true - name: backend - podAnnotations: {} - deploymentAnnotations: {} - replicaCount: 2 - image: - repository: infisical/backend - tag: "latest" - pullPolicy: IfNotPresent - kubeSecretRef: "" - service: - annotations: {} - type: ClusterIP - nodePort: "" - - backendEnvironmentVariables: - ENCRYPTION_KEY: MUST_REPLACE - JWT_SIGNUP_SECRET: MUST_REPLACE - JWT_REFRESH_SECRET: MUST_REPLACE - JWT_AUTH_SECRET: MUST_REPLACE - JWT_SERVICE_SECRET: MUST_REPLACE - SMTP_HOST: MUST_REPLACE - SMTP_PORT: 587 - SMTP_SECURE: false - SMTP_FROM_NAME: Infisical - SMTP_FROM_ADDRESS: MUST_REPLACE - SMTP_USERNAME: MUST_REPLACE - SMTP_PASSWORD: MUST_REPLACE - SITE_URL: infisical.local - - ## Mongo DB persistence - mongodb: - enabled: true - - ## By default the backend will be connected to a Mongo instance within the cluster - ## However, it is recommended to add a managed document DB connection string for production-use (DBaaS) - ## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/ - ## e.g. "mongodb://:@:/" - mongodbConnection: - externalMongoDBConnectionString: "" - - ingress: - enabled: true - annotations: - kubernetes.io/ingress.class: "nginx" - # cert-manager.io/issuer: letsencrypt-nginx - hostName: infisical.local ## <- Replace with your own domain - frontend: - path: / - pathType: Prefix - backend: - path: /api - pathType: Prefix - tls: [] - # - secretName: letsencrypt-nginx - # hosts: - # - infisical.local - - mailhog: - enabled: false - ``` - - - Once you have a local copy of the values file, fill our the required environment variables and save the file. - - - #### 2. Install Infisical Helm repository + #### 1. Install Infisical Helm repository ```bash helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' @@ -137,23 +44,22 @@ Self-hosted Infisical allows you to maintain your sensitive information within y helm repo update ``` - #### 3. Install the Helm chart + #### 2. Install the Helm chart By default, the helm chart will be installed on your default namespace. If you wish to install the Chart on a different namespace, you may specify that by adding the `--namespace ` to your `helm install` command. ```bash ## Installs to default namespace - helm install infisical-helm-charts/infisical --generate-name --values + helm install infisical-helm-charts/infisical --generate-name ``` - - If you have not filled out all of the required environment variables, you will see an error message prompting you to - do so. - - - #### 4. Your Infisical installation is complete and should be running on the host name you specified in Ingress in `values.yaml`. + #### 3. Access Infisical + Allow 3-5 minutes for the deployment to complete. Once done, you should now be able to access Infisical on the IP address exposed via Ingress on your load balancer. If you are not sure what the IP address is run `kubectl get ingress` to view the external IP address exposing Infisical. + #### Custom configuration + To configure environment variables, database and deployments, you'll need to set the parameters in a `values.yaml` file. To view all available parameters [visit here](https://github.com/Infisical/infisical/tree/main/helm-charts/infisical#parameters) + 1. Install Docker on your VM diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ad099a70d..384f3a31d 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -24,11 +24,13 @@ "@radix-ui/react-hover-card": "^1.0.3", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.3", + "@radix-ui/react-popper": "^1.1.1", "@radix-ui/react-progress": "^1.0.1", "@radix-ui/react-select": "^1.2.0", "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.4", "@reduxjs/toolkit": "^1.8.3", "@stripe/react-stripe-js": "^1.16.3", "@stripe/stripe-js": "^1.46.0", @@ -3310,12 +3312,25 @@ } }, "node_modules/@radix-ui/react-arrow": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", - "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", "dependencies": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.1" + "@radix-ui/react-primitive": "1.0.2" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-arrow/node_modules/@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" }, "peerDependencies": { "react": "^16.8 || ^17.0 || ^18.0", @@ -3520,6 +3535,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-hover-card/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-id": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.0.tgz", @@ -3575,6 +3625,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-menu/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-popover": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.0.3.tgz", @@ -3602,7 +3687,20 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, - "node_modules/@radix-ui/react-popper": { + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-popper": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", @@ -3624,6 +3722,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-popper": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.2", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.2", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-portal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz", @@ -3732,6 +3865,41 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-select/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-slot": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.1.tgz", @@ -3807,6 +3975,65 @@ "react-dom": "^16.8 || ^17.0 || ^18.0" } }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.4.tgz", + "integrity": "sha512-2h0BvJipRSv7nszzOCdAqb8fH7+Igz24GV1M4aCb/3UPfeMQ+clb1UM5/z2msW3Khir795OCQw2G74YGxjueWw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.0", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "@radix-ui/react-visually-hidden": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + }, + "peerDependencies": { + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", @@ -24530,12 +24757,23 @@ } }, "@radix-ui/react-arrow": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", - "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.2.tgz", + "integrity": "sha512-fqYwhhI9IarZ0ll2cUSfKuXHlJK0qE4AfnRrPBbRwEH/4mGQn04/QFGomLi8TXWIdv9WJk//KgGm+aDxVIr1wA==", "requires": { "@babel/runtime": "^7.13.10", - "@radix-ui/react-primitive": "1.0.1" + "@radix-ui/react-primitive": "1.0.2" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + } } }, "@radix-ui/react-checkbox": { @@ -24690,6 +24928,35 @@ "@radix-ui/react-presence": "1.0.0", "@radix-ui/react-primitive": "1.0.1", "@radix-ui/react-use-controllable-state": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-id": { @@ -24734,6 +25001,35 @@ "@radix-ui/react-use-callback-ref": "1.0.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-popover": { @@ -24757,24 +25053,64 @@ "@radix-ui/react-use-controllable-state": "1.0.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-popper": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", - "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.1.tgz", + "integrity": "sha512-keYDcdMPNMjSC8zTsZ8wezUMiWM9Yj14wtF3s0PTIs9srnEPC9Kt2Gny1T3T81mmSeyDjZxsD9N5WCwNNb712w==", "requires": { "@babel/runtime": "^7.13.10", "@floating-ui/react-dom": "0.7.2", - "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-arrow": "1.0.2", "@radix-ui/react-compose-refs": "1.0.0", "@radix-ui/react-context": "1.0.0", - "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-primitive": "1.0.2", "@radix-ui/react-use-callback-ref": "1.0.0", "@radix-ui/react-use-layout-effect": "1.0.0", "@radix-ui/react-use-rect": "1.0.0", "@radix-ui/react-use-size": "1.0.0", "@radix-ui/rect": "1.0.0" + }, + "dependencies": { + "@radix-ui/react-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz", + "integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.1" + } + } } }, "@radix-ui/react-portal": { @@ -24859,6 +25195,35 @@ "@radix-ui/react-visually-hidden": "1.0.1", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.5" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } } }, "@radix-ui/react-slot": { @@ -24921,6 +25286,55 @@ "@radix-ui/react-visually-hidden": "1.0.1" } }, + "@radix-ui/react-tooltip": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.0.4.tgz", + "integrity": "sha512-2h0BvJipRSv7nszzOCdAqb8fH7+Igz24GV1M4aCb/3UPfeMQ+clb1UM5/z2msW3Khir795OCQw2G74YGxjueWw==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.0", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-dismissable-layer": "1.0.2", + "@radix-ui/react-id": "1.0.0", + "@radix-ui/react-popper": "1.1.0", + "@radix-ui/react-portal": "1.0.1", + "@radix-ui/react-presence": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-slot": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.0", + "@radix-ui/react-visually-hidden": "1.0.1" + }, + "dependencies": { + "@radix-ui/react-arrow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.0.1.tgz", + "integrity": "sha512-1yientwXqXcErDHEv8av9ZVNEBldH8L9scVR3is20lL+jOCfcJyMFZFEY5cgIrgexsq1qggSXqiEL/d/4f+QXA==", + "requires": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.1" + } + }, + "@radix-ui/react-popper": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.1.0.tgz", + "integrity": "sha512-07U7jpI0dZcLRAxT7L9qs6HecSoPhDSJybF7mEGHJDBDv+ZoGCvIlva0s+WxMXwJEav+ckX3hAlXBtnHmuvlCQ==", + "requires": { + "@babel/runtime": "^7.13.10", + "@floating-ui/react-dom": "0.7.2", + "@radix-ui/react-arrow": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.0", + "@radix-ui/react-context": "1.0.0", + "@radix-ui/react-primitive": "1.0.1", + "@radix-ui/react-use-callback-ref": "1.0.0", + "@radix-ui/react-use-layout-effect": "1.0.0", + "@radix-ui/react-use-rect": "1.0.0", + "@radix-ui/react-use-size": "1.0.0", + "@radix-ui/rect": "1.0.0" + } + } + } + }, "@radix-ui/react-use-callback-ref": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index d53e40ca7..0e1d3a907 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -31,11 +31,13 @@ "@radix-ui/react-hover-card": "^1.0.3", "@radix-ui/react-label": "^2.0.0", "@radix-ui/react-popover": "^1.0.3", + "@radix-ui/react-popper": "^1.1.1", "@radix-ui/react-progress": "^1.0.1", "@radix-ui/react-select": "^1.2.0", "@radix-ui/react-switch": "^1.0.1", "@radix-ui/react-tabs": "^1.0.2", "@radix-ui/react-toast": "^1.1.2", + "@radix-ui/react-tooltip": "^1.0.4", "@reduxjs/toolkit": "^1.8.3", "@stripe/react-stripe-js": "^1.16.3", "@stripe/stripe-js": "^1.46.0", diff --git a/frontend/public/data/frequentConstants.ts b/frontend/public/data/frequentConstants.ts index 8a69465ac..40c88f65b 100644 --- a/frontend/public/data/frequentConstants.ts +++ b/frontend/public/data/frequentConstants.ts @@ -12,9 +12,11 @@ const integrationSlugNameMapping: Mapping = { 'github': 'GitHub', 'gitlab': 'GitLab', 'render': 'Render', + 'railway': 'Railway', 'flyio': 'Fly.io', 'circleci': 'CircleCI', - 'travisci': 'TravisCI' + 'travisci': 'TravisCI', + 'supabase': 'Supabase' } const envMapping: Mapping = { diff --git a/frontend/public/images/integrations/Railway.png b/frontend/public/images/integrations/Railway.png new file mode 100644 index 000000000..61d53cf62 Binary files /dev/null and b/frontend/public/images/integrations/Railway.png differ diff --git a/frontend/public/images/integrations/Supabase.png b/frontend/public/images/integrations/Supabase.png new file mode 100644 index 000000000..843be59ca Binary files /dev/null and b/frontend/public/images/integrations/Supabase.png differ diff --git a/frontend/src/components/analytics/posthog.ts b/frontend/src/components/analytics/posthog.ts index 29208a795..b6f3e4341 100644 --- a/frontend/src/components/analytics/posthog.ts +++ b/frontend/src/components/analytics/posthog.ts @@ -6,7 +6,7 @@ import { ENV, POSTHOG_API_KEY, POSTHOG_HOST } from '../utilities/config'; export const initPostHog = () => { // @ts-ignore - console.log("Init Infisical") + console.log("Hi there ๐Ÿ‘‹") try { if (typeof window !== 'undefined') { // @ts-ignore diff --git a/frontend/src/components/basic/Layout.tsx b/frontend/src/components/basic/Layout.tsx index e76b99cc3..4063df46b 100644 --- a/frontend/src/components/basic/Layout.tsx +++ b/frontend/src/components/basic/Layout.tsx @@ -176,6 +176,7 @@ const Layout = ({ children }: LayoutProps) => { useEffect(() => { // Put a user in a workspace if they're not in one yet + const putUserInWorkSpace = async () => { if (tempLocalStorage('orgData.id') === '') { const userOrgs = await getOrganizations(); diff --git a/frontend/src/components/integrations/Integration.tsx b/frontend/src/components/integrations/Integration.tsx index e01c31e2b..2467566c4 100644 --- a/frontend/src/components/integrations/Integration.tsx +++ b/frontend/src/components/integrations/Integration.tsx @@ -45,6 +45,7 @@ type Props = { handleDeleteIntegration: (args: { integration: Integration }) => void; }; +// TODO: refactor const IntegrationTile = ({ integration, integrations, @@ -55,7 +56,6 @@ const IntegrationTile = ({ handleDeleteIntegration }: Props) => { - // set initial environment. This find will only execute when component is mounting const [integrationEnvironment, setIntegrationEnvironment] = useState( environments.find(({ slug }) => slug === integration?.environment) || { name: '', @@ -176,6 +176,21 @@ const IntegrationTile = ({ /> ); + case 'railway': + return ( +
+
ENVIRONMENT
+ +
+ ); default: return
; } diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index 57a3f96fd..c8af1ce46 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -1,41 +1,89 @@ +import { useRouter } from 'next/router'; import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useOrganization, useWorkspace } from '@app/context'; +import { Select, SelectItem, Tooltip } from '../v2'; + +// TODO: make links clickable and clean up + /** * This is the component at the top of almost every page. * It shows how to navigate to a certain page. * It future these links should also be clickable and hoverable - * @param obj - * @param obj.pageName - Name of the page - * @param obj.isProjectRelated - whether this page is related to project or now (determine if it's 2 or 3 navigation steps) + * @param {object} obj + * @param {string} obj.pageName - Name of the page + * @param {boolean} obj.isProjectRelated - whether or not this page is related to project (determine if it's 2 or 3 navigation steps) + * @param {boolean} obj.isOrganizationRelated - whether or not this page is related to organization (determine if it's 2 or 3 navigation steps) + * @param {string} obj.currentEnv - current environment inside a project + * @param {string} obj.userAvailableEnvs - environments that are available to a user in this project (used for the dropdown) + * @param {string} obj.onEnvChange - the action that happens when an env is changed * @returns */ export default function NavHeader({ pageName, - isProjectRelated + isProjectRelated, + isOrganizationRelated, + currentEnv, + userAvailableEnvs, + onEnvChange }: { pageName: string; isProjectRelated?: boolean; + isOrganizationRelated?: boolean; + currentEnv?: string; + userAvailableEnvs?: any[]; + onEnvChange?: (slug: string) => void; }): JSX.Element { const { currentWorkspace } = useWorkspace(); const { currentOrg } = useOrganization(); + const router = useRouter() return (
{currentOrg?.name?.charAt(0)}
-
{currentOrg?.name}
+
{currentOrg?.name}
{isProjectRelated && ( <> - -
{currentWorkspace?.name}
+ +
{currentWorkspace?.name}
+ + )} + {isOrganizationRelated && ( + <> + +
Organization Settings
)} -
{pageName}
+ {pageName === 'Secrets' + ? {pageName} + :
{pageName}
} + {currentEnv && + <> + +
+ + + +
+ }
); } diff --git a/frontend/src/components/utilities/cryptography/crypto.ts b/frontend/src/components/utilities/cryptography/crypto.ts index 961dee83f..830306d5a 100644 --- a/frontend/src/components/utilities/cryptography/crypto.ts +++ b/frontend/src/components/utilities/cryptography/crypto.ts @@ -5,12 +5,52 @@ import aes from './aes-256-gcm'; const nacl = require('tweetnacl'); nacl.util = require('tweetnacl-util'); +/** + * Return new base64, NaCl, public-private key pair. + * @returns {Object} obj + * @returns {String} obj.publicKey - base64, NaCl, public key + * @returns {String} obj.privateKey - base64, NaCl, private key + */ +const generateKeyPair = () => { + const pair = nacl.box.keyPair(); + + return ({ + publicKey: nacl.util.encodeBase64(pair.publicKey), + privateKey: nacl.util.encodeBase64(pair.secretKey) + }); +} + type EncryptAsymmetricProps = { plaintext: string; publicKey: string; privateKey: string; }; +/** + * Verify that private key [privateKey] is the one that corresponds to + * the public key [publicKey] + * @param {Object} + * @param {String} - base64-encoded Nacl private key + * @param {String} - base64-encoded Nacl public key + */ +const verifyPrivateKey = ({ + privateKey, + publicKey +}: { + privateKey: string; + publicKey: string; +}) => { + const derivedPublicKey = nacl.util.encodeBase64( + nacl.box.keyPair.fromSecretKey( + nacl.util.decodeBase64(privateKey) + ).publicKey + ); + + if (derivedPublicKey !== publicKey) { + throw new Error('Failed to verify private key'); + } +} + /** * Derive a key from password [password] and salt [salt] using Argon2id * @param {Object} obj @@ -20,7 +60,7 @@ type EncryptAsymmetricProps = { * @param {Number} obj.time - number of iterations * @param {Number} obj.parallelism - desired parallelism * @param {Number} obj.hashLen - desired hash length (i.e. byte-length of derived key) - * @returns + * @returns */ const deriveArgonKey = async ({ password, @@ -39,8 +79,8 @@ const deriveArgonKey = async ({ }) => { let derivedKey; try { - derivedKey = await argon2.hash({ - pass: password, + derivedKey = await argon2.hash({ + pass: password, salt, type: argon2.ArgonType.Argon2id, mem, @@ -53,7 +93,7 @@ const deriveArgonKey = async ({ } return derivedKey; -} +}; /** * Return assymmetrically encrypted [plaintext] using [publicKey] where @@ -173,6 +213,7 @@ type DecryptSymmetricProps = { * */ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): string => { + if (!ciphertext) return ''; let plaintext; try { plaintext = aes.decrypt({ ciphertext, iv, tag, secret: key }); @@ -184,10 +225,11 @@ const decryptSymmetric = ({ ciphertext, iv, tag, key }: DecryptSymmetricProps): return plaintext; }; -export { - decryptAssymmetric, - decryptSymmetric, +export { + decryptAssymmetric, + decryptSymmetric, deriveArgonKey, encryptAssymmetric, - encryptSymmetric -}; + encryptSymmetric, + generateKeyPair, + verifyPrivateKey}; diff --git a/frontend/src/components/v2/Button/Button.tsx b/frontend/src/components/v2/Button/Button.tsx index 8c6bf91b2..17ae7b867 100644 --- a/frontend/src/components/v2/Button/Button.tsx +++ b/frontend/src/components/v2/Button/Button.tsx @@ -26,17 +26,20 @@ const buttonVariants = cva( colorSchema: { primary: ['bg-primary', 'text-black', 'border-primary bg-opacity-80 hover:bg-opacity-100'], secondary: ['bg-mineshaft', 'text-gray-300', 'border-mineshaft hover:bg-opacity-80'], - danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90'] + danger: ['bg-red', 'text-white', 'border-red hover:bg-opacity-90'], + gray: ['bg-bunker-500', 'text-bunker-200'] }, variant: { solid: '', outline: ['bg-transparent', 'border-2', 'border-solid'], plain: '', + selected: '', + outline_bg: '', // a constant color not in use on hover or click goes colorSchema color star: 'text-bunker-200 bg-mineshaft-500' }, isDisabled: { - true: 'bg-mineshaft opacity-40 cursor-not-allowed', + true: 'bg-mineshaft text-white opacity-50 cursor-not-allowed', false: '' }, isFullWidth: { @@ -60,6 +63,21 @@ const buttonVariants = cva( variant: 'star', className: 'hover:bg-primary hover:text-black' }, + { + colorSchema: 'primary', + variant: 'selected', + className: 'bg-primary/10 border border-primary/50 text-bunker-200' + }, + { + colorSchema: 'primary', + variant: 'outline_bg', + className: 'bg-mineshaft-800 border border-mineshaft-600 hover:bg-primary/[0.15] hover:border-primary/60 text-bunker-200' + }, + { + colorSchema: 'secondary', + variant: 'star', + className: 'bg-mineshaft-700 border border-mineshaft-600 hover:bg-mineshaft hover:text-white' + }, { colorSchema: 'danger', variant: 'star', @@ -85,6 +103,11 @@ const buttonVariants = cva( variant: 'plain', className: 'text-primary' }, + { + colorSchema: 'gray', + variant: 'plain', + className: 'bg-transparent text-bunker-200' + }, { colorSchema: 'secondary', variant: 'plain', @@ -155,26 +178,26 @@ export const Button = forwardRef( className="absolute rounded-xl" /> )} - {leftIcon} - - {children} - + {children} +
{rightIcon} - +
); } diff --git a/frontend/src/components/v2/Card/Card.tsx b/frontend/src/components/v2/Card/Card.tsx index 095018614..0f4a659cf 100644 --- a/frontend/src/components/v2/Card/Card.tsx +++ b/frontend/src/components/v2/Card/Card.tsx @@ -25,7 +25,7 @@ export type CardFooterProps = { }; export const CardFooter = ({ children, className }: CardFooterProps) => ( -
{children}
+
{children}
); export type CardBodyProps = { diff --git a/frontend/src/components/v2/Drawer/Drawer.stories.tsx b/frontend/src/components/v2/Drawer/Drawer.stories.tsx new file mode 100644 index 000000000..9925d8cae --- /dev/null +++ b/frontend/src/components/v2/Drawer/Drawer.stories.tsx @@ -0,0 +1,32 @@ +import type { Meta, StoryObj } from '@storybook/react'; + +import { Button } from '../Button'; +import { Drawer, DrawerContent, DrawerContentProps, DrawerTrigger } from './Drawer'; + +const meta: Meta = { + title: 'Components/Drawer', + component: Drawer, + tags: ['v2'], + argTypes: {} +}; + +export default meta; +type Story = StoryObj; + +const Template = (args: DrawerContentProps) => ( + + + + + Hello world + +); + +export const Basic: Story = { + render: (args) =>