mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 16:26:37 +00:00
Merge branch 'main' into fix/typo
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
backend/node_modules
|
||||||
|
frontend/node_modules
|
||||||
+13
-9
@@ -1,5 +1,6 @@
|
|||||||
# Keys
|
# Keys
|
||||||
# Required key for platform encryption/decryption ops
|
# Required key for platform encryption/decryption ops
|
||||||
|
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION
|
||||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
@@ -8,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
|
|||||||
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
|
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
|
||||||
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
|
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
|
||||||
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
|
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
|
||||||
|
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
|
||||||
|
|
||||||
# JWT lifetime
|
# JWT lifetime
|
||||||
# Optional lifetimes for JWT tokens expressed in seconds or a string
|
# Optional lifetimes for JWT tokens expressed in seconds or a string
|
||||||
@@ -15,6 +17,7 @@ JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
|
|||||||
JWT_AUTH_LIFETIME=
|
JWT_AUTH_LIFETIME=
|
||||||
JWT_REFRESH_LIFETIME=
|
JWT_REFRESH_LIFETIME=
|
||||||
JWT_SIGNUP_LIFETIME=
|
JWT_SIGNUP_LIFETIME=
|
||||||
|
JWT_PROVIDER_AUTH_LIFETIME=
|
||||||
|
|
||||||
# MongoDB
|
# MongoDB
|
||||||
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
|
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
|
||||||
@@ -30,14 +33,12 @@ MONGO_PASSWORD=example
|
|||||||
# Required
|
# Required
|
||||||
SITE_URL=http://localhost:8080
|
SITE_URL=http://localhost:8080
|
||||||
|
|
||||||
# Mail/SMTP
|
# Mail/SMTP
|
||||||
SMTP_HOST= # required
|
SMTP_HOST=
|
||||||
SMTP_USERNAME= # required
|
SMTP_PORT=
|
||||||
SMTP_PASSWORD= # required
|
SMTP_NAME=
|
||||||
SMTP_PORT=587
|
SMTP_USERNAME=
|
||||||
SMTP_SECURE=false
|
SMTP_PASSWORD=
|
||||||
SMTP_FROM_ADDRESS= # required
|
|
||||||
SMTP_FROM_NAME=Infisical
|
|
||||||
|
|
||||||
# Integration
|
# Integration
|
||||||
# Optional only if integration is used
|
# Optional only if integration is used
|
||||||
@@ -66,4 +67,7 @@ STRIPE_WEBHOOK_SECRET=
|
|||||||
STRIPE_PRODUCT_STARTER=
|
STRIPE_PRODUCT_STARTER=
|
||||||
STRIPE_PRODUCT_TEAM=
|
STRIPE_PRODUCT_TEAM=
|
||||||
STRIPE_PRODUCT_PRO=
|
STRIPE_PRODUCT_PRO=
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||||
|
|
||||||
|
CLIENT_ID_GOOGLE=
|
||||||
|
CLIENT_SECRET_GOOGLE=
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 19 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
@@ -13,6 +13,7 @@ services:
|
|||||||
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
|
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
|
||||||
- MONGO_USERNAME=test
|
- MONGO_USERNAME=test
|
||||||
- MONGO_PASSWORD=example
|
- MONGO_PASSWORD=example
|
||||||
|
- ENCRYPTION_KEY=a984ecdf82ec779e55dbcc21303a900f
|
||||||
networks:
|
networks:
|
||||||
- infisical-test
|
- infisical-test
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -6,7 +6,7 @@ frontend:
|
|||||||
secrets.infisical.com/auto-reload: "true"
|
secrets.infisical.com/auto-reload: "true"
|
||||||
replicaCount: 2
|
replicaCount: 2
|
||||||
image:
|
image:
|
||||||
repository: infisical/frontend
|
repository: infisical/staging_deployment_frontend
|
||||||
tag: "latest"
|
tag: "latest"
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
kubeSecretRef: managed-secret-frontend
|
kubeSecretRef: managed-secret-frontend
|
||||||
@@ -25,7 +25,7 @@ backend:
|
|||||||
secrets.infisical.com/auto-reload: "true"
|
secrets.infisical.com/auto-reload: "true"
|
||||||
replicaCount: 2
|
replicaCount: 2
|
||||||
image:
|
image:
|
||||||
repository: infisical/backend
|
repository: infisical/staging_deployment_backend
|
||||||
tag: "latest"
|
tag: "latest"
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
kubeSecretRef: managed-backend-secret
|
kubeSecretRef: managed-backend-secret
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
name: Release production images (frontend, backend)
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "infisical/v*.*.*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
backend-image:
|
||||||
|
name: Build backend image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: extract_version
|
||||||
|
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
|
run: npm ci --only-production
|
||||||
|
working-directory: backend
|
||||||
|
- name: 🧪 Run tests
|
||||||
|
run: npm run test:ci
|
||||||
|
working-directory: backend
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
- name: 📦 Build backend and export to Docker
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
load: true
|
||||||
|
context: backend
|
||||||
|
tags: infisical/backend:test
|
||||||
|
- name: ⏻ Spawn backend container and dependencies
|
||||||
|
run: |
|
||||||
|
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||||
|
- name: 🧪 Test backend image
|
||||||
|
run: |
|
||||||
|
./.github/resources/healthcheck.sh infisical-backend-test
|
||||||
|
- name: ⏻ Shut down backend container and dependencies
|
||||||
|
run: |
|
||||||
|
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||||
|
- name: 🏗️ Build backend and push
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
push: true
|
||||||
|
context: backend
|
||||||
|
tags: |
|
||||||
|
infisical/backend:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/backend:latest
|
||||||
|
infisical/backend:${{ steps.extract_version.outputs.version }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
|
frontend-image:
|
||||||
|
name: Build frontend image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Extract version from tag
|
||||||
|
id: extract_version
|
||||||
|
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
- name: 📦 Build frontend and export to Docker
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
load: true
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
project: 64mmf0n610
|
||||||
|
context: frontend
|
||||||
|
tags: infisical/frontend:test
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
- name: ⏻ Spawn frontend container
|
||||||
|
run: |
|
||||||
|
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
||||||
|
- name: 🧪 Test frontend image
|
||||||
|
run: |
|
||||||
|
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||||
|
- name: ⏻ Shut down frontend container
|
||||||
|
run: |
|
||||||
|
docker stop infisical-frontend-test
|
||||||
|
- name: 🏗️ Build frontend and push
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
push: true
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
context: frontend
|
||||||
|
tags: |
|
||||||
|
infisical/frontend:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/frontend:latest
|
||||||
|
infisical/frontend:${{ steps.extract_version.outputs.version }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
@@ -5,10 +5,15 @@ jobs:
|
|||||||
backend-image:
|
backend-image:
|
||||||
name: Build backend image
|
name: Build backend image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
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
|
- name: Save commit hashes for tag
|
||||||
id: commit
|
id: commit
|
||||||
uses: pr-mpt/actions-commit-hash@v2
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
@@ -45,14 +50,14 @@ jobs:
|
|||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
push: true
|
push: true
|
||||||
context: backend
|
context: backend
|
||||||
tags: infisical/backend:${{ steps.commit.outputs.short }},
|
tags: |
|
||||||
infisical/backend:latest
|
infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/staging_deployment_backend:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
frontend-image:
|
frontend-image:
|
||||||
name: Build frontend image
|
name: Build frontend image
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -75,12 +80,12 @@ jobs:
|
|||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
project: 64mmf0n610
|
project: 64mmf0n610
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:test
|
tags: infisical/staging_deployment_frontend:test
|
||||||
build-args: |
|
build-args: |
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
- name: ⏻ Spawn frontend container
|
- name: ⏻ Spawn frontend container
|
||||||
run: |
|
run: |
|
||||||
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
|
docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
|
||||||
- name: 🧪 Test frontend image
|
- name: 🧪 Test frontend image
|
||||||
run: |
|
run: |
|
||||||
./.github/resources/healthcheck.sh infisical-frontend-test
|
./.github/resources/healthcheck.sh infisical-frontend-test
|
||||||
@@ -94,8 +99,9 @@ jobs:
|
|||||||
push: true
|
push: true
|
||||||
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:${{ steps.commit.outputs.short }},
|
tags: |
|
||||||
infisical/frontend:latest
|
infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
|
||||||
|
infisical/staging_deployment_frontend:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
build-args: |
|
build-args: |
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
@@ -122,17 +128,17 @@ jobs:
|
|||||||
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||||
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
- 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
|
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
|
run: kubectl config set-context --current --namespace=gamma
|
||||||
- name: test kubectl
|
- name: test kubectl
|
||||||
run: kubectl get ingress
|
run: kubectl get ingress
|
||||||
- name: Download helm values to file and upgrade gamma deploy
|
- name: Download helm values to file and upgrade gamma deploy
|
||||||
run: |
|
run: |
|
||||||
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||||
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods
|
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait
|
||||||
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
||||||
echo "Helm upgrade failed"
|
echo "Helm upgrade failed"
|
||||||
exit 1
|
exit 1
|
||||||
else
|
else
|
||||||
echo "Helm upgrade was successful"
|
echo "Helm upgrade was successful"
|
||||||
fi
|
fi
|
||||||
@@ -13,6 +13,7 @@ jobs:
|
|||||||
check-be-pr:
|
check-be-pr:
|
||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
@@ -26,17 +27,17 @@ jobs:
|
|||||||
- name: 📦 Install dependencies
|
- name: 📦 Install dependencies
|
||||||
run: npm ci --only-production
|
run: npm ci --only-production
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
- name: 🧪 Run tests
|
# - name: 🧪 Run tests
|
||||||
run: npm run test:ci
|
# run: npm run test:ci
|
||||||
working-directory: backend
|
# working-directory: backend
|
||||||
- name: 📁 Upload test results
|
# - name: 📁 Upload test results
|
||||||
uses: actions/upload-artifact@v3
|
# uses: actions/upload-artifact@v3
|
||||||
if: always()
|
# if: always()
|
||||||
with:
|
# with:
|
||||||
name: be-test-results
|
# name: be-test-results
|
||||||
path: |
|
# path: |
|
||||||
./backend/reports
|
# ./backend/reports
|
||||||
./backend/coverage
|
# ./backend/coverage
|
||||||
- name: 🏗️ Run build
|
- name: 🏗️ Run build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
|
|||||||
@@ -2,40 +2,35 @@ name: Check Frontend Pull Request
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [ opened, synchronize ]
|
types: [opened, synchronize]
|
||||||
paths:
|
paths:
|
||||||
- 'frontend/**'
|
- "frontend/**"
|
||||||
- '!frontend/README.md'
|
- "!frontend/README.md"
|
||||||
- '!frontend/.*'
|
- "!frontend/.*"
|
||||||
- 'frontend/.eslintrc.js'
|
- "frontend/.eslintrc.js"
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
check-fe-pr:
|
check-fe-pr:
|
||||||
name: Check
|
name: Check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
-
|
- name: ☁️ Checkout source
|
||||||
name: ☁️ Checkout source
|
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
-
|
- name: 🔧 Setup Node 16
|
||||||
name: 🔧 Setup Node 16
|
|
||||||
uses: actions/setup-node@v3
|
uses: actions/setup-node@v3
|
||||||
with:
|
with:
|
||||||
node-version: '16'
|
node-version: "16"
|
||||||
cache: 'npm'
|
cache: "npm"
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
-
|
- name: 📦 Install dependencies
|
||||||
name: 📦 Install dependencies
|
|
||||||
run: npm ci --only-production --ignore-scripts
|
run: npm ci --only-production --ignore-scripts
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
# -
|
# -
|
||||||
# name: 🧪 Run tests
|
# name: 🧪 Run tests
|
||||||
# run: npm run test:ci
|
# run: npm run test:ci
|
||||||
# working-directory: frontend
|
# working-directory: frontend
|
||||||
-
|
- name: 🏗️ Run build
|
||||||
name: 🏗️ Run build
|
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: frontend
|
working-directory: frontend
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
name: Release standalone docker image
|
||||||
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
infisical-standalone:
|
||||||
|
name: Build infisical standalone image
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: 📦 Install dependencies to test all dependencies
|
||||||
|
run: npm ci --only-production
|
||||||
|
working-directory: backend
|
||||||
|
- uses: paulhatch/[email protected]
|
||||||
|
id: version
|
||||||
|
with:
|
||||||
|
# The prefix to use to identify tags
|
||||||
|
tag_prefix: "infisical-standalone/v"
|
||||||
|
# A string which, if present in a git commit, indicates that a change represents a
|
||||||
|
# major (breaking) change, supports regular expressions wrapped with '/'
|
||||||
|
major_pattern: "(MAJOR)"
|
||||||
|
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
|
||||||
|
minor_pattern: "(MINOR)"
|
||||||
|
# A string to determine the format of the version output
|
||||||
|
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
|
||||||
|
# Optional path to check for changes. If any changes are detected in the path the
|
||||||
|
# 'changed' output will true. Enter multiple paths separated by spaces.
|
||||||
|
change_path: "backend,frontend"
|
||||||
|
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
|
||||||
|
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
|
||||||
|
enable_prerelease_mode: true
|
||||||
|
# - name: 🧪 Run tests
|
||||||
|
# run: npm run test:ci
|
||||||
|
# working-directory: backend
|
||||||
|
- name: version output
|
||||||
|
run: |
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.major }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.minor }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.patch }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.version }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.version_type }}"
|
||||||
|
echo "Output Value: ${{ steps.version.outputs.increment }}"
|
||||||
|
- name: Save commit hashes for tag
|
||||||
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
|
- name: 🐋 Login to Docker Hub
|
||||||
|
uses: docker/login-action@v2
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
|
- name: 📦 Build backend and export to Docker
|
||||||
|
uses: depot/build-push-action@v1
|
||||||
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
push: true
|
||||||
|
context: .
|
||||||
|
tags: |
|
||||||
|
infisical/infisical:latest
|
||||||
|
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
file: Dockerfile.standalone-infisical
|
||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
# run only against tags
|
# run only against tags
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "infisical-cli/v*.*.*"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -41,13 +41,15 @@ jobs:
|
|||||||
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||||
- uses: goreleaser/goreleaser-action@v4
|
- uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser-pro
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||||
|
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
|
||||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||||
AUR_KEY: ${{ secrets.AUR_KEY }}
|
AUR_KEY: ${{ secrets.AUR_KEY }}
|
||||||
|
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
|
||||||
- uses: actions/setup-python@v4
|
- uses: actions/setup-python@v4
|
||||||
- run: pip install --upgrade cloudsmith-cli
|
- run: pip install --upgrade cloudsmith-cli
|
||||||
- name: Publish to CloudSmith
|
- name: Publish to CloudSmith
|
||||||
|
|||||||
+16
-15
@@ -11,10 +11,16 @@ before:
|
|||||||
- ./cli/scripts/completions.sh
|
- ./cli/scripts/completions.sh
|
||||||
- ./cli/scripts/manpages.sh
|
- ./cli/scripts/manpages.sh
|
||||||
|
|
||||||
|
monorepo:
|
||||||
|
tag_prefix: infisical-cli/
|
||||||
|
dir: cli
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- id: darwin-build
|
- id: darwin-build
|
||||||
binary: infisical
|
binary: infisical
|
||||||
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
ldflags:
|
||||||
|
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||||
|
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
env:
|
env:
|
||||||
@@ -32,7 +38,9 @@ builds:
|
|||||||
env:
|
env:
|
||||||
- CGO_ENABLED=0
|
- CGO_ENABLED=0
|
||||||
binary: infisical
|
binary: infisical
|
||||||
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
ldflags:
|
||||||
|
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||||
|
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
|
||||||
flags:
|
flags:
|
||||||
- -trimpath
|
- -trimpath
|
||||||
goos:
|
goos:
|
||||||
@@ -61,10 +69,10 @@ archives:
|
|||||||
- goos: windows
|
- goos: windows
|
||||||
format: zip
|
format: zip
|
||||||
files:
|
files:
|
||||||
- README*
|
- ../README*
|
||||||
- LICENSE*
|
- ../LICENSE*
|
||||||
- manpages/*
|
- ../manpages/*
|
||||||
- completions/*
|
- ../completions/*
|
||||||
|
|
||||||
release:
|
release:
|
||||||
replace_existing_draft: true
|
replace_existing_draft: true
|
||||||
@@ -74,14 +82,7 @@ checksum:
|
|||||||
name_template: "checksums.txt"
|
name_template: "checksums.txt"
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ incpatch .Version }}-devel"
|
name_template: "{{ .Version }}-devel"
|
||||||
|
|
||||||
changelog:
|
|
||||||
sort: asc
|
|
||||||
filters:
|
|
||||||
exclude:
|
|
||||||
- "^docs:"
|
|
||||||
- "^test:"
|
|
||||||
|
|
||||||
# publishers:
|
# publishers:
|
||||||
# - name: fury.io
|
# - name: fury.io
|
||||||
@@ -164,7 +165,7 @@ aurs:
|
|||||||
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
|
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
|
||||||
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
|
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
|
||||||
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
|
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
|
||||||
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
|
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical"
|
||||||
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
|
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
|
||||||
# man pages
|
# man pages
|
||||||
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
|
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
|
||||||
|
|||||||
@@ -3,3 +3,5 @@
|
|||||||
. "$(dirname -- "$0")/_/husky.sh"
|
. "$(dirname -- "$0")/_/husky.sh"
|
||||||
|
|
||||||
npx lint-staged
|
npx lint-staged
|
||||||
|
|
||||||
|
infisical scan git-changes --staged -v
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
.github/resources/docker-compose.be-test.yml:generic-api-key:16
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
ARG POSTHOG_HOST=https://app.posthog.com
|
||||||
|
ARG POSTHOG_API_KEY=posthog-api-key
|
||||||
|
|
||||||
|
FROM node:16-alpine AS frontend-dependencies
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN npm ci --only-production --ignore-scripts
|
||||||
|
|
||||||
|
# Rebuild the source code only when needed
|
||||||
|
FROM node:16-alpine AS frontend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy dependencies
|
||||||
|
COPY --from=frontend-dependencies /app/node_modules ./node_modules
|
||||||
|
# Copy all files
|
||||||
|
COPY /frontend .
|
||||||
|
|
||||||
|
ENV NODE_ENV production
|
||||||
|
ENV NEXT_PUBLIC_ENV production
|
||||||
|
ARG POSTHOG_HOST
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
|
||||||
|
ARG POSTHOG_API_KEY
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
|
||||||
|
ARG INTERCOM_ID
|
||||||
|
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
|
||||||
|
|
||||||
|
# Build
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production image
|
||||||
|
FROM node:16-alpine AS frontend-runner
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN addgroup --system --gid 1001 nodejs
|
||||||
|
RUN adduser --system --uid 1001 nextjs
|
||||||
|
|
||||||
|
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
|
||||||
|
VOLUME /app/.next/cache/images
|
||||||
|
|
||||||
|
ARG POSTHOG_API_KEY
|
||||||
|
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
|
||||||
|
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
|
||||||
|
ARG INTERCOM_ID
|
||||||
|
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
||||||
|
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
|
||||||
|
|
||||||
|
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
|
||||||
|
COPY --from=frontend-builder /app/public ./public
|
||||||
|
RUN chown nextjs:nodejs ./public/data
|
||||||
|
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
|
||||||
|
USER nextjs
|
||||||
|
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED 1
|
||||||
|
|
||||||
|
##
|
||||||
|
## BACKEND
|
||||||
|
##
|
||||||
|
FROM node:16-alpine AS backend-build
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm ci --only-production
|
||||||
|
|
||||||
|
COPY /backend .
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:16-alpine AS backend-runner
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY backend/package*.json ./
|
||||||
|
RUN npm ci --only-production
|
||||||
|
|
||||||
|
COPY --from=backend-build /app .
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM node:16-alpine AS production
|
||||||
|
|
||||||
|
WORKDIR /
|
||||||
|
|
||||||
|
# Install PM2
|
||||||
|
RUN npm install -g pm2
|
||||||
|
# Copy ecosystem.config.js
|
||||||
|
COPY ecosystem.config.js .
|
||||||
|
|
||||||
|
RUN apk add --no-cache nginx
|
||||||
|
|
||||||
|
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
|
||||||
|
|
||||||
|
COPY --from=backend-runner /app /backend
|
||||||
|
|
||||||
|
COPY --from=frontend-runner /app/ /app/
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
|
ENV HTTPS_ENABLED false
|
||||||
|
|
||||||
|
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
|
||||||
|
|
||||||
|
|
||||||
+7
-3
@@ -1,9 +1,13 @@
|
|||||||
# Security Policy
|
# Security Policy
|
||||||
|
|
||||||
## Supported Versions
|
## Supported versions
|
||||||
|
|
||||||
We always recommend using the latest version of Infisical to ensure you get all security updates.
|
We always recommend using the latest version of Infisical to ensure you get all security updates.
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting vulnerabilities
|
||||||
|
|
||||||
Please report security vulnerabilities or concerns to team@infisical.com.
|
Please do not file GitHub issues or post on our public forum for security vulnerabilities, as they are public!
|
||||||
|
|
||||||
|
Infisical takes security issues very seriously. If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible.
|
||||||
|
|
||||||
|
Note that this security address should be used only for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly.
|
||||||
Generated
+3268
-3322
File diff suppressed because it is too large
Load Diff
+21
-14
@@ -1,39 +1,45 @@
|
|||||||
{
|
{
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-secrets-manager": "^3.281.0",
|
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
||||||
"@godaddy/terminus": "^4.11.2",
|
"@godaddy/terminus": "^4.12.0",
|
||||||
"@octokit/rest": "^19.0.5",
|
"@octokit/rest": "^19.0.5",
|
||||||
"@sentry/tracing": "^7.39.0",
|
"@sentry/node": "^7.49.0",
|
||||||
"@sentry/node": "^7.40.0",
|
"@sentry/tracing": "^7.48.0",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"@types/libsodium-wrappers": "^0.7.10",
|
"@types/libsodium-wrappers": "^0.7.10",
|
||||||
|
"argon2": "^0.30.3",
|
||||||
"await-to-js": "^3.0.0",
|
"await-to-js": "^3.0.0",
|
||||||
"aws-sdk": "^2.1324.0",
|
"aws-sdk": "^2.1364.0",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.3.5",
|
||||||
"axios-retry": "^3.4.0",
|
"axios-retry": "^3.4.0",
|
||||||
"bcrypt": "^5.1.0",
|
"bcrypt": "^5.1.0",
|
||||||
"bigint-conversion": "^2.2.2",
|
"bigint-conversion": "^2.4.0",
|
||||||
"builder-pattern": "^2.2.0",
|
"builder-pattern": "^2.2.0",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
|
"express-async-errors": "^3.1.1",
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
"infisical-node": "^1.0.37",
|
"infisical-node": "^1.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jsonwebtoken": "^9.0.0",
|
"jsonwebtoken": "^9.0.0",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"libsodium-wrappers": "^0.7.10",
|
"libsodium-wrappers": "^0.7.10",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mongoose": "^6.10.2",
|
"mongoose": "^6.10.5",
|
||||||
|
"nanoid": "^3.3.6",
|
||||||
|
"node-cache": "^5.1.2",
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.8.0",
|
||||||
"posthog-node": "^2.5.4",
|
"passport": "^0.6.0",
|
||||||
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
"posthog-node": "^2.6.0",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
"request-ip": "^3.3.0",
|
"rate-limit-mongo": "^2.3.2",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"stripe": "^10.7.0",
|
"stripe": "^10.7.0",
|
||||||
"swagger-autogen": "^2.22.0",
|
"swagger-autogen": "^2.22.0",
|
||||||
@@ -52,12 +58,12 @@
|
|||||||
"start": "node build/index.js",
|
"start": "node build/index.js",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"swagger-autogen": "node ./swagger/index.ts",
|
"swagger-autogen": "node ./swagger/index.ts",
|
||||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||||
"lint-staged": "lint-staged",
|
"lint-staged": "lint-staged",
|
||||||
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
"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",
|
"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"
|
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
|
||||||
},
|
},
|
||||||
@@ -80,11 +86,12 @@
|
|||||||
"@types/cookie-parser": "^1.4.3",
|
"@types/cookie-parser": "^1.4.3",
|
||||||
"@types/cors": "^2.8.12",
|
"@types/cors": "^2.8.12",
|
||||||
"@types/express": "^4.17.14",
|
"@types/express": "^4.17.14",
|
||||||
"@types/jest": "^29.2.4",
|
"@types/jest": "^29.5.0",
|
||||||
"@types/jsonwebtoken": "^8.5.9",
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
"@types/lodash": "^4.14.191",
|
"@types/lodash": "^4.14.191",
|
||||||
"@types/node": "^18.11.3",
|
"@types/node": "^18.11.3",
|
||||||
"@types/nodemailer": "^6.4.6",
|
"@types/nodemailer": "^6.4.6",
|
||||||
|
"@types/passport": "^1.0.12",
|
||||||
"@types/supertest": "^2.0.12",
|
"@types/supertest": "^2.0.12",
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
"@types/swagger-jsdoc": "^6.0.1",
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
"@types/swagger-ui-express": "^4.1.3",
|
||||||
|
|||||||
+653
-7
@@ -1532,7 +1532,15 @@
|
|||||||
"/api/v1/invite-org/signup": {
|
"/api/v1/invite-org/signup": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "",
|
"description": "",
|
||||||
"parameters": [],
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "host",
|
||||||
|
"in": "header",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK"
|
||||||
@@ -2071,6 +2079,15 @@
|
|||||||
"targetEnvironment": {
|
"targetEnvironment": {
|
||||||
"example": "any"
|
"example": "any"
|
||||||
},
|
},
|
||||||
|
"targetEnvironmentId": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"targetService": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"targetServiceId": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
"owner": {
|
"owner": {
|
||||||
"example": "any"
|
"example": "any"
|
||||||
},
|
},
|
||||||
@@ -2297,6 +2314,13 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "teamId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -2309,6 +2333,107 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v1/integration-auth/{integrationAuthId}/teams": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "integrationAuthId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/integration-auth/{integrationAuthId}/vercel/branches": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "integrationAuthId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "appId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/integration-auth/{integrationAuthId}/railway/environments": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "integrationAuthId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "appId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v1/integration-auth/{integrationAuthId}/railway/services": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "integrationAuthId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "appId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v2/signup/complete-account/signup": {
|
"/api/v2/signup/complete-account/signup": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "",
|
"description": "",
|
||||||
@@ -2870,9 +2995,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
@@ -2882,6 +3004,26 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v2/organizations/{organizationId}/service-accounts": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "organizationId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/v2/workspace/{workspaceId}/environments": {
|
"/api/v2/workspace/{workspaceId}/environments": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "",
|
"description": "",
|
||||||
@@ -4018,9 +4160,6 @@
|
|||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK"
|
||||||
},
|
|
||||||
"400": {
|
|
||||||
"description": "Bad Request"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"requestBody": {
|
"requestBody": {
|
||||||
@@ -4073,6 +4212,138 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/service-accounts/me": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/service-accounts/{serviceAccountId}": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "serviceAccountId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "serviceAccountId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/service-accounts/": {
|
||||||
|
"post": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/service-accounts/{serviceAccountId}/name": {
|
||||||
|
"patch": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "serviceAccountId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"example": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "serviceAccountId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "serviceAccountId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
"description": "OK"
|
"description": "OK"
|
||||||
@@ -4080,6 +4351,90 @@
|
|||||||
"400": {
|
"400": {
|
||||||
"description": "Bad Request"
|
"description": "Bad Request"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"environment": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"workspaceId": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"read": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"write": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"encryptedKey": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"nonce": {
|
||||||
|
"example": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}": {
|
||||||
|
"delete": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "serviceAccountId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "serviceAccountWorkspacePermissionId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v2/service-accounts/{serviceAccountId}/keys": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "serviceAccountId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -4149,6 +4504,297 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/api/v3/secrets/": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "environment",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v3/secrets/{secretName}": {
|
||||||
|
"post": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "secretName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretKeyCiphertext": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretKeyIV": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretKeyTag": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretValueCiphertext": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretValueIV": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretValueTag": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretCommentCiphertext": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretCommentIV": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretCommentTag": {
|
||||||
|
"example": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "secretName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "environment",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "type",
|
||||||
|
"in": "query",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"patch": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "secretName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretValueCiphertext": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretValueIV": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"secretValueTag": {
|
||||||
|
"example": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "secretName",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"example": "any"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"example": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v3/workspaces/{workspaceId}/secrets/blind-index-status": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v3/workspaces/{workspaceId}/secrets": {
|
||||||
|
"get": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/api/v3/workspaces/{workspaceId}/secrets/names": {
|
||||||
|
"post": {
|
||||||
|
"description": "",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "workspaceId",
|
||||||
|
"in": "path",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretsToUpdate": {
|
||||||
|
"example": "any"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/api/status": {
|
"/api/status": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "",
|
"description": "",
|
||||||
|
|||||||
+93
-51
@@ -1,51 +1,93 @@
|
|||||||
import infisical from 'infisical-node';
|
import InfisicalClient from 'infisical-node';
|
||||||
export const getPort = () => infisical.get('PORT')! || 4000;
|
|
||||||
export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP');
|
export const client = new InfisicalClient({
|
||||||
export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!;
|
token: process.env.INFISICAL_TOKEN!
|
||||||
export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10;
|
});
|
||||||
export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d';
|
|
||||||
export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!;
|
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
|
||||||
export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
|
export const getEncryptionKey = async () => {
|
||||||
export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
|
const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue;
|
||||||
export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d';
|
return secretValue === '' ? undefined : secretValue;
|
||||||
export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!;
|
}
|
||||||
export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
|
export const getRootEncryptionKey = async () => {
|
||||||
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
|
const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue;
|
||||||
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
|
return secretValue === '' ? undefined : secretValue;
|
||||||
export const getMongoURL = () => infisical.get('MONGO_URL')!;
|
}
|
||||||
export const getNodeEnv = () => infisical.get('NODE_ENV')!;
|
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true'
|
||||||
export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
|
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
|
||||||
export const getLokiHost = () => infisical.get('LOKI_HOST')!;
|
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
|
||||||
export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
|
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
|
||||||
export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!;
|
export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
|
||||||
export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!;
|
export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
|
||||||
export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!;
|
export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d';
|
||||||
export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!;
|
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
|
||||||
export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!;
|
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
|
||||||
export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!;
|
export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m';
|
||||||
export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!;
|
export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue;
|
||||||
export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!;
|
export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m';
|
||||||
export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!;
|
export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue;
|
||||||
export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!;
|
export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue;
|
||||||
export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!;
|
export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production';
|
||||||
export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!;
|
export const getVerboseErrorOutput = async () => (await client.getSecret('VERBOSE_ERROR_OUTPUT')).secretValue === 'true' && true;
|
||||||
export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com';
|
export const getLokiHost = async () => (await client.getSecret('LOKI_HOST')).secretValue;
|
||||||
export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
export const getClientIdAzure = async () => (await client.getSecret('CLIENT_ID_AZURE')).secretValue;
|
||||||
export const getSentryDSN = () => infisical.get('SENTRY_DSN')!;
|
export const getClientIdHeroku = async () => (await client.getSecret('CLIENT_ID_HEROKU')).secretValue;
|
||||||
export const getSiteURL = () => infisical.get('SITE_URL')!;
|
export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_VERCEL')).secretValue;
|
||||||
export const getSmtpHost = () => infisical.get('SMTP_HOST')!;
|
export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue;
|
||||||
export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false;
|
export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue;
|
||||||
export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587;
|
export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue;
|
||||||
export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!;
|
export const getClientIdGoogle = async () => (await client.getSecret('CLIENT_ID_GOOGLE')).secretValue;
|
||||||
export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!;
|
export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue;
|
||||||
export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!;
|
export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue;
|
||||||
export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical';
|
export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue;
|
||||||
export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!;
|
export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue;
|
||||||
export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!;
|
export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue;
|
||||||
export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!;
|
export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue;
|
||||||
export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!;
|
export const getClientSecretGoogle = async () => (await client.getSecret('CLIENT_SECRET_GOOGLE')).secretValue;
|
||||||
export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
|
export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue;
|
||||||
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
|
export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com';
|
||||||
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
|
export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
||||||
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
|
export const getSentryDSN = async () => (await client.getSecret('SENTRY_DSN')).secretValue;
|
||||||
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
|
export const getSiteURL = async () => (await client.getSecret('SITE_URL')).secretValue;
|
||||||
|
export const getSmtpHost = async () => (await client.getSecret('SMTP_HOST')).secretValue;
|
||||||
|
export const getSmtpSecure = async () => (await client.getSecret('SMTP_SECURE')).secretValue === 'true' || false;
|
||||||
|
export const getSmtpPort = async () => parseInt((await client.getSecret('SMTP_PORT')).secretValue) || 587;
|
||||||
|
export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAME')).secretValue;
|
||||||
|
export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue;
|
||||||
|
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
|
||||||
|
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
|
||||||
|
|
||||||
|
export const getLicenseKey = async () => {
|
||||||
|
const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue;
|
||||||
|
return secretValue === '' ? undefined : secretValue;
|
||||||
|
}
|
||||||
|
export const getLicenseServerKey = async () => {
|
||||||
|
const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
|
||||||
|
return secretValue === '' ? undefined : secretValue;
|
||||||
|
}
|
||||||
|
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
|
||||||
|
|
||||||
|
// TODO: deprecate from here
|
||||||
|
export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue;
|
||||||
|
export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue;
|
||||||
|
export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue;
|
||||||
|
export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue;
|
||||||
|
export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue;
|
||||||
|
export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue;
|
||||||
|
|
||||||
|
export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true;
|
||||||
|
export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue;
|
||||||
|
export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true
|
||||||
|
export const getHttpsEnabled = async () => {
|
||||||
|
if ((await getNodeEnv()) != "production") {
|
||||||
|
// no https for anything other than prod
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") {
|
||||||
|
// default when no value present
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true
|
||||||
|
}
|
||||||
@@ -1,10 +1,24 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import axiosRetry from 'axios-retry';
|
import axiosRetry from 'axios-retry';
|
||||||
|
import {
|
||||||
|
getLicenseServerKeyAuthToken,
|
||||||
|
setLicenseServerKeyAuthToken,
|
||||||
|
getLicenseKeyAuthToken,
|
||||||
|
setLicenseKeyAuthToken
|
||||||
|
} from './storage';
|
||||||
|
import {
|
||||||
|
getLicenseKey,
|
||||||
|
getLicenseServerKey,
|
||||||
|
getLicenseServerUrl
|
||||||
|
} from './index';
|
||||||
|
|
||||||
const axiosInstance = axios.create();
|
// should have JWT to interact with the license server
|
||||||
|
export const licenseServerKeyRequest = axios.create();
|
||||||
|
export const licenseKeyRequest = axios.create();
|
||||||
|
export const standardRequest = axios.create();
|
||||||
|
|
||||||
// add retry functionality to the axios instance
|
// add retry functionality to the axios instance
|
||||||
axiosRetry(axiosInstance, {
|
axiosRetry(standardRequest, {
|
||||||
retries: 3,
|
retries: 3,
|
||||||
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
|
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
|
||||||
retryCondition: (error) => {
|
retryCondition: (error) => {
|
||||||
@@ -13,4 +27,98 @@ axiosRetry(axiosInstance, {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export default axiosInstance;
|
export const refreshLicenseServerKeyToken = async () => {
|
||||||
|
const licenseServerKey = await getLicenseServerKey();
|
||||||
|
const licenseServerUrl = await getLicenseServerUrl();
|
||||||
|
|
||||||
|
const { data: { token } } = await standardRequest.post(
|
||||||
|
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': licenseServerKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setLicenseServerKeyAuthToken(token);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const refreshLicenseKeyToken = async () => {
|
||||||
|
const licenseKey = await getLicenseKey();
|
||||||
|
const licenseServerUrl = await getLicenseServerUrl();
|
||||||
|
|
||||||
|
const { data: { token } } = await standardRequest.post(
|
||||||
|
`${licenseServerUrl}/api/auth/v1/license-login`, {},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-API-KEY': licenseKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
setLicenseKeyAuthToken(token);
|
||||||
|
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
licenseServerKeyRequest.interceptors.request.use((config) => {
|
||||||
|
const token = getLicenseServerKeyAuthToken();
|
||||||
|
|
||||||
|
if (token && config.headers) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, (err) => {
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
licenseServerKeyRequest.interceptors.response.use((response) => {
|
||||||
|
return response
|
||||||
|
}, async function (err) {
|
||||||
|
const originalRequest = err.config;
|
||||||
|
|
||||||
|
if (err.response.status === 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
|
||||||
|
// refresh
|
||||||
|
const token = await refreshLicenseServerKeyToken();
|
||||||
|
|
||||||
|
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||||
|
return licenseServerKeyRequest(originalRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
licenseKeyRequest.interceptors.request.use((config) => {
|
||||||
|
const token = getLicenseKeyAuthToken();
|
||||||
|
|
||||||
|
if (token && config.headers) {
|
||||||
|
// eslint-disable-next-line no-param-reassign
|
||||||
|
config.headers.Authorization = `Bearer ${token}`;
|
||||||
|
}
|
||||||
|
return config;
|
||||||
|
}, (err) => {
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
|
|
||||||
|
licenseKeyRequest.interceptors.response.use((response) => {
|
||||||
|
return response
|
||||||
|
}, async function (err) {
|
||||||
|
const originalRequest = err.config;
|
||||||
|
|
||||||
|
if (err.response.status === 401 && !originalRequest._retry) {
|
||||||
|
originalRequest._retry = true;
|
||||||
|
|
||||||
|
// refresh
|
||||||
|
const token = await refreshLicenseKeyToken();
|
||||||
|
|
||||||
|
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
|
||||||
|
return licenseKeyRequest(originalRequest);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.reject(err);
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
const MemoryLicenseServerKeyTokenStorage = () => {
|
||||||
|
let authToken: string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
setToken: (token: string) => {
|
||||||
|
authToken = token;
|
||||||
|
},
|
||||||
|
getToken: () => authToken
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MemoryLicenseKeyTokenStorage = () => {
|
||||||
|
let authToken: string;
|
||||||
|
|
||||||
|
return {
|
||||||
|
setToken: (token: string) => {
|
||||||
|
authToken = token;
|
||||||
|
},
|
||||||
|
getToken: () => authToken
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
|
||||||
|
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
|
||||||
|
|
||||||
|
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
|
||||||
|
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
|
||||||
|
|
||||||
|
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
|
||||||
|
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;
|
||||||
@@ -1,29 +1,39 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User, LoginSRPDetail } from '../../models';
|
import {
|
||||||
|
User,
|
||||||
|
LoginSRPDetail,
|
||||||
|
TokenVersion
|
||||||
|
} from '../../models';
|
||||||
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
|
||||||
import { checkUserDevice } from '../../helpers/user';
|
import { checkUserDevice } from '../../helpers/user';
|
||||||
import {
|
import {
|
||||||
ACTION_LOGIN,
|
ACTION_LOGIN,
|
||||||
ACTION_LOGOUT
|
ACTION_LOGOUT,
|
||||||
|
AUTH_MODE_JWT
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
import { BadRequestError } from '../../utils/errors';
|
import {
|
||||||
|
BadRequestError,
|
||||||
|
UnauthorizedRequestError
|
||||||
|
} from '../../utils/errors';
|
||||||
import { EELogService } from '../../ee/services';
|
import { EELogService } from '../../ee/services';
|
||||||
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||||
import {
|
import {
|
||||||
getNodeEnv,
|
|
||||||
getJwtRefreshSecret,
|
getJwtRefreshSecret,
|
||||||
getJwtAuthLifetime,
|
getJwtAuthLifetime,
|
||||||
getJwtAuthSecret
|
getJwtAuthSecret,
|
||||||
|
getHttpsEnabled
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
userId: string;
|
userId: string;
|
||||||
|
refreshVersion?: number;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,47 +44,39 @@ declare module 'jsonwebtoken' {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
clientPublicKey
|
||||||
clientPublicKey
|
}: { email: string; clientPublicKey: string } = req.body;
|
||||||
}: { email: string; clientPublicKey: string } = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||||
email: email,
|
email: email,
|
||||||
clientPublicKey: clientPublicKey,
|
clientPublicKey: clientPublicKey,
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
}, { upsert: true, returnNewDocument: false })
|
}, { upsert: true, returnNewDocument: false })
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serverPublicKey,
|
serverPublicKey,
|
||||||
salt: user.salt
|
salt: user.salt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to start authentication process'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,84 +87,80 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
try {
|
const { email, clientProof } = req.body;
|
||||||
const { email, clientProof } = req.body;
|
const user = await User.findOne({
|
||||||
const user = await User.findOne({
|
email
|
||||||
email
|
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
||||||
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
|
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
if (!loginSRPDetailFromDB) {
|
||||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// issue tokens
|
// issue tokens
|
||||||
|
|
||||||
await checkUserDevice({
|
await checkUserDevice({
|
||||||
user,
|
user,
|
||||||
ip: req.ip,
|
ip: req.realIP,
|
||||||
userAgent: req.headers['user-agent'] ?? ''
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
const tokens = await issueAuthTokens({
|
||||||
|
userId: user._id,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: getNodeEnv() === 'production' ? true : false
|
secure: await getHttpsEnabled()
|
||||||
});
|
});
|
||||||
|
|
||||||
const loginAction = await EELogService.createAction({
|
const loginAction = await EELogService.createAction({
|
||||||
name: ACTION_LOGIN,
|
name: ACTION_LOGIN,
|
||||||
userId: user._id
|
userId: user._id
|
||||||
});
|
});
|
||||||
|
|
||||||
loginAction && await EELogService.createLog({
|
|
||||||
userId: user._id,
|
|
||||||
actions: [loginAction],
|
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
|
||||||
|
|
||||||
// return (access) token in response
|
|
||||||
return res.status(200).send({
|
|
||||||
token: tokens.token,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
|
||||||
iv: user.iv,
|
|
||||||
tag: user.tag
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).send({
|
loginAction && await EELogService.createLog({
|
||||||
message: 'Failed to authenticate. Try again?'
|
userId: user._id,
|
||||||
|
actions: [loginAction],
|
||||||
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
|
ipAddress: req.realIP
|
||||||
|
});
|
||||||
|
|
||||||
|
// return (access) token in response
|
||||||
|
return res.status(200).send({
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} catch (err) {
|
return res.status(400).send({
|
||||||
Sentry.setUser(null);
|
message: 'Failed to authenticate. Try again?'
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
message: 'Failed to authenticate. Try again?'
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -172,44 +170,59 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const logout = async (req: Request, res: Response) => {
|
export const logout = async (req: Request, res: Response) => {
|
||||||
try {
|
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
|
||||||
await clearTokens({
|
await clearTokens(req.authData.tokenVersionId)
|
||||||
userId: req.user._id.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
// clear httpOnly cookie
|
|
||||||
res.cookie('jid', '', {
|
|
||||||
httpOnly: true,
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'strict',
|
|
||||||
secure: getNodeEnv() === 'production' ? true : false
|
|
||||||
});
|
|
||||||
|
|
||||||
const logoutAction = await EELogService.createAction({
|
|
||||||
name: ACTION_LOGOUT,
|
|
||||||
userId: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
logoutAction && await EELogService.createLog({
|
|
||||||
userId: req.user._id,
|
|
||||||
actions: [logoutAction],
|
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to logout'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// clear httpOnly cookie
|
||||||
|
res.cookie('jid', '', {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: (await 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],
|
||||||
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
|
ipAddress: req.realIP
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully logged out.'
|
message: 'Successfully logged out.'
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCommonPasswords = async (req: Request, res: Response) => {
|
||||||
|
const commonPasswords = fs.readFileSync(
|
||||||
|
path.resolve(__dirname, '../../data/' + 'common_passwords.txt'),
|
||||||
|
'utf8'
|
||||||
|
).split('\n');
|
||||||
|
|
||||||
|
return res.status(200).send(commonPasswords);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const revokeAllSessions = async (req: Request, res: Response) => {
|
||||||
|
await TokenVersion.updateMany({
|
||||||
|
user: req.user._id
|
||||||
|
}, {
|
||||||
|
$inc: {
|
||||||
|
refreshVersion: 1,
|
||||||
|
accessVersion: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully revoked all sessions.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return user is authenticated
|
* Return user is authenticated
|
||||||
* @param req
|
* @param req
|
||||||
@@ -223,47 +236,55 @@ export const checkAuth = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new token by redeeming refresh token
|
* Return new JWT access token by first validating the refresh token
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getNewToken = async (req: Request, res: Response) => {
|
export const getNewToken = async (req: Request, res: Response) => {
|
||||||
try {
|
const refreshToken = req.cookies.jid;
|
||||||
const refreshToken = req.cookies.jid;
|
|
||||||
|
|
||||||
if (!refreshToken) {
|
if (!refreshToken) {
|
||||||
throw new Error('Failed to find token in request cookies');
|
throw new Error('Failed to find refresh token in request cookies');
|
||||||
}
|
|
||||||
|
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
|
||||||
jwt.verify(refreshToken, getJwtRefreshSecret())
|
|
||||||
);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
_id: decodedToken.userId
|
|
||||||
}).select('+publicKey');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to authenticate unfound user');
|
|
||||||
if (!user?.publicKey)
|
|
||||||
throw new Error('Failed to authenticate not fully set up account');
|
|
||||||
|
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
userId: decodedToken.userId
|
|
||||||
},
|
|
||||||
expiresIn: getJwtAuthLifetime(),
|
|
||||||
secret: getJwtAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
token
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Invalid request'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(refreshToken, await getJwtRefreshSecret())
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
_id: decodedToken.userId
|
||||||
|
}).select('+publicKey +refreshVersion +accessVersion');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to authenticate unfound user');
|
||||||
|
if (!user?.publicKey)
|
||||||
|
throw new Error('Failed to authenticate not fully set up account');
|
||||||
|
|
||||||
|
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
|
||||||
|
|
||||||
|
if (!tokenVersion) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to validate refresh token'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
|
||||||
|
message: 'Failed to validate refresh token'
|
||||||
|
});
|
||||||
|
|
||||||
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: decodedToken.userId,
|
||||||
|
tokenVersionId: tokenVersion._id.toString(),
|
||||||
|
accessVersion: tokenVersion.refreshVersion
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtAuthLifetime(),
|
||||||
|
secret: await getJwtAuthSecret()
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
token
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
||||||
|
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import { Types } from 'mongoose';
|
||||||
import { Bot, BotKey } from '../../models';
|
import { Bot, BotKey } from '../../models';
|
||||||
import { createBot } from '../../helpers/bot';
|
import { createBot } from '../../helpers/bot';
|
||||||
|
|
||||||
@@ -16,33 +16,24 @@ interface BotKey {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||||
let bot;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
bot = await Bot.findOne({
|
let bot = await Bot.findOne({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!bot) {
|
if (!bot) {
|
||||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||||
// -> create a new bot and return it
|
// -> create a new bot and return it
|
||||||
bot = await createBot({
|
bot = await createBot({
|
||||||
name: 'Infisical Bot',
|
name: 'Infisical Bot',
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get bot for workspace'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
bot
|
bot
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -52,54 +43,44 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||||
let bot;
|
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||||
try {
|
|
||||||
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
if (isActive) {
|
||||||
|
// bot state set to active -> share workspace key with bot
|
||||||
if (isActive) {
|
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||||
// bot state set to active -> share workspace key with bot
|
return res.status(400).send({
|
||||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
message: 'Failed to set bot state to active - missing bot key'
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to set bot state to active - missing bot key'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await BotKey.findOneAndUpdate({
|
|
||||||
workspace: req.bot.workspace
|
|
||||||
}, {
|
|
||||||
encryptedKey: botKey.encryptedKey,
|
|
||||||
nonce: botKey.nonce,
|
|
||||||
sender: req.user._id,
|
|
||||||
bot: req.bot._id,
|
|
||||||
workspace: req.bot.workspace
|
|
||||||
}, {
|
|
||||||
upsert: true,
|
|
||||||
new: true
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// case: bot state set to inactive -> delete bot's workspace key
|
|
||||||
await BotKey.deleteOne({
|
|
||||||
bot: req.bot._id
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
bot = await Bot.findOneAndUpdate({
|
await BotKey.findOneAndUpdate({
|
||||||
_id: req.bot._id
|
workspace: req.bot.workspace
|
||||||
}, {
|
}, {
|
||||||
isActive
|
encryptedKey: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
sender: req.user._id,
|
||||||
|
bot: req.bot._id,
|
||||||
|
workspace: req.bot.workspace
|
||||||
}, {
|
}, {
|
||||||
|
upsert: true,
|
||||||
new: true
|
new: true
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
if (!bot) throw new Error('Failed to update bot active state');
|
// case: bot state set to inactive -> delete bot's workspace key
|
||||||
|
await BotKey.deleteOne({
|
||||||
} catch (err) {
|
bot: req.bot._id
|
||||||
Sentry.setUser({ email: req.user.email });
|
});
|
||||||
Sentry.captureException(err);
|
}
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to update bot active state'
|
let bot = await Bot.findOneAndUpdate({
|
||||||
});
|
_id: req.bot._id
|
||||||
}
|
}, {
|
||||||
|
isActive
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to update bot active state');
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
bot
|
bot
|
||||||
|
|||||||
@@ -1,45 +1,40 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
Bot
|
Bot
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
|
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
|
||||||
import { IntegrationService } from '../../services';
|
import { IntegrationService } from '../../services';
|
||||||
import {
|
import {
|
||||||
getApps,
|
getApps,
|
||||||
getTeams,
|
getTeams,
|
||||||
revokeAccess
|
revokeAccess
|
||||||
} from '../../integrations';
|
} from '../../integrations';
|
||||||
|
import {
|
||||||
|
INTEGRATION_VERCEL_API_URL,
|
||||||
|
INTEGRATION_RAILWAY_API_URL
|
||||||
|
} from '../../variables';
|
||||||
|
import { standardRequest } from '../../config/request';
|
||||||
|
|
||||||
/***
|
/***
|
||||||
* Return integration authorization with id [integrationAuthId]
|
* Return integration authorization with id [integrationAuthId]
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuth = async (req: Request, res: Response) => {
|
export const getIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
let integrationAuth;
|
const { integrationAuthId } = req.params;
|
||||||
try {
|
const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
const { integrationAuthId } = req.params;
|
|
||||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
if (!integrationAuth) return res.status(400).send({
|
||||||
|
message: 'Failed to find integration authorization'
|
||||||
if (!integrationAuth) return res.status(400).send({
|
});
|
||||||
message: 'Failed to find integration authorization'
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get integration authorization'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrationAuth
|
integrationAuth
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getIntegrationOptions = async (req: Request, res: Response) => {
|
export const getIntegrationOptions = async (req: Request, res: Response) => {
|
||||||
const INTEGRATION_OPTIONS = getIntegrationOptionsFunc();
|
const INTEGRATION_OPTIONS = await getIntegrationOptionsFunc();
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrationOptions: INTEGRATION_OPTIONS,
|
integrationOptions: INTEGRATION_OPTIONS,
|
||||||
@@ -56,33 +51,25 @@ export const oAuthExchange = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
try {
|
const { workspaceId, code, integration } = req.body;
|
||||||
const { workspaceId, code, integration } = req.body;
|
if (!INTEGRATION_SET.has(integration))
|
||||||
if (!INTEGRATION_SET.has(integration))
|
throw new Error('Failed to validate integration');
|
||||||
throw new Error('Failed to validate integration');
|
|
||||||
|
const environments = req.membership.workspace?.environments || [];
|
||||||
const environments = req.membership.workspace?.environments || [];
|
if(environments.length === 0){
|
||||||
if(environments.length === 0){
|
throw new Error("Failed to get environments")
|
||||||
throw new Error("Failed to get environments")
|
}
|
||||||
}
|
|
||||||
|
const integrationAuth = await IntegrationService.handleOAuthExchange({
|
||||||
const integrationAuth = await IntegrationService.handleOAuthExchange({
|
workspaceId,
|
||||||
workspaceId,
|
integration,
|
||||||
integration,
|
code,
|
||||||
code,
|
environment: environments[0].slug,
|
||||||
environment: environments[0].slug,
|
});
|
||||||
});
|
|
||||||
|
return res.status(200).send({
|
||||||
return res.status(200).send({
|
integrationAuth
|
||||||
integrationAuth
|
});
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get OAuth2 code-token exchange'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,53 +86,53 @@ export const saveIntegrationAccessToken = async (
|
|||||||
// TODO: check if access token is valid for each integration
|
// TODO: check if access token is valid for each integration
|
||||||
|
|
||||||
let integrationAuth;
|
let integrationAuth;
|
||||||
try {
|
const {
|
||||||
const {
|
workspaceId,
|
||||||
workspaceId,
|
accessId,
|
||||||
accessId,
|
accessToken,
|
||||||
accessToken,
|
url,
|
||||||
integration
|
namespace,
|
||||||
}: {
|
integration
|
||||||
workspaceId: string;
|
}: {
|
||||||
accessId: string | null;
|
workspaceId: string;
|
||||||
accessToken: string;
|
accessId: string | null;
|
||||||
integration: string;
|
accessToken: string;
|
||||||
} = req.body;
|
url: string;
|
||||||
|
namespace: string;
|
||||||
|
integration: string;
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
const bot = await Bot.findOne({
|
const bot = await Bot.findOne({
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
isActive: true
|
isActive: true
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||||
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
integration
|
integration
|
||||||
}, {
|
}, {
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
integration
|
integration,
|
||||||
}, {
|
url,
|
||||||
new: true,
|
namespace,
|
||||||
upsert: true
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
});
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}, {
|
||||||
// encrypt and save integration access details
|
new: true,
|
||||||
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
upsert: true
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
});
|
||||||
accessId,
|
|
||||||
accessToken,
|
// encrypt and save integration access details
|
||||||
accessExpiresAt: undefined
|
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||||
});
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessId,
|
||||||
if (!integrationAuth) throw new Error('Failed to save integration access token');
|
accessToken,
|
||||||
} catch (err) {
|
accessExpiresAt: undefined
|
||||||
Sentry.setUser({ email: req.user.email });
|
});
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
if (!integrationAuth) throw new Error('Failed to save integration access token');
|
||||||
message: 'Failed to save access token for integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrationAuth
|
integrationAuth
|
||||||
@@ -159,22 +146,13 @@ export const saveIntegrationAccessToken = async (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
let apps;
|
const teamId = req.query.teamId as string;
|
||||||
try {
|
|
||||||
const teamId = req.query.teamId as string;
|
const apps = await getApps({
|
||||||
|
integrationAuth: req.integrationAuth,
|
||||||
apps = await getApps({
|
accessToken: req.accessToken,
|
||||||
integrationAuth: req.integrationAuth,
|
...teamId && { teamId }
|
||||||
accessToken: req.accessToken,
|
});
|
||||||
...teamId && { teamId }
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get integration authorization applications",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apps
|
apps
|
||||||
@@ -188,22 +166,203 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
|
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
|
||||||
let teams;
|
const teams = await getTeams({
|
||||||
try {
|
integrationAuth: req.integrationAuth,
|
||||||
teams = await getTeams({
|
accessToken: req.accessToken
|
||||||
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 standardRequest.get(
|
||||||
|
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
|
||||||
|
{
|
||||||
|
params,
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${req.accessToken}`,
|
||||||
|
'Accept-Encoding': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
branches = data.map((b) => b.ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
branches
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of Railway environments for Railway project with
|
||||||
|
* id [appId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
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 standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
|
||||||
|
query,
|
||||||
|
variables,
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${req.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
environments = edges.map((e: RailwayEnvironment) => {
|
||||||
Sentry.captureException(err);
|
return ({
|
||||||
return res.status(400).send({
|
name: e.node.name,
|
||||||
message: "Failed to get integration authorization teams"
|
environmentId: e.node.id
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
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 standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
|
||||||
|
query,
|
||||||
|
variables
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': `Bearer ${req.accessToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
services = edges.map((e: RailwayService) => ({
|
||||||
|
name: e.node.name,
|
||||||
|
serviceId: e.node.id
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
services
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -214,19 +373,10 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
let integrationAuth;
|
const integrationAuth = await revokeAccess({
|
||||||
try {
|
integrationAuth: req.integrationAuth,
|
||||||
integrationAuth = await revokeAccess({
|
accessToken: req.accessToken,
|
||||||
integrationAuth: req.integrationAuth,
|
});
|
||||||
accessToken: req.accessToken,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to delete integration authorization",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrationAuth,
|
integrationAuth,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
Integration
|
Integration
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
@@ -14,54 +13,50 @@ import { eventPushSecrets } from '../../events';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createIntegration = async (req: Request, res: Response) => {
|
export const createIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
const {
|
||||||
|
integrationAuthId,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
isActive,
|
||||||
|
sourceEnvironment,
|
||||||
|
targetEnvironment,
|
||||||
|
targetEnvironmentId,
|
||||||
|
targetService,
|
||||||
|
targetServiceId,
|
||||||
|
owner,
|
||||||
|
path,
|
||||||
|
region
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
||||||
|
|
||||||
try {
|
// initialize new integration after saving integration access token
|
||||||
const {
|
const integration = await new Integration({
|
||||||
integrationAuthId,
|
workspace: req.integrationAuth.workspace._id,
|
||||||
app,
|
environment: sourceEnvironment,
|
||||||
appId,
|
isActive,
|
||||||
isActive,
|
app,
|
||||||
sourceEnvironment,
|
appId,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
owner,
|
targetEnvironmentId,
|
||||||
path,
|
targetService,
|
||||||
region
|
targetServiceId,
|
||||||
} = req.body;
|
owner,
|
||||||
|
path,
|
||||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
region,
|
||||||
|
integration: req.integrationAuth.integration,
|
||||||
// initialize new integration after saving integration access token
|
integrationAuth: new Types.ObjectId(integrationAuthId)
|
||||||
integration = await new Integration({
|
}).save();
|
||||||
workspace: req.integrationAuth.workspace._id,
|
|
||||||
environment: sourceEnvironment,
|
if (integration) {
|
||||||
isActive,
|
// trigger event - push secrets
|
||||||
app,
|
EventService.handleEvent({
|
||||||
appId,
|
event: eventPushSecrets({
|
||||||
targetEnvironment,
|
workspaceId: integration.workspace,
|
||||||
owner,
|
environment: sourceEnvironment
|
||||||
path,
|
})
|
||||||
region,
|
});
|
||||||
integration: req.integrationAuth.integration,
|
}
|
||||||
integrationAuth: new Types.ObjectId(integrationAuthId)
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
if (integration) {
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: integration.workspace.toString()
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to create integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration,
|
integration,
|
||||||
@@ -75,51 +70,43 @@ export const createIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateIntegration = async (req: Request, res: Response) => {
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
|
||||||
|
|
||||||
// TODO: add integration-specific validation to ensure that each
|
// TODO: add integration-specific validation to ensure that each
|
||||||
// integration has the correct fields populated in [Integration]
|
// integration has the correct fields populated in [Integration]
|
||||||
|
|
||||||
try {
|
const {
|
||||||
const {
|
environment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
|
owner, // github-specific integration param
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const integration = await Integration.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_id: req.integration._id,
|
||||||
|
},
|
||||||
|
{
|
||||||
environment,
|
environment,
|
||||||
isActive,
|
isActive,
|
||||||
app,
|
app,
|
||||||
appId,
|
appId,
|
||||||
targetEnvironment,
|
targetEnvironment,
|
||||||
owner, // github-specific integration param
|
owner,
|
||||||
} = req.body;
|
},
|
||||||
|
{
|
||||||
integration = await Integration.findOneAndUpdate(
|
new: true,
|
||||||
{
|
|
||||||
_id: req.integration._id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
owner,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (integration) {
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: integration.workspace.toString(),
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
);
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
if (integration) {
|
||||||
return res.status(400).send({
|
// trigger event - push secrets
|
||||||
message: "Failed to update integration",
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: integration.workspace,
|
||||||
|
environment
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -136,22 +123,13 @@ export const updateIntegration = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
const { integrationId } = req.params;
|
||||||
try {
|
|
||||||
const { integrationId } = req.params;
|
|
||||||
|
|
||||||
integration = await Integration.findOneAndDelete({
|
const integration = await Integration.findOneAndDelete({
|
||||||
_id: integrationId,
|
_id: integrationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!integration) throw new Error("Failed to find integration");
|
if (!integration) throw new Error("Failed to find integration");
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to delete integration",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integration,
|
integration,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Key } from '../../models';
|
import { Key } from '../../models';
|
||||||
import { findMembership } from '../../helpers/membership';
|
import { findMembership } from '../../helpers/membership';
|
||||||
|
|
||||||
@@ -11,34 +10,26 @@ import { findMembership } from '../../helpers/membership';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const uploadKey = async (req: Request, res: Response) => {
|
export const uploadKey = async (req: Request, res: Response) => {
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
const { key } = req.body;
|
||||||
const { key } = req.body;
|
|
||||||
|
|
||||||
// validate membership of receiver
|
// validate membership of receiver
|
||||||
const receiverMembership = await findMembership({
|
const receiverMembership = await findMembership({
|
||||||
user: key.userId,
|
user: key.userId,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!receiverMembership) {
|
if (!receiverMembership) {
|
||||||
throw new Error('Failed receiver membership validation for workspace');
|
throw new Error('Failed receiver membership validation for workspace');
|
||||||
}
|
}
|
||||||
|
|
||||||
await new Key({
|
await new Key({
|
||||||
encryptedKey: key.encryptedKey,
|
encryptedKey: key.encryptedKey,
|
||||||
nonce: key.nonce,
|
nonce: key.nonce,
|
||||||
sender: req.user._id,
|
sender: req.user._id,
|
||||||
receiver: key.userId,
|
receiver: key.userId,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}).save();
|
}).save();
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to upload key to workspace'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully uploaded key to workspace'
|
message: 'Successfully uploaded key to workspace'
|
||||||
@@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getLatestKey = async (req: Request, res: Response) => {
|
export const getLatestKey = async (req: Request, res: Response) => {
|
||||||
let latestKey;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// get latest key
|
// get latest key
|
||||||
latestKey = await Key.find({
|
const latestKey = await Key.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
receiver: req.user._id
|
receiver: req.user._id
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.populate('sender', '+publicKey');
|
.populate('sender', '+publicKey');
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get latest key'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const resObj: any = {};
|
const resObj: any = {};
|
||||||
|
|
||||||
@@ -79,4 +61,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
return res.status(200).send(resObj);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { Membership, MembershipOrg, User, Key } from '../../models';
|
import { Membership, MembershipOrg, User, Key } from '../../models';
|
||||||
import {
|
import {
|
||||||
@@ -16,25 +15,16 @@ import { getSiteURL } from '../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const validateMembership = async (req: Request, res: Response) => {
|
export const validateMembership = async (req: Request, res: Response) => {
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
// validate membership
|
||||||
|
const membership = await findMembership({
|
||||||
|
user: req.user._id,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
// validate membership
|
if (!membership) {
|
||||||
const membership = await findMembership({
|
throw new Error('Failed to validate membership');
|
||||||
user: req.user._id,
|
}
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Failed to validate membership');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed workspace connection check'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Workspace membership confirmed'
|
message: 'Workspace membership confirmed'
|
||||||
@@ -48,48 +38,39 @@ export const validateMembership = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteMembership = async (req: Request, res: Response) => {
|
export const deleteMembership = async (req: Request, res: Response) => {
|
||||||
let deletedMembership;
|
const { membershipId } = req.params;
|
||||||
try {
|
|
||||||
const { membershipId } = req.params;
|
|
||||||
|
|
||||||
// check if membership to delete exists
|
// check if membership to delete exists
|
||||||
const membershipToDelete = await Membership.findOne({
|
const membershipToDelete = await Membership.findOne({
|
||||||
_id: membershipId
|
_id: membershipId
|
||||||
}).populate('user');
|
}).populate('user');
|
||||||
|
|
||||||
if (!membershipToDelete) {
|
if (!membershipToDelete) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to delete workspace membership that doesn't exist"
|
"Failed to delete workspace membership that doesn't exist"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user is a member and admin of the workspace
|
// check if user is a member and admin of the workspace
|
||||||
// whose membership we wish to delete
|
// whose membership we wish to delete
|
||||||
const membership = await Membership.findOne({
|
const membership = await Membership.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
workspace: membershipToDelete.workspace
|
workspace: membershipToDelete.workspace
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw new Error('Failed to validate workspace membership');
|
throw new Error('Failed to validate workspace membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membership.role !== ADMIN) {
|
if (membership.role !== ADMIN) {
|
||||||
// user is not an admin member of the workspace
|
// user is not an admin member of the workspace
|
||||||
throw new Error('Insufficient role for deleting workspace membership');
|
throw new Error('Insufficient role for deleting workspace membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete workspace membership
|
// delete workspace membership
|
||||||
deletedMembership = await deleteMember({
|
const deletedMembership = await deleteMember({
|
||||||
membershipId: membershipToDelete._id.toString()
|
membershipId: membershipToDelete._id.toString()
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
deletedMembership
|
deletedMembership
|
||||||
@@ -103,49 +84,40 @@ export const deleteMembership = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeMembershipRole = async (req: Request, res: Response) => {
|
export const changeMembershipRole = async (req: Request, res: Response) => {
|
||||||
let membershipToChangeRole;
|
const { membershipId } = req.params;
|
||||||
try {
|
const { role } = req.body;
|
||||||
const { membershipId } = req.params;
|
|
||||||
const { role } = req.body;
|
|
||||||
|
|
||||||
if (![ADMIN, MEMBER].includes(role)) {
|
if (![ADMIN, MEMBER].includes(role)) {
|
||||||
throw new Error('Failed to validate role');
|
throw new Error('Failed to validate role');
|
||||||
}
|
}
|
||||||
|
|
||||||
// validate target membership
|
// validate target membership
|
||||||
membershipToChangeRole = await findMembership({
|
const membershipToChangeRole = await findMembership({
|
||||||
_id: membershipId
|
_id: membershipId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipToChangeRole) {
|
if (!membershipToChangeRole) {
|
||||||
throw new Error('Failed to find membership to change role');
|
throw new Error('Failed to find membership to change role');
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user is a member and admin of target membership's
|
// check if user is a member and admin of target membership's
|
||||||
// workspace
|
// workspace
|
||||||
const membership = await findMembership({
|
const membership = await findMembership({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
workspace: membershipToChangeRole.workspace
|
workspace: membershipToChangeRole.workspace
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw new Error('Failed to validate membership');
|
throw new Error('Failed to validate membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membership.role !== ADMIN) {
|
if (membership.role !== ADMIN) {
|
||||||
// user is not an admin member of the workspace
|
// user is not an admin member of the workspace
|
||||||
throw new Error('Insufficient role for changing member roles');
|
throw new Error('Insufficient role for changing member roles');
|
||||||
}
|
}
|
||||||
|
|
||||||
membershipToChangeRole.role = role;
|
membershipToChangeRole.role = role;
|
||||||
await membershipToChangeRole.save();
|
await membershipToChangeRole.save();
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to change membership role'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
membership: membershipToChangeRole
|
membership: membershipToChangeRole
|
||||||
@@ -159,75 +131,66 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
||||||
let invitee, latestKey;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { email }: { email: string } = req.body;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { email }: { email: string } = req.body;
|
|
||||||
|
|
||||||
invitee = await User.findOne({
|
const invitee = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+publicKey');
|
}).select('+publicKey');
|
||||||
|
|
||||||
if (!invitee || !invitee?.publicKey)
|
if (!invitee || !invitee?.publicKey)
|
||||||
throw new Error('Failed to validate invitee');
|
throw new Error('Failed to validate invitee');
|
||||||
|
|
||||||
// validate invitee's workspace membership - ensure member isn't
|
// validate invitee's workspace membership - ensure member isn't
|
||||||
// already a member of the workspace
|
// already a member of the workspace
|
||||||
const inviteeMembership = await Membership.findOne({
|
const inviteeMembership = await Membership.findOne({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inviteeMembership)
|
if (inviteeMembership)
|
||||||
throw new Error('Failed to add existing member of workspace');
|
throw new Error('Failed to add existing member of workspace');
|
||||||
|
|
||||||
// validate invitee's organization membership - ensure that only
|
// validate invitee's organization membership - ensure that only
|
||||||
// (accepted) organization members can be added to the workspace
|
// (accepted) organization members can be added to the workspace
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
organization: req.membership.workspace.organization,
|
organization: req.membership.workspace.organization,
|
||||||
status: ACCEPTED
|
status: ACCEPTED
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg)
|
if (!membershipOrg)
|
||||||
throw new Error("Failed to validate invitee's organization membership");
|
throw new Error("Failed to validate invitee's organization membership");
|
||||||
|
|
||||||
// get latest key
|
// get latest key
|
||||||
latestKey = await Key.findOne({
|
const latestKey = await Key.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
receiver: req.user._id
|
receiver: req.user._id
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.populate('sender', '+publicKey');
|
.populate('sender', '+publicKey');
|
||||||
|
|
||||||
// create new workspace membership
|
// create new workspace membership
|
||||||
const m = await new Membership({
|
const m = await new Membership({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
role: MEMBER
|
role: MEMBER
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'workspaceInvitation.handlebars',
|
template: 'workspaceInvitation.handlebars',
|
||||||
subjectLine: 'Infisical workspace invitation',
|
subjectLine: 'Infisical workspace invitation',
|
||||||
recipients: [invitee.email],
|
recipients: [invitee.email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
inviterFirstName: req.user.firstName,
|
inviterFirstName: req.user.firstName,
|
||||||
inviterEmail: req.user.email,
|
inviterEmail: req.user.email,
|
||||||
workspaceName: req.membership.workspace.name,
|
workspaceName: req.membership.workspace.name,
|
||||||
callback_url: getSiteURL() + '/login'
|
callback_url: (await getSiteURL()) + '/login'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to invite user to workspace'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
invitee,
|
invitee,
|
||||||
latestKey
|
latestKey
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { MembershipOrg, Organization, User } from '../../models';
|
import { MembershipOrg, Organization, User } from '../../models';
|
||||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||||
import { createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
import { TokenService } from '../../services';
|
import { TokenService } from '../../services';
|
||||||
|
import { EELicenseService } from '../../ee/services';
|
||||||
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
|
||||||
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
||||||
|
import { validateUserEmail } from '../../validation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete organization membership with id [membershipOrgId] from organization
|
* Delete organization membership with id [membershipOrgId] from organization
|
||||||
@@ -16,52 +18,43 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteMembershipOrg = async (req: Request, res: Response) => {
|
export const deleteMembershipOrg = async (req: Request, res: Response) => {
|
||||||
let membershipOrgToDelete;
|
const { membershipOrgId } = req.params;
|
||||||
try {
|
|
||||||
const { membershipOrgId } = req.params;
|
|
||||||
|
|
||||||
// check if organization membership to delete exists
|
// check if organization membership to delete exists
|
||||||
membershipOrgToDelete = await MembershipOrg.findOne({
|
const membershipOrgToDelete = await MembershipOrg.findOne({
|
||||||
_id: membershipOrgId
|
_id: membershipOrgId
|
||||||
}).populate('user');
|
}).populate('user');
|
||||||
|
|
||||||
if (!membershipOrgToDelete) {
|
if (!membershipOrgToDelete) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to delete organization membership that doesn't exist"
|
"Failed to delete organization membership that doesn't exist"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if user is a member and admin of the organization
|
// check if user is a member and admin of the organization
|
||||||
// whose membership we wish to delete
|
// whose membership we wish to delete
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: membershipOrgToDelete.organization
|
organization: membershipOrgToDelete.organization
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership');
|
throw new Error('Failed to validate organization membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
|
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
|
||||||
// user is not an admin member of the organization
|
// user is not an admin member of the organization
|
||||||
throw new Error('Insufficient role for deleting organization membership');
|
throw new Error('Insufficient role for deleting organization membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete organization membership
|
// delete organization membership
|
||||||
const deletedMembershipOrg = await deleteMemberFromOrg({
|
const deletedMembershipOrg = await deleteMemberFromOrg({
|
||||||
membershipOrgId: membershipOrgToDelete._id.toString()
|
membershipOrgId: membershipOrgToDelete._id.toString()
|
||||||
});
|
});
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
await updateSubscriptionOrgQuantity({
|
||||||
organizationId: membershipOrg.organization.toString()
|
organizationId: membershipOrg.organization.toString()
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete organization membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return membershipOrgToDelete;
|
return membershipOrgToDelete;
|
||||||
};
|
};
|
||||||
@@ -77,14 +70,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
|||||||
// [membershipOrgId]
|
// [membershipOrgId]
|
||||||
|
|
||||||
let membershipToChangeRole;
|
let membershipToChangeRole;
|
||||||
// try {
|
|
||||||
// } catch (err) {
|
|
||||||
// Sentry.setUser({ email: req.user.email });
|
|
||||||
// Sentry.captureException(err);
|
|
||||||
// return res.status(400).send({
|
|
||||||
// message: 'Failed to change organization membership role'
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
membershipOrg: membershipToChangeRole
|
membershipOrg: membershipToChangeRole
|
||||||
@@ -100,103 +85,114 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
||||||
let invitee, inviteeMembershipOrg, completeInviteLink;
|
let invitee, inviteeMembershipOrg, completeInviteLink;
|
||||||
try {
|
const { organizationId, inviteeEmail } = req.body;
|
||||||
const { organizationId, inviteeEmail } = req.body;
|
const host = req.headers.host;
|
||||||
const host = req.headers.host;
|
const siteUrl = `${req.protocol}://${host}`;
|
||||||
const siteUrl = `${req.protocol}://${host}`;
|
|
||||||
|
|
||||||
// validate membership
|
// validate membership
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership');
|
throw new Error('Failed to validate organization membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(organizationId);
|
||||||
|
|
||||||
|
if (plan.memberLimit !== null) {
|
||||||
|
// case: limit imposed on number of members allowed
|
||||||
|
|
||||||
|
if (plan.membersUsed >= plan.memberLimit) {
|
||||||
|
// case: number of members used exceeds the number of members allowed
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
invitee = await User.findOne({
|
invitee = await User.findOne({
|
||||||
email: inviteeEmail
|
email: inviteeEmail
|
||||||
}).select('+publicKey');
|
}).select('+publicKey');
|
||||||
|
|
||||||
if (invitee) {
|
if (invitee) {
|
||||||
// case: invitee is an existing user
|
// case: invitee is an existing user
|
||||||
|
|
||||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Failed to invite an existing member of the organization'
|
'Failed to invite an existing member of the organization'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!inviteeMembershipOrg) {
|
if (!inviteeMembershipOrg) {
|
||||||
await new MembershipOrg({
|
|
||||||
user: invitee,
|
await new MembershipOrg({
|
||||||
inviteEmail: inviteeEmail,
|
user: invitee,
|
||||||
organization: organizationId,
|
inviteEmail: inviteeEmail,
|
||||||
role: MEMBER,
|
organization: organizationId,
|
||||||
status: invitee?.publicKey ? ACCEPTED : INVITED
|
role: MEMBER,
|
||||||
}).save();
|
status: INVITED
|
||||||
}
|
}).save();
|
||||||
} else {
|
}
|
||||||
// check if invitee has been invited before
|
} else {
|
||||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
// check if invitee has been invited before
|
||||||
inviteEmail: inviteeEmail,
|
inviteeMembershipOrg = await MembershipOrg.findOne({
|
||||||
organization: organizationId
|
inviteEmail: inviteeEmail,
|
||||||
});
|
organization: organizationId
|
||||||
|
});
|
||||||
|
|
||||||
if (!inviteeMembershipOrg) {
|
if (!inviteeMembershipOrg) {
|
||||||
// case: invitee has never been invited before
|
// case: invitee has never been invited before
|
||||||
|
|
||||||
|
// validate that email is not disposable
|
||||||
|
validateUserEmail(inviteeEmail);
|
||||||
|
|
||||||
await new MembershipOrg({
|
await new MembershipOrg({
|
||||||
inviteEmail: inviteeEmail,
|
inviteEmail: inviteeEmail,
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
role: MEMBER,
|
role: MEMBER,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const organization = await Organization.findOne({ _id: organizationId });
|
const organization = await Organization.findOne({ _id: organizationId });
|
||||||
|
|
||||||
if (organization) {
|
if (organization) {
|
||||||
const token = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
|
||||||
email: inviteeEmail,
|
|
||||||
organizationId: organization._id
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendMail({
|
const token = await TokenService.createToken({
|
||||||
template: 'organizationInvitation.handlebars',
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||||
subjectLine: 'Infisical organization invitation',
|
email: inviteeEmail,
|
||||||
recipients: [inviteeEmail],
|
organizationId: organization._id
|
||||||
substitutions: {
|
});
|
||||||
inviterFirstName: req.user.firstName,
|
|
||||||
inviterEmail: req.user.email,
|
|
||||||
organizationName: organization.name,
|
|
||||||
email: inviteeEmail,
|
|
||||||
token,
|
|
||||||
callback_url: getSiteURL() + '/signupinvite'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!getSmtpConfigured()) {
|
await sendMail({
|
||||||
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
|
template: 'organizationInvitation.handlebars',
|
||||||
}
|
subjectLine: 'Infisical organization invitation',
|
||||||
}
|
recipients: [inviteeEmail],
|
||||||
|
substitutions: {
|
||||||
|
inviterFirstName: req.user.firstName,
|
||||||
|
inviterEmail: req.user.email,
|
||||||
|
organizationName: organization.name,
|
||||||
|
email: inviteeEmail,
|
||||||
|
organizationId: organization._id.toString(),
|
||||||
|
token,
|
||||||
|
callback_url: (await getSiteURL()) + '/signupinvite'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({ organizationId });
|
if (!(await getSmtpConfigured())) {
|
||||||
} catch (err) {
|
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
|
||||||
Sentry.setUser(null);
|
}
|
||||||
Sentry.captureException(err);
|
}
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to send organization invite'
|
await updateSubscriptionOrgQuantity({ organizationId });
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
||||||
@@ -212,61 +208,62 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
||||||
let user, token;
|
let user;
|
||||||
try {
|
const {
|
||||||
const { email, code } = req.body;
|
email,
|
||||||
|
organizationId,
|
||||||
|
code
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
user = await User.findOne({ email }).select('+publicKey');
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED
|
status: INVITED,
|
||||||
});
|
organization: new Types.ObjectId(organizationId)
|
||||||
|
});
|
||||||
|
|
||||||
if (!membershipOrg)
|
if (!membershipOrg)
|
||||||
throw new Error('Failed to find any invitations for email');
|
throw new Error('Failed to find any invitations for email');
|
||||||
|
|
||||||
await TokenService.validateToken({
|
await TokenService.validateToken({
|
||||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
type: TOKEN_EMAIL_ORG_INVITATION,
|
||||||
email,
|
email,
|
||||||
organizationId: membershipOrg.organization,
|
organizationId: membershipOrg.organization,
|
||||||
token: code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
// membership can be approved and redirected to login/dashboard
|
// membership can be approved and redirected to login/dashboard
|
||||||
membershipOrg.status = ACCEPTED;
|
membershipOrg.status = ACCEPTED;
|
||||||
await membershipOrg.save();
|
await membershipOrg.save();
|
||||||
|
|
||||||
|
await updateSubscriptionOrgQuantity({
|
||||||
|
organizationId
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully verified email',
|
message: 'Successfully verified email',
|
||||||
user,
|
user,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// initialize user account
|
// initialize user account
|
||||||
user = await new User({
|
user = await new User({
|
||||||
email
|
email
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
// generate temporary signup token
|
// generate temporary signup token
|
||||||
token = createToken({
|
const token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId: user._id.toString()
|
userId: user._id.toString()
|
||||||
},
|
},
|
||||||
expiresIn: getJwtSignupLifetime(),
|
expiresIn: await getJwtSignupLifetime(),
|
||||||
secret: getJwtSignupSecret()
|
secret: await getJwtSignupSecret()
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
error: 'Failed email magic link verification for organization invitation'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully verified email',
|
message: 'Successfully verified email',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import {
|
import {
|
||||||
@@ -15,20 +14,12 @@ import _ from 'lodash';
|
|||||||
import { getStripeSecretKey, getSiteURL } from '../../config';
|
import { getStripeSecretKey, getSiteURL } from '../../config';
|
||||||
|
|
||||||
export const getOrganizations = async (req: Request, res: Response) => {
|
export const getOrganizations = async (req: Request, res: Response) => {
|
||||||
let organizations;
|
const organizations = (
|
||||||
try {
|
await MembershipOrg.find({
|
||||||
organizations = (
|
user: req.user._id,
|
||||||
await MembershipOrg.find({
|
status: ACCEPTED
|
||||||
user: req.user._id
|
}).populate('organization')
|
||||||
}).populate('organization')
|
).map((m) => m.organization);
|
||||||
).map((m) => m.organization);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get organizations'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
organizations
|
organizations
|
||||||
@@ -43,33 +34,24 @@ export const getOrganizations = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createOrganization = async (req: Request, res: Response) => {
|
export const createOrganization = async (req: Request, res: Response) => {
|
||||||
let organization;
|
const { organizationName } = req.body;
|
||||||
try {
|
|
||||||
const { organizationName } = req.body;
|
|
||||||
|
|
||||||
if (organizationName.length < 1) {
|
if (organizationName.length < 1) {
|
||||||
throw new Error('Organization names must be at least 1-character long');
|
throw new Error('Organization names must be at least 1-character long');
|
||||||
}
|
}
|
||||||
|
|
||||||
// create organization and add user as member
|
// create organization and add user as member
|
||||||
organization = await create({
|
const organization = await create({
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
name: organizationName
|
name: organizationName
|
||||||
});
|
});
|
||||||
|
|
||||||
await addMembershipsOrg({
|
await addMembershipsOrg({
|
||||||
userIds: [req.user._id.toString()],
|
userIds: [req.user._id.toString()],
|
||||||
organizationId: organization._id.toString(),
|
organizationId: organization._id.toString(),
|
||||||
roles: [OWNER],
|
roles: [OWNER],
|
||||||
statuses: [ACCEPTED]
|
statuses: [ACCEPTED]
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to create organization'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
organization
|
organization
|
||||||
@@ -83,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getOrganization = async (req: Request, res: Response) => {
|
export const getOrganization = async (req: Request, res: Response) => {
|
||||||
let organization;
|
const organization = req.organization
|
||||||
try {
|
|
||||||
organization = req.membershipOrg.organization;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to find organization'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
organization
|
organization
|
||||||
});
|
});
|
||||||
@@ -106,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
||||||
let users;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
users = await MembershipOrg.find({
|
const users = await MembershipOrg.find({
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).populate('user', '+publicKey');
|
}).populate('user', '+publicKey');
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get organization members'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
users
|
users
|
||||||
@@ -136,35 +99,26 @@ export const getOrganizationWorkspaces = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let workspaces;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
const workspacesSet = new Set(
|
const workspacesSet = new Set(
|
||||||
(
|
(
|
||||||
await Workspace.find(
|
await Workspace.find(
|
||||||
{
|
{
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
},
|
},
|
||||||
'_id'
|
'_id'
|
||||||
)
|
)
|
||||||
).map((w) => w._id.toString())
|
).map((w) => w._id.toString())
|
||||||
);
|
);
|
||||||
|
|
||||||
workspaces = (
|
const workspaces = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
user: req.user._id
|
user: req.user._id
|
||||||
}).populate('workspace')
|
}).populate('workspace')
|
||||||
)
|
)
|
||||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||||
.map((m) => m.workspace);
|
.map((m) => m.workspace);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get my workspaces'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspaces
|
workspaces
|
||||||
@@ -178,29 +132,20 @@ export const getOrganizationWorkspaces = async (
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeOrganizationName = async (req: Request, res: Response) => {
|
export const changeOrganizationName = async (req: Request, res: Response) => {
|
||||||
let organization;
|
const { organizationId } = req.params;
|
||||||
try {
|
const { name } = req.body;
|
||||||
const { organizationId } = req.params;
|
|
||||||
const { name } = req.body;
|
|
||||||
|
|
||||||
organization = await Organization.findOneAndUpdate(
|
const organization = await Organization.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: organizationId
|
_id: organizationId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name
|
name
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to change organization name'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully changed organization name',
|
message: 'Successfully changed organization name',
|
||||||
@@ -218,20 +163,11 @@ export const getOrganizationIncidentContacts = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let incidentContactsOrg;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
incidentContactsOrg = await IncidentContactOrg.find({
|
const incidentContactsOrg = await IncidentContactOrg.find({
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get organization incident contacts'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
incidentContactsOrg
|
incidentContactsOrg
|
||||||
@@ -248,23 +184,14 @@ export const addOrganizationIncidentContact = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let incidentContactOrg;
|
const { organizationId } = req.params;
|
||||||
try {
|
const { email } = req.body;
|
||||||
const { organizationId } = req.params;
|
|
||||||
const { email } = req.body;
|
|
||||||
|
|
||||||
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
||||||
{ email, organization: organizationId },
|
{ email, organization: organizationId },
|
||||||
{ email, organization: organizationId },
|
{ email, organization: organizationId },
|
||||||
{ upsert: true, new: true }
|
{ upsert: true, new: true }
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to add incident contact for organization'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
incidentContactOrg
|
incidentContactOrg
|
||||||
@@ -281,22 +208,13 @@ export const deleteOrganizationIncidentContact = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let incidentContactOrg;
|
const { organizationId } = req.params;
|
||||||
try {
|
const { email } = req.body;
|
||||||
const { organizationId } = req.params;
|
|
||||||
const { email } = req.body;
|
|
||||||
|
|
||||||
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
||||||
email,
|
email,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete organization incident contact'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully deleted organization incident contact',
|
message: 'Successfully deleted organization incident contact',
|
||||||
@@ -316,41 +234,33 @@ export const createOrganizationPortalSession = async (
|
|||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let session;
|
let session;
|
||||||
try {
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
const stripe = new Stripe(getStripeSecretKey(), {
|
apiVersion: '2022-08-01'
|
||||||
apiVersion: '2022-08-01'
|
});
|
||||||
});
|
|
||||||
|
|
||||||
// check if there is a payment method on file
|
// check if there is a payment method on file
|
||||||
const paymentMethods = await stripe.paymentMethods.list({
|
const paymentMethods = await stripe.paymentMethods.list({
|
||||||
customer: req.membershipOrg.organization.customerId,
|
customer: req.organization.customerId,
|
||||||
type: 'card'
|
type: 'card'
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (paymentMethods.data.length < 1) {
|
||||||
|
// case: no payment method on file
|
||||||
|
session = await stripe.checkout.sessions.create({
|
||||||
|
customer: req.organization.customerId,
|
||||||
|
mode: 'setup',
|
||||||
|
payment_method_types: ['card'],
|
||||||
|
success_url: (await getSiteURL()) + '/dashboard',
|
||||||
|
cancel_url: (await getSiteURL()) + '/dashboard'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
session = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: req.organization.customerId,
|
||||||
|
return_url: (await getSiteURL()) + '/dashboard'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (paymentMethods.data.length < 1) {
|
return res.status(200).send({ url: session.url });
|
||||||
// case: no payment method on file
|
|
||||||
session = await stripe.checkout.sessions.create({
|
|
||||||
customer: req.membershipOrg.organization.customerId,
|
|
||||||
mode: 'setup',
|
|
||||||
payment_method_types: ['card'],
|
|
||||||
success_url: getSiteURL() + '/dashboard',
|
|
||||||
cancel_url: getSiteURL() + '/dashboard'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
session = await stripe.billingPortal.sessions.create({
|
|
||||||
customer: req.membershipOrg.organization.customerId,
|
|
||||||
return_url: getSiteURL() + '/dashboard'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({ url: session.url });
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to redirect to organization billing portal'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -363,22 +273,13 @@ export const getOrganizationSubscriptions = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let subscriptions;
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
try {
|
apiVersion: '2022-08-01'
|
||||||
const stripe = new Stripe(getStripeSecretKey(), {
|
});
|
||||||
apiVersion: '2022-08-01'
|
|
||||||
});
|
const subscriptions = await stripe.subscriptions.list({
|
||||||
|
customer: req.organization.customerId
|
||||||
subscriptions = await stripe.subscriptions.list({
|
});
|
||||||
customer: req.membershipOrg.organization.customerId
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get organization subscriptions'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
subscriptions
|
subscriptions
|
||||||
@@ -424,4 +325,4 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
return res.json(userToWorkspaceIds);
|
return res.json(userToWorkspaceIds);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
|
||||||
import { createToken } from '../../helpers/auth';
|
import {
|
||||||
import { sendMail } from '../../helpers/nodemailer';
|
createToken,
|
||||||
|
sendMail,
|
||||||
|
clearTokens
|
||||||
|
} from '../../helpers';
|
||||||
import { TokenService } from '../../services';
|
import { TokenService } from '../../services';
|
||||||
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
|
import {
|
||||||
|
TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
|
AUTH_MODE_JWT
|
||||||
|
} from '../../variables';
|
||||||
import { BadRequestError } from '../../utils/errors';
|
import { BadRequestError } from '../../utils/errors';
|
||||||
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
|
import {
|
||||||
|
getSiteURL,
|
||||||
|
getJwtSignupLifetime,
|
||||||
|
getJwtSignupSecret,
|
||||||
|
getHttpsEnabled
|
||||||
|
} from '../../config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Password reset step 1: Send email verification link to email [email]
|
* Password reset step 1: Send email verification link to email [email]
|
||||||
@@ -20,43 +30,35 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
|
|||||||
*/
|
*/
|
||||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
export const emailPasswordReset = async (req: Request, res: Response) => {
|
||||||
let email: string;
|
let email: string;
|
||||||
try {
|
email = req.body.email;
|
||||||
email = req.body.email;
|
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select('+publicKey');
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (!user || !user?.publicKey) {
|
if (!user || !user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
|
|
||||||
return res.status(403).send({
|
return res.status(200).send({
|
||||||
error: 'Failed to send email verification for password reset'
|
message:"If an account exists with this email, a password reset link has been sent"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = await TokenService.createToken({
|
const token = await TokenService.createToken({
|
||||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'passwordReset.handlebars',
|
template: 'passwordReset.handlebars',
|
||||||
subjectLine: 'Infisical password reset',
|
subjectLine: 'Infisical password reset',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
email,
|
email,
|
||||||
token,
|
token,
|
||||||
callback_url: getSiteURL() + '/password-reset'
|
callback_url: (await getSiteURL()) + '/password-reset'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to send email for account recovery'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: `Sent an email for account recovery to ${email}`
|
message:"If an account exists with this email, a password reset link has been sent"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,40 +69,31 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
||||||
let user, token;
|
const { email, code } = req.body;
|
||||||
try {
|
|
||||||
const { email, code } = req.body;
|
|
||||||
|
|
||||||
user = await User.findOne({ email }).select('+publicKey');
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (!user || !user?.publicKey) {
|
if (!user || !user?.publicKey) {
|
||||||
// case: user doesn't exist with email [email] or
|
// case: user doesn't exist with email [email] or
|
||||||
// hasn't even completed their account
|
// hasn't even completed their account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed email verification for password reset'
|
error: 'Failed email verification for password reset'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await TokenService.validateToken({
|
await TokenService.validateToken({
|
||||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
type: TOKEN_EMAIL_PASSWORD_RESET,
|
||||||
email,
|
email,
|
||||||
token: code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
// generate temporary password-reset token
|
// generate temporary password-reset token
|
||||||
token = createToken({
|
const token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
userId: user._id.toString()
|
userId: user._id.toString()
|
||||||
},
|
},
|
||||||
expiresIn: getJwtSignupLifetime(),
|
expiresIn: await getJwtSignupLifetime(),
|
||||||
secret: getJwtSignupSecret()
|
secret: await getJwtSignupSecret()
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed email verification for password reset'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully verified email',
|
message: 'Successfully verified email',
|
||||||
@@ -117,44 +110,38 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const srp1 = async (req: Request, res: Response) => {
|
export const srp1 = async (req: Request, res: Response) => {
|
||||||
// return salt, serverPublicKey as part of first step of SRP protocol
|
// return salt, serverPublicKey as part of first step of SRP protocol
|
||||||
try {
|
|
||||||
const { clientPublicKey } = req.body;
|
const { clientPublicKey } = req.body;
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
|
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
|
||||||
email: req.user.email,
|
email: req.user.email,
|
||||||
clientPublicKey: clientPublicKey,
|
clientPublicKey: clientPublicKey,
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
}, { upsert: true, returnNewDocument: false })
|
}, { upsert: true, returnNewDocument: false })
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serverPublicKey,
|
|
||||||
salt: user.salt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
error: 'Failed to start change password process'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change account SRP authentication information for user
|
* Change account SRP authentication information for user
|
||||||
@@ -165,80 +152,85 @@ export const srp1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changePassword = async (req: Request, res: Response) => {
|
export const changePassword = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
clientProof,
|
||||||
clientProof,
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier
|
||||||
verifier
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email: req.user.email
|
email: req.user.email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
if (!loginSRPDetailFromDB) {
|
||||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// change password
|
// change password
|
||||||
|
|
||||||
await User.findByIdAndUpdate(
|
await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
|
||||||
|
await clearTokens(req.authData.tokenVersionId)
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
// clear httpOnly cookie
|
||||||
message: 'Successfully changed password'
|
|
||||||
});
|
res.cookie('jid', '', {
|
||||||
}
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: (await getHttpsEnabled()) as boolean
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(400).send({
|
return res.status(200).send({
|
||||||
error: 'Failed to change password. Try again?'
|
message: 'Successfully changed password'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} catch (err) {
|
return res.status(400).send({
|
||||||
Sentry.setUser({ email: req.user.email });
|
error: 'Failed to change password. Try again?'
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
error: 'Failed to change password. Try again?'
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -252,69 +244,61 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
// requires verifying [clientProof] as part of second step of SRP protocol
|
// requires verifying [clientProof] as part of second step of SRP protocol
|
||||||
// as initiated in /srp1
|
// as initiated in /srp1
|
||||||
|
|
||||||
try {
|
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
||||||
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
|
req.body;
|
||||||
req.body;
|
const user = await User.findOne({
|
||||||
const user = await User.findOne({
|
email: req.user.email
|
||||||
email: req.user.email
|
}).select('+salt +verifier');
|
||||||
}).select('+salt +verifier');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
if (!loginSRPDetailFromDB) {
|
||||||
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
|
||||||
}
|
}
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier,
|
verifier: user.verifier,
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(
|
server.setClientPublicKey(
|
||||||
loginSRPDetailFromDB.clientPublicKey
|
loginSRPDetailFromDB.clientPublicKey
|
||||||
);
|
);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
// create new or replace backup private key
|
// create new or replace backup private key
|
||||||
|
|
||||||
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
||||||
{ user: req.user._id },
|
{ user: req.user._id },
|
||||||
{
|
{
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
{ upsert: true, new: true }
|
{ upsert: true, new: true }
|
||||||
).select('+user, encryptedPrivateKey');
|
).select('+user, encryptedPrivateKey');
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully updated backup private key',
|
message: 'Successfully updated backup private key',
|
||||||
backupPrivateKey
|
backupPrivateKey
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to update backup private key'
|
message: 'Failed to update backup private key'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to update backup private key'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -324,20 +308,11 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
||||||
let backupPrivateKey;
|
const backupPrivateKey = await BackupPrivateKey.findOne({
|
||||||
try {
|
user: req.user._id
|
||||||
backupPrivateKey = await BackupPrivateKey.findOne({
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
user: req.user._id
|
|
||||||
}).select('+encryptedPrivateKey +iv +tag');
|
|
||||||
|
|
||||||
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get backup private key'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
backupPrivateKey
|
backupPrivateKey
|
||||||
@@ -345,44 +320,36 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const resetPassword = async (req: Request, res: Response) => {
|
export const resetPassword = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier,
|
||||||
verifier,
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
await User.findByIdAndUpdate(
|
await User.findByIdAndUpdate(
|
||||||
req.user._id.toString(),
|
req.user._id.toString(),
|
||||||
{
|
{
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
iv: encryptedPrivateKeyIV,
|
iv: encryptedPrivateKeyIV,
|
||||||
tag: encryptedPrivateKeyTag,
|
tag: encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get backup private key'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully reset password'
|
message: 'Successfully reset password'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import { Types } from 'mongoose';
|
||||||
import { Key, Secret } from '../../models';
|
import { Key, Secret } from '../../models';
|
||||||
import {
|
import {
|
||||||
v1PushSecrets as push,
|
v1PushSecrets as push,
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
import { pushKeys } from '../../helpers/key';
|
import { pushKeys } from '../../helpers/key';
|
||||||
import { eventPushSecrets } from '../../events';
|
import { eventPushSecrets } from '../../events';
|
||||||
import { EventService } from '../../services';
|
import { EventService } from '../../services';
|
||||||
import { getPostHogClient } from '../../services';
|
import { TelemetryService } from '../../services';
|
||||||
|
|
||||||
interface PushSecret {
|
interface PushSecret {
|
||||||
ciphertextKey: string;
|
ciphertextKey: string;
|
||||||
@@ -36,65 +36,56 @@ interface PushSecret {
|
|||||||
*/
|
*/
|
||||||
export const pushSecrets = async (req: Request, res: Response) => {
|
export const pushSecrets = async (req: Request, res: Response) => {
|
||||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
|
let { secrets }: { secrets: PushSecret[] } = req.body;
|
||||||
|
const { keys, environment, channel } = req.body;
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
try {
|
// validate environment
|
||||||
const postHogClient = getPostHogClient();
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
const { keys, environment, channel } = req.body;
|
throw new Error('Failed to validate environment');
|
||||||
const { workspaceId } = req.params;
|
}
|
||||||
|
|
||||||
// validate environment
|
// sanitize secrets
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
secrets = secrets.filter(
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
|
||||||
throw new Error('Failed to validate environment');
|
);
|
||||||
}
|
|
||||||
|
|
||||||
// sanitize secrets
|
await push({
|
||||||
secrets = secrets.filter(
|
userId: req.user._id,
|
||||||
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
|
workspaceId,
|
||||||
);
|
environment,
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
|
||||||
await push({
|
await pushKeys({
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
keys
|
||||||
secrets
|
});
|
||||||
});
|
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pushed',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await pushKeys({
|
// trigger event - push secrets
|
||||||
userId: req.user._id,
|
EventService.handleEvent({
|
||||||
workspaceId,
|
event: eventPushSecrets({
|
||||||
keys
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
});
|
environment
|
||||||
|
})
|
||||||
|
});
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: 'secrets pushed',
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: channel ? channel : 'cli'
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to upload workspace secrets'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully uploaded workspace secrets'
|
message: 'Successfully uploaded workspace secrets'
|
||||||
@@ -111,55 +102,48 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
export const pullSecrets = async (req: Request, res: Response) => {
|
export const pullSecrets = async (req: Request, res: Response) => {
|
||||||
let secrets;
|
let secrets;
|
||||||
let key;
|
let key;
|
||||||
try {
|
|
||||||
const postHogClient = getPostHogClient();
|
|
||||||
const environment: string = req.query.environment as string;
|
|
||||||
const channel: string = req.query.channel as string;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const environment: string = req.query.environment as string;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
const channel: string = req.query.channel as string;
|
||||||
throw new Error('Failed to validate environment');
|
const { workspaceId } = req.params;
|
||||||
}
|
|
||||||
|
|
||||||
secrets = await pull({
|
// validate environment
|
||||||
userId: req.user._id.toString(),
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
workspaceId,
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
environment,
|
throw new Error('Failed to validate environment');
|
||||||
channel: channel ? channel : 'cli',
|
}
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
|
||||||
|
|
||||||
key = await Key.findOne({
|
secrets = await pull({
|
||||||
workspace: workspaceId,
|
userId: req.user._id.toString(),
|
||||||
receiver: req.user._id
|
workspaceId,
|
||||||
})
|
environment,
|
||||||
.sort({ createdAt: -1 })
|
channel: channel ? channel : 'cli',
|
||||||
.populate('sender', '+publicKey');
|
ipAddress: req.realIP
|
||||||
|
});
|
||||||
if (channel !== 'cli') {
|
|
||||||
secrets = reformatPullSecrets({ secrets });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postHogClient) {
|
key = await Key.findOne({
|
||||||
// capture secrets pushed event in production
|
workspace: workspaceId,
|
||||||
postHogClient.capture({
|
receiver: req.user._id
|
||||||
distinctId: req.user.email,
|
})
|
||||||
event: 'secrets pulled',
|
.sort({ createdAt: -1 })
|
||||||
properties: {
|
.populate('sender', '+publicKey');
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
if (channel !== 'cli') {
|
||||||
workspaceId,
|
secrets = reformatPullSecrets({ secrets });
|
||||||
channel: channel ? channel : 'cli'
|
}
|
||||||
}
|
|
||||||
});
|
if (postHogClient) {
|
||||||
}
|
// capture secrets pushed event in production
|
||||||
} catch (err) {
|
postHogClient.capture({
|
||||||
Sentry.setUser({ email: req.user.email });
|
distinctId: req.user.email,
|
||||||
Sentry.captureException(err);
|
event: 'secrets pulled',
|
||||||
return res.status(400).send({
|
properties: {
|
||||||
message: 'Failed to pull workspace secrets'
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,54 +164,47 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
|||||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||||
let secrets;
|
let secrets;
|
||||||
let key;
|
let key;
|
||||||
try {
|
|
||||||
const postHogClient = getPostHogClient();
|
|
||||||
const environment: string = req.query.environment as string;
|
|
||||||
const channel: string = req.query.channel as string;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const environment: string = req.query.environment as string;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
const channel: string = req.query.channel as string;
|
||||||
throw new Error('Failed to validate environment');
|
const { workspaceId } = req.params;
|
||||||
}
|
|
||||||
|
|
||||||
secrets = await pull({
|
// validate environment
|
||||||
userId: req.serviceToken.user._id.toString(),
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
workspaceId,
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
environment,
|
throw new Error('Failed to validate environment');
|
||||||
channel: 'cli',
|
}
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
|
||||||
|
|
||||||
key = {
|
secrets = await pull({
|
||||||
encryptedKey: req.serviceToken.encryptedKey,
|
userId: req.serviceToken.user._id.toString(),
|
||||||
nonce: req.serviceToken.nonce,
|
workspaceId,
|
||||||
sender: {
|
environment,
|
||||||
publicKey: req.serviceToken.publicKey
|
channel: 'cli',
|
||||||
},
|
ipAddress: req.realIP
|
||||||
receiver: req.serviceToken.user,
|
});
|
||||||
workspace: req.serviceToken.workspace
|
|
||||||
};
|
|
||||||
|
|
||||||
if (postHogClient) {
|
key = {
|
||||||
// capture secrets pulled event in production
|
encryptedKey: req.serviceToken.encryptedKey,
|
||||||
postHogClient.capture({
|
nonce: req.serviceToken.nonce,
|
||||||
distinctId: req.serviceToken.user.email,
|
sender: {
|
||||||
event: 'secrets pulled',
|
publicKey: req.serviceToken.publicKey
|
||||||
properties: {
|
},
|
||||||
numberOfSecrets: secrets.length,
|
receiver: req.serviceToken.user,
|
||||||
environment,
|
workspace: req.serviceToken.workspace
|
||||||
workspaceId,
|
};
|
||||||
channel: channel ? channel : 'cli'
|
|
||||||
}
|
if (postHogClient) {
|
||||||
});
|
// capture secrets pulled event in production
|
||||||
}
|
postHogClient.capture({
|
||||||
} catch (err) {
|
distinctId: req.serviceToken.user.email,
|
||||||
Sentry.setUser({ email: req.serviceToken.user.email });
|
event: 'secrets pulled',
|
||||||
Sentry.captureException(err);
|
properties: {
|
||||||
return res.status(400).send({
|
numberOfSecrets: secrets.length,
|
||||||
message: 'Failed to pull workspace secrets'
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -235,4 +212,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
secrets: reformatPullSecrets({ secrets }),
|
secrets: reformatPullSecrets({ secrets }),
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { Secret } from "../../models";
|
||||||
|
import Folder from "../../models/folder";
|
||||||
|
import { BadRequestError } from "../../utils/errors";
|
||||||
|
import {
|
||||||
|
appendFolder,
|
||||||
|
deleteFolderById,
|
||||||
|
getAllFolderIds,
|
||||||
|
searchByFolderIdWithDir,
|
||||||
|
searchByFolderId,
|
||||||
|
validateFolderName,
|
||||||
|
generateFolderId,
|
||||||
|
getParentFromFolderId,
|
||||||
|
} from "../../services/FolderService";
|
||||||
|
import { ADMIN, MEMBER } from "../../variables";
|
||||||
|
import { validateMembership } from "../../helpers/membership";
|
||||||
|
import { FolderVersion } from "../../ee/models";
|
||||||
|
import { EESecretService } from "../../ee/services";
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
// verify workspace id/environment
|
||||||
|
export const createFolder = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environment, folderName, parentFolderId } = req.body;
|
||||||
|
if (!validateFolderName(folderName)) {
|
||||||
|
throw BadRequestError({
|
||||||
|
message: "Folder name cannot contain spaces. Only underscore and dashes",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const folders = await Folder.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
}).lean();
|
||||||
|
// space has no folders initialized
|
||||||
|
if (!folders) {
|
||||||
|
const id = generateFolderId();
|
||||||
|
const folder = new Folder({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
nodes: {
|
||||||
|
id: "root",
|
||||||
|
name: "root",
|
||||||
|
version: 1,
|
||||||
|
children: [{ id, name: folderName, children: [], version: 1 }],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
await folder.save();
|
||||||
|
const folderVersion = new FolderVersion({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
nodes: folder.nodes,
|
||||||
|
});
|
||||||
|
await folderVersion.save();
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
return res.json({ folder: { id, name: folderName } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
|
||||||
|
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||||
|
|
||||||
|
const parentFolder = searchByFolderId(folders.nodes, parentFolderId);
|
||||||
|
const folderVersion = new FolderVersion({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
nodes: parentFolder,
|
||||||
|
});
|
||||||
|
await folderVersion.save();
|
||||||
|
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: parentFolderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({ folder });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateFolderById = async (req: Request, res: Response) => {
|
||||||
|
const { folderId } = req.params;
|
||||||
|
const { name, workspaceId, environment } = req.body;
|
||||||
|
|
||||||
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
|
if (!folders) {
|
||||||
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that user is a member of the workspace
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
|
||||||
|
if (!parentFolder) {
|
||||||
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
|
}
|
||||||
|
const folder = parentFolder.children.find(({ id }) => id === folderId);
|
||||||
|
if (!folder) {
|
||||||
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
parentFolder.version += 1;
|
||||||
|
folder.name = name;
|
||||||
|
|
||||||
|
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||||
|
const folderVersion = new FolderVersion({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
nodes: parentFolder,
|
||||||
|
});
|
||||||
|
await folderVersion.save();
|
||||||
|
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: parentFolder.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.json({
|
||||||
|
message: "Successfully updated folder",
|
||||||
|
folder: { name: folder.name, id: folder.id },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteFolder = async (req: Request, res: Response) => {
|
||||||
|
const { folderId } = req.params;
|
||||||
|
const { workspaceId, environment } = req.body;
|
||||||
|
|
||||||
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
|
if (!folders) {
|
||||||
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that user is a member of the workspace
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
});
|
||||||
|
|
||||||
|
const delOp = deleteFolderById(folders.nodes, folderId);
|
||||||
|
if (!delOp) {
|
||||||
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
|
}
|
||||||
|
const { deletedNode: delFolder, parent: parentFolder } = delOp;
|
||||||
|
|
||||||
|
parentFolder.version += 1;
|
||||||
|
const delFolderIds = getAllFolderIds(delFolder);
|
||||||
|
|
||||||
|
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||||
|
const folderVersion = new FolderVersion({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
nodes: parentFolder,
|
||||||
|
});
|
||||||
|
await folderVersion.save();
|
||||||
|
if (delFolderIds.length) {
|
||||||
|
await Secret.deleteMany({
|
||||||
|
folder: { $in: delFolderIds.map(({ id }) => id) },
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: parentFolder.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO: validate workspace
|
||||||
|
export const getFolders = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environment, parentFolderId } = req.query as {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
parentFolderId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
|
if (!folders) {
|
||||||
|
res.send({ folders: [], dir: [] });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check that user is a member of the workspace
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!parentFolderId) {
|
||||||
|
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
}));
|
||||||
|
res.send({ folders: rootFolders });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const folderBySearch = searchByFolderIdWithDir(folders.nodes, parentFolderId);
|
||||||
|
if (!folderBySearch) {
|
||||||
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
|
}
|
||||||
|
const { folder, dir } = folderBySearch;
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
folders: folder.children.map(({ id, name }) => ({ id, name })),
|
||||||
|
dir,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
|||||||
workspaceId
|
workspaceId
|
||||||
},
|
},
|
||||||
expiresIn: expiresIn,
|
expiresIn: expiresIn,
|
||||||
secret: getJwtServiceSecret()
|
secret: await getJwtServiceSecret()
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { User } from '../../models';
|
import { User } from '../../models';
|
||||||
import {
|
import {
|
||||||
sendEmailVerification,
|
sendEmailVerification,
|
||||||
@@ -8,6 +7,7 @@ import {
|
|||||||
import { createToken } from '../../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { BadRequestError } from '../../utils/errors';
|
import { BadRequestError } from '../../utils/errors';
|
||||||
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
|
||||||
|
import { validateUserEmail } from '../../validation';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
||||||
@@ -18,35 +18,22 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
|
|||||||
*/
|
*/
|
||||||
export const beginEmailSignup = async (req: Request, res: Response) => {
|
export const beginEmailSignup = async (req: Request, res: Response) => {
|
||||||
let email: string;
|
let email: string;
|
||||||
try {
|
email = req.body.email;
|
||||||
email = req.body.email;
|
|
||||||
|
// validate that email is not disposable
|
||||||
|
validateUserEmail(email);
|
||||||
|
|
||||||
if (getInviteOnlySignup()) {
|
const user = await User.findOne({ email }).select('+publicKey');
|
||||||
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
if (user && user?.publicKey) {
|
||||||
const userCount = await User.countDocuments({})
|
// case: user has already completed account
|
||||||
if (userCount != 0) {
|
|
||||||
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select('+publicKey');
|
return res.status(403).send({
|
||||||
if (user && user?.publicKey) {
|
error: 'Failed to send email verification code for complete account'
|
||||||
// case: user has already completed account
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(403).send({
|
// send send verification email
|
||||||
error: 'Failed to send email verification code for complete account'
|
await sendEmailVerification({ email });
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// send send verification email
|
|
||||||
await sendEmailVerification({ email });
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
error: 'Failed to send email verification code'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: `Sent an email verification code to ${email}`
|
message: `Sent an email verification code to ${email}`
|
||||||
@@ -62,47 +49,47 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
||||||
let user, token;
|
let user, token;
|
||||||
try {
|
const { email, code } = req.body;
|
||||||
const { email, code } = req.body;
|
|
||||||
|
|
||||||
// initialize user account
|
// initialize user account
|
||||||
user = await User.findOne({ email }).select('+publicKey');
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (user && user?.publicKey) {
|
if (user && user?.publicKey) {
|
||||||
// case: user has already completed account
|
// case: user has already completed account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed email verification for complete user'
|
error: 'Failed email verification for complete user'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// verify email
|
if (await getInviteOnlySignup()) {
|
||||||
if (getSmtpConfigured()) {
|
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
|
||||||
await checkEmailVerification({
|
const userCount = await User.countDocuments({})
|
||||||
email,
|
if (userCount != 0) {
|
||||||
code
|
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!user) {
|
// verify email
|
||||||
user = await new User({
|
if (await getSmtpConfigured()) {
|
||||||
email
|
await checkEmailVerification({
|
||||||
}).save();
|
email,
|
||||||
}
|
code
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// generate temporary signup token
|
if (!user) {
|
||||||
token = createToken({
|
user = await new User({
|
||||||
payload: {
|
email
|
||||||
userId: user._id.toString()
|
}).save();
|
||||||
},
|
}
|
||||||
expiresIn: getJwtSignupLifetime(),
|
|
||||||
secret: getJwtSignupSecret()
|
// generate temporary signup token
|
||||||
});
|
token = createToken({
|
||||||
} catch (err) {
|
payload: {
|
||||||
Sentry.setUser(null);
|
userId: user._id.toString()
|
||||||
Sentry.captureException(err);
|
},
|
||||||
return res.status(400).send({
|
expiresIn: await getJwtSignupLifetime(),
|
||||||
error: 'Failed email verification'
|
secret: await getJwtSignupSecret()
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfuly verified email',
|
message: 'Successfuly verified email',
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
||||||
|
|
||||||
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const handleWebhook = async (req: Request, res: Response) => {
|
export const handleWebhook = async (req: Request, res: Response) => {
|
||||||
let event;
|
// check request for valid stripe signature
|
||||||
try {
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
// check request for valid stripe signature
|
apiVersion: '2022-08-01'
|
||||||
const stripe = new Stripe(getStripeSecretKey(), {
|
});
|
||||||
apiVersion: '2022-08-01'
|
|
||||||
});
|
|
||||||
|
|
||||||
const sig = req.headers['stripe-signature'] as string;
|
const sig = req.headers['stripe-signature'] as string;
|
||||||
event = stripe.webhooks.constructEvent(
|
const event = stripe.webhooks.constructEvent(
|
||||||
req.body,
|
req.body,
|
||||||
sig,
|
sig,
|
||||||
getStripeWebhookSecret()
|
await getStripeWebhookSecret()
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
error: 'Failed to process webhook'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case '':
|
case '':
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { UserAction } from '../../models';
|
import { UserAction } from '../../models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -11,28 +10,19 @@ import { UserAction } from '../../models';
|
|||||||
export const addUserAction = async (req: Request, res: Response) => {
|
export const addUserAction = async (req: Request, res: Response) => {
|
||||||
// add/record new action [action] for user with id [req.user._id]
|
// add/record new action [action] for user with id [req.user._id]
|
||||||
|
|
||||||
let userAction;
|
const { action } = req.body;
|
||||||
try {
|
|
||||||
const { action } = req.body;
|
|
||||||
|
|
||||||
userAction = await UserAction.findOneAndUpdate(
|
const userAction = await UserAction.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
action
|
action
|
||||||
},
|
},
|
||||||
{ user: req.user._id, action },
|
{ user: req.user._id, action },
|
||||||
{
|
{
|
||||||
new: true,
|
new: true,
|
||||||
upsert: true
|
upsert: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to record user action'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully recorded user action',
|
message: 'Successfully recorded user action',
|
||||||
@@ -48,21 +38,12 @@ export const addUserAction = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const getUserAction = async (req: Request, res: Response) => {
|
export const getUserAction = async (req: Request, res: Response) => {
|
||||||
// get user action [action] for user with id [req.user._id]
|
// get user action [action] for user with id [req.user._id]
|
||||||
let userAction;
|
const action: string = req.query.action as string;
|
||||||
try {
|
|
||||||
const action: string = req.query.action as string;
|
|
||||||
|
|
||||||
userAction = await UserAction.findOne({
|
const userAction = await UserAction.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
action
|
action
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get user action'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
userAction
|
userAction
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from "express";
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
Membership,
|
Membership,
|
||||||
@@ -14,6 +13,7 @@ import {
|
|||||||
createWorkspace as create,
|
createWorkspace as create,
|
||||||
deleteWorkspace as deleteWork,
|
deleteWorkspace as deleteWork,
|
||||||
} from "../../helpers/workspace";
|
} from "../../helpers/workspace";
|
||||||
|
import { EELicenseService } from '../../ee/services';
|
||||||
import { addMemberships } from "../../helpers/membership";
|
import { addMemberships } from "../../helpers/membership";
|
||||||
import { ADMIN } from "../../variables";
|
import { ADMIN } from "../../variables";
|
||||||
|
|
||||||
@@ -24,27 +24,18 @@ import { ADMIN } from "../../variables";
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
||||||
let publicKeys;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
publicKeys = (
|
const publicKeys = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
}).populate<{ user: IUser }>("user", "publicKey")
|
}).populate<{ user: IUser }>("user", "publicKey")
|
||||||
).map((member) => {
|
).map((member) => {
|
||||||
return {
|
return {
|
||||||
publicKey: member.user.publicKey,
|
publicKey: member.user.publicKey,
|
||||||
userId: member.user._id,
|
userId: member.user._id,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get workspace member public keys",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
publicKeys,
|
publicKeys,
|
||||||
@@ -58,20 +49,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||||
let users;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
users = await Membership.find({
|
const users = await Membership.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
}).populate("user", "+publicKey");
|
}).populate("user", "+publicKey");
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get workspace members",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
users,
|
users,
|
||||||
@@ -85,20 +67,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
export const getWorkspaces = async (req: Request, res: Response) => {
|
||||||
let workspaces;
|
const workspaces = (
|
||||||
try {
|
await Membership.find({
|
||||||
workspaces = (
|
user: req.user._id,
|
||||||
await Membership.find({
|
}).populate("workspace")
|
||||||
user: req.user._id,
|
).map((m) => m.workspace);
|
||||||
}).populate("workspace")
|
|
||||||
).map((m) => m.workspace);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get workspaces",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspaces,
|
workspaces,
|
||||||
@@ -112,20 +85,11 @@ export const getWorkspaces = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspace = async (req: Request, res: Response) => {
|
export const getWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
workspace = await Workspace.findOne({
|
const workspace = await Workspace.findOne({
|
||||||
_id: workspaceId,
|
_id: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get workspace",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspace,
|
workspace,
|
||||||
@@ -140,43 +104,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createWorkspace = async (req: Request, res: Response) => {
|
export const createWorkspace = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceName, organizationId } = req.body;
|
||||||
try {
|
|
||||||
const { workspaceName, organizationId } = req.body;
|
|
||||||
|
|
||||||
// validate organization membership
|
// validate organization membership
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
organization: organizationId,
|
organization: organizationId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error("Failed to validate organization membership");
|
throw new Error("Failed to validate organization membership");
|
||||||
}
|
|
||||||
|
|
||||||
if (workspaceName.length < 1) {
|
|
||||||
throw new Error("Workspace names must be at least 1-character long");
|
|
||||||
}
|
|
||||||
|
|
||||||
// create workspace and add user as member
|
|
||||||
workspace = await create({
|
|
||||||
name: workspaceName,
|
|
||||||
organizationId,
|
|
||||||
});
|
|
||||||
|
|
||||||
await addMemberships({
|
|
||||||
userIds: [req.user._id],
|
|
||||||
workspaceId: workspace._id.toString(),
|
|
||||||
roles: [ADMIN],
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to create workspace",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(organizationId);
|
||||||
|
|
||||||
|
if (plan.workspaceLimit !== null) {
|
||||||
|
// case: limit imposed on number of workspaces allowed
|
||||||
|
if (plan.workspacesUsed >= plan.workspaceLimit) {
|
||||||
|
// case: number of workspaces used exceeds the number of workspaces allowed
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (workspaceName.length < 1) {
|
||||||
|
throw new Error("Workspace names must be at least 1-character long");
|
||||||
|
}
|
||||||
|
|
||||||
|
// create workspace and add user as member
|
||||||
|
const workspace = await create({
|
||||||
|
name: workspaceName,
|
||||||
|
organizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
await addMemberships({
|
||||||
|
userIds: [req.user._id],
|
||||||
|
workspaceId: workspace._id.toString(),
|
||||||
|
roles: [ADMIN],
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
workspace,
|
workspace,
|
||||||
});
|
});
|
||||||
@@ -189,20 +156,12 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
export const deleteWorkspace = async (req: Request, res: Response) => {
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// delete workspace
|
// delete workspace
|
||||||
await deleteWork({
|
await deleteWork({
|
||||||
id: workspaceId,
|
id: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to delete workspace",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: "Successfully deleted workspace",
|
message: "Successfully deleted workspace",
|
||||||
@@ -216,29 +175,20 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { name } = req.body;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { name } = req.body;
|
|
||||||
|
|
||||||
workspace = await Workspace.findOneAndUpdate(
|
const workspace = await Workspace.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: workspaceId,
|
_id: workspaceId,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name,
|
name,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true,
|
new: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to change workspace name",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: "Successfully changed workspace name",
|
message: "Successfully changed workspace name",
|
||||||
@@ -253,20 +203,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
||||||
let integrations;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
integrations = await Integration.find({
|
const integrations = await Integration.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get workspace integrations",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
integrations,
|
integrations,
|
||||||
@@ -283,20 +224,11 @@ export const getWorkspaceIntegrationAuthorizations = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let authorizations;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
authorizations = await IntegrationAuth.find({
|
const authorizations = await IntegrationAuth.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get workspace integration authorizations",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
authorizations,
|
authorizations,
|
||||||
@@ -313,21 +245,12 @@ export const getWorkspaceServiceTokens = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let serviceTokens;
|
const { workspaceId } = req.params;
|
||||||
try {
|
// ?? FIX.
|
||||||
const { workspaceId } = req.params;
|
const serviceTokens = await ServiceToken.find({
|
||||||
// ?? FIX.
|
user: req.user._id,
|
||||||
serviceTokens = await ServiceToken.find({
|
workspace: workspaceId,
|
||||||
user: req.user._id,
|
});
|
||||||
workspace: workspaceId,
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get workspace service tokens",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokens,
|
serviceTokens,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
@@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getAPIKeyData = async (req: Request, res: Response) => {
|
export const getAPIKeyData = async (req: Request, res: Response) => {
|
||||||
let apiKeyData;
|
const apiKeyData = await APIKeyData.find({
|
||||||
try {
|
user: req.user._id
|
||||||
apiKeyData = await APIKeyData.find({
|
});
|
||||||
user: req.user._id
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get API key data'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apiKeyData
|
apiKeyData
|
||||||
@@ -38,38 +28,30 @@ export const getAPIKeyData = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||||
let apiKey, apiKeyData;
|
const { name, expiresIn } = req.body;
|
||||||
try {
|
|
||||||
const { name, expiresIn } = req.body;
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString('hex');
|
|
||||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
|
||||||
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
|
|
||||||
apiKeyData = await new APIKeyData({
|
|
||||||
name,
|
|
||||||
expiresAt,
|
|
||||||
user: req.user._id,
|
|
||||||
secretHash
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
// return api key data without sensitive data
|
|
||||||
apiKeyData = await APIKeyData.findById(apiKeyData._id);
|
|
||||||
|
|
||||||
if (!apiKeyData) throw new Error('Failed to find API key data');
|
|
||||||
|
|
||||||
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to API key data'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
const secret = crypto.randomBytes(16).toString('hex');
|
||||||
|
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||||
|
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||||
|
|
||||||
|
let apiKeyData = await new APIKeyData({
|
||||||
|
name,
|
||||||
|
lastUsed: new Date(),
|
||||||
|
expiresAt,
|
||||||
|
user: req.user._id,
|
||||||
|
secretHash
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// return api key data without sensitive data
|
||||||
|
// FIX: fix this any
|
||||||
|
apiKeyData = await APIKeyData.findById(apiKeyData._id) as any
|
||||||
|
|
||||||
|
if (!apiKeyData) throw new Error('Failed to find API key data');
|
||||||
|
|
||||||
|
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apiKey,
|
apiKey,
|
||||||
apiKeyData
|
apiKeyData
|
||||||
@@ -83,21 +65,10 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
||||||
let apiKeyData;
|
const { apiKeyDataId } = req.params;
|
||||||
try {
|
const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
||||||
const { apiKeyDataId } = req.params;
|
|
||||||
|
|
||||||
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete API key data'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apiKeyData
|
apiKeyData
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import * as bigintConversion from 'bigint-conversion';
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
const jsrp = require('jsrp');
|
const jsrp = require('jsrp');
|
||||||
import { User, LoginSRPDetail } from '../../models';
|
import { User, LoginSRPDetail } from '../../models';
|
||||||
@@ -17,9 +16,9 @@ import {
|
|||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
||||||
import {
|
import {
|
||||||
getNodeEnv,
|
|
||||||
getJwtMfaLifetime,
|
getJwtMfaLifetime,
|
||||||
getJwtMfaSecret
|
getJwtMfaSecret,
|
||||||
|
getHttpsEnabled
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
@@ -35,47 +34,40 @@ declare module 'jsonwebtoken' {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
clientPublicKey
|
||||||
clientPublicKey
|
}: { email: string; clientPublicKey: string } = req.body;
|
||||||
}: { email: string; clientPublicKey: string } = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier');
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
const server = new jsrp.server();
|
const server = new jsrp.server();
|
||||||
server.init(
|
server.init(
|
||||||
{
|
{
|
||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||||
email: email,
|
email: email,
|
||||||
clientPublicKey: clientPublicKey,
|
clientPublicKey: clientPublicKey,
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
}, { upsert: true, returnNewDocument: false });
|
}, { upsert: true, returnNewDocument: false });
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serverPublicKey,
|
serverPublicKey,
|
||||||
salt: user.salt
|
salt: user.salt
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to start authentication process'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -86,149 +78,143 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
try {
|
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
|
||||||
|
|
||||||
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
|
const { email, clientProof } = req.body;
|
||||||
|
const user = await User.findOne({
|
||||||
|
email
|
||||||
|
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
|
||||||
|
|
||||||
const { email, clientProof } = req.body;
|
if (!user) throw new Error('Failed to find user');
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
|
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||||
|
|
||||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
if (!loginSRPDetail) {
|
||||||
|
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||||
|
}
|
||||||
|
|
||||||
if (!loginSRPDetail) {
|
const server = new jsrp.server();
|
||||||
return BadRequestError(Error("Failed to find login details for SRP"))
|
server.init(
|
||||||
}
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: loginSRPDetail.serverBInt
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||||
|
|
||||||
const server = new jsrp.server();
|
// compare server and client shared keys
|
||||||
server.init(
|
if (server.checkClientProof(clientProof)) {
|
||||||
{
|
if (user.isMfaEnabled) {
|
||||||
salt: user.salt,
|
// case: user has MFA enabled
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetail.serverBInt
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
// generate temporary MFA token
|
||||||
if (server.checkClientProof(clientProof)) {
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
if (user.isMfaEnabled) {
|
userId: user._id.toString()
|
||||||
// case: user has MFA enabled
|
},
|
||||||
|
expiresIn: await getJwtMfaLifetime(),
|
||||||
// generate temporary MFA token
|
secret: await getJwtMfaSecret()
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
userId: user._id.toString()
|
|
||||||
},
|
|
||||||
expiresIn: getJwtMfaLifetime(),
|
|
||||||
secret: getJwtMfaSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_MFA,
|
|
||||||
email
|
|
||||||
});
|
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
|
||||||
await sendMail({
|
|
||||||
template: 'emailMfa.handlebars',
|
|
||||||
subjectLine: 'Infisical MFA code',
|
|
||||||
recipients: [email],
|
|
||||||
substitutions: {
|
|
||||||
code
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
mfaEnabled: true,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkUserDevice({
|
|
||||||
user,
|
|
||||||
ip: req.ip,
|
|
||||||
userAgent: req.headers['user-agent'] ?? ''
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// issue tokens
|
const code = await TokenService.createToken({
|
||||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
type: TOKEN_EMAIL_MFA,
|
||||||
|
email
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'strict',
|
|
||||||
secure: getNodeEnv() === 'production' ? true : false
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// case: user does not have MFA enablgged
|
// send MFA code [code] to [email]
|
||||||
// return (access) token in response
|
await sendMail({
|
||||||
|
template: 'emailMfa.handlebars',
|
||||||
interface ResponseData {
|
subjectLine: 'Infisical MFA code',
|
||||||
mfaEnabled: boolean;
|
recipients: [email],
|
||||||
encryptionVersion: any;
|
substitutions: {
|
||||||
protectedKey?: string;
|
code
|
||||||
protectedKeyIV?: string;
|
}
|
||||||
protectedKeyTag?: string;
|
|
||||||
token: string;
|
|
||||||
publicKey?: string;
|
|
||||||
encryptedPrivateKey?: string;
|
|
||||||
iv?: string;
|
|
||||||
tag?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response: ResponseData = {
|
|
||||||
mfaEnabled: false,
|
|
||||||
encryptionVersion: user.encryptionVersion,
|
|
||||||
token: tokens.token,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
|
||||||
iv: user.iv,
|
|
||||||
tag: user.tag
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
user?.protectedKey &&
|
|
||||||
user?.protectedKeyIV &&
|
|
||||||
user?.protectedKeyTag
|
|
||||||
) {
|
|
||||||
response.protectedKey = user.protectedKey;
|
|
||||||
response.protectedKeyIV = user.protectedKeyIV
|
|
||||||
response.protectedKeyTag = user.protectedKeyTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginAction = await EELogService.createAction({
|
|
||||||
name: ACTION_LOGIN,
|
|
||||||
userId: user._id
|
|
||||||
});
|
});
|
||||||
|
|
||||||
loginAction && await EELogService.createLog({
|
return res.status(200).send({
|
||||||
userId: user._id,
|
mfaEnabled: true,
|
||||||
actions: [loginAction],
|
token
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
|
||||||
ipAddress: req.ip
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send(response);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(400).send({
|
await checkUserDevice({
|
||||||
message: 'Failed to authenticate. Try again?'
|
user,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueAuthTokens({
|
||||||
|
userId: user._id,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: await getHttpsEnabled()
|
||||||
|
});
|
||||||
|
|
||||||
|
// case: user does not have MFA enabled
|
||||||
|
// return (access) token in response
|
||||||
|
|
||||||
|
interface ResponseData {
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
encryptionVersion: any;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
token: string;
|
||||||
|
publicKey?: string;
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
iv?: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ResponseData = {
|
||||||
|
mfaEnabled: false,
|
||||||
|
encryptionVersion: user.encryptionVersion,
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user?.protectedKey &&
|
||||||
|
user?.protectedKeyIV &&
|
||||||
|
user?.protectedKeyTag
|
||||||
|
) {
|
||||||
|
response.protectedKey = user.protectedKey;
|
||||||
|
response.protectedKeyIV = user.protectedKeyIV
|
||||||
|
response.protectedKeyTag = user.protectedKeyTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 res.status(200).send(response);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
} catch (err) {
|
return res.status(400).send({
|
||||||
Sentry.setUser(null);
|
message: 'Failed to authenticate. Try again?'
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
}
|
||||||
message: 'Failed to authenticate. Try again?'
|
);
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -237,30 +223,22 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const sendMfaToken = async (req: Request, res: Response) => {
|
export const sendMfaToken = async (req: Request, res: Response) => {
|
||||||
try {
|
const { email } = req.body;
|
||||||
const { email } = req.body;
|
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
const code = await TokenService.createToken({
|
||||||
type: TOKEN_EMAIL_MFA,
|
type: TOKEN_EMAIL_MFA,
|
||||||
email
|
email
|
||||||
});
|
});
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
// send MFA code [code] to [email]
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'emailMfa.handlebars',
|
template: 'emailMfa.handlebars',
|
||||||
subjectLine: 'Infisical MFA code',
|
subjectLine: 'Infisical MFA code',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code
|
code
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to send MFA code'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully sent new MFA code'
|
message: 'Successfully sent new MFA code'
|
||||||
@@ -284,25 +262,31 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
const user = await User.findOne({
|
const user = await User.findOne({
|
||||||
email
|
email
|
||||||
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
|
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
|
||||||
|
|
||||||
if (!user) throw new Error('Failed to find user');
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
await LoginSRPDetail.deleteOne({ userId: user.id })
|
||||||
|
|
||||||
await checkUserDevice({
|
await checkUserDevice({
|
||||||
user,
|
user,
|
||||||
ip: req.ip,
|
ip: req.realIP,
|
||||||
userAgent: req.headers['user-agent'] ?? ''
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueAuthTokens({ userId: user._id.toString() });
|
const tokens = await issueAuthTokens({
|
||||||
|
userId: user._id,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
// store (refresh) token in httpOnly cookie
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
path: '/',
|
path: '/',
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
secure: getNodeEnv() === 'production' ? true : false
|
secure: await getHttpsEnabled()
|
||||||
});
|
});
|
||||||
|
|
||||||
interface VerifyMfaTokenRes {
|
interface VerifyMfaTokenRes {
|
||||||
@@ -353,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
|
|||||||
userId: user._id,
|
userId: user._id,
|
||||||
actions: [loginAction],
|
actions: [loginAction],
|
||||||
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
channel: getChannelFromUserAgent(req.headers['user-agent']),
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
return res.status(200).send(resObj);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ServiceToken,
|
ServiceToken,
|
||||||
@@ -9,9 +8,10 @@ import {
|
|||||||
Membership,
|
Membership,
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import { SecretVersion } from '../../ee/models';
|
import { SecretVersion } from '../../ee/models';
|
||||||
|
import { EELicenseService } from '../../ee/services';
|
||||||
import { BadRequestError } from '../../utils/errors';
|
import { BadRequestError } from '../../utils/errors';
|
||||||
import _ from 'lodash';
|
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
|
* Create new workspace environment named [environmentName] under workspace with id
|
||||||
@@ -25,30 +25,24 @@ export const createWorkspaceEnvironment = async (
|
|||||||
) => {
|
) => {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { environmentName, environmentSlug } = req.body;
|
const { environmentName, environmentSlug } = req.body;
|
||||||
try {
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
if (
|
||||||
if (
|
!workspace ||
|
||||||
!workspace ||
|
workspace?.environments.find(
|
||||||
workspace?.environments.find(
|
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||||
({ name, slug }) => slug === environmentSlug || environmentName === name
|
)
|
||||||
)
|
) {
|
||||||
) {
|
throw new Error('Failed to create workspace environment');
|
||||||
throw new Error('Failed to create workspace environment');
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace?.environments.push({
|
|
||||||
name: environmentName,
|
|
||||||
slug: environmentSlug.toLowerCase(),
|
|
||||||
});
|
|
||||||
await workspace.save();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to create new workspace environment',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
workspace?.environments.push({
|
||||||
|
name: environmentName,
|
||||||
|
slug: environmentSlug.toLowerCase(),
|
||||||
|
});
|
||||||
|
await workspace.save();
|
||||||
|
|
||||||
|
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully created new environment',
|
message: 'Successfully created new environment',
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
@@ -72,75 +66,67 @@ export const renameWorkspaceEnvironment = async (
|
|||||||
) => {
|
) => {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
||||||
try {
|
// user should pass both new slug and env name
|
||||||
// user should pass both new slug and env name
|
if (!environmentSlug || !environmentName) {
|
||||||
if (!environmentSlug || !environmentName) {
|
throw new Error('Invalid environment given.');
|
||||||
throw new Error('Invalid environment given.');
|
|
||||||
}
|
|
||||||
|
|
||||||
// atomic update the env to avoid conflict
|
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
|
||||||
if (!workspace) {
|
|
||||||
throw new Error('Failed to create workspace environment');
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEnvExist = workspace.environments.some(
|
|
||||||
({ name, slug }) =>
|
|
||||||
slug !== oldEnvironmentSlug &&
|
|
||||||
(name === environmentName || slug === environmentSlug)
|
|
||||||
);
|
|
||||||
if (isEnvExist) {
|
|
||||||
throw new Error('Invalid environment given');
|
|
||||||
}
|
|
||||||
|
|
||||||
const envIndex = workspace?.environments.findIndex(
|
|
||||||
({ slug }) => slug === oldEnvironmentSlug
|
|
||||||
);
|
|
||||||
if (envIndex === -1) {
|
|
||||||
throw new Error('Invalid environment given');
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace.environments[envIndex].name = environmentName;
|
|
||||||
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
|
||||||
|
|
||||||
await workspace.save();
|
|
||||||
await Secret.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await SecretVersion.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await ServiceToken.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await ServiceTokenData.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await Integration.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await Membership.updateMany(
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
|
||||||
},
|
|
||||||
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
|
||||||
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to update workspace environment',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// atomic update the env to avoid conflict
|
||||||
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
|
if (!workspace) {
|
||||||
|
throw new Error('Failed to create workspace environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
const isEnvExist = workspace.environments.some(
|
||||||
|
({ name, slug }) =>
|
||||||
|
slug !== oldEnvironmentSlug &&
|
||||||
|
(name === environmentName || slug === environmentSlug)
|
||||||
|
);
|
||||||
|
if (isEnvExist) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === oldEnvironmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments[envIndex].name = environmentName;
|
||||||
|
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
||||||
|
|
||||||
|
await workspace.save();
|
||||||
|
await Secret.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await ServiceToken.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await ServiceTokenData.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await Integration.updateMany(
|
||||||
|
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||||
|
{ environment: environmentSlug }
|
||||||
|
);
|
||||||
|
await Membership.updateMany(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
||||||
|
},
|
||||||
|
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
||||||
|
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully update environment',
|
message: 'Successfully update environment',
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
@@ -163,57 +149,50 @@ export const deleteWorkspaceEnvironment = async (
|
|||||||
) => {
|
) => {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { environmentSlug } = req.body;
|
const { environmentSlug } = req.body;
|
||||||
try {
|
// atomic update the env to avoid conflict
|
||||||
// atomic update the env to avoid conflict
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
if (!workspace) {
|
||||||
if (!workspace) {
|
throw new Error('Failed to create workspace environment');
|
||||||
throw new Error('Failed to create workspace environment');
|
|
||||||
}
|
|
||||||
|
|
||||||
const envIndex = workspace?.environments.findIndex(
|
|
||||||
({ slug }) => slug === environmentSlug
|
|
||||||
);
|
|
||||||
if (envIndex === -1) {
|
|
||||||
throw new Error('Invalid environment given');
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace.environments.splice(envIndex, 1);
|
|
||||||
await workspace.save();
|
|
||||||
|
|
||||||
// clean up
|
|
||||||
await Secret.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await SecretVersion.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await ServiceToken.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await ServiceTokenData.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await Integration.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug,
|
|
||||||
});
|
|
||||||
await Membership.updateMany(
|
|
||||||
{ workspace: workspaceId },
|
|
||||||
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
|
||||||
)
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete workspace environment',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const envIndex = workspace?.environments.findIndex(
|
||||||
|
({ slug }) => slug === environmentSlug
|
||||||
|
);
|
||||||
|
if (envIndex === -1) {
|
||||||
|
throw new Error('Invalid environment given');
|
||||||
|
}
|
||||||
|
|
||||||
|
workspace.environments.splice(envIndex, 1);
|
||||||
|
await workspace.save();
|
||||||
|
|
||||||
|
// clean up
|
||||||
|
await Secret.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await SecretVersion.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await ServiceToken.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await ServiceTokenData.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await Integration.deleteMany({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
await Membership.updateMany(
|
||||||
|
{ workspace: workspaceId },
|
||||||
|
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
||||||
|
);
|
||||||
|
|
||||||
|
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully deleted environment',
|
message: 'Successfully deleted environment',
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
@@ -244,8 +223,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
|
|||||||
throw BadRequestError()
|
throw BadRequestError()
|
||||||
}
|
}
|
||||||
relatedWorkspace.environments.forEach(environment => {
|
relatedWorkspace.environments.forEach(environment => {
|
||||||
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
|
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
|
||||||
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
|
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
|
||||||
if (isReadBlocked && isWriteBlocked) {
|
if (isReadBlocked && isWriteBlocked) {
|
||||||
return
|
return
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import * as serviceTokenDataController from './serviceTokenDataController';
|
|||||||
import * as apiKeyDataController from './apiKeyDataController';
|
import * as apiKeyDataController from './apiKeyDataController';
|
||||||
import * as secretController from './secretController';
|
import * as secretController from './secretController';
|
||||||
import * as secretsController from './secretsController';
|
import * as secretsController from './secretsController';
|
||||||
|
import * as serviceAccountsController from './serviceAccountsController';
|
||||||
import * as environmentController from './environmentController';
|
import * as environmentController from './environmentController';
|
||||||
import * as tagController from './tagController';
|
import * as tagController from './tagController';
|
||||||
|
|
||||||
@@ -20,6 +21,7 @@ export {
|
|||||||
apiKeyDataController,
|
apiKeyDataController,
|
||||||
secretController,
|
secretController,
|
||||||
secretsController,
|
secretsController,
|
||||||
|
serviceAccountsController,
|
||||||
environmentController,
|
environmentController,
|
||||||
tagController
|
tagController
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
MembershipOrg,
|
MembershipOrg,
|
||||||
Membership,
|
Membership,
|
||||||
Workspace
|
Workspace,
|
||||||
|
ServiceAccount
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
|
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
|
||||||
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||||
@@ -47,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let memberships;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
memberships = await MembershipOrg.find({
|
const memberships = await MembershipOrg.find({
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).populate('user', '+publicKey');
|
}).populate('user', '+publicKey');
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get organization memberships'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
memberships
|
memberships
|
||||||
@@ -126,26 +118,17 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const { membershipId } = req.params;
|
||||||
try {
|
const { role } = req.body;
|
||||||
const { membershipId } = req.params;
|
|
||||||
const { role } = req.body;
|
const membership = await MembershipOrg.findByIdAndUpdate(
|
||||||
|
membershipId,
|
||||||
membership = await MembershipOrg.findByIdAndUpdate(
|
{
|
||||||
membershipId,
|
role
|
||||||
{
|
}, {
|
||||||
role
|
new: true
|
||||||
}, {
|
}
|
||||||
new: true
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to update organization membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
@@ -195,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const { membershipId } = req.params;
|
||||||
try {
|
|
||||||
const { membershipId } = req.params;
|
// delete organization membership
|
||||||
|
const membership = await deleteMembershipOrg({
|
||||||
// delete organization membership
|
membershipOrgId: membershipId
|
||||||
membership = await deleteMembershipOrg({
|
});
|
||||||
membershipOrgId: membershipId
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
await updateSubscriptionOrgQuantity({
|
||||||
organizationId: membership.organization.toString()
|
organizationId: membership.organization.toString()
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete organization membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
@@ -260,37 +234,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let workspaces;
|
const { organizationId } = req.params;
|
||||||
try {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
|
|
||||||
const workspacesSet = new Set(
|
const workspacesSet = new Set(
|
||||||
(
|
(
|
||||||
await Workspace.find(
|
await Workspace.find(
|
||||||
{
|
{
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
},
|
},
|
||||||
'_id'
|
'_id'
|
||||||
)
|
)
|
||||||
).map((w) => w._id.toString())
|
).map((w) => w._id.toString())
|
||||||
);
|
);
|
||||||
|
|
||||||
workspaces = (
|
const workspaces = (
|
||||||
await Membership.find({
|
await Membership.find({
|
||||||
user: req.user._id
|
user: req.user._id
|
||||||
}).populate('workspace')
|
}).populate('workspace')
|
||||||
)
|
)
|
||||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||||
.map((m) => m.workspace);
|
.map((m) => m.workspace);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
return res.status(200).send({
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get organization workspaces'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
workspaces
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,8 +6,10 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre
|
|||||||
const { ValidationError } = mongoose.Error;
|
const { ValidationError } = mongoose.Error;
|
||||||
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||||
import { AnyBulkWriteOperation } from 'mongodb';
|
import { AnyBulkWriteOperation } from 'mongodb';
|
||||||
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
||||||
import { getPostHogClient } from '../../services';
|
import { TelemetryService } from '../../services';
|
||||||
|
import { User } from "../../models";
|
||||||
|
import { AccountNotFoundError } from '../../utils/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||||
@@ -15,7 +17,7 @@ import { getPostHogClient } from '../../services';
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const createSecret = async (req: Request, res: Response) => {
|
export const createSecret = async (req: Request, res: Response) => {
|
||||||
const postHogClient = getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||||
const { workspaceId, environment } = req.params
|
const { workspaceId, environment } = req.params
|
||||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||||
@@ -34,7 +36,9 @@ export const createSecret = async (req: Request, res: Response) => {
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: secretToCreate.type,
|
type: secretToCreate.type,
|
||||||
user: new Types.ObjectId(req.user._id)
|
user: new Types.ObjectId(req.user._id),
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -68,7 +72,7 @@ export const createSecret = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const createSecrets = async (req: Request, res: Response) => {
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
const postHogClient = getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||||
const { workspaceId, environment } = req.params
|
const { workspaceId, environment } = req.params
|
||||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||||
@@ -90,7 +94,9 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: rawSecret.type,
|
type: rawSecret.type,
|
||||||
user: new Types.ObjectId(req.user._id)
|
user: new Types.ObjectId(req.user._id),
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}
|
}
|
||||||
|
|
||||||
sanitizedSecretesToCreate.push(safeUpdateFields)
|
sanitizedSecretesToCreate.push(safeUpdateFields)
|
||||||
@@ -130,7 +136,7 @@ export const createSecrets = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
const postHogClient = getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretIdsToDelete: string[] = req.body.secretIds
|
const secretIdsToDelete: string[] = req.body.secretIds
|
||||||
|
|
||||||
@@ -184,7 +190,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const deleteSecret = async (req: Request, res: Response) => {
|
export const deleteSecret = async (req: Request, res: Response) => {
|
||||||
const postHogClient = getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
await Secret.findByIdAndDelete(req._secret._id)
|
await Secret.findByIdAndDelete(req._secret._id)
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
@@ -213,7 +219,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateSecrets = async (req: Request, res: Response) => {
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
const postHogClient = getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||||
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||||
@@ -281,7 +287,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateSecret = async (req: Request, res: Response) => {
|
export const updateSecret = async (req: Request, res: Response) => {
|
||||||
const postHogClient = getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const { workspaceId, environmentName } = req.params
|
const { workspaceId, environmentName } = req.params
|
||||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||||
|
|
||||||
@@ -335,20 +341,23 @@ export const updateSecret = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getSecrets = async (req: Request, res: Response) => {
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
const postHogClient = getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const { environment } = req.query;
|
const { environment } = req.query;
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||||
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
|
let userEmail: string | undefined = undefined // used for posthog
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
userId = req.user._id;
|
userId = req.user._id;
|
||||||
userEmail = req.user.email;
|
userEmail = req.user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (req.serviceTokenData) {
|
if (req.serviceTokenData) {
|
||||||
userId = req.serviceTokenData.user._id
|
userId = req.serviceTokenData.user;
|
||||||
userEmail = req.serviceTokenData.user.email;
|
|
||||||
|
const user = await User.findById(req.serviceTokenData.user, 'email');
|
||||||
|
if (!user) throw AccountNotFoundError();
|
||||||
|
userEmail = user.email;
|
||||||
}
|
}
|
||||||
|
|
||||||
const [err, secrets] = await to(Secret.find(
|
const [err, secrets] = await to(Secret.find(
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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, await 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
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,22 +1,27 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Request, Response } from "express";
|
||||||
import { Request, Response } from 'express';
|
import crypto from "crypto";
|
||||||
import crypto from 'crypto';
|
import bcrypt from "bcrypt";
|
||||||
import bcrypt from 'bcrypt';
|
import { User, ServiceAccount, ServiceTokenData } from "../../models";
|
||||||
|
import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
|
||||||
import {
|
import {
|
||||||
ServiceTokenData
|
PERMISSION_READ_SECRETS,
|
||||||
} from '../../models';
|
AUTH_MODE_JWT,
|
||||||
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
import { ABILITY_READ } from '../../variables/organization';
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
import { getSaltRounds } from '../../config';
|
} from "../../variables";
|
||||||
|
import { getSaltRounds } from "../../config";
|
||||||
|
import { BadRequestError } from "../../utils/errors";
|
||||||
|
import Folder from "../../models/folder";
|
||||||
|
import { getFolderByPath } from "../../services/FolderService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return service token data associated with service token on request
|
* Return service token data associated with service token on request
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getServiceTokenData = async (req: Request, res: Response) => {
|
export const getServiceTokenData = async (req: Request, res: Response) => {
|
||||||
/*
|
/*
|
||||||
#swagger.summary = 'Return Infisical Token data'
|
#swagger.summary = 'Return Infisical Token data'
|
||||||
#swagger.description = 'Return Infisical Token data'
|
#swagger.description = 'Return Infisical Token data'
|
||||||
|
|
||||||
@@ -29,115 +34,135 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
|
|||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"serviceTokenData": {
|
"serviceTokenData": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
$ref: "#/components/schemas/ServiceTokenData",
|
$ref: "#/components/schemas/ServiceTokenData",
|
||||||
"description": "Details of service token"
|
"description": "Details of service token"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
return res.status(200).json(req.serviceTokenData);
|
if (!(req.authData.authPayload instanceof ServiceTokenData))
|
||||||
}
|
throw BadRequestError({
|
||||||
|
message: "Failed accepted client validation for service token data",
|
||||||
|
});
|
||||||
|
|
||||||
|
const serviceTokenData = await ServiceTokenData.findById(
|
||||||
|
req.authData.authPayload._id
|
||||||
|
)
|
||||||
|
.select("+encryptedKey +iv +tag")
|
||||||
|
.populate("user");
|
||||||
|
|
||||||
|
return res.status(200).json(serviceTokenData);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create new service token data for workspace with id [workspaceId] and
|
* Create new service token data for workspace with id [workspaceId] and
|
||||||
* environment [environment].
|
* environment [environment].
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||||
let serviceToken, serviceTokenData;
|
let serviceTokenData;
|
||||||
|
|
||||||
try {
|
const {
|
||||||
const {
|
name,
|
||||||
name,
|
workspaceId,
|
||||||
workspaceId,
|
environment,
|
||||||
environment,
|
encryptedKey,
|
||||||
encryptedKey,
|
iv,
|
||||||
iv,
|
tag,
|
||||||
tag,
|
expiresIn,
|
||||||
expiresIn,
|
secretPath,
|
||||||
permissions
|
permissions,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
|
const folders = await Folder.findOne({
|
||||||
if (!hasAccess) {
|
workspace: workspaceId,
|
||||||
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
environment,
|
||||||
}
|
});
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString('hex');
|
if (folders) {
|
||||||
const secretHash = await bcrypt.hash(secret, getSaltRounds());
|
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||||
|
if (folder == undefined) {
|
||||||
const expiresAt = new Date();
|
throw BadRequestError({ message: "Path for service token does not exist" })
|
||||||
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'
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
const secret = crypto.randomBytes(16).toString("hex");
|
||||||
serviceToken,
|
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
||||||
serviceTokenData
|
|
||||||
});
|
let expiresAt;
|
||||||
}
|
if (expiresIn) {
|
||||||
|
expiresAt = new Date();
|
||||||
|
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
secretPath,
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete service token data with id [serviceTokenDataId].
|
* Delete service token data with id [serviceTokenDataId].
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||||
let serviceTokenData;
|
const { serviceTokenDataId } = req.params;
|
||||||
try {
|
|
||||||
const { serviceTokenDataId } = req.params;
|
|
||||||
|
|
||||||
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
|
||||||
|
serviceTokenDataId
|
||||||
|
);
|
||||||
|
|
||||||
} catch (err) {
|
return res.status(200).send({
|
||||||
Sentry.setUser({ email: req.user.email });
|
serviceTokenData,
|
||||||
Sentry.captureException(err);
|
});
|
||||||
return res.status(400).send({
|
};
|
||||||
message: 'Failed to delete service token data'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serviceTokenData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function UnauthorizedRequestError(arg0: { message: string; }) {
|
|
||||||
throw new Error('Function not implemented.');
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { User, MembershipOrg } from '../../models';
|
import { User, MembershipOrg } from '../../models';
|
||||||
import { completeAccount } from '../../helpers/user';
|
import { completeAccount } from '../../helpers/user';
|
||||||
import {
|
import {
|
||||||
@@ -7,8 +6,9 @@ import {
|
|||||||
} from '../../helpers/signup';
|
} from '../../helpers/signup';
|
||||||
import { issueAuthTokens } from '../../helpers/auth';
|
import { issueAuthTokens } from '../../helpers/auth';
|
||||||
import { INVITED, ACCEPTED } from '../../variables';
|
import { INVITED, ACCEPTED } from '../../variables';
|
||||||
import request from '../../config/request';
|
import { standardRequest } from '../../config/request';
|
||||||
import { getNodeEnv, getLoopsApiKey } from '../../config';
|
import { getLoopsApiKey, getHttpsEnabled } from '../../config';
|
||||||
|
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
@@ -19,123 +19,130 @@ import { getNodeEnv, getLoopsApiKey } from '../../config';
|
|||||||
*/
|
*/
|
||||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||||
let user, token, refreshToken;
|
let user, token, refreshToken;
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
firstName,
|
||||||
firstName,
|
lastName,
|
||||||
lastName,
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
publicKey,
|
||||||
publicKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier,
|
||||||
verifier,
|
organizationName
|
||||||
organizationName
|
}: {
|
||||||
}: {
|
email: string;
|
||||||
email: string;
|
firstName: string;
|
||||||
firstName: string;
|
lastName: string;
|
||||||
lastName: string;
|
protectedKey: string;
|
||||||
protectedKey: string;
|
protectedKeyIV: string;
|
||||||
protectedKeyIV: string;
|
protectedKeyTag: string;
|
||||||
protectedKeyTag: string;
|
publicKey: string;
|
||||||
publicKey: string;
|
encryptedPrivateKey: string;
|
||||||
encryptedPrivateKey: string;
|
encryptedPrivateKeyIV: string;
|
||||||
encryptedPrivateKeyIV: string;
|
encryptedPrivateKeyTag: string;
|
||||||
encryptedPrivateKeyTag: string;
|
salt: string;
|
||||||
salt: string;
|
verifier: string;
|
||||||
verifier: string;
|
organizationName: string;
|
||||||
organizationName: string;
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
// get user
|
||||||
user = await User.findOne({ email });
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
|
||||||
// case 1: user doesn't exist.
|
|
||||||
// case 2: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: 'Failed to complete account for complete user'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// complete setting up user's account
|
if (!user || (user && user?.publicKey)) {
|
||||||
user = await completeAccount({
|
// case 1: user doesn't exist.
|
||||||
userId: user._id.toString(),
|
// case 2: user has already completed account
|
||||||
firstName,
|
return res.status(403).send({
|
||||||
lastName,
|
error: 'Failed to complete account for complete user'
|
||||||
encryptionVersion: 2,
|
});
|
||||||
protectedKey,
|
}
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user)
|
// complete setting up user's account
|
||||||
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
user = await completeAccount({
|
||||||
|
userId: user._id.toString(),
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
|
||||||
// initialize default organization and workspace
|
if (!user)
|
||||||
await initializeDefaultOrg({
|
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
||||||
organizationName,
|
|
||||||
user
|
|
||||||
});
|
|
||||||
|
|
||||||
// update organization membership statuses that are
|
// initialize default organization and workspace
|
||||||
// invited to completed with user attached
|
await initializeDefaultOrg({
|
||||||
await MembershipOrg.updateMany(
|
organizationName,
|
||||||
{
|
user
|
||||||
inviteEmail: email,
|
});
|
||||||
status: INVITED
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user,
|
|
||||||
status: ACCEPTED
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// issue tokens
|
// update organization membership statuses that are
|
||||||
const tokens = await issueAuthTokens({
|
// invited to completed with user attached
|
||||||
userId: user._id.toString()
|
const membershipsToUpdate = await MembershipOrg.find({
|
||||||
});
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
});
|
||||||
|
|
||||||
|
membershipsToUpdate.forEach(async (membership) => {
|
||||||
|
await updateSubscriptionOrgQuantity({
|
||||||
|
organizationId: membership.organization.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
token = tokens.token;
|
// update organization membership statuses that are
|
||||||
|
// invited to completed with user attached
|
||||||
|
await MembershipOrg.updateMany(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
status: ACCEPTED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// sending a welcome email to new users
|
// issue tokens
|
||||||
if (getLoopsApiKey()) {
|
const tokens = await issueAuthTokens({
|
||||||
await request.post("https://app.loops.so/api/v1/events/send", {
|
userId: user._id,
|
||||||
"email": email,
|
ip: req.realIP,
|
||||||
"eventName": "Sign Up",
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
"firstName": firstName,
|
});
|
||||||
"lastName": lastName
|
|
||||||
}, {
|
|
||||||
headers: {
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": "Bearer " + getLoopsApiKey()
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
token = tokens.token;
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
// sending a welcome email to new users
|
||||||
path: '/',
|
if (await getLoopsApiKey()) {
|
||||||
sameSite: 'strict',
|
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
|
||||||
secure: getNodeEnv() === 'production' ? true : false
|
"email": email,
|
||||||
});
|
"eventName": "Sign Up",
|
||||||
} catch (err) {
|
"firstName": firstName,
|
||||||
Sentry.setUser(null);
|
"lastName": lastName
|
||||||
Sentry.captureException(err);
|
}, {
|
||||||
return res.status(400).send({
|
headers: {
|
||||||
message: 'Failed to complete account setup'
|
"Accept": "application/json",
|
||||||
});
|
"Authorization": "Bearer " + (await getLoopsApiKey())
|
||||||
}
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: await getHttpsEnabled()
|
||||||
|
});
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully set up account',
|
message: 'Successfully set up account',
|
||||||
@@ -153,98 +160,103 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
export const completeAccountInvite = async (req: Request, res: Response) => {
|
||||||
let user, token, refreshToken;
|
let user, token, refreshToken;
|
||||||
try {
|
const {
|
||||||
const {
|
email,
|
||||||
email,
|
firstName,
|
||||||
firstName,
|
lastName,
|
||||||
lastName,
|
protectedKey,
|
||||||
protectedKey,
|
protectedKeyIV,
|
||||||
protectedKeyIV,
|
protectedKeyTag,
|
||||||
protectedKeyTag,
|
publicKey,
|
||||||
publicKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyTag,
|
||||||
encryptedPrivateKeyTag,
|
salt,
|
||||||
salt,
|
verifier
|
||||||
verifier
|
} = req.body;
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
// get user
|
||||||
user = await User.findOne({ email });
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
if (!user || (user && user?.publicKey)) {
|
||||||
// case 1: user doesn't exist.
|
// case 1: user doesn't exist.
|
||||||
// case 2: user has already completed account
|
// case 2: user has already completed account
|
||||||
return res.status(403).send({
|
return res.status(403).send({
|
||||||
error: 'Failed to complete account for complete user'
|
error: 'Failed to complete account for complete user'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
status: INVITED
|
status: INVITED
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
if (!membershipOrg) throw new Error('Failed to find invitations for email');
|
||||||
|
|
||||||
// complete setting up user's account
|
// complete setting up user's account
|
||||||
user = await completeAccount({
|
user = await completeAccount({
|
||||||
userId: user._id.toString(),
|
userId: user._id.toString(),
|
||||||
firstName,
|
firstName,
|
||||||
lastName,
|
lastName,
|
||||||
encryptionVersion: 2,
|
encryptionVersion: 2,
|
||||||
protectedKey,
|
protectedKey,
|
||||||
protectedKeyIV,
|
protectedKeyIV,
|
||||||
protectedKeyTag,
|
protectedKeyTag,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey,
|
encryptedPrivateKey,
|
||||||
encryptedPrivateKeyIV,
|
encryptedPrivateKeyIV,
|
||||||
encryptedPrivateKeyTag,
|
encryptedPrivateKeyTag,
|
||||||
salt,
|
salt,
|
||||||
verifier
|
verifier
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user)
|
if (!user)
|
||||||
throw new Error('Failed to complete account for non-existent user');
|
throw new Error('Failed to complete account for non-existent user');
|
||||||
|
|
||||||
|
// update organization membership statuses that are
|
||||||
|
// invited to completed with user attached
|
||||||
|
const membershipsToUpdate = await MembershipOrg.find({
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
});
|
||||||
|
|
||||||
|
membershipsToUpdate.forEach(async (membership) => {
|
||||||
|
await updateSubscriptionOrgQuantity({
|
||||||
|
organizationId: membership.organization.toString()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// update organization membership statuses that are
|
await MembershipOrg.updateMany(
|
||||||
// invited to completed with user attached
|
{
|
||||||
await MembershipOrg.updateMany(
|
inviteEmail: email,
|
||||||
{
|
status: INVITED
|
||||||
inviteEmail: email,
|
},
|
||||||
status: INVITED
|
{
|
||||||
},
|
user,
|
||||||
{
|
status: ACCEPTED
|
||||||
user,
|
}
|
||||||
status: ACCEPTED
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// issue tokens
|
// issue tokens
|
||||||
const tokens = await issueAuthTokens({
|
const tokens = await issueAuthTokens({
|
||||||
userId: user._id.toString()
|
userId: user._id,
|
||||||
});
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
token = tokens.token;
|
token = tokens.token;
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: await getHttpsEnabled()
|
||||||
|
});
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie('jid', tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: '/',
|
|
||||||
sameSite: 'strict',
|
|
||||||
secure: getNodeEnv() === 'production' ? true : false
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to complete account setup'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully set up account',
|
message: 'Successfully set up account',
|
||||||
user,
|
user,
|
||||||
token
|
token
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Membership, Secret,
|
Membership, Secret,
|
||||||
@@ -69,4 +68,4 @@ export const getWorkspaceTags = async (req: Request, res: Response) => {
|
|||||||
return res.json({
|
return res.json({
|
||||||
workspaceTags
|
workspaceTags
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
User,
|
User,
|
||||||
MembershipOrg
|
MembershipOrg
|
||||||
@@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let user;
|
const user = await User
|
||||||
try {
|
.findById(req.user._id)
|
||||||
user = await User
|
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
|
||||||
.findById(req.user._id)
|
|
||||||
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get current user'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
user
|
user
|
||||||
@@ -64,29 +54,20 @@ export const getMe = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
||||||
let user;
|
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
||||||
try {
|
req.user.isMfaEnabled = isMfaEnabled;
|
||||||
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
|
|
||||||
req.user.isMfaEnabled = isMfaEnabled;
|
if (isMfaEnabled) {
|
||||||
|
// TODO: adapt this route/controller
|
||||||
if (isMfaEnabled) {
|
// to work for different forms of MFA
|
||||||
// TODO: adapt this route/controller
|
req.user.mfaMethods = ['email'];
|
||||||
// to work for different forms of MFA
|
} else {
|
||||||
req.user.mfaMethods = ['email'];
|
req.user.mfaMethods = [];
|
||||||
} else {
|
|
||||||
req.user.mfaMethods = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
await req.user.save();
|
|
||||||
|
|
||||||
user = req.user;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to update current user's MFA status"
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await req.user.save();
|
||||||
|
|
||||||
|
const user = req.user;
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
user
|
user
|
||||||
@@ -126,22 +107,13 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let organizations;
|
const organizations = (
|
||||||
try {
|
await MembershipOrg.find({
|
||||||
organizations = (
|
user: req.user._id
|
||||||
await MembershipOrg.find({
|
}).populate('organization')
|
||||||
user: req.user._id
|
).map((m) => m.organization);
|
||||||
}).populate('organization')
|
|
||||||
).map((m) => m.organization);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to get current user's organizations"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
organizations
|
organizations
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
@@ -19,7 +18,7 @@ import {
|
|||||||
reformatPullSecrets
|
reformatPullSecrets
|
||||||
} from '../../helpers/secret';
|
} from '../../helpers/secret';
|
||||||
import { pushKeys } from '../../helpers/key';
|
import { pushKeys } from '../../helpers/key';
|
||||||
import { getPostHogClient, EventService } from '../../services';
|
import { TelemetryService, EventService } from '../../services';
|
||||||
import { eventPushSecrets } from '../../events';
|
import { eventPushSecrets } from '../../events';
|
||||||
|
|
||||||
interface V2PushSecret {
|
interface V2PushSecret {
|
||||||
@@ -47,65 +46,57 @@ interface V2PushSecret {
|
|||||||
*/
|
*/
|
||||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
try {
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const postHogClient = getPostHogClient();
|
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
const { keys, environment, channel } = req.body;
|
||||||
const { keys, environment, channel } = req.body;
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
// sanitize secrets
|
// sanitize secrets
|
||||||
secrets = secrets.filter(
|
secrets = secrets.filter(
|
||||||
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
||||||
);
|
);
|
||||||
|
|
||||||
await push({
|
await push({
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
secrets,
|
secrets,
|
||||||
channel: channel ? channel : 'cli',
|
channel: channel ? channel : 'cli',
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
await pushKeys({
|
await pushKeys({
|
||||||
userId: req.user._id,
|
userId: req.user._id,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
});
|
});
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pushed',
|
event: 'secrets pushed',
|
||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel ? channel : 'cli'
|
channel: channel ? channel : 'cli'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// trigger event - push secrets
|
// trigger event - push secrets
|
||||||
EventService.handleEvent({
|
EventService.handleEvent({
|
||||||
event: eventPushSecrets({
|
event: eventPushSecrets({
|
||||||
workspaceId
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
})
|
environment
|
||||||
});
|
})
|
||||||
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to upload workspace secrets'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully uploaded workspace secrets'
|
message: 'Successfully uploaded workspace secrets'
|
||||||
@@ -121,56 +112,48 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const pullSecrets = async (req: Request, res: Response) => {
|
export const pullSecrets = async (req: Request, res: Response) => {
|
||||||
let secrets;
|
let secrets;
|
||||||
try {
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
const postHogClient = getPostHogClient();
|
const environment: string = req.query.environment as string;
|
||||||
const environment: string = req.query.environment as string;
|
const channel: string = req.query.channel as string;
|
||||||
const channel: string = req.query.channel as string;
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
let userId;
|
let userId;
|
||||||
if (req.user) {
|
if (req.user) {
|
||||||
userId = req.user._id.toString();
|
userId = req.user._id.toString();
|
||||||
} else if (req.serviceTokenData) {
|
} else if (req.serviceTokenData) {
|
||||||
userId = req.serviceTokenData.user._id
|
userId = req.serviceTokenData.user.toString();
|
||||||
}
|
}
|
||||||
// validate environment
|
// validate environment
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets = await pull({
|
secrets = await pull({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
channel: channel ? channel : 'cli',
|
channel: channel ? channel : 'cli',
|
||||||
ipAddress: req.ip
|
ipAddress: req.realIP
|
||||||
});
|
});
|
||||||
|
|
||||||
if (channel !== 'cli') {
|
if (channel !== 'cli') {
|
||||||
secrets = reformatPullSecrets({ secrets });
|
secrets = reformatPullSecrets({ secrets });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
// capture secrets pushed event in production
|
// capture secrets pushed event in production
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
distinctId: req.user.email,
|
distinctId: req.user.email,
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: channel ? channel : 'cli'
|
channel: channel ? channel : 'cli'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to pull workspace secrets'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secrets
|
secrets
|
||||||
@@ -207,22 +190,14 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let key;
|
let key;
|
||||||
try {
|
const { workspaceId } = req.params;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
key = await Key.findOne({
|
key = await Key.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
receiver: req.user._id
|
receiver: req.user._id
|
||||||
}).populate('sender', '+publicKey');
|
}).populate('sender', '+publicKey');
|
||||||
|
|
||||||
if (!key) throw new Error('Failed to find workspace key');
|
if (!key) throw new Error('Failed to find workspace key');
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get workspace key'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json(key);
|
return res.status(200).json(key);
|
||||||
}
|
}
|
||||||
@@ -230,23 +205,13 @@ export const getWorkspaceServiceTokenData = async (
|
|||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
let serviceTokenData;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
serviceTokenData = await ServiceTokenData
|
const serviceTokenData = await ServiceTokenData
|
||||||
.find({
|
.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
})
|
})
|
||||||
.select('+encryptedKey +iv +tag');
|
.select('+encryptedKey +iv +tag');
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get workspace service token data'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceTokenData
|
serviceTokenData
|
||||||
@@ -293,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let memberships;
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
memberships = await Membership.find({
|
const memberships = await Membership.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}).populate('user', '+publicKey');
|
}).populate('user', '+publicKey');
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get workspace memberships'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
memberships
|
memberships
|
||||||
@@ -373,29 +329,20 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const {
|
||||||
try {
|
membershipId
|
||||||
const {
|
} = req.params;
|
||||||
membershipId
|
const { role } = req.body;
|
||||||
} = req.params;
|
|
||||||
const { role } = req.body;
|
const membership = await Membership.findByIdAndUpdate(
|
||||||
|
membershipId,
|
||||||
membership = await Membership.findByIdAndUpdate(
|
{
|
||||||
membershipId,
|
role
|
||||||
{
|
}, {
|
||||||
role
|
new: true
|
||||||
}, {
|
}
|
||||||
new: true
|
);
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to update workspace membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
});
|
});
|
||||||
@@ -444,27 +391,18 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let membership;
|
const {
|
||||||
try {
|
membershipId
|
||||||
const {
|
} = req.params;
|
||||||
membershipId
|
|
||||||
} = req.params;
|
const membership = await Membership.findByIdAndDelete(membershipId);
|
||||||
|
|
||||||
membership = await Membership.findByIdAndDelete(membershipId);
|
if (!membership) throw new Error('Failed to delete workspace membership');
|
||||||
|
|
||||||
if (!membership) throw new Error('Failed to delete workspace membership');
|
await Key.deleteMany({
|
||||||
|
receiver: membership.user,
|
||||||
await Key.deleteMany({
|
workspace: membership.workspace
|
||||||
receiver: membership.user,
|
});
|
||||||
workspace: membership.workspace
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete workspace membership'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
membership
|
membership
|
||||||
@@ -478,33 +416,23 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
||||||
let workspace;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { autoCapitalization } = req.body;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { autoCapitalization } = req.body;
|
|
||||||
|
|
||||||
workspace = await Workspace.findOneAndUpdate(
|
const workspace = await Workspace.findOneAndUpdate(
|
||||||
{
|
{
|
||||||
_id: workspaceId
|
_id: workspaceId
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
autoCapitalization
|
autoCapitalization
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to change autoCapitalization setting'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Successfully changed autoCapitalization setting',
|
message: 'Successfully changed autoCapitalization setting',
|
||||||
workspace
|
workspace
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,264 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-var-requires */
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import * as bigintConversion from 'bigint-conversion';
|
||||||
|
const jsrp = require('jsrp');
|
||||||
|
import { User, LoginSRPDetail } from '../../models';
|
||||||
|
import { issueAuthTokens, createToken, validateProviderAuthToken } from '../../helpers/auth';
|
||||||
|
import { checkUserDevice } from '../../helpers/user';
|
||||||
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
|
import { TokenService } from '../../services';
|
||||||
|
import { EELogService } from '../../ee/services';
|
||||||
|
import { BadRequestError, InternalServerError } from '../../utils/errors';
|
||||||
|
import {
|
||||||
|
TOKEN_EMAIL_MFA,
|
||||||
|
ACTION_LOGIN
|
||||||
|
} from '../../variables';
|
||||||
|
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
|
||||||
|
import {
|
||||||
|
getJwtMfaLifetime,
|
||||||
|
getJwtMfaSecret,
|
||||||
|
getHttpsEnabled,
|
||||||
|
} from '../../config';
|
||||||
|
import { AuthProvider } from '../../models/user';
|
||||||
|
|
||||||
|
declare module 'jsonwebtoken' {
|
||||||
|
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
|
||||||
|
userId: string;
|
||||||
|
email: string;
|
||||||
|
authProvider: AuthProvider;
|
||||||
|
isUserCompleted: boolean,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const login1 = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
providerAuthToken,
|
||||||
|
clientPublicKey
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
clientPublicKey: string,
|
||||||
|
providerAuthToken?: string;
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
email,
|
||||||
|
}).select('+salt +verifier');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
if (user.authProvider) {
|
||||||
|
await validateProviderAuthToken({
|
||||||
|
email,
|
||||||
|
user,
|
||||||
|
providerAuthToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
// generate server-side public key
|
||||||
|
const serverPublicKey = server.getPublicKey();
|
||||||
|
await LoginSRPDetail.findOneAndReplace({
|
||||||
|
email: email
|
||||||
|
}, {
|
||||||
|
email,
|
||||||
|
userId: user.id,
|
||||||
|
clientPublicKey: clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
|
}, { upsert: true, returnNewDocument: false });
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serverPublicKey,
|
||||||
|
salt: user.salt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to start authentication process'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
||||||
|
* private key
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const login2 = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
|
||||||
|
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
|
||||||
|
|
||||||
|
const { email, clientProof, providerAuthToken } = req.body;
|
||||||
|
|
||||||
|
const user = await User.findOne({
|
||||||
|
email,
|
||||||
|
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
|
||||||
|
|
||||||
|
if (!user) throw new Error('Failed to find user');
|
||||||
|
|
||||||
|
if (user.authProvider) {
|
||||||
|
await validateProviderAuthToken({
|
||||||
|
email,
|
||||||
|
user,
|
||||||
|
providerAuthToken,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
|
||||||
|
|
||||||
|
if (!loginSRPDetail) {
|
||||||
|
return BadRequestError(Error("Failed to find login details for SRP"))
|
||||||
|
}
|
||||||
|
|
||||||
|
const server = new jsrp.server();
|
||||||
|
server.init(
|
||||||
|
{
|
||||||
|
salt: user.salt,
|
||||||
|
verifier: user.verifier,
|
||||||
|
b: loginSRPDetail.serverBInt
|
||||||
|
},
|
||||||
|
async () => {
|
||||||
|
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
||||||
|
|
||||||
|
// compare server and client shared keys
|
||||||
|
if (server.checkClientProof(clientProof)) {
|
||||||
|
|
||||||
|
if (user.isMfaEnabled) {
|
||||||
|
// case: user has MFA enabled
|
||||||
|
|
||||||
|
// generate temporary MFA token
|
||||||
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId: user._id.toString()
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtMfaLifetime(),
|
||||||
|
secret: await getJwtMfaSecret()
|
||||||
|
});
|
||||||
|
|
||||||
|
const code = await TokenService.createToken({
|
||||||
|
type: TOKEN_EMAIL_MFA,
|
||||||
|
email
|
||||||
|
});
|
||||||
|
|
||||||
|
// send MFA code [code] to [email]
|
||||||
|
await sendMail({
|
||||||
|
template: 'emailMfa.handlebars',
|
||||||
|
subjectLine: 'Infisical MFA code',
|
||||||
|
recipients: [user.email],
|
||||||
|
substitutions: {
|
||||||
|
code
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
mfaEnabled: true,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkUserDevice({
|
||||||
|
user,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueAuthTokens({
|
||||||
|
userId: user._id,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: await getHttpsEnabled()
|
||||||
|
});
|
||||||
|
|
||||||
|
// case: user does not have MFA enablgged
|
||||||
|
// return (access) token in response
|
||||||
|
|
||||||
|
interface ResponseData {
|
||||||
|
mfaEnabled: boolean;
|
||||||
|
encryptionVersion: any;
|
||||||
|
protectedKey?: string;
|
||||||
|
protectedKeyIV?: string;
|
||||||
|
protectedKeyTag?: string;
|
||||||
|
token: string;
|
||||||
|
publicKey?: string;
|
||||||
|
encryptedPrivateKey?: string;
|
||||||
|
iv?: string;
|
||||||
|
tag?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response: ResponseData = {
|
||||||
|
mfaEnabled: false,
|
||||||
|
encryptionVersion: user.encryptionVersion,
|
||||||
|
token: tokens.token,
|
||||||
|
publicKey: user.publicKey,
|
||||||
|
encryptedPrivateKey: user.encryptedPrivateKey,
|
||||||
|
iv: user.iv,
|
||||||
|
tag: user.tag
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
user?.protectedKey &&
|
||||||
|
user?.protectedKeyIV &&
|
||||||
|
user?.protectedKeyTag
|
||||||
|
) {
|
||||||
|
response.protectedKey = user.protectedKey;
|
||||||
|
response.protectedKeyIV = user.protectedKeyIV
|
||||||
|
response.protectedKeyTag = user.protectedKeyTag;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.realIP
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to authenticate. Try again?'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import * as secretsController from './secretsController';
|
||||||
|
import * as workspacesController from './workspacesController';
|
||||||
|
import * as authController from './authController';
|
||||||
|
import * as signupController from './signupController';
|
||||||
|
|
||||||
|
export {
|
||||||
|
authController,
|
||||||
|
secretsController,
|
||||||
|
signupController,
|
||||||
|
workspacesController,
|
||||||
|
}
|
||||||
@@ -0,0 +1,420 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { Types } from "mongoose";
|
||||||
|
import { SecretService, EventService } from "../../services";
|
||||||
|
import { eventPushSecrets } from "../../events";
|
||||||
|
import { BotService } from "../../services";
|
||||||
|
import { repackageSecretToRaw } from "../../helpers/secrets";
|
||||||
|
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secrets for workspace with id [workspaceId] and environment
|
||||||
|
* [environment] in plaintext
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||||
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
const environment = req.query.environment as string;
|
||||||
|
const secretPath = req.query.secretPath as string;
|
||||||
|
|
||||||
|
const secrets = await SecretService.getSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
authData: req.authData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const key = await BotService.getWorkspaceKeyWithBot({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: secrets.map((secret) => {
|
||||||
|
const rep = repackageSecretToRaw({
|
||||||
|
secret,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return rep;
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret with name [secretName] in plaintext
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
const environment = req.query.environment as string;
|
||||||
|
const secretPath = req.query.secretPath as string;
|
||||||
|
const type = req.query.type as "shared" | "personal" | undefined;
|
||||||
|
|
||||||
|
const secret = await SecretService.getSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath,
|
||||||
|
authData: req.authData,
|
||||||
|
});
|
||||||
|
|
||||||
|
const key = await BotService.getWorkspaceKeyWithBot({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret: repackageSecretToRaw({
|
||||||
|
secret,
|
||||||
|
key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create secret with name [secretName] in plaintext
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecretRaw = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretValue,
|
||||||
|
secretComment,
|
||||||
|
secretPath = "/"
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const key = await BotService.getWorkspaceKeyWithBot({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: secretName,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: secretValue,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: secretComment,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secret = await SecretService.createSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
|
||||||
|
secretKeyIV: secretKeyEncrypted.iv,
|
||||||
|
secretKeyTag: secretKeyEncrypted.tag,
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
secretPath,
|
||||||
|
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||||
|
secretCommentIV: secretCommentEncrypted.iv,
|
||||||
|
secretCommentTag: secretCommentEncrypted.tag
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretWithoutBlindIndex = secret.toObject();
|
||||||
|
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret: repackageSecretToRaw({
|
||||||
|
secret: secretWithoutBlindIndex,
|
||||||
|
key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update secret with name [secretName]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretValue,
|
||||||
|
secretPath = "/",
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const key = await BotService.getWorkspaceKeyWithBot({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: secretValue,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secret = await SecretService.updateSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||||
|
secretValueIV: secretValueEncrypted.iv,
|
||||||
|
secretValueTag: secretValueEncrypted.tag,
|
||||||
|
secretPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret: repackageSecretToRaw({
|
||||||
|
secret,
|
||||||
|
key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete secret with name [secretName]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath = "/"
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const { secret } = await SecretService.deleteSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const key = await BotService.getWorkspaceKeyWithBot({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret: repackageSecretToRaw({
|
||||||
|
secret,
|
||||||
|
key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get secrets for workspace with id [workspaceId] and environment
|
||||||
|
* [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
const environment = req.query.environment as string;
|
||||||
|
const secretPath = req.query.secretPath as string;
|
||||||
|
|
||||||
|
const secrets = await SecretService.getSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
authData: req.authData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret with name [secretName]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getSecretByName = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
const environment = req.query.environment as string;
|
||||||
|
const secretPath = req.query.secretPath as string;
|
||||||
|
const type = req.query.type as "shared" | "personal" | undefined;
|
||||||
|
|
||||||
|
const secret = await SecretService.getSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath,
|
||||||
|
authData: req.authData,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create secret with name [secretName]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecret = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag,
|
||||||
|
secretPath = "/",
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const secret = await SecretService.createSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretPath,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretWithoutBlindIndex = secret.toObject();
|
||||||
|
delete secretWithoutBlindIndex.secretBlindIndex;
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret: secretWithoutBlindIndex,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update secret with name [secretName]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const updateSecretByName = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretPath = "/",
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const secret = await SecretService.updateSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete secret with name [secretName]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||||
|
const { secretName } = req.params;
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
secretPath = "/"
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const { secret } = await SecretService.deleteSecret({
|
||||||
|
secretName,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
type,
|
||||||
|
authData: req.authData,
|
||||||
|
secretPath,
|
||||||
|
});
|
||||||
|
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,194 @@
|
|||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { User, MembershipOrg } from '../../models';
|
||||||
|
import { completeAccount } from '../../helpers/user';
|
||||||
|
import {
|
||||||
|
initializeDefaultOrg
|
||||||
|
} from '../../helpers/signup';
|
||||||
|
import { issueAuthTokens, validateProviderAuthToken } from '../../helpers/auth';
|
||||||
|
import { INVITED, ACCEPTED } from '../../variables';
|
||||||
|
import { standardRequest } from '../../config/request';
|
||||||
|
import { getLoopsApiKey, getHttpsEnabled, getJwtSignupSecret } from '../../config';
|
||||||
|
import { BadRequestError } from '../../utils/errors';
|
||||||
|
import { TelemetryService } from '../../services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete setting up user by adding their personal and auth information as part of the
|
||||||
|
* signup flow
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const completeAccountSignup = async (req: Request, res: Response) => {
|
||||||
|
let user, token, refreshToken;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
email,
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier,
|
||||||
|
organizationName,
|
||||||
|
providerAuthToken,
|
||||||
|
attributionSource,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
protectedKey: string;
|
||||||
|
protectedKeyIV: string;
|
||||||
|
protectedKeyTag: string;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
encryptedPrivateKeyIV: string;
|
||||||
|
encryptedPrivateKeyTag: string;
|
||||||
|
salt: string;
|
||||||
|
verifier: string;
|
||||||
|
organizationName: string;
|
||||||
|
providerAuthToken?: string;
|
||||||
|
attributionSource?: string;
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
user = await User.findOne({ email });
|
||||||
|
|
||||||
|
if (!user || (user && user?.publicKey)) {
|
||||||
|
// case 1: user doesn't exist.
|
||||||
|
// case 2: user has already completed account
|
||||||
|
return res.status(403).send({
|
||||||
|
error: 'Failed to complete account for complete user'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (providerAuthToken) {
|
||||||
|
await validateProviderAuthToken({
|
||||||
|
email,
|
||||||
|
providerAuthToken,
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
|
||||||
|
if (AUTH_TOKEN_TYPE === null) {
|
||||||
|
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') {
|
||||||
|
throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` })
|
||||||
|
}
|
||||||
|
if (AUTH_TOKEN_VALUE === null) {
|
||||||
|
throw BadRequestError({
|
||||||
|
message: 'Missing Authorization Body in the request header',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (decodedToken.userId !== user.id) {
|
||||||
|
throw BadRequestError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// complete setting up user's account
|
||||||
|
user = await completeAccount({
|
||||||
|
userId: user._id.toString(),
|
||||||
|
firstName,
|
||||||
|
lastName,
|
||||||
|
encryptionVersion: 2,
|
||||||
|
protectedKey,
|
||||||
|
protectedKeyIV,
|
||||||
|
protectedKeyTag,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey,
|
||||||
|
encryptedPrivateKeyIV,
|
||||||
|
encryptedPrivateKeyTag,
|
||||||
|
salt,
|
||||||
|
verifier
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user)
|
||||||
|
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
|
||||||
|
|
||||||
|
// initialize default organization and workspace
|
||||||
|
await initializeDefaultOrg({
|
||||||
|
organizationName,
|
||||||
|
user
|
||||||
|
});
|
||||||
|
|
||||||
|
// update organization membership statuses that are
|
||||||
|
// invited to completed with user attached
|
||||||
|
await MembershipOrg.updateMany(
|
||||||
|
{
|
||||||
|
inviteEmail: email,
|
||||||
|
status: INVITED
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user,
|
||||||
|
status: ACCEPTED
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const tokens = await issueAuthTokens({
|
||||||
|
userId: user._id,
|
||||||
|
ip: req.realIP,
|
||||||
|
userAgent: req.headers['user-agent'] ?? ''
|
||||||
|
});
|
||||||
|
|
||||||
|
token = tokens.token;
|
||||||
|
|
||||||
|
// sending a welcome email to new users
|
||||||
|
if (await getLoopsApiKey()) {
|
||||||
|
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
|
||||||
|
"email": email,
|
||||||
|
"eventName": "Sign Up",
|
||||||
|
"firstName": firstName,
|
||||||
|
"lastName": lastName
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": "Bearer " + (await getLoopsApiKey())
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// store (refresh) token in httpOnly cookie
|
||||||
|
res.cookie('jid', tokens.refreshToken, {
|
||||||
|
httpOnly: true,
|
||||||
|
path: '/',
|
||||||
|
sameSite: 'strict',
|
||||||
|
secure: await getHttpsEnabled()
|
||||||
|
});
|
||||||
|
|
||||||
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'User Signed Up',
|
||||||
|
distinctId: email,
|
||||||
|
properties: {
|
||||||
|
email,
|
||||||
|
attributionSource
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to complete account setup'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully set up account',
|
||||||
|
user,
|
||||||
|
token
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { Secret } from '../../models';
|
||||||
|
import { SecretService } from'../../services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether or not all secrets in workspace with id [workspaceId]
|
||||||
|
* are blind-indexed
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
const secretsWithoutBlindIndex = await Secret.countDocuments({
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
secretBlindIndex: {
|
||||||
|
$exists: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send(secretsWithoutBlindIndex === 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all secrets for workspace with id [workspaceId]
|
||||||
|
*/
|
||||||
|
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
const secrets = await Secret.find({
|
||||||
|
workspace: new Types.ObjectId (workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update blind indices for secrets in workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||||
|
interface SecretToUpdate {
|
||||||
|
secretName: string;
|
||||||
|
_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const {
|
||||||
|
secretsToUpdate
|
||||||
|
}: {
|
||||||
|
secretsToUpdate: SecretToUpdate[];
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
// get secret blind index salt
|
||||||
|
const salt = await SecretService.getSecretBlindIndexSalt({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId)
|
||||||
|
});
|
||||||
|
|
||||||
|
// update secret blind indices
|
||||||
|
const operations = await Promise.all(
|
||||||
|
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => {
|
||||||
|
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
||||||
|
secretName: secretToUpdate.secretName,
|
||||||
|
salt
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
_id: new Types.ObjectId(secretToUpdate._id)
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
secretBlindIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
await Secret.bulkWrite(operations);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully named workspace secrets'
|
||||||
|
});
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Action, SecretVersion } from '../../models';
|
import { Action, SecretVersion } from '../../models';
|
||||||
import { ActionNotFoundError } from '../../../utils/errors';
|
import { ActionNotFoundError } from '../../../utils/errors';
|
||||||
|
|
||||||
@@ -28,4 +27,4 @@ export const getAction = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
action
|
action
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { EELicenseService } from '../../services';
|
||||||
|
import { getLicenseServerUrl } from '../../../config';
|
||||||
|
import { licenseServerKeyRequest } from '../../../config/request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return available cloud product information.
|
||||||
|
* Note: Nicely formatted to easily construct a table from
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getCloudProducts = async (req: Request, res: Response) => {
|
||||||
|
const billingCycle = req.query['billing-cycle'] as string;
|
||||||
|
|
||||||
|
if (EELicenseService.instanceType === 'cloud') {
|
||||||
|
const { data } = await licenseServerKeyRequest.get(
|
||||||
|
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
head: [],
|
||||||
|
rows: []
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,15 +1,19 @@
|
|||||||
import * as stripeController from './stripeController';
|
import * as stripeController from './stripeController';
|
||||||
import * as secretController from './secretController';
|
import * as secretController from './secretController';
|
||||||
import * as secretSnapshotController from './secretSnapshotController';
|
import * as secretSnapshotController from './secretSnapshotController';
|
||||||
|
import * as organizationsController from './organizationsController';
|
||||||
import * as workspaceController from './workspaceController';
|
import * as workspaceController from './workspaceController';
|
||||||
import * as actionController from './actionController';
|
import * as actionController from './actionController';
|
||||||
import * as membershipController from './membershipController';
|
import * as membershipController from './membershipController';
|
||||||
|
import * as cloudProductsController from './cloudProductsController';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
stripeController,
|
stripeController,
|
||||||
secretController,
|
secretController,
|
||||||
secretSnapshotController,
|
secretSnapshotController,
|
||||||
|
organizationsController,
|
||||||
workspaceController,
|
workspaceController,
|
||||||
actionController,
|
actionController,
|
||||||
membershipController
|
membershipController,
|
||||||
|
cloudProductsController
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,8 @@ import { Request, Response } from "express";
|
|||||||
import { Membership, Workspace } from "../../../models";
|
import { Membership, Workspace } from "../../../models";
|
||||||
import { IMembershipPermission } from "../../../models/membership";
|
import { IMembershipPermission } from "../../../models/membership";
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
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 { Builder } from "builder-pattern"
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
|
|
||||||
@@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
|
|||||||
const { membershipId } = req.params;
|
const { membershipId } = req.params;
|
||||||
const { permissions } = req.body;
|
const { permissions } = req.body;
|
||||||
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
|
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" })
|
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { getLicenseServerUrl } from '../../../config';
|
||||||
|
import { licenseServerKeyRequest } from '../../../config/request';
|
||||||
|
import { EELicenseService } from '../../services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the organization's current plan and allowed feature set
|
||||||
|
*/
|
||||||
|
export const getOrganizationPlan = async (req: Request, res: Response) => {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
const workspaceId = req.query.workspaceId as string;
|
||||||
|
|
||||||
|
const plan = await EELicenseService.getPlan(organizationId, workspaceId);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
plan,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the organization plan to product with id [productId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateOrganizationPlan = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
productId
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const { data } = await licenseServerKeyRequest.patch(
|
||||||
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`,
|
||||||
|
{
|
||||||
|
productId
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the organization's payment methods on file
|
||||||
|
*/
|
||||||
|
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
|
||||||
|
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
|
||||||
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
pmtMethods
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return a Stripe session URL to add payment method for organization
|
||||||
|
*/
|
||||||
|
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||||
|
const {
|
||||||
|
success_url,
|
||||||
|
cancel_url
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const { data: { url } } = await licenseServerKeyRequest.post(
|
||||||
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
|
||||||
|
{
|
||||||
|
success_url,
|
||||||
|
cancel_url
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
url
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
|
||||||
|
const { pmtMethodId } = req.params;
|
||||||
|
|
||||||
|
const { data } = await licenseServerKeyRequest.delete(
|
||||||
|
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return res.status(200).send(data);
|
||||||
|
}
|
||||||
@@ -1,16 +1,15 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import { Secret } from "../../../models";
|
||||||
import { Secret } from '../../../models';
|
import { SecretVersion } from "../../models";
|
||||||
import { SecretVersion } from '../../models';
|
import { EESecretService } from "../../services";
|
||||||
import { EESecretService } from '../../services';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret versions for secret with id [secretId]
|
* Return secret versions for secret with id [secretId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||||
/*
|
/*
|
||||||
#swagger.summary = 'Return secret versions'
|
#swagger.summary = 'Return secret versions'
|
||||||
#swagger.description = 'Return secret versions'
|
#swagger.description = 'Return secret versions'
|
||||||
|
|
||||||
@@ -55,41 +54,34 @@ import { EESecretService } from '../../services';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let secretVersions;
|
const { secretId, workspaceId, environment, folderId } = req.params;
|
||||||
try {
|
|
||||||
const { secretId } = req.params;
|
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
secretVersions = await SecretVersion.find({
|
|
||||||
secret: secretId
|
|
||||||
})
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.skip(offset)
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
} catch (err) {
|
const secretVersions = await SecretVersion.find({
|
||||||
Sentry.setUser({ email: req.user.email });
|
secret: secretId,
|
||||||
Sentry.captureException(err);
|
workspace: workspaceId,
|
||||||
return res.status(400).send({
|
environment,
|
||||||
message: 'Failed to get secret versions'
|
folder: folderId,
|
||||||
});
|
})
|
||||||
}
|
.sort({ createdAt: -1 })
|
||||||
|
.skip(offset)
|
||||||
return res.status(200).send({
|
.limit(limit);
|
||||||
secretVersions
|
|
||||||
});
|
return res.status(200).send({
|
||||||
}
|
secretVersions,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roll back secret with id [secretId] to version [version]
|
* Roll back secret with id [secretId] to version [version]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||||
/*
|
/*
|
||||||
#swagger.summary = 'Roll back secret to a version.'
|
#swagger.summary = 'Roll back secret to a version.'
|
||||||
#swagger.description = 'Roll back secret to a version.'
|
#swagger.description = 'Roll back secret to a version.'
|
||||||
|
|
||||||
@@ -137,88 +129,92 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let secret;
|
const { secretId } = req.params;
|
||||||
try {
|
const { version } = req.body;
|
||||||
const { secretId } = req.params;
|
|
||||||
const { version } = req.body;
|
|
||||||
|
|
||||||
// validate secret version
|
|
||||||
const oldSecretVersion = await SecretVersion.findOne({
|
|
||||||
secret: secretId,
|
|
||||||
version
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!oldSecretVersion) throw new Error('Failed to find secret version');
|
|
||||||
|
|
||||||
const {
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
} = oldSecretVersion;
|
|
||||||
|
|
||||||
// update secret
|
|
||||||
secret = await Secret.findByIdAndUpdate(
|
|
||||||
secretId,
|
|
||||||
{
|
|
||||||
$inc: {
|
|
||||||
version: 1
|
|
||||||
},
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!secret) throw new Error('Failed to find and update secret');
|
|
||||||
|
|
||||||
// add new secret version
|
// validate secret version
|
||||||
await new SecretVersion({
|
const oldSecretVersion = await SecretVersion.findOne({
|
||||||
secret: secretId,
|
secret: secretId,
|
||||||
version: secret.version,
|
version,
|
||||||
workspace,
|
}).select("+secretBlindIndex");
|
||||||
type,
|
|
||||||
user,
|
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
||||||
environment,
|
|
||||||
isDeleted: false,
|
const {
|
||||||
secretKeyCiphertext,
|
workspace,
|
||||||
secretKeyIV,
|
type,
|
||||||
secretKeyTag,
|
user,
|
||||||
secretValueCiphertext,
|
environment,
|
||||||
secretValueIV,
|
secretBlindIndex,
|
||||||
secretValueTag
|
secretKeyCiphertext,
|
||||||
}).save();
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
// take secret snapshot
|
secretValueCiphertext,
|
||||||
await EESecretService.takeSecretSnapshot({
|
secretValueIV,
|
||||||
workspaceId: secret.workspace.toString()
|
secretValueTag,
|
||||||
});
|
algorithm,
|
||||||
|
folder,
|
||||||
} catch (err) {
|
keyEncoding,
|
||||||
Sentry.setUser({ email: req.user.email });
|
} = oldSecretVersion;
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
// update secret
|
||||||
message: 'Failed to roll back secret version'
|
const secret = await Secret.findByIdAndUpdate(
|
||||||
});
|
secretId,
|
||||||
}
|
{
|
||||||
|
$inc: {
|
||||||
return res.status(200).send({
|
version: 1,
|
||||||
secret
|
},
|
||||||
});
|
workspace,
|
||||||
}
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
folderId: folder,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
new: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!secret) throw new Error("Failed to find and update secret");
|
||||||
|
|
||||||
|
// add new secret version
|
||||||
|
await new SecretVersion({
|
||||||
|
secret: secretId,
|
||||||
|
version: secret.version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
folder,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: secret.workspace,
|
||||||
|
environment,
|
||||||
|
folderId: folder,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,39 +1,45 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import {
|
||||||
import { SecretSnapshot } from '../../models';
|
ISecretVersion,
|
||||||
|
SecretSnapshot,
|
||||||
|
TFolderRootVersionSchema,
|
||||||
|
} from "../../models";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret snapshot with id [secretSnapshotId]
|
* Return secret snapshot with id [secretSnapshotId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||||
let secretSnapshot;
|
const { secretSnapshotId } = req.params;
|
||||||
try {
|
|
||||||
const { secretSnapshotId } = req.params;
|
|
||||||
|
|
||||||
secretSnapshot = await SecretSnapshot
|
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
|
||||||
.findById(secretSnapshotId)
|
.lean()
|
||||||
.populate({
|
.populate<{ secretVersions: ISecretVersion[] }>({
|
||||||
path: 'secretVersions',
|
path: 'secretVersions',
|
||||||
populate: {
|
populate: {
|
||||||
path: 'tags',
|
path: 'tags',
|
||||||
model: 'Tag',
|
model: 'Tag'
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
|
||||||
|
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
const folderId = secretSnapshot.folderId;
|
||||||
Sentry.captureException(err);
|
// to show only the folder required secrets
|
||||||
return res.status(400).send({
|
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
|
||||||
message: 'Failed to get secret snapshot'
|
({ folder }) => folder === folderId
|
||||||
});
|
);
|
||||||
}
|
|
||||||
|
secretSnapshot.folderVersion =
|
||||||
return res.status(200).send({
|
secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({
|
||||||
secretSnapshot
|
id,
|
||||||
});
|
name,
|
||||||
}
|
})) as any;
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secretSnapshot,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
||||||
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const handleWebhook = async (req: Request, res: Response) => {
|
export const handleWebhook = async (req: Request, res: Response) => {
|
||||||
let event;
|
const stripe = new Stripe(await getStripeSecretKey(), {
|
||||||
try {
|
apiVersion: '2022-08-01'
|
||||||
const stripe = new Stripe(getStripeSecretKey(), {
|
});
|
||||||
apiVersion: '2022-08-01'
|
|
||||||
});
|
|
||||||
|
|
||||||
// check request for valid stripe signature
|
// check request for valid stripe signature
|
||||||
const sig = req.headers['stripe-signature'] as string;
|
const sig = req.headers['stripe-signature'] as string;
|
||||||
event = stripe.webhooks.constructEvent(
|
const event = stripe.webhooks.constructEvent(
|
||||||
req.body,
|
req.body,
|
||||||
sig,
|
sig,
|
||||||
getStripeWebhookSecret()
|
await getStripeWebhookSecret()
|
||||||
);
|
);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
error: 'Failed to process webhook'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case '':
|
case '':
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import { PipelineStage, Types } from "mongoose";
|
||||||
import { Types } from 'mongoose';
|
import { Secret } from "../../../models";
|
||||||
import {
|
import {
|
||||||
Secret
|
SecretSnapshot,
|
||||||
} from '../../../models';
|
Log,
|
||||||
import {
|
SecretVersion,
|
||||||
SecretSnapshot,
|
ISecretVersion,
|
||||||
Log,
|
FolderVersion,
|
||||||
SecretVersion,
|
TFolderRootVersionSchema,
|
||||||
ISecretVersion
|
} from "../../models";
|
||||||
} from '../../models';
|
import { EESecretService } from "../../services";
|
||||||
import { EESecretService } from '../../services';
|
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
|
||||||
import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
import Folder, { TFolderSchema } from "../../../models/folder";
|
||||||
|
import { searchByFolderId } from "../../../services/FolderService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret snapshots for workspace with id [workspaceId]
|
* Return secret snapshots for workspace with id [workspaceId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
|
export const getWorkspaceSecretSnapshots = async (
|
||||||
/*
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
/*
|
||||||
#swagger.summary = 'Return project secret snapshot ids'
|
#swagger.summary = 'Return project secret snapshot ids'
|
||||||
#swagger.description = 'Return project secret snapshots ids'
|
#swagger.description = 'Return project secret snapshots ids'
|
||||||
|
|
||||||
@@ -64,66 +68,60 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let secretSnapshots;
|
const { workspaceId } = req.params;
|
||||||
try {
|
const { environment, folderId } = req.query;
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
secretSnapshots = await SecretSnapshot.find({
|
|
||||||
workspace: workspaceId
|
|
||||||
})
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.skip(offset)
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
} catch (err) {
|
const secretSnapshots = await SecretSnapshot.find({
|
||||||
Sentry.setUser({ email: req.user.email });
|
workspace: workspaceId,
|
||||||
Sentry.captureException(err);
|
environment,
|
||||||
return res.status(400).send({
|
folderId: folderId || "root",
|
||||||
message: 'Failed to get secret snapshots'
|
})
|
||||||
});
|
.sort({ createdAt: -1 })
|
||||||
}
|
.skip(offset)
|
||||||
|
.limit(limit);
|
||||||
return res.status(200).send({
|
|
||||||
secretSnapshots
|
return res.status(200).send({
|
||||||
});
|
secretSnapshots,
|
||||||
}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return count of secret snapshots for workspace with id [workspaceId]
|
* Return count of secret snapshots for workspace with id [workspaceId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
|
export const getWorkspaceSecretSnapshotsCount = async (
|
||||||
let count;
|
req: Request,
|
||||||
try {
|
res: Response
|
||||||
const { workspaceId } = req.params;
|
) => {
|
||||||
count = await SecretSnapshot.countDocuments({
|
const { workspaceId } = req.params;
|
||||||
workspace: workspaceId
|
const { environment, folderId } = req.query;
|
||||||
});
|
|
||||||
} catch (err) {
|
const count = await SecretSnapshot.countDocuments({
|
||||||
Sentry.setUser({ email: req.user.email });
|
workspace: workspaceId,
|
||||||
Sentry.captureException(err);
|
environment,
|
||||||
return res.status(400).send({
|
folderId: folderId || "root",
|
||||||
message: 'Failed to count number of secret snapshots'
|
});
|
||||||
});
|
|
||||||
}
|
return res.status(200).send({
|
||||||
|
count,
|
||||||
return res.status(200).send({
|
});
|
||||||
count
|
};
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
|
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
|
export const rollbackWorkspaceSecretSnapshot = async (
|
||||||
/*
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
/*
|
||||||
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.'
|
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.'
|
||||||
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
|
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
|
||||||
|
|
||||||
@@ -173,160 +171,329 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let secrets;
|
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
const { version } = req.body;
|
|
||||||
|
|
||||||
// validate secret snapshot
|
|
||||||
const secretSnapshot = await SecretSnapshot.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
version
|
|
||||||
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
|
|
||||||
|
|
||||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
|
||||||
|
|
||||||
// TODO: fix any
|
const { workspaceId } = req.params;
|
||||||
const oldSecretVersionsObj: any = secretSnapshot.secretVersions
|
const { version, environment, folderId = "root" } = req.body;
|
||||||
.reduce((accumulator, s) => ({
|
|
||||||
...accumulator,
|
|
||||||
[`${s.secret.toString()}`]: s
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
|
||||||
secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: fix any
|
|
||||||
const latestSecretVersions: any = (await SecretVersion.find({
|
|
||||||
_id: {
|
|
||||||
$in: latestSecretVersionIds.map((s) => s.versionId)
|
|
||||||
}
|
|
||||||
}, 'secret version'))
|
|
||||||
.reduce((accumulator, s) => ({
|
|
||||||
...accumulator,
|
|
||||||
[`${s.secret.toString()}`]: s
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
// delete existing secrets
|
|
||||||
await Secret.deleteMany({
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
// add secrets
|
// validate secret snapshot
|
||||||
secrets = await Secret.insertMany(
|
const secretSnapshot = await SecretSnapshot.findOne({
|
||||||
secretSnapshot.secretVersions.map((sv) => {
|
workspace: workspaceId,
|
||||||
const secretId = sv.secret;
|
version,
|
||||||
const {
|
environment,
|
||||||
workspace,
|
folderId: folderId,
|
||||||
type,
|
})
|
||||||
user,
|
.populate<{ secretVersions: ISecretVersion[] }>({
|
||||||
environment,
|
path: "secretVersions",
|
||||||
secretKeyCiphertext,
|
select: "+secretBlindIndex",
|
||||||
secretKeyIV,
|
})
|
||||||
secretKeyTag,
|
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||||
secretKeyHash,
|
|
||||||
secretValueCiphertext,
|
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
const snapshotFolderTree = secretSnapshot.folderVersion;
|
||||||
secretValueHash,
|
const latestFolderTree = await Folder.findOne({
|
||||||
createdAt
|
workspace: workspaceId,
|
||||||
} = oldSecretVersionsObj[secretId.toString()];
|
environment,
|
||||||
|
});
|
||||||
return ({
|
|
||||||
_id: secretId,
|
const latestFolderVersion = await FolderVersion.findOne({
|
||||||
version: latestSecretVersions[secretId.toString()].version + 1,
|
environment,
|
||||||
workspace,
|
workspace: workspaceId,
|
||||||
type,
|
"nodes.id": folderId,
|
||||||
user,
|
}).sort({ "nodes.version": -1 });
|
||||||
environment,
|
|
||||||
secretKeyCiphertext,
|
const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
|
||||||
secretKeyIV,
|
const secretIds: Types.ObjectId[] = [];
|
||||||
secretKeyTag,
|
const folderIds: string[] = [folderId];
|
||||||
secretKeyHash,
|
|
||||||
secretValueCiphertext,
|
secretSnapshot.secretVersions.forEach((snapSecVer) => {
|
||||||
secretValueIV,
|
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
||||||
secretValueTag,
|
secretIds.push(snapSecVer.secret);
|
||||||
secretValueHash,
|
});
|
||||||
secretCommentCiphertext: '',
|
|
||||||
secretCommentIV: '',
|
// the parent node from current latest one
|
||||||
secretCommentTag: '',
|
// this will be modified according to the snapshot and latest snapshots
|
||||||
createdAt
|
const newFolderTree =
|
||||||
});
|
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
|
||||||
})
|
|
||||||
);
|
if (newFolderTree) {
|
||||||
|
newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
|
||||||
// add secret versions
|
const queue = [newFolderTree];
|
||||||
await SecretVersion.insertMany(
|
// a bfs algorithm in which we take the latest snapshots of all the folders in a level
|
||||||
secrets.map(({
|
while (queue.length) {
|
||||||
_id,
|
const groupByFolderId: Record<string, TFolderSchema> = {};
|
||||||
version,
|
// the original queue is popped out completely to get what ever in a level
|
||||||
workspace,
|
// subqueue is filled with all the children thus next level folders
|
||||||
type,
|
// subQueue will then be transfered to the oriinal queue
|
||||||
user,
|
const subQueue: TFolderSchema[] = [];
|
||||||
environment,
|
// get everything inside a level
|
||||||
secretKeyCiphertext,
|
while (queue.length) {
|
||||||
secretKeyIV,
|
const folder = queue.pop() as TFolderSchema;
|
||||||
secretKeyTag,
|
folder.children.forEach((el) => {
|
||||||
secretKeyHash,
|
folderIds.push(el.id); // push ids and data into queu
|
||||||
secretValueCiphertext,
|
subQueue.push(el);
|
||||||
secretValueIV,
|
// to modify the original tree very fast we keep a reference object
|
||||||
secretValueTag,
|
// key with folder id and pointing to the various nodes
|
||||||
secretValueHash
|
groupByFolderId[el.id] = el;
|
||||||
}) => ({
|
});
|
||||||
_id: new Types.ObjectId(),
|
}
|
||||||
secret: _id,
|
// get latest snapshots of all the folder
|
||||||
version,
|
const matchWsFoldersPipeline = {
|
||||||
workspace,
|
$match: {
|
||||||
type,
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
user,
|
environment,
|
||||||
environment,
|
folderId: {
|
||||||
isDeleted: false,
|
$in: Object.keys(groupByFolderId),
|
||||||
secretKeyCiphertext,
|
},
|
||||||
secretKeyIV,
|
},
|
||||||
secretKeyTag,
|
};
|
||||||
secretKeyHash,
|
const sortByFolderIdAndVersion: PipelineStage = {
|
||||||
secretValueCiphertext,
|
$sort: { folderId: 1, version: -1 },
|
||||||
secretValueIV,
|
};
|
||||||
secretValueTag,
|
const pickLatestVersionOfEachFolder = {
|
||||||
secretValueHash
|
$group: {
|
||||||
}))
|
_id: "$folderId",
|
||||||
);
|
latestVersion: { $first: "$version" },
|
||||||
|
doc: {
|
||||||
// update secret versions of restored secrets as not deleted
|
$first: "$$ROOT",
|
||||||
await SecretVersion.updateMany({
|
},
|
||||||
secret: {
|
},
|
||||||
$in: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
};
|
||||||
}
|
const populateSecVersion = {
|
||||||
}, {
|
$lookup: {
|
||||||
isDeleted: false
|
from: SecretVersion.collection.name,
|
||||||
});
|
localField: "doc.secretVersions",
|
||||||
|
foreignField: "_id",
|
||||||
// take secret snapshot
|
as: "doc.secretVersions",
|
||||||
await EESecretService.takeSecretSnapshot({
|
},
|
||||||
workspaceId
|
};
|
||||||
});
|
const populateFolderVersion = {
|
||||||
} catch (err) {
|
$lookup: {
|
||||||
Sentry.setUser({ email: req.user.email });
|
from: FolderVersion.collection.name,
|
||||||
Sentry.captureException(err);
|
localField: "doc.folderVersion",
|
||||||
return res.status(400).send({
|
foreignField: "_id",
|
||||||
message: 'Failed to roll back secret snapshot'
|
as: "doc.folderVersion",
|
||||||
});
|
},
|
||||||
|
};
|
||||||
|
const unwindFolderVerField = {
|
||||||
|
$unwind: {
|
||||||
|
path: "$doc.folderVersion",
|
||||||
|
preserveNullAndEmptyArrays: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
|
||||||
|
await SecretSnapshot.aggregate([
|
||||||
|
matchWsFoldersPipeline,
|
||||||
|
sortByFolderIdAndVersion,
|
||||||
|
pickLatestVersionOfEachFolder,
|
||||||
|
populateSecVersion,
|
||||||
|
populateFolderVersion,
|
||||||
|
unwindFolderVerField,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// recursive snapshotting each level
|
||||||
|
latestSnapshotsByFolders.forEach((snap) => {
|
||||||
|
// mutate the folder tree to update the nodes to the latest version tree
|
||||||
|
// we are reconstructing the folder tree by latest snapshots here
|
||||||
|
if (groupByFolderId[snap.doc.folderId]) {
|
||||||
|
groupByFolderId[snap.doc.folderId].children =
|
||||||
|
snap.doc?.folderVersion?.nodes?.children || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// push all children of next level snapshots
|
||||||
|
if (snap.doc.folderVersion?.nodes?.children) {
|
||||||
|
queue.push(...snap.doc.folderVersion.nodes.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
snap.doc.secretVersions.forEach((snapSecVer) => {
|
||||||
|
// record all the secrets
|
||||||
|
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
||||||
|
secretIds.push(snapSecVer.secret);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
queue.push(...subQueue);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return res.status(200).send({
|
|
||||||
secrets
|
// TODO: fix any
|
||||||
});
|
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
||||||
}
|
secretIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: fix any
|
||||||
|
const latestSecretVersions: any = (
|
||||||
|
await SecretVersion.find(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: latestSecretVersionIds.map((s) => s.versionId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"secret version"
|
||||||
|
)
|
||||||
|
).reduce(
|
||||||
|
(accumulator, s) => ({
|
||||||
|
...accumulator,
|
||||||
|
[`${s.secret.toString()}`]: s,
|
||||||
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const secDelQuery: Record<string, unknown> = {
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
// undefined means root thus collect all secrets
|
||||||
|
};
|
||||||
|
if (folderId !== "root" && folderIds.length)
|
||||||
|
secDelQuery.folder = { $in: folderIds };
|
||||||
|
|
||||||
|
// delete existing secrets
|
||||||
|
await Secret.deleteMany(secDelQuery);
|
||||||
|
await Folder.deleteOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
|
||||||
|
// add secrets
|
||||||
|
const secrets = await Secret.insertMany(
|
||||||
|
Object.keys(oldSecretVersionsObj).map((sv) => {
|
||||||
|
const {
|
||||||
|
secret: secretId,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretBlindIndex,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
createdAt,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
} = oldSecretVersionsObj[sv];
|
||||||
|
|
||||||
|
return {
|
||||||
|
_id: secretId,
|
||||||
|
version: latestSecretVersions[secretId.toString()].version + 1,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretBlindIndex: secretBlindIndex ?? undefined,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext: "",
|
||||||
|
secretCommentIV: "",
|
||||||
|
secretCommentTag: "",
|
||||||
|
createdAt,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// add secret versions
|
||||||
|
const secretV = await SecretVersion.insertMany(
|
||||||
|
secrets.map(
|
||||||
|
({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretBlindIndex,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretBlindIndex: secretBlindIndex ?? undefined,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (newFolderTree && latestFolderTree) {
|
||||||
|
// save the updated folder tree to the present one
|
||||||
|
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
|
||||||
|
latestFolderTree._id = new Types.ObjectId();
|
||||||
|
latestFolderTree.isNew = true;
|
||||||
|
await latestFolderTree.save();
|
||||||
|
|
||||||
|
// create new folder version
|
||||||
|
const newFolderVersion = new FolderVersion({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
nodes: newFolderTree,
|
||||||
|
});
|
||||||
|
await newFolderVersion.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update secret versions of restored secrets as not deleted
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{
|
||||||
|
secret: {
|
||||||
|
$in: Object.keys(oldSecretVersionsObj).map(
|
||||||
|
(sv) => oldSecretVersionsObj[sv].secret
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDeleted: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
folderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return (audit) logs for workspace with id [workspaceId]
|
* Return (audit) logs for workspace with id [workspaceId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
||||||
/*
|
/*
|
||||||
#swagger.summary = 'Return project (audit) logs'
|
#swagger.summary = 'Return project (audit) logs'
|
||||||
#swagger.description = 'Return project (audit) logs'
|
#swagger.description = 'Return project (audit) logs'
|
||||||
|
|
||||||
@@ -392,43 +559,32 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let logs
|
const { workspaceId } = req.params;
|
||||||
try {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
const sortBy: string = req.query.sortBy as string;
|
const sortBy: string = req.query.sortBy as string;
|
||||||
const userId: string = req.query.userId as string;
|
const userId: string = req.query.userId as string;
|
||||||
const actionNames: string = req.query.actionNames as string;
|
const actionNames: string = req.query.actionNames as string;
|
||||||
|
|
||||||
logs = await Log.find({
|
|
||||||
workspace: workspaceId,
|
|
||||||
...( userId ? { user: userId } : {}),
|
|
||||||
...(
|
|
||||||
actionNames
|
|
||||||
? {
|
|
||||||
actionNames: {
|
|
||||||
$in: actionNames.split(',')
|
|
||||||
}
|
|
||||||
} : {}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
|
|
||||||
.skip(offset)
|
|
||||||
.limit(limit)
|
|
||||||
.populate('actions')
|
|
||||||
.populate('user');
|
|
||||||
|
|
||||||
} catch (err) {
|
const logs = await Log.find({
|
||||||
Sentry.setUser({ email: req.user.email });
|
workspace: workspaceId,
|
||||||
Sentry.captureException(err);
|
...(userId ? { user: userId } : {}),
|
||||||
return res.status(400).send({
|
...(actionNames
|
||||||
message: 'Failed to get workspace logs'
|
? {
|
||||||
});
|
actionNames: {
|
||||||
}
|
$in: actionNames.split(","),
|
||||||
|
},
|
||||||
return res.status(200).send({
|
}
|
||||||
logs
|
: {}),
|
||||||
});
|
})
|
||||||
}
|
.sort({ createdAt: sortBy === "recent" ? -1 : 1 })
|
||||||
|
.skip(offset)
|
||||||
|
.limit(limit)
|
||||||
|
.populate("actions")
|
||||||
|
.populate("user serviceAccount serviceTokenData");
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
logs,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { Action } from '../models';
|
import { Action } from '../models';
|
||||||
import {
|
import {
|
||||||
@@ -24,39 +23,37 @@ import {
|
|||||||
const createActionUpdateSecret = async ({
|
const createActionUpdateSecret = async ({
|
||||||
name,
|
name,
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds
|
secretIds
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
userId: Types.ObjectId;
|
userId?: Types.ObjectId;
|
||||||
|
serviceAccountId?: Types.ObjectId;
|
||||||
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||||
try {
|
secretIds,
|
||||||
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
n: 2
|
||||||
secretIds,
|
}))
|
||||||
n: 2
|
.map((s) => ({
|
||||||
}))
|
oldSecretVersion: s.versions[0]._id,
|
||||||
.map((s) => ({
|
newSecretVersion: s.versions[1]._id
|
||||||
oldSecretVersion: s.versions[0]._id,
|
}));
|
||||||
newSecretVersion: s.versions[1]._id
|
|
||||||
}));
|
const action = await new Action({
|
||||||
|
name,
|
||||||
action = await new Action({
|
user: userId,
|
||||||
name,
|
serviceAccount: serviceAccountId,
|
||||||
user: userId,
|
serviceTokenData: serviceTokenDataId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
payload: {
|
payload: {
|
||||||
secretVersions: latestSecretVersions
|
secretVersions: latestSecretVersions
|
||||||
}
|
}
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create update secret action');
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -72,68 +69,66 @@ const createActionUpdateSecret = async ({
|
|||||||
const createActionSecret = async ({
|
const createActionSecret = async ({
|
||||||
name,
|
name,
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds
|
secretIds
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
userId: Types.ObjectId;
|
userId?: Types.ObjectId;
|
||||||
|
serviceAccountId?: Types.ObjectId;
|
||||||
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
// case: action is adding, deleting, or reading secrets
|
||||||
try {
|
// -> add new secret versions
|
||||||
// case: action is adding, deleting, or reading secrets
|
const latestSecretVersions = (await getLatestSecretVersionIds({
|
||||||
// -> add new secret versions
|
secretIds
|
||||||
const latestSecretVersions = (await getLatestSecretVersionIds({
|
}))
|
||||||
secretIds
|
.map((s) => ({
|
||||||
}))
|
newSecretVersion: s.versionId
|
||||||
.map((s) => ({
|
}));
|
||||||
newSecretVersion: s.versionId
|
|
||||||
}));
|
const action = await new Action({
|
||||||
|
name,
|
||||||
action = await new Action({
|
user: userId,
|
||||||
name,
|
serviceAccount: serviceAccountId,
|
||||||
user: userId,
|
serviceTokenData: serviceTokenDataId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
payload: {
|
payload: {
|
||||||
secretVersions: latestSecretVersions
|
secretVersions: latestSecretVersions
|
||||||
}
|
}
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create action create/read/delete secret action');
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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 {Object} obj
|
||||||
* @param {String} obj.name - name of action
|
* @param {String} obj.name - name of action
|
||||||
* @param {String} obj.userId - id of user associated with action
|
* @param {String} obj.userId - id of user associated with action
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const createActionUser = ({
|
const createActionClient = ({
|
||||||
name,
|
name,
|
||||||
userId
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
userId: Types.ObjectId;
|
userId?: Types.ObjectId;
|
||||||
|
serviceAccountId?: Types.ObjectId;
|
||||||
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
const action = new Action({
|
||||||
try {
|
name,
|
||||||
action = new Action({
|
user: userId,
|
||||||
name,
|
serviceAccount: serviceAccountId,
|
||||||
user: userId
|
serviceTokenData: serviceTokenDataId
|
||||||
}).save();
|
}).save();
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create user action');
|
|
||||||
}
|
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
}
|
}
|
||||||
@@ -149,49 +144,47 @@ const createActionUser = ({
|
|||||||
const createActionHelper = async ({
|
const createActionHelper = async ({
|
||||||
name,
|
name,
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds,
|
secretIds,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
userId: Types.ObjectId;
|
userId?: Types.ObjectId;
|
||||||
|
serviceAccountId?: Types.ObjectId;
|
||||||
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
workspaceId?: Types.ObjectId;
|
workspaceId?: Types.ObjectId;
|
||||||
secretIds?: Types.ObjectId[];
|
secretIds?: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
let action;
|
let action;
|
||||||
try {
|
switch (name) {
|
||||||
switch (name) {
|
case ACTION_LOGIN:
|
||||||
case ACTION_LOGIN:
|
case ACTION_LOGOUT:
|
||||||
case ACTION_LOGOUT:
|
action = await createActionClient({
|
||||||
action = await createActionUser({
|
name,
|
||||||
name,
|
userId
|
||||||
userId
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case ACTION_ADD_SECRETS:
|
||||||
case ACTION_ADD_SECRETS:
|
case ACTION_READ_SECRETS:
|
||||||
case ACTION_READ_SECRETS:
|
case ACTION_DELETE_SECRETS:
|
||||||
case ACTION_DELETE_SECRETS:
|
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
action = await createActionSecret({
|
||||||
action = await createActionSecret({
|
name,
|
||||||
name,
|
userId,
|
||||||
userId,
|
workspaceId,
|
||||||
workspaceId,
|
secretIds
|
||||||
secretIds
|
});
|
||||||
});
|
break;
|
||||||
break;
|
case ACTION_UPDATE_SECRETS:
|
||||||
case ACTION_UPDATE_SECRETS:
|
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
||||||
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
|
action = await createActionUpdateSecret({
|
||||||
action = await createActionUpdateSecret({
|
name,
|
||||||
name,
|
userId,
|
||||||
userId,
|
workspaceId,
|
||||||
workspaceId,
|
secretIds
|
||||||
secretIds
|
});
|
||||||
});
|
break;
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create action');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return action;
|
return action;
|
||||||
@@ -199,4 +192,4 @@ const createActionHelper = async ({
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
createActionHelper
|
createActionHelper
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import { Membership } from "../../models";
|
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 })
|
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||||
if (!membershipForWorkspace) {
|
if (!membershipForWorkspace) {
|
||||||
return false
|
return false
|
||||||
@@ -18,15 +19,15 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
|
|||||||
return true
|
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 })
|
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||||
if (!membershipForWorkspace) {
|
if (!membershipForWorkspace) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||||
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||||
|
|
||||||
// case: you have write only if read is blocked and write is not
|
// case: you have write only if read is blocked and write is not
|
||||||
if (isReadDisallowed && !isWriteDisallowed) {
|
if (isReadDisallowed && !isWriteDisallowed) {
|
||||||
@@ -36,15 +37,15 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
|
|||||||
return false
|
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 })
|
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||||
if (!membershipForWorkspace) {
|
if (!membershipForWorkspace) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
|
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
||||||
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
|
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
||||||
|
|
||||||
if (isReadBlocked && isWriteDisallowed) {
|
if (isReadBlocked && isWriteDisallowed) {
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Log,
|
Log,
|
||||||
IAction
|
IAction
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an (audit) log
|
* Create an (audit) log
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -16,36 +16,35 @@ import {
|
|||||||
*/
|
*/
|
||||||
const createLogHelper = async ({
|
const createLogHelper = async ({
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions,
|
actions,
|
||||||
channel,
|
channel,
|
||||||
ipAddress
|
ipAddress
|
||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId;
|
userId?: Types.ObjectId;
|
||||||
|
serviceAccountId?: Types.ObjectId;
|
||||||
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
workspaceId?: Types.ObjectId;
|
workspaceId?: Types.ObjectId;
|
||||||
actions: IAction[];
|
actions: IAction[];
|
||||||
channel: string;
|
channel: string;
|
||||||
ipAddress: string;
|
ipAddress: string;
|
||||||
}) => {
|
}) => {
|
||||||
let log;
|
const log = await new Log({
|
||||||
try {
|
user: userId,
|
||||||
log = await new Log({
|
serviceAccount: serviceAccountId,
|
||||||
user: userId,
|
serviceTokenData: serviceTokenDataId,
|
||||||
workspace: workspaceId ?? undefined,
|
workspace: workspaceId ?? undefined,
|
||||||
actionNames: actions.map((a) => a.name),
|
actionNames: actions.map((a) => a.name),
|
||||||
actions,
|
actions,
|
||||||
channel,
|
channel,
|
||||||
ipAddress
|
ipAddress
|
||||||
}).save();
|
}).save();
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create log');
|
|
||||||
}
|
|
||||||
|
|
||||||
return log;
|
return log;
|
||||||
}
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createLogHelper
|
createLogHelper
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import * as Sentry from '@sentry/node';
|
import { Secret, ISecret } from "../../models";
|
||||||
import {
|
import {
|
||||||
Secret,
|
SecretSnapshot,
|
||||||
ISecret
|
SecretVersion,
|
||||||
} from '../../models';
|
ISecretVersion,
|
||||||
import {
|
FolderVersion,
|
||||||
SecretSnapshot,
|
} from "../models";
|
||||||
SecretVersion,
|
|
||||||
ISecretVersion
|
|
||||||
} from '../models';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||||
@@ -19,56 +16,70 @@ import {
|
|||||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||||
*/
|
*/
|
||||||
const takeSecretSnapshotHelper = async ({
|
const takeSecretSnapshotHelper = async ({
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId = "root",
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
folderId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
// get all folder ids
|
||||||
|
const secretIds = (
|
||||||
|
await Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
folder: folderId,
|
||||||
|
},
|
||||||
|
"_id"
|
||||||
|
).lean()
|
||||||
|
).map((s) => s._id);
|
||||||
|
|
||||||
let secretSnapshot;
|
const latestSecretVersions = (
|
||||||
try {
|
await SecretVersion.aggregate([
|
||||||
const secretIds = (await Secret.find({
|
{
|
||||||
workspace: workspaceId
|
$match: {
|
||||||
}, '_id')).map((s) => s._id);
|
environment,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
secret: {
|
||||||
|
$in: secretIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$secret",
|
||||||
|
version: { $max: "$version" },
|
||||||
|
versionId: { $max: "$_id" }, // secret version id
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 },
|
||||||
|
},
|
||||||
|
]).exec()
|
||||||
|
).map((s) => s.versionId);
|
||||||
|
const latestFolderVersion = await FolderVersion.findOne({
|
||||||
|
environment,
|
||||||
|
workspace: workspaceId,
|
||||||
|
"nodes.id": folderId,
|
||||||
|
}).sort({ "nodes.version": -1 });
|
||||||
|
|
||||||
const latestSecretVersions = (await SecretVersion.aggregate([
|
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||||
{
|
workspace: workspaceId,
|
||||||
$match: {
|
}).sort({ version: -1 });
|
||||||
secret: {
|
|
||||||
$in: secretIds
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' },
|
|
||||||
versionId: { $max: '$_id' } // secret version id
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec())
|
|
||||||
.map((s) => s.versionId);
|
|
||||||
|
|
||||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
const secretSnapshot = await new SecretSnapshot({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).sort({ version: -1 });
|
environment,
|
||||||
|
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||||
|
secretVersions: latestSecretVersions,
|
||||||
|
folderId,
|
||||||
|
folderVersion: latestFolderVersion,
|
||||||
|
}).save();
|
||||||
|
|
||||||
secretSnapshot = await new SecretSnapshot({
|
return secretSnapshot;
|
||||||
workspace: workspaceId,
|
};
|
||||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
|
||||||
secretVersions: latestSecretVersions
|
|
||||||
}).save();
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to take a secret snapshot');
|
|
||||||
}
|
|
||||||
|
|
||||||
return secretSnapshot;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||||
@@ -77,93 +88,35 @@ const takeSecretSnapshotHelper = async ({
|
|||||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||||
*/
|
*/
|
||||||
const addSecretVersionsHelper = async ({
|
const addSecretVersionsHelper = async ({
|
||||||
secretVersions
|
secretVersions,
|
||||||
}: {
|
}: {
|
||||||
secretVersions: ISecretVersion[]
|
secretVersions: ISecretVersion[];
|
||||||
}) => {
|
}) => {
|
||||||
let newSecretVersions;
|
const newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
||||||
try {
|
|
||||||
newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(`Failed to add secret versions [err=${err}]`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return newSecretVersions;
|
return newSecretVersions;
|
||||||
}
|
};
|
||||||
|
|
||||||
const markDeletedSecretVersionsHelper = async ({
|
const markDeletedSecretVersionsHelper = async ({
|
||||||
secretIds
|
secretIds,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
await SecretVersion.updateMany(
|
||||||
await SecretVersion.updateMany({
|
{
|
||||||
secret: { $in: secretIds }
|
secret: { $in: secretIds },
|
||||||
}, {
|
},
|
||||||
isDeleted: true
|
{
|
||||||
}, {
|
isDeleted: true,
|
||||||
new: true
|
},
|
||||||
});
|
{
|
||||||
} catch (err) {
|
new: true,
|
||||||
Sentry.setUser(null);
|
}
|
||||||
Sentry.captureException(err);
|
);
|
||||||
throw new Error('Failed to mark secret versions as deleted');
|
};
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize secret versioning by setting previously unversioned
|
|
||||||
* secrets to version 1 and begin populating secret versions.
|
|
||||||
*/
|
|
||||||
const initSecretVersioningHelper = async () => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
await Secret.updateMany(
|
|
||||||
{ version: { $exists: false } },
|
|
||||||
{ $set: { version: 1 } }
|
|
||||||
);
|
|
||||||
|
|
||||||
const unversionedSecrets: ISecret[] = await Secret.aggregate([
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: 'secretversions',
|
|
||||||
localField: '_id',
|
|
||||||
foreignField: 'secret',
|
|
||||||
as: 'versions',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
versions: { $size: 0 },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (unversionedSecrets.length > 0) {
|
|
||||||
await addSecretVersionsHelper({
|
|
||||||
secretVersions: unversionedSecrets.map((s, idx) => ({
|
|
||||||
...s,
|
|
||||||
secret: s._id,
|
|
||||||
version: s.version ? s.version : 1,
|
|
||||||
isDeleted: false,
|
|
||||||
workspace: s.workspace,
|
|
||||||
environment: s.environment
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to ensure that secrets are versioned');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper,
|
markDeletedSecretVersionsHelper,
|
||||||
initSecretVersioningHelper
|
};
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,110 +1,82 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from "mongoose";
|
||||||
import { Types } from 'mongoose';
|
import { SecretVersion } from "../models";
|
||||||
import { SecretVersion } from '../models';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return latest secret versions for secrets with ids [secretIds]
|
* Return latest secret versions for secrets with ids [secretIds]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getLatestSecretVersionIds = async ({
|
const getLatestSecretVersionIds = async ({
|
||||||
secretIds
|
secretIds,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
|
const latestSecretVersionIds = await SecretVersion.aggregate([
|
||||||
interface LatestSecretVersionId {
|
{
|
||||||
_id: Types.ObjectId;
|
$match: {
|
||||||
version: number;
|
secret: {
|
||||||
versionId: Types.ObjectId;
|
$in: secretIds,
|
||||||
}
|
},
|
||||||
|
},
|
||||||
let latestSecretVersionIds: LatestSecretVersionId[];
|
},
|
||||||
try {
|
{
|
||||||
latestSecretVersionIds = (await SecretVersion.aggregate([
|
$group: {
|
||||||
{
|
_id: "$secret",
|
||||||
$match: {
|
version: { $max: "$version" },
|
||||||
secret: {
|
versionId: { $max: "$_id" }, // id of latest secret version
|
||||||
$in: secretIds
|
},
|
||||||
}
|
},
|
||||||
}
|
{
|
||||||
},
|
$sort: { version: -1 },
|
||||||
{
|
},
|
||||||
$group: {
|
]).exec();
|
||||||
_id: '$secret',
|
|
||||||
version: { $max: '$version' },
|
return latestSecretVersionIds;
|
||||||
versionId: { $max: '$_id' } // id of latest secret version
|
};
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
.exec());
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get latest secret versions');
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestSecretVersionIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return latest [n] secret versions for secrets with ids [secretIds]
|
* Return latest [n] secret versions for secrets with ids [secretIds]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||||
* @param {Number} obj.n - number of latest secret versions to return for each secret
|
* @param {Number} obj.n - number of latest secret versions to return for each secret
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getLatestNSecretSecretVersionIds = async ({
|
const getLatestNSecretSecretVersionIds = async ({
|
||||||
secretIds,
|
secretIds,
|
||||||
n
|
n,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
n: number;
|
n: number;
|
||||||
}) => {
|
}) => {
|
||||||
|
// TODO: optimize query
|
||||||
// TODO: optimize query
|
const latestNSecretVersions = await SecretVersion.aggregate([
|
||||||
let latestNSecretVersions;
|
{
|
||||||
try {
|
$match: {
|
||||||
latestNSecretVersions = (await SecretVersion.aggregate([
|
secret: {
|
||||||
{
|
$in: secretIds,
|
||||||
$match: {
|
},
|
||||||
secret: {
|
},
|
||||||
$in: secretIds,
|
},
|
||||||
},
|
{
|
||||||
},
|
$sort: { version: -1 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$sort: { version: -1 },
|
$group: {
|
||||||
},
|
_id: "$secret",
|
||||||
{
|
versions: { $push: "$$ROOT" },
|
||||||
$group: {
|
},
|
||||||
_id: "$secret",
|
},
|
||||||
versions: { $push: "$$ROOT" },
|
{
|
||||||
},
|
$project: {
|
||||||
},
|
_id: 0,
|
||||||
{
|
secret: "$_id",
|
||||||
$project: {
|
versions: { $slice: ["$versions", n] },
|
||||||
_id: 0,
|
},
|
||||||
secret: "$_id",
|
},
|
||||||
versions: { $slice: ["$versions", n] },
|
]);
|
||||||
},
|
|
||||||
}
|
|
||||||
]));
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get latest n secret versions');
|
|
||||||
}
|
|
||||||
|
|
||||||
return latestNSecretVersions;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
return latestNSecretVersions;
|
||||||
getLatestSecretVersionIds,
|
};
|
||||||
getLatestNSecretSecretVersionIds
|
|
||||||
}
|
export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds };
|
||||||
|
|||||||
@@ -15,32 +15,28 @@ import {
|
|||||||
const requireSecretSnapshotAuth = ({
|
const requireSecretSnapshotAuth = ({
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
}: {
|
}: {
|
||||||
acceptedRoles: string[];
|
acceptedRoles: Array<'admin' | 'member'>;
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
const { secretSnapshotId } = req.params;
|
||||||
const { secretSnapshotId } = req.params;
|
|
||||||
|
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
||||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
|
||||||
|
if (!secretSnapshot) {
|
||||||
if (!secretSnapshot) {
|
return next(SecretSnapshotNotFoundError({
|
||||||
return next(SecretSnapshotNotFoundError({
|
message: 'Failed to find secret snapshot'
|
||||||
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' }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id,
|
||||||
|
workspaceId: secretSnapshot.workspace,
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
req.secretSnapshot = secretSnapshot as any;
|
||||||
|
|
||||||
|
next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
export interface IAction {
|
export interface IAction {
|
||||||
name: string;
|
name: string;
|
||||||
user?: Types.ObjectId,
|
user?: Types.ObjectId,
|
||||||
|
serviceAccount?: Types.ObjectId,
|
||||||
|
serviceTokenData?: Types.ObjectId,
|
||||||
workspace?: Types.ObjectId,
|
workspace?: Types.ObjectId,
|
||||||
payload?: {
|
payload?: {
|
||||||
secretVersions?: Types.ObjectId[]
|
secretVersions?: Types.ObjectId[]
|
||||||
@@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
|
|||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'User',
|
ref: 'User'
|
||||||
required: true
|
},
|
||||||
|
serviceAccount: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'ServiceAccount'
|
||||||
|
},
|
||||||
|
serviceTokenData: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'ServiceTokenData'
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
|
|||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { model, Schema, Types } from "mongoose";
|
||||||
|
|
||||||
|
export type TFolderRootVersionSchema = {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
nodes: TFolderVersionSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFolderVersionSchema = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: number;
|
||||||
|
children: TFolderVersionSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const folderVersionSchema = new Schema<TFolderVersionSchema>({
|
||||||
|
id: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
default: "root",
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
default: "root",
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
required: true,
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
folderVersionSchema.add({ children: [folderVersionSchema] });
|
||||||
|
|
||||||
|
const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "Workspace",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
nodes: folderVersionSchema,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const FolderVersion = model<TFolderRootVersionSchema>(
|
||||||
|
"FolderVersion",
|
||||||
|
folderRootVersionSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FolderVersion;
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
|
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
||||||
import SecretVersion, { ISecretVersion } from './secretVersion';
|
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
||||||
import Log, { ILog } from './log';
|
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
|
||||||
import Action, { IAction } from './action';
|
import Log, { ILog } from "./log";
|
||||||
|
import Action, { IAction } from "./action";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
ISecretSnapshot,
|
ISecretSnapshot,
|
||||||
SecretVersion,
|
SecretVersion,
|
||||||
ISecretVersion,
|
ISecretVersion,
|
||||||
Log,
|
FolderVersion,
|
||||||
ILog,
|
TFolderRootVersionSchema,
|
||||||
Action,
|
Log,
|
||||||
IAction
|
ILog,
|
||||||
}
|
Action,
|
||||||
|
IAction,
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
export interface ILog {
|
export interface ILog {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
user?: Types.ObjectId;
|
user?: Types.ObjectId;
|
||||||
|
serviceAccount?: Types.ObjectId;
|
||||||
|
serviceTokenData?: Types.ObjectId;
|
||||||
workspace?: Types.ObjectId;
|
workspace?: Types.ObjectId;
|
||||||
actionNames: string[];
|
actionNames: string[];
|
||||||
actions: Types.ObjectId[];
|
actions: Types.ObjectId[];
|
||||||
@@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
|
|||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'User'
|
ref: 'User'
|
||||||
},
|
},
|
||||||
|
serviceAccount: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'ServiceAccount'
|
||||||
|
},
|
||||||
|
serviceTokenData: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'ServiceTokenData'
|
||||||
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace'
|
ref: 'Workspace'
|
||||||
|
|||||||
@@ -1,33 +1,54 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
|
|
||||||
export interface ISecretSnapshot {
|
export interface ISecretSnapshot {
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
version: number;
|
environment: string;
|
||||||
secretVersions: Types.ObjectId[];
|
folderId: string | "root";
|
||||||
|
version: number;
|
||||||
|
secretVersions: Types.ObjectId[];
|
||||||
|
folderVersion: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
},
|
|
||||||
version: {
|
|
||||||
type: Number,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
secretVersions: [{
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: 'SecretVersion',
|
|
||||||
required: true
|
|
||||||
}]
|
|
||||||
},
|
},
|
||||||
{
|
environment: {
|
||||||
timestamps: true
|
type: String,
|
||||||
}
|
required: true,
|
||||||
|
},
|
||||||
|
folderId: {
|
||||||
|
type: String,
|
||||||
|
default: "root",
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
secretVersions: [
|
||||||
|
{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "SecretVersion",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
folderVersion: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "FolderVersion",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
|
const SecretSnapshot = model<ISecretSnapshot>(
|
||||||
|
"SecretSnapshot",
|
||||||
|
secretSnapshotSchema
|
||||||
|
);
|
||||||
|
|
||||||
export default SecretSnapshot;
|
export default SecretSnapshot;
|
||||||
|
|||||||
@@ -1,97 +1,132 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
} from '../../variables';
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64,
|
||||||
|
} from "../../variables";
|
||||||
|
|
||||||
export interface ISecretVersion {
|
export interface ISecretVersion {
|
||||||
secret: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
version: number;
|
secret: Types.ObjectId;
|
||||||
workspace: Types.ObjectId; // new
|
version: number;
|
||||||
type: string; // new
|
workspace: Types.ObjectId; // new
|
||||||
user?: Types.ObjectId; // new
|
type: string; // new
|
||||||
environment: string; // new
|
user?: Types.ObjectId; // new
|
||||||
isDeleted: boolean;
|
environment: string; // new
|
||||||
secretKeyCiphertext: string;
|
isDeleted: boolean;
|
||||||
secretKeyIV: string;
|
secretBlindIndex?: string;
|
||||||
secretKeyTag: string;
|
secretKeyCiphertext: string;
|
||||||
secretValueCiphertext: string;
|
secretKeyIV: string;
|
||||||
secretValueIV: string;
|
secretKeyTag: string;
|
||||||
secretValueTag: string;
|
secretValueCiphertext: string;
|
||||||
tags?: string[];
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
algorithm: "aes-256-gcm";
|
||||||
|
keyEncoding: "utf8" | "base64";
|
||||||
|
createdAt: string;
|
||||||
|
folder?: string;
|
||||||
|
tags?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||||
{
|
{
|
||||||
secret: { // could be deleted
|
secret: {
|
||||||
type: Schema.Types.ObjectId,
|
// could be deleted
|
||||||
ref: 'Secret',
|
type: Schema.Types.ObjectId,
|
||||||
required: true
|
ref: "Secret",
|
||||||
},
|
required: true,
|
||||||
version: {
|
},
|
||||||
type: Number,
|
version: {
|
||||||
default: 1,
|
type: Number,
|
||||||
required: true
|
default: 1,
|
||||||
},
|
required: true,
|
||||||
workspace: {
|
},
|
||||||
type: Schema.Types.ObjectId,
|
workspace: {
|
||||||
ref: 'Workspace',
|
type: Schema.Types.ObjectId,
|
||||||
required: true
|
ref: "Workspace",
|
||||||
},
|
required: true,
|
||||||
type: {
|
},
|
||||||
type: String,
|
type: {
|
||||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
type: String,
|
||||||
required: true
|
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||||
},
|
required: true,
|
||||||
user: {
|
},
|
||||||
// user associated with the personal secret
|
user: {
|
||||||
type: Schema.Types.ObjectId,
|
// user associated with the personal secret
|
||||||
ref: 'User'
|
type: Schema.Types.ObjectId,
|
||||||
},
|
ref: "User",
|
||||||
environment: {
|
},
|
||||||
type: String,
|
environment: {
|
||||||
required: true
|
type: String,
|
||||||
},
|
required: true,
|
||||||
isDeleted: { // consider removing field
|
},
|
||||||
type: Boolean,
|
isDeleted: {
|
||||||
default: false,
|
// consider removing field
|
||||||
required: true
|
type: Boolean,
|
||||||
},
|
default: false,
|
||||||
secretKeyCiphertext: {
|
required: true,
|
||||||
type: String,
|
},
|
||||||
required: true
|
secretBlindIndex: {
|
||||||
},
|
type: String,
|
||||||
secretKeyIV: {
|
select: false,
|
||||||
type: String, // symmetric
|
},
|
||||||
required: true
|
secretKeyCiphertext: {
|
||||||
},
|
type: String,
|
||||||
secretKeyTag: {
|
required: true,
|
||||||
type: String, // symmetric
|
},
|
||||||
required: true
|
secretKeyIV: {
|
||||||
},
|
type: String, // symmetric
|
||||||
secretValueCiphertext: {
|
required: true,
|
||||||
type: String,
|
},
|
||||||
required: true
|
secretKeyTag: {
|
||||||
},
|
type: String, // symmetric
|
||||||
secretValueIV: {
|
required: true,
|
||||||
type: String, // symmetric
|
},
|
||||||
required: true
|
secretValueCiphertext: {
|
||||||
},
|
type: String,
|
||||||
secretValueTag: {
|
required: true,
|
||||||
type: String, // symmetric
|
},
|
||||||
required: true
|
secretValueIV: {
|
||||||
},
|
type: String, // symmetric
|
||||||
tags: {
|
required: true,
|
||||||
ref: 'Tag',
|
},
|
||||||
type: [Schema.Types.ObjectId],
|
secretValueTag: {
|
||||||
default: []
|
type: String, // symmetric
|
||||||
},
|
required: true,
|
||||||
},
|
},
|
||||||
{
|
algorithm: {
|
||||||
timestamps: true
|
// the encryption algorithm used
|
||||||
}
|
type: String,
|
||||||
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
|
required: true,
|
||||||
|
default: ALGORITHM_AES_256_GCM,
|
||||||
|
},
|
||||||
|
keyEncoding: {
|
||||||
|
type: String,
|
||||||
|
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||||
|
required: true,
|
||||||
|
default: ENCODING_SCHEME_UTF8,
|
||||||
|
},
|
||||||
|
folder: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
tags: {
|
||||||
|
ref: 'Tag',
|
||||||
|
type: [Schema.Types.ObjectId],
|
||||||
|
default: []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
const SecretVersion = model<ISecretVersion>(
|
||||||
|
"SecretVersion",
|
||||||
|
secretVersionSchema
|
||||||
|
);
|
||||||
|
|
||||||
export default SecretVersion;
|
export default SecretVersion;
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../../middleware';
|
||||||
|
import { query } from 'express-validator';
|
||||||
|
import { cloudProductsController } from '../../controllers/v1';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
query('billing-cycle').exists().isIn(['monthly', 'yearly']),
|
||||||
|
validateRequest,
|
||||||
|
cloudProductsController.getCloudProducts
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,11 +1,15 @@
|
|||||||
import secret from './secret';
|
import secret from './secret';
|
||||||
import secretSnapshot from './secretSnapshot';
|
import secretSnapshot from './secretSnapshot';
|
||||||
|
import organizations from './organizations';
|
||||||
import workspace from './workspace';
|
import workspace from './workspace';
|
||||||
import action from './action';
|
import action from './action';
|
||||||
|
import cloudProducts from './cloudProducts';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
secret,
|
secret,
|
||||||
secretSnapshot,
|
secretSnapshot,
|
||||||
|
organizations,
|
||||||
workspace,
|
workspace,
|
||||||
action
|
action,
|
||||||
|
cloudProducts
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireOrganizationAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../../middleware';
|
||||||
|
import { param, body, query } from 'express-validator';
|
||||||
|
import { organizationsController } from '../../controllers/v1';
|
||||||
|
import {
|
||||||
|
OWNER, ADMIN, MEMBER, ACCEPTED
|
||||||
|
} from '../../../variables';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:organizationId/plan',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
query('workspaceId').optional().isString(),
|
||||||
|
validateRequest,
|
||||||
|
organizationsController.getOrganizationPlan
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/:organizationId/plan',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
body('productId').exists().isString(),
|
||||||
|
validateRequest,
|
||||||
|
organizationsController.updateOrganizationPlan
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:organizationId/billing-details/payment-methods',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationsController.getOrganizationPmtMethods
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:organizationId/billing-details/payment-methods',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
body('success_url').exists().isString(),
|
||||||
|
body('cancel_url').exists().isString(),
|
||||||
|
validateRequest,
|
||||||
|
organizationsController.addOrganizationPmtMethod
|
||||||
|
);
|
||||||
|
|
||||||
|
router.delete(
|
||||||
|
'/:organizationId/billing-details/payment-methods/:pmtMethodId',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireOrganizationAuth({
|
||||||
|
acceptedRoles: [OWNER, ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [ACCEPTED]
|
||||||
|
}),
|
||||||
|
param('organizationId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
organizationsController.deleteOrganizationPmtMethod
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -7,7 +7,12 @@ import {
|
|||||||
} from '../../../middleware';
|
} from '../../../middleware';
|
||||||
import { query, param, body } from 'express-validator';
|
import { query, param, body } from 'express-validator';
|
||||||
import { secretController } from '../../controllers/v1';
|
import { secretController } from '../../controllers/v1';
|
||||||
import { ADMIN, MEMBER } from '../../../variables';
|
import {
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
PERMISSION_READ_SECRETS,
|
||||||
|
PERMISSION_WRITE_SECRETS
|
||||||
|
} from '../../../variables';
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:secretId/secret-versions',
|
'/:secretId/secret-versions',
|
||||||
@@ -15,7 +20,8 @@ router.get(
|
|||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
}),
|
}),
|
||||||
requireSecretAuth({
|
requireSecretAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
requiredPermissions: [PERMISSION_READ_SECRETS]
|
||||||
}),
|
}),
|
||||||
param('secretId').exists().trim(),
|
param('secretId').exists().trim(),
|
||||||
query('offset').exists().isInt(),
|
query('offset').exists().isInt(),
|
||||||
@@ -30,7 +36,8 @@ router.post(
|
|||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
}),
|
}),
|
||||||
requireSecretAuth({
|
requireSecretAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
|
||||||
}),
|
}),
|
||||||
param('secretId').exists().trim(),
|
param('secretId').exists().trim(),
|
||||||
body('version').exists().isInt(),
|
body('version').exists().isInt(),
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
validateRequest
|
validateRequest
|
||||||
} from '../../../middleware';
|
} from '../../../middleware';
|
||||||
import { param, body } from 'express-validator';
|
import { param } from 'express-validator';
|
||||||
import { ADMIN, MEMBER } from '../../../variables';
|
import { ADMIN, MEMBER } from '../../../variables';
|
||||||
import { secretSnapshotController } from '../../controllers/v1';
|
import { secretSnapshotController } from '../../controllers/v1';
|
||||||
|
|
||||||
|
|||||||
@@ -1,72 +1,82 @@
|
|||||||
import express from 'express';
|
import express from "express";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
validateRequest
|
validateRequest,
|
||||||
} from '../../../middleware';
|
} from "../../../middleware";
|
||||||
import { param, query, body } from 'express-validator';
|
import { param, query, body } from "express-validator";
|
||||||
import { ADMIN, MEMBER } from '../../../variables';
|
import { ADMIN, MEMBER } from "../../../variables";
|
||||||
import { workspaceController } from '../../controllers/v1';
|
import { workspaceController } from "../../controllers/v1";
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/secret-snapshots',
|
"/:workspaceId/secret-snapshots",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ["jwt", "apiKey"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
}),
|
locationWorkspaceId: "params",
|
||||||
param('workspaceId').exists().trim(),
|
}),
|
||||||
query('offset').exists().isInt(),
|
param("workspaceId").exists().trim(),
|
||||||
query('limit').exists().isInt(),
|
query("environment").isString().exists().trim(),
|
||||||
validateRequest,
|
query("folderId").default("root").isString().trim(),
|
||||||
workspaceController.getWorkspaceSecretSnapshots
|
query("offset").exists().isInt(),
|
||||||
|
query("limit").exists().isInt(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceSecretSnapshots
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/secret-snapshots/count',
|
"/:workspaceId/secret-snapshots/count",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
}),
|
locationWorkspaceId: "params",
|
||||||
param('workspaceId').exists().trim(),
|
}),
|
||||||
validateRequest,
|
param("workspaceId").exists().trim(),
|
||||||
workspaceController.getWorkspaceSecretSnapshotsCount
|
query("environment").isString().exists().trim(),
|
||||||
|
query("folderId").default("root").isString().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceSecretSnapshotsCount
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/:workspaceId/secret-snapshots/rollback',
|
"/:workspaceId/secret-snapshots/rollback",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ["jwt", "apiKey"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
}),
|
locationWorkspaceId: "params",
|
||||||
param('workspaceId').exists().trim(),
|
}),
|
||||||
body('version').exists().isInt(),
|
param("workspaceId").exists().trim(),
|
||||||
validateRequest,
|
body("environment").isString().exists().trim(),
|
||||||
workspaceController.rollbackWorkspaceSecretSnapshot
|
query("folderId").default("root").isString().exists().trim(),
|
||||||
|
body("version").exists().isInt(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.rollbackWorkspaceSecretSnapshot
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/logs',
|
"/:workspaceId/logs",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ["jwt", "apiKey"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
}),
|
locationWorkspaceId: "params",
|
||||||
param('workspaceId').exists().trim(),
|
}),
|
||||||
query('offset').exists().isInt(),
|
param("workspaceId").exists().trim(),
|
||||||
query('limit').exists().isInt(),
|
query("offset").exists().isInt(),
|
||||||
query('sortBy'),
|
query("limit").exists().isInt(),
|
||||||
query('userId'),
|
query("sortBy"),
|
||||||
query('actionNames'),
|
query("userId"),
|
||||||
validateRequest,
|
query("actionNames"),
|
||||||
workspaceController.getWorkspaceLogs
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceLogs
|
||||||
);
|
);
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
|
|||||||
@@ -1,12 +1,150 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import NodeCache from 'node-cache';
|
||||||
|
import {
|
||||||
|
getLicenseKey,
|
||||||
|
getLicenseServerKey,
|
||||||
|
getLicenseServerUrl
|
||||||
|
} from '../../config';
|
||||||
|
import {
|
||||||
|
licenseKeyRequest,
|
||||||
|
licenseServerKeyRequest,
|
||||||
|
refreshLicenseServerKeyToken,
|
||||||
|
refreshLicenseKeyToken
|
||||||
|
} from '../../config/request';
|
||||||
|
import { Organization } from '../../models';
|
||||||
|
import { OrganizationNotFoundError } from '../../utils/errors';
|
||||||
|
|
||||||
|
interface FeatureSet {
|
||||||
|
_id: string | null;
|
||||||
|
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
|
||||||
|
tier: number;
|
||||||
|
workspaceLimit: number | null;
|
||||||
|
workspacesUsed: number;
|
||||||
|
memberLimit: number | null;
|
||||||
|
membersUsed: number;
|
||||||
|
environmentLimit: number | null;
|
||||||
|
environmentsUsed: number;
|
||||||
|
secretVersioning: boolean;
|
||||||
|
pitRecovery: boolean;
|
||||||
|
rbac: boolean;
|
||||||
|
customRateLimits: boolean;
|
||||||
|
customAlerts: boolean;
|
||||||
|
auditLogs: boolean;
|
||||||
|
envLimit?: number | null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to handle Enterprise Edition license actions
|
* Class to handle license/plan configurations:
|
||||||
|
* - Infisical Cloud: Fetch and cache customer plans in [localFeatureSet]
|
||||||
|
* - Self-hosted regular: Use default global feature set
|
||||||
|
* - Self-hosted enterprise: Fetch and update global feature set
|
||||||
*/
|
*/
|
||||||
class EELicenseService {
|
class EELicenseService {
|
||||||
|
|
||||||
private readonly _isLicenseValid: boolean;
|
private readonly _isLicenseValid: boolean; // TODO: deprecate
|
||||||
|
|
||||||
|
public instanceType: 'self-hosted' | 'enterprise-self-hosted' | 'cloud' = 'self-hosted';
|
||||||
|
|
||||||
|
public globalFeatureSet: FeatureSet = {
|
||||||
|
_id: null,
|
||||||
|
slug: null,
|
||||||
|
tier: -1,
|
||||||
|
workspaceLimit: null,
|
||||||
|
workspacesUsed: 0,
|
||||||
|
memberLimit: null,
|
||||||
|
membersUsed: 0,
|
||||||
|
environmentLimit: null,
|
||||||
|
environmentsUsed: 0,
|
||||||
|
secretVersioning: true,
|
||||||
|
pitRecovery: true,
|
||||||
|
rbac: true,
|
||||||
|
customRateLimits: true,
|
||||||
|
customAlerts: true,
|
||||||
|
auditLogs: false,
|
||||||
|
envLimit: null
|
||||||
|
}
|
||||||
|
|
||||||
|
public localFeatureSet: NodeCache;
|
||||||
|
|
||||||
constructor(licenseKey: string) {
|
constructor() {
|
||||||
this._isLicenseValid = true;
|
this._isLicenseValid = true;
|
||||||
|
this.localFeatureSet = new NodeCache({
|
||||||
|
stdTTL: 300
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getPlan(organizationId: string, workspaceId?: string): Promise<FeatureSet> {
|
||||||
|
try {
|
||||||
|
if (this.instanceType === 'cloud') {
|
||||||
|
const cachedPlan = this.localFeatureSet.get<FeatureSet>(`${organizationId}-${workspaceId ?? ''}`);
|
||||||
|
if (cachedPlan) {
|
||||||
|
return cachedPlan;
|
||||||
|
}
|
||||||
|
|
||||||
|
const organization = await Organization.findById(organizationId);
|
||||||
|
if (!organization) throw OrganizationNotFoundError();
|
||||||
|
|
||||||
|
let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`;
|
||||||
|
|
||||||
|
if (workspaceId) {
|
||||||
|
url += `?workspaceId=${workspaceId}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: { currentPlan } } = await licenseServerKeyRequest.get(url);
|
||||||
|
|
||||||
|
// cache fetched plan for organization
|
||||||
|
this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ''}`, currentPlan);
|
||||||
|
|
||||||
|
return currentPlan;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
return this.globalFeatureSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.globalFeatureSet;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refreshPlan(organizationId: string, workspaceId?: string) {
|
||||||
|
if (this.instanceType === 'cloud') {
|
||||||
|
this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ''}`);
|
||||||
|
await this.getPlan(organizationId, workspaceId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initGlobalFeatureSet() {
|
||||||
|
const licenseServerKey = await getLicenseServerKey();
|
||||||
|
const licenseKey = await getLicenseKey();
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (licenseServerKey) {
|
||||||
|
// license server key is present -> validate it
|
||||||
|
const token = await refreshLicenseServerKeyToken()
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
this.instanceType = 'cloud';
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (licenseKey) {
|
||||||
|
// license key is present -> validate it
|
||||||
|
const token = await refreshLicenseKeyToken();
|
||||||
|
|
||||||
|
if (token) {
|
||||||
|
const { data: { currentPlan } } = await licenseKeyRequest.get(
|
||||||
|
`${await getLicenseServerUrl()}/api/license/v1/plan`
|
||||||
|
);
|
||||||
|
|
||||||
|
this.globalFeatureSet = currentPlan;
|
||||||
|
this.instanceType = 'enterprise-self-hosted';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// case: self-hosted free
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get isLicenseValid(): boolean {
|
public get isLicenseValid(): boolean {
|
||||||
@@ -14,4 +152,4 @@ class EELicenseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default new EELicenseService('N/A');
|
export default new EELicenseService();
|
||||||
|
|||||||
@@ -26,12 +26,16 @@ class EELogService {
|
|||||||
*/
|
*/
|
||||||
static async createLog({
|
static async createLog({
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions,
|
actions,
|
||||||
channel,
|
channel,
|
||||||
ipAddress
|
ipAddress
|
||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId;
|
userId?: Types.ObjectId;
|
||||||
|
serviceAccountId?: Types.ObjectId;
|
||||||
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
workspaceId?: Types.ObjectId;
|
workspaceId?: Types.ObjectId;
|
||||||
actions: IAction[];
|
actions: IAction[];
|
||||||
channel: string;
|
channel: string;
|
||||||
@@ -40,6 +44,8 @@ class EELogService {
|
|||||||
if (!EELicenseService.isLicenseValid) return null;
|
if (!EELicenseService.isLicenseValid) return null;
|
||||||
return await createLogHelper({
|
return await createLogHelper({
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions,
|
actions,
|
||||||
channel,
|
channel,
|
||||||
@@ -59,17 +65,23 @@ class EELogService {
|
|||||||
static async createAction({
|
static async createAction({
|
||||||
name,
|
name,
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds
|
secretIds
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
userId: Types.ObjectId;
|
userId?: Types.ObjectId;
|
||||||
|
serviceAccountId?: Types.ObjectId;
|
||||||
|
serviceTokenDataId?: Types.ObjectId;
|
||||||
workspaceId?: Types.ObjectId;
|
workspaceId?: Types.ObjectId;
|
||||||
secretIds?: Types.ObjectId[];
|
secretIds?: Types.ObjectId[];
|
||||||
}) {
|
}) {
|
||||||
return await createActionHelper({
|
return await createActionHelper({
|
||||||
name,
|
name,
|
||||||
userId,
|
userId,
|
||||||
|
serviceAccountId,
|
||||||
|
serviceTokenDataId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds
|
secretIds
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { ISecretVersion } from '../models';
|
import { ISecretVersion } from '../models';
|
||||||
import {
|
import {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper,
|
markDeletedSecretVersionsHelper,
|
||||||
initSecretVersioningHelper
|
|
||||||
} from '../helpers/secret';
|
} from '../helpers/secret';
|
||||||
import EELicenseService from './EELicenseService';
|
import EELicenseService from './EELicenseService';
|
||||||
|
|
||||||
@@ -12,67 +11,65 @@ import EELicenseService from './EELicenseService';
|
|||||||
* Class to handle Enterprise Edition secret actions
|
* Class to handle Enterprise Edition secret actions
|
||||||
*/
|
*/
|
||||||
class EESecretService {
|
class EESecretService {
|
||||||
|
/**
|
||||||
/**
|
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
* [workspaceId] under a new snapshot with incremented version under the
|
||||||
* [workspaceId] under a new snapshot with incremented version under the
|
* SecretSnapshot collection.
|
||||||
* SecretSnapshot collection.
|
* Requires a valid license key [licenseKey]
|
||||||
* Requires a valid license key [licenseKey]
|
* @param {Object} obj
|
||||||
* @param {Object} obj
|
* @param {String} obj.workspaceId
|
||||||
* @param {String} obj.workspaceId
|
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
|
||||||
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
|
*/
|
||||||
*/
|
static async takeSecretSnapshot({
|
||||||
static async takeSecretSnapshot({
|
workspaceId,
|
||||||
workspaceId
|
environment,
|
||||||
}: {
|
folderId,
|
||||||
workspaceId: string;
|
}: {
|
||||||
}) {
|
workspaceId: Types.ObjectId;
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
environment: string;
|
||||||
return await takeSecretSnapshotHelper({ workspaceId });
|
folderId?: string;
|
||||||
}
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
/**
|
return await takeSecretSnapshotHelper({
|
||||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
workspaceId,
|
||||||
* @param {Object} obj
|
environment,
|
||||||
* @param {Object[]} obj.secretVersions
|
folderId,
|
||||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
});
|
||||||
*/
|
}
|
||||||
static async addSecretVersions({
|
|
||||||
secretVersions
|
|
||||||
}: {
|
|
||||||
secretVersions: ISecretVersion[];
|
|
||||||
}) {
|
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
|
||||||
return await addSecretVersionsHelper({
|
|
||||||
secretVersions
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark secret versions associated with secrets with ids [secretIds]
|
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||||
* as deleted.
|
* @param {Object} obj
|
||||||
* @param {Object} obj
|
* @param {Object[]} obj.secretVersions
|
||||||
* @param {ObjectId[]} obj.secretIds - secret ids
|
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||||
*/
|
*/
|
||||||
static async markDeletedSecretVersions({
|
static async addSecretVersions({
|
||||||
secretIds
|
secretVersions,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretVersions: ISecretVersion[];
|
||||||
}) {
|
}) {
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
await markDeletedSecretVersionsHelper({
|
return await addSecretVersionsHelper({
|
||||||
secretIds
|
secretVersions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize secret versioning by setting previously unversioned
|
* Mark secret versions associated with secrets with ids [secretIds]
|
||||||
* secrets to version 1 and begin populating secret versions.
|
* as deleted.
|
||||||
*/
|
* @param {Object} obj
|
||||||
static async initSecretVersioning() {
|
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
*/
|
||||||
await initSecretVersioningHelper();
|
static async markDeletedSecretVersions({
|
||||||
}
|
secretIds,
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
|
await markDeletedSecretVersionsHelper({
|
||||||
|
secretIds,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EESecretService;
|
export default EESecretService;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS
|
EVENT_PULL_SECRETS
|
||||||
@@ -22,13 +23,16 @@ interface PushSecret {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const eventPushSecrets = ({
|
const eventPushSecrets = ({
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
|
environment?: string;
|
||||||
}) => {
|
}) => {
|
||||||
return ({
|
return ({
|
||||||
name: EVENT_PUSH_SECRETS,
|
name: EVENT_PUSH_SECRETS,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
environment,
|
||||||
payload: {
|
payload: {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+232
-140
@@ -1,15 +1,19 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from 'mongoose';
|
||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import {
|
import {
|
||||||
IUser,
|
IUser,
|
||||||
User,
|
User,
|
||||||
ServiceTokenData,
|
ServiceTokenData,
|
||||||
APIKeyData
|
ServiceAccount,
|
||||||
|
APIKeyData,
|
||||||
|
TokenVersion,
|
||||||
|
ITokenVersion
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
AccountNotFoundError,
|
AccountNotFoundError,
|
||||||
ServiceTokenDataNotFoundError,
|
ServiceTokenDataNotFoundError,
|
||||||
|
ServiceAccountNotFoundError,
|
||||||
APIKeyDataNotFoundError,
|
APIKeyDataNotFoundError,
|
||||||
UnauthorizedRequestError,
|
UnauthorizedRequestError,
|
||||||
BadRequestError
|
BadRequestError
|
||||||
@@ -17,27 +21,33 @@ import {
|
|||||||
import {
|
import {
|
||||||
getJwtAuthLifetime,
|
getJwtAuthLifetime,
|
||||||
getJwtAuthSecret,
|
getJwtAuthSecret,
|
||||||
|
getJwtProviderAuthSecret,
|
||||||
getJwtRefreshLifetime,
|
getJwtRefreshLifetime,
|
||||||
getJwtRefreshSecret
|
getJwtRefreshSecret
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import {
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_API_KEY
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.headers - HTTP request headers object
|
* @param {Object} obj.headers - HTTP request headers object
|
||||||
*/
|
*/
|
||||||
const validateAuthMode = ({
|
export const validateAuthMode = ({
|
||||||
headers,
|
headers,
|
||||||
acceptedAuthModes
|
acceptedAuthModes
|
||||||
}: {
|
}: {
|
||||||
headers: { [key: string]: string | string[] | undefined },
|
headers: { [key: string]: string | string[] | undefined },
|
||||||
acceptedAuthModes: string[]
|
acceptedAuthModes: string[]
|
||||||
}) => {
|
}) => {
|
||||||
// TODO: refactor middleware
|
|
||||||
const apiKey = headers['x-api-key'];
|
const apiKey = headers['x-api-key'];
|
||||||
const authHeader = headers['authorization'];
|
const authHeader = headers['authorization'];
|
||||||
|
|
||||||
let authTokenType, authTokenValue;
|
let authMode, authTokenValue;
|
||||||
if (apiKey === undefined && authHeader === undefined) {
|
if (apiKey === undefined && authHeader === undefined) {
|
||||||
// case: no auth or X-API-KEY header present
|
// case: no auth or X-API-KEY header present
|
||||||
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
|
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
|
||||||
@@ -45,7 +55,7 @@ const validateAuthMode = ({
|
|||||||
|
|
||||||
if (typeof apiKey === 'string') {
|
if (typeof apiKey === 'string') {
|
||||||
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
|
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
|
||||||
authTokenType = 'apiKey';
|
authMode = AUTH_MODE_API_KEY;
|
||||||
authTokenValue = apiKey;
|
authTokenValue = apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,20 +71,24 @@ const validateAuthMode = ({
|
|||||||
|
|
||||||
switch (tokenValue.split('.', 1)[0]) {
|
switch (tokenValue.split('.', 1)[0]) {
|
||||||
case 'st':
|
case 'st':
|
||||||
authTokenType = 'serviceToken';
|
authMode = AUTH_MODE_SERVICE_TOKEN;
|
||||||
|
break;
|
||||||
|
case 'sa':
|
||||||
|
authMode = AUTH_MODE_SERVICE_ACCOUNT;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
authTokenType = 'jwt';
|
authMode = AUTH_MODE_JWT;
|
||||||
}
|
}
|
||||||
|
|
||||||
authTokenValue = tokenValue;
|
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 ({
|
return ({
|
||||||
authTokenType,
|
authMode,
|
||||||
authTokenValue
|
authTokenValue
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -85,32 +99,42 @@ const validateAuthMode = ({
|
|||||||
* @param {String} obj.authTokenValue - JWT token value
|
* @param {String} obj.authTokenValue - JWT token value
|
||||||
* @returns {User} user - user corresponding to JWT token
|
* @returns {User} user - user corresponding to JWT token
|
||||||
*/
|
*/
|
||||||
const getAuthUserPayload = async ({
|
export const getAuthUserPayload = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: {
|
}: {
|
||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
}) => {
|
}) => {
|
||||||
let user;
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
try {
|
jwt.verify(authTokenValue, await getJwtAuthSecret())
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
);
|
||||||
jwt.verify(authTokenValue, getJwtAuthSecret())
|
|
||||||
);
|
|
||||||
|
|
||||||
user = await User.findOne({
|
const user = await User.findOne({
|
||||||
_id: decodedToken.userId
|
_id: new Types.ObjectId(decodedToken.userId)
|
||||||
}).select('+publicKey');
|
}).select('+publicKey +accessVersion');
|
||||||
|
|
||||||
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
|
if (!user) throw AccountNotFoundError({ message: 'Failed to find user' });
|
||||||
|
|
||||||
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
|
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' });
|
||||||
|
|
||||||
} catch (err) {
|
const tokenVersion = await TokenVersion.findOneAndUpdate({
|
||||||
throw UnauthorizedRequestError({
|
_id: new Types.ObjectId(decodedToken.tokenVersionId),
|
||||||
message: 'Failed to authenticate JWT token'
|
user: user._id
|
||||||
});
|
}, {
|
||||||
}
|
lastUsed: new Date()
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!tokenVersion) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to validate access token'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to validate access token'
|
||||||
|
});
|
||||||
|
|
||||||
return user;
|
return ({
|
||||||
|
user,
|
||||||
|
tokenVersionId: tokenVersion._id
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -119,88 +143,124 @@ const getAuthUserPayload = async ({
|
|||||||
* @param {String} obj.authTokenValue - service token value
|
* @param {String} obj.authTokenValue - service token value
|
||||||
* @returns {ServiceTokenData} serviceTokenData - service token data
|
* @returns {ServiceTokenData} serviceTokenData - service token data
|
||||||
*/
|
*/
|
||||||
const getAuthSTDPayload = async ({
|
export const getAuthSTDPayload = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: {
|
}: {
|
||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
}) => {
|
}) => {
|
||||||
let serviceTokenData;
|
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||||
try {
|
|
||||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
|
||||||
|
|
||||||
// TODO: optimize double query
|
let serviceTokenData = await ServiceTokenData
|
||||||
serviceTokenData = await ServiceTokenData
|
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
|
||||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
|
|
||||||
|
|
||||||
if (!serviceTokenData) {
|
if (!serviceTokenData) {
|
||||||
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||||
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||||
// case: service token expired
|
// case: service token expired
|
||||||
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
|
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) {
|
|
||||||
throw UnauthorizedRequestError({
|
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');
|
||||||
|
|
||||||
|
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||||
|
|
||||||
return serviceTokenData;
|
return serviceTokenData;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return service account access key payload
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.authTokenValue - service account access token value
|
||||||
|
* @returns {ServiceAccount} serviceAccount
|
||||||
|
*/
|
||||||
|
export 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]
|
* Return API key data payload corresponding to API key [authTokenValue]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.authTokenValue - API key value
|
* @param {String} obj.authTokenValue - API key value
|
||||||
* @returns {APIKeyData} apiKeyData - API key data
|
* @returns {APIKeyData} apiKeyData - API key data
|
||||||
*/
|
*/
|
||||||
const getAuthAPIKeyPayload = async ({
|
export const getAuthAPIKeyPayload = async ({
|
||||||
authTokenValue
|
authTokenValue
|
||||||
}: {
|
}: {
|
||||||
authTokenValue: string;
|
authTokenValue: string;
|
||||||
}) => {
|
}) => {
|
||||||
let user;
|
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||||
try {
|
|
||||||
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
|
||||||
|
|
||||||
const apiKeyData = await APIKeyData
|
let apiKeyData = await APIKeyData
|
||||||
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
||||||
.populate('user', '+publicKey');
|
.populate<{ user: IUser }>('user', '+publicKey');
|
||||||
|
|
||||||
if (!apiKeyData) {
|
if (!apiKeyData) {
|
||||||
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||||
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
|
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
|
||||||
// case: API key expired
|
// case: API key expired
|
||||||
await APIKeyData.findByIdAndDelete(apiKeyData._id);
|
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) {
|
|
||||||
throw UnauthorizedRequestError({
|
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'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,32 +275,58 @@ const getAuthAPIKeyPayload = async ({
|
|||||||
* @return {String} obj.token - issued JWT token
|
* @return {String} obj.token - issued JWT token
|
||||||
* @return {String} obj.refreshToken - issued refresh token
|
* @return {String} obj.refreshToken - issued refresh token
|
||||||
*/
|
*/
|
||||||
const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
export const issueAuthTokens = async ({
|
||||||
let token: string;
|
userId,
|
||||||
let refreshToken: string;
|
ip,
|
||||||
try {
|
userAgent
|
||||||
// issue tokens
|
}: {
|
||||||
token = createToken({
|
userId: Types.ObjectId;
|
||||||
payload: {
|
ip: string;
|
||||||
userId
|
userAgent: string;
|
||||||
},
|
}) => {
|
||||||
expiresIn: getJwtAuthLifetime(),
|
let tokenVersion: ITokenVersion | null;
|
||||||
secret: getJwtAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
refreshToken = createToken({
|
// continue with (session) token version matching existing ip and user agent
|
||||||
payload: {
|
tokenVersion = await TokenVersion.findOne({
|
||||||
userId
|
user: userId,
|
||||||
},
|
ip,
|
||||||
expiresIn: getJwtRefreshLifetime(),
|
userAgent
|
||||||
secret: getJwtRefreshSecret()
|
});
|
||||||
});
|
|
||||||
} catch (err) {
|
if (!tokenVersion) {
|
||||||
Sentry.setUser(null);
|
// case: no existing ip and user agent exists
|
||||||
Sentry.captureException(err);
|
// -> create new (session) token version for ip and user agent
|
||||||
throw new Error('Failed to issue tokens');
|
tokenVersion = await new TokenVersion({
|
||||||
|
user: userId,
|
||||||
|
refreshVersion: 0,
|
||||||
|
accessVersion: 0,
|
||||||
|
ip,
|
||||||
|
userAgent,
|
||||||
|
lastUsed: new Date()
|
||||||
|
}).save();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// issue tokens
|
||||||
|
const token = createToken({
|
||||||
|
payload: {
|
||||||
|
userId,
|
||||||
|
tokenVersionId: tokenVersion._id.toString(),
|
||||||
|
accessVersion: tokenVersion.accessVersion
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtAuthLifetime(),
|
||||||
|
secret: await getJwtAuthSecret()
|
||||||
|
});
|
||||||
|
|
||||||
|
const refreshToken = createToken({
|
||||||
|
payload: {
|
||||||
|
userId,
|
||||||
|
tokenVersionId: tokenVersion._id.toString(),
|
||||||
|
refreshVersion: tokenVersion.refreshVersion
|
||||||
|
},
|
||||||
|
expiresIn: await getJwtRefreshLifetime(),
|
||||||
|
secret: await getJwtRefreshSecret()
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
token,
|
token,
|
||||||
refreshToken
|
refreshToken
|
||||||
@@ -252,20 +338,17 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.userId - id of user whose tokens are cleared.
|
* @param {String} obj.userId - id of user whose tokens are cleared.
|
||||||
*/
|
*/
|
||||||
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
|
export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void> => {
|
||||||
try {
|
// increment refreshVersion on user by 1
|
||||||
// increment refreshVersion on user by 1
|
|
||||||
User.findOneAndUpdate({
|
await TokenVersion.findOneAndUpdate({
|
||||||
_id: userId
|
_id: tokenVersionId
|
||||||
}, {
|
}, {
|
||||||
$inc: {
|
$inc: {
|
||||||
refreshVersion: 1
|
refreshVersion: 1,
|
||||||
}
|
accessVersion: 1
|
||||||
});
|
}
|
||||||
} catch (err) {
|
});
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -276,7 +359,7 @@ const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
|
|||||||
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
|
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
|
||||||
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
|
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
|
||||||
*/
|
*/
|
||||||
const createToken = ({
|
export const createToken = ({
|
||||||
payload,
|
payload,
|
||||||
expiresIn,
|
expiresIn,
|
||||||
secret
|
secret
|
||||||
@@ -285,23 +368,32 @@ const createToken = ({
|
|||||||
expiresIn: string | number;
|
expiresIn: string | number;
|
||||||
secret: string;
|
secret: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
return jwt.sign(payload, secret, {
|
||||||
return jwt.sign(payload, secret, {
|
expiresIn
|
||||||
expiresIn
|
});
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create a token');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export const validateProviderAuthToken = async ({
|
||||||
validateAuthMode,
|
email,
|
||||||
getAuthUserPayload,
|
user,
|
||||||
getAuthSTDPayload,
|
providerAuthToken,
|
||||||
getAuthAPIKeyPayload,
|
}: {
|
||||||
createToken,
|
email: string;
|
||||||
issueAuthTokens,
|
user: IUser,
|
||||||
clearTokens
|
providerAuthToken?: string;
|
||||||
};
|
}) => {
|
||||||
|
if (!providerAuthToken) {
|
||||||
|
throw new Error('Invalid authentication request.');
|
||||||
|
}
|
||||||
|
|
||||||
|
const decodedToken = <jwt.ProviderAuthJwtPayload>(
|
||||||
|
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
|
||||||
|
);
|
||||||
|
|
||||||
|
if (
|
||||||
|
decodedToken.authProvider !== user.authProvider ||
|
||||||
|
decodedToken.email !== email
|
||||||
|
) {
|
||||||
|
throw new Error('Invalid authentication credentials.')
|
||||||
|
}
|
||||||
|
}
|
||||||
+203
-175
@@ -1,57 +1,101 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
BotKey,
|
BotKey,
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
IUser
|
IUser
|
||||||
} from '../models';
|
} from "../models";
|
||||||
import {
|
import {
|
||||||
generateKeyPair,
|
generateKeyPair,
|
||||||
encryptSymmetric,
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
decryptSymmetric,
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
decryptAsymmetric
|
decryptAsymmetric
|
||||||
} from '../utils/crypto';
|
} from '../utils/crypto';
|
||||||
import { SECRET_SHARED } from '../variables';
|
import {
|
||||||
import { getEncryptionKey } from '../config';
|
SECRET_SHARED,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8,
|
||||||
|
ENCODING_SCHEME_BASE64
|
||||||
|
} from "../variables";
|
||||||
|
import {
|
||||||
|
getEncryptionKey,
|
||||||
|
getRootEncryptionKey,
|
||||||
|
client
|
||||||
|
} from "../config";
|
||||||
|
import { InternalServerError } from "../utils/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.name - name of bot
|
* @param {String} obj.name - name of bot
|
||||||
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||||
*/
|
*/
|
||||||
const createBot = async ({
|
export const createBot = async ({
|
||||||
name,
|
name,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
let bot;
|
const encryptionKey = await getEncryptionKey();
|
||||||
try {
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
const { publicKey, privateKey } = generateKeyPair();
|
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
plaintext: privateKey,
|
|
||||||
key: getEncryptionKey()
|
if (rootEncryptionKey) {
|
||||||
});
|
const {
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
bot = await new Bot({
|
return await new Bot({
|
||||||
name,
|
name,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
publicKey,
|
publicKey,
|
||||||
encryptedPrivateKey: ciphertext,
|
encryptedPrivateKey: ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag
|
tag,
|
||||||
}).save();
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
} catch (err) {
|
keyEncoding: ENCODING_SCHEME_BASE64
|
||||||
Sentry.setUser(null);
|
}).save();
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to create bot');
|
} else if (encryptionKey) {
|
||||||
}
|
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||||
|
plaintext: privateKey,
|
||||||
return bot;
|
key: await getEncryptionKey(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return await new Bot({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}).save();
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: 'Failed to create new bot due to missing encryption key'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
|
||||||
|
* @param {Types.ObjectId} workspaceId - id of workspace to check
|
||||||
|
*/
|
||||||
|
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
|
||||||
|
const botKey = await BotKey.exists({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
return botKey ? false : true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,126 +105,126 @@ const createBot = async ({
|
|||||||
* @param {String} obj.workspaceId - id of workspace
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
* @param {String} obj.environment - environment
|
* @param {String} obj.environment - environment
|
||||||
*/
|
*/
|
||||||
const getSecretsHelper = async ({
|
export const getSecretsBotHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) => {
|
}) => {
|
||||||
const content = {} as any;
|
const content = {} as any;
|
||||||
try {
|
const key = await getKey({ workspaceId: workspaceId });
|
||||||
const key = await getKey({ workspaceId });
|
const secrets = await Secret.find({
|
||||||
const secrets = await Secret.find({
|
workspace: workspaceId,
|
||||||
workspace: workspaceId,
|
environment,
|
||||||
environment,
|
type: SECRET_SHARED,
|
||||||
type: SECRET_SHARED
|
});
|
||||||
});
|
|
||||||
|
|
||||||
secrets.forEach((secret: ISecret) => {
|
|
||||||
const secretKey = decryptSymmetric({
|
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
|
||||||
iv: secret.secretKeyIV,
|
|
||||||
tag: secret.secretKeyTag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
secrets.forEach((secret: ISecret) => {
|
||||||
ciphertext: secret.secretValueCiphertext,
|
const secretKey = decryptSymmetric128BitHexKeyUTF8({
|
||||||
iv: secret.secretValueIV,
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
tag: secret.secretValueTag,
|
iv: secret.secretKeyIV,
|
||||||
key
|
tag: secret.secretKeyTag,
|
||||||
});
|
key,
|
||||||
|
});
|
||||||
|
|
||||||
content[secretKey] = secretValue;
|
const secretValue = decryptSymmetric128BitHexKeyUTF8({
|
||||||
});
|
ciphertext: secret.secretValueCiphertext,
|
||||||
} catch (err) {
|
iv: secret.secretValueIV,
|
||||||
Sentry.setUser(null);
|
tag: secret.secretValueTag,
|
||||||
Sentry.captureException(err);
|
key,
|
||||||
throw new Error('Failed to get secrets');
|
});
|
||||||
}
|
|
||||||
|
|
||||||
return content;
|
content[secretKey] = secretValue;
|
||||||
}
|
});
|
||||||
|
|
||||||
|
return content;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return bot's copy of the workspace key for workspace
|
* Return bot's copy of the workspace key for workspace
|
||||||
* with id [workspaceId]
|
* with id [workspaceId]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
* @returns {String} key - decrypted workspace key
|
* @returns {String} key - decrypted workspace key
|
||||||
*/
|
*/
|
||||||
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
|
||||||
let key;
|
const encryptionKey = await getEncryptionKey();
|
||||||
try {
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
const botKey = await BotKey.findOne({
|
|
||||||
workspace: workspaceId
|
const botKey = await BotKey.findOne({
|
||||||
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
workspace: workspaceId,
|
||||||
|
})
|
||||||
if (!botKey) throw new Error('Failed to find bot key');
|
.populate<{ sender: IUser }>("sender", "publicKey");
|
||||||
|
|
||||||
const bot = await Bot.findOne({
|
if (!botKey) throw new Error("Failed to find bot key");
|
||||||
workspace: workspaceId
|
|
||||||
}).select('+encryptedPrivateKey +iv +tag');
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
if (!bot) throw new Error('Failed to find bot');
|
}).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
|
||||||
if (!bot.isActive) throw new Error('Bot is not active');
|
|
||||||
|
if (!bot) throw new Error("Failed to find bot");
|
||||||
const privateKeyBot = decryptSymmetric({
|
if (!bot.isActive) throw new Error("Bot is not active");
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
|
||||||
iv: bot.iv,
|
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
|
||||||
tag: bot.tag,
|
// case: encoding scheme is base64
|
||||||
key: getEncryptionKey()
|
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
|
||||||
});
|
|
||||||
|
return decryptAsymmetric({
|
||||||
key = decryptAsymmetric({
|
ciphertext: botKey.encryptedKey,
|
||||||
ciphertext: botKey.encryptedKey,
|
nonce: botKey.nonce,
|
||||||
nonce: botKey.nonce,
|
publicKey: botKey.sender.publicKey as string,
|
||||||
publicKey: botKey.sender.publicKey as string,
|
privateKey: privateKeyBot,
|
||||||
privateKey: privateKeyBot
|
});
|
||||||
});
|
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get workspace key');
|
|
||||||
}
|
|
||||||
|
|
||||||
return key;
|
// case: encoding scheme is utf8
|
||||||
}
|
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
key: encryptionKey
|
||||||
|
});
|
||||||
|
|
||||||
|
return decryptAsymmetric({
|
||||||
|
ciphertext: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
publicKey: botKey.sender.publicKey as string,
|
||||||
|
privateKey: privateKeyBot,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({
|
||||||
|
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return symmetrically encrypted [plaintext] using the
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
* key for workspace with id [workspaceId]
|
* key for workspace with id [workspaceId]
|
||||||
* @param {Object} obj1
|
* @param {Object} obj1
|
||||||
* @param {String} obj1.workspaceId - id of workspace
|
* @param {String} obj1.workspaceId - id of workspace
|
||||||
* @param {String} obj1.plaintext - plaintext to encrypt
|
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||||
*/
|
*/
|
||||||
const encryptSymmetricHelper = async ({
|
export const encryptSymmetricHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
plaintext
|
plaintext,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
plaintext: string;
|
plaintext: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const key = await getKey({ workspaceId: workspaceId });
|
||||||
try {
|
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
||||||
const key = await getKey({ workspaceId });
|
plaintext,
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
key,
|
||||||
plaintext,
|
});
|
||||||
key
|
|
||||||
});
|
return {
|
||||||
|
ciphertext,
|
||||||
return ({
|
iv,
|
||||||
ciphertext,
|
tag,
|
||||||
iv,
|
};
|
||||||
tag
|
};
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform symmetric encryption with bot');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Return symmetrically decrypted [ciphertext] using the
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
* key for workspace with id [workspaceId]
|
* key for workspace with id [workspaceId]
|
||||||
@@ -190,40 +234,24 @@ const encryptSymmetricHelper = async ({
|
|||||||
* @param {String} obj.iv - iv
|
* @param {String} obj.iv - iv
|
||||||
* @param {String} obj.tag - tag
|
* @param {String} obj.tag - tag
|
||||||
*/
|
*/
|
||||||
const decryptSymmetricHelper = async ({
|
export const decryptSymmetricHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
}: {
|
||||||
|
workspaceId: Types.ObjectId;
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}) => {
|
||||||
|
const key = await getKey({ workspaceId: workspaceId });
|
||||||
|
const plaintext = decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext,
|
ciphertext,
|
||||||
iv,
|
iv,
|
||||||
tag
|
tag,
|
||||||
}: {
|
key,
|
||||||
workspaceId: string;
|
});
|
||||||
ciphertext: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
}) => {
|
|
||||||
let plaintext;
|
|
||||||
try {
|
|
||||||
const key = await getKey({ workspaceId });
|
|
||||||
const plaintext = decryptSymmetric({
|
|
||||||
ciphertext,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
return plaintext;
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to perform symmetric decryption with bot');
|
|
||||||
}
|
|
||||||
|
|
||||||
return plaintext;
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
return plaintext;
|
||||||
createBot,
|
};
|
||||||
getSecretsHelper,
|
|
||||||
encryptSymmetricHelper,
|
|
||||||
decryptSymmetricHelper
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import mongoose from 'mongoose';
|
import mongoose from 'mongoose';
|
||||||
import { EESecretService } from '../ee/services';
|
|
||||||
import { getLogger } from '../utils/logger';
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -8,7 +7,7 @@ import { getLogger } from '../utils/logger';
|
|||||||
* @param {String} obj.mongoURL - mongo connection string
|
* @param {String} obj.mongoURL - mongo connection string
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const initDatabaseHelper = async ({
|
export const initDatabaseHelper = async ({
|
||||||
mongoURL
|
mongoURL
|
||||||
}: {
|
}: {
|
||||||
mongoURL: string;
|
mongoURL: string;
|
||||||
@@ -19,11 +18,10 @@ const initDatabaseHelper = async ({
|
|||||||
// allow empty strings to pass the required validator
|
// allow empty strings to pass the required validator
|
||||||
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
|
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
|
||||||
|
|
||||||
getLogger("database").info("Database connection established");
|
(await getLogger("database")).info("Database connection established");
|
||||||
|
|
||||||
await EESecretService.initSecretVersioning();
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
|
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return mongoose.connection;
|
return mongoose.connection;
|
||||||
@@ -32,7 +30,7 @@ const initDatabaseHelper = async ({
|
|||||||
/**
|
/**
|
||||||
* Close database conection
|
* Close database conection
|
||||||
*/
|
*/
|
||||||
const closeDatabaseHelper = async () => {
|
export const closeDatabaseHelper = async () => {
|
||||||
return Promise.all([
|
return Promise.all([
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
if (mongoose.connection && mongoose.connection.readyState == 1) {
|
||||||
@@ -43,9 +41,4 @@ const closeDatabaseHelper = async () => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
]);
|
]);
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
initDatabaseHelper,
|
|
||||||
closeDatabaseHelper
|
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Bot, IBot } from '../models';
|
import { Types } from "mongoose";
|
||||||
import * as Sentry from '@sentry/node';
|
import { Bot, IBot } from "../models";
|
||||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
import { EVENT_PUSH_SECRETS } from "../variables";
|
||||||
import { IntegrationService } from '../services';
|
import { IntegrationService } from "../services";
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
name: string;
|
name: string;
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
payload: any;
|
environment?: string;
|
||||||
|
payload: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -17,35 +18,23 @@ interface Event {
|
|||||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||||
*/
|
*/
|
||||||
const handleEventHelper = async ({
|
export const handleEventHelper = async ({ event }: { event: Event }) => {
|
||||||
event
|
const { workspaceId, environment } = event;
|
||||||
}: {
|
|
||||||
event: Event;
|
|
||||||
}) => {
|
|
||||||
const { workspaceId } = event;
|
|
||||||
|
|
||||||
// TODO: moduralize bot check into separate function
|
|
||||||
const bot = await Bot.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
isActive: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bot) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
switch (event.name) {
|
|
||||||
case EVENT_PUSH_SECRETS:
|
|
||||||
IntegrationService.syncIntegrations({
|
|
||||||
workspaceId
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
// TODO: moduralize bot check into separate function
|
||||||
handleEventHelper
|
const bot = await Bot.findOne({
|
||||||
}
|
workspace: workspaceId,
|
||||||
|
isActive: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) return;
|
||||||
|
|
||||||
|
switch (event.name) {
|
||||||
|
case EVENT_PUSH_SECRETS:
|
||||||
|
IntegrationService.syncIntegrations({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
export * from './auth';
|
||||||
|
export * from './bot';
|
||||||
|
export * from './database';
|
||||||
|
export * from './event';
|
||||||
|
export * from './integration';
|
||||||
|
export * from './key';
|
||||||
|
export * from './membership';
|
||||||
|
export * from './membershipOrg';
|
||||||
|
export * from './nodemailer';
|
||||||
|
export * from './organization';
|
||||||
|
export * from './rateLimiter';
|
||||||
|
export * from './secret';
|
||||||
|
export * from './secrets';
|
||||||
|
export * from './signup';
|
||||||
|
export * from './token';
|
||||||
|
export * from './user';
|
||||||
|
export * from './workspace';
|
||||||
+190
-237
@@ -1,17 +1,20 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Bot,
|
Bot,
|
||||||
Integration,
|
Integration,
|
||||||
IntegrationAuth,
|
IntegrationAuth
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||||
import { BotService } from '../services';
|
import { BotService } from '../services';
|
||||||
import {
|
import {
|
||||||
INTEGRATION_VERCEL,
|
INTEGRATION_VERCEL,
|
||||||
INTEGRATION_NETLIFY
|
INTEGRATION_NETLIFY,
|
||||||
|
ALGORITHM_AES_256_GCM,
|
||||||
|
ENCODING_SCHEME_UTF8
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import { UnauthorizedRequestError } from '../utils/errors';
|
import {
|
||||||
import RequestError from '../utils/requestError';
|
UnauthorizedRequestError,
|
||||||
|
} from '../utils/errors';
|
||||||
|
|
||||||
interface Update {
|
interface Update {
|
||||||
workspace: string;
|
workspace: string;
|
||||||
@@ -32,7 +35,7 @@ interface Update {
|
|||||||
* @param {String} obj.code - code
|
* @param {String} obj.code - code
|
||||||
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
|
||||||
*/
|
*/
|
||||||
const handleOAuthExchangeHelper = async ({
|
export const handleOAuthExchangeHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
code,
|
code,
|
||||||
@@ -43,66 +46,59 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
code: string;
|
code: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) => {
|
}) => {
|
||||||
let integrationAuth;
|
const bot = await Bot.findOne({
|
||||||
try {
|
workspace: workspaceId,
|
||||||
const bot = await Bot.findOne({
|
isActive: true
|
||||||
workspace: workspaceId,
|
});
|
||||||
isActive: true
|
|
||||||
|
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||||
|
|
||||||
|
// exchange code for access and refresh tokens
|
||||||
|
const res = await exchangeCode({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
const update: Update = {
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_VERCEL:
|
||||||
|
update.teamId = res.teamId;
|
||||||
|
break;
|
||||||
|
case INTEGRATION_NETLIFY:
|
||||||
|
update.accountId = res.accountId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}, update, {
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.refreshToken) {
|
||||||
|
// case: refresh token returned from exchange
|
||||||
|
// set integration auth refresh token
|
||||||
|
await setIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
refreshToken: res.refreshToken
|
||||||
});
|
});
|
||||||
|
}
|
||||||
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
|
||||||
|
if (res.accessToken) {
|
||||||
// exchange code for access and refresh tokens
|
// case: access token returned from exchange
|
||||||
const res = await exchangeCode({
|
// set integration auth access token
|
||||||
integration,
|
await setIntegrationAuthAccessHelper({
|
||||||
code
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessId: null,
|
||||||
|
accessToken: res.accessToken,
|
||||||
|
accessExpiresAt: res.accessExpiresAt
|
||||||
});
|
});
|
||||||
|
|
||||||
const update: Update = {
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (integration) {
|
|
||||||
case INTEGRATION_VERCEL:
|
|
||||||
update.teamId = res.teamId;
|
|
||||||
break;
|
|
||||||
case INTEGRATION_NETLIFY:
|
|
||||||
update.accountId = res.accountId;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration
|
|
||||||
}, update, {
|
|
||||||
new: true,
|
|
||||||
upsert: true
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.refreshToken) {
|
|
||||||
// case: refresh token returned from exchange
|
|
||||||
// set integration auth refresh token
|
|
||||||
await setIntegrationAuthRefreshHelper({
|
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
|
||||||
refreshToken: res.refreshToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (res.accessToken) {
|
|
||||||
// case: access token returned from exchange
|
|
||||||
// set integration auth access token
|
|
||||||
await setIntegrationAuthAccessHelper({
|
|
||||||
integrationAuthId: integrationAuth._id.toString(),
|
|
||||||
accessId: null,
|
|
||||||
accessToken: res.accessToken,
|
|
||||||
accessExpiresAt: res.accessExpiresAt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return integrationAuth;
|
return integrationAuth;
|
||||||
@@ -113,49 +109,47 @@ const handleOAuthExchangeHelper = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.workspaceId - id of workspace
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
*/
|
*/
|
||||||
const syncIntegrationsHelper = async ({
|
export const syncIntegrationsHelper = async ({
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId;
|
||||||
|
environment?: string;
|
||||||
}) => {
|
}) => {
|
||||||
let integrations;
|
const integrations = await Integration.find({
|
||||||
try {
|
workspace: workspaceId,
|
||||||
integrations = await Integration.find({
|
...(environment ? {
|
||||||
workspace: workspaceId,
|
environment
|
||||||
isActive: true,
|
} : {}),
|
||||||
app: { $ne: null }
|
isActive: true,
|
||||||
|
app: { $ne: null }
|
||||||
|
});
|
||||||
|
|
||||||
|
// for each workspace integration, sync/push secrets
|
||||||
|
// to that integration
|
||||||
|
for await (const integration of integrations) {
|
||||||
|
// get workspace, environment (shared) secrets
|
||||||
|
const secrets = await BotService.getSecrets({ // issue here?
|
||||||
|
workspaceId: integration.workspace,
|
||||||
|
environment: integration.environment
|
||||||
});
|
});
|
||||||
|
|
||||||
// for each workspace integration, sync/push secrets
|
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||||
// to that integration
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
for await (const integration of integrations) {
|
|
||||||
// get workspace, environment (shared) secrets
|
// get integration auth access token
|
||||||
const secrets = await BotService.getSecrets({ // issue here?
|
const access = await getIntegrationAuthAccessHelper({
|
||||||
workspaceId: integration.workspace.toString(),
|
integrationAuthId: integration.integrationAuth
|
||||||
environment: integration.environment
|
});
|
||||||
});
|
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
// sync secrets to integration
|
||||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
await syncSecrets({
|
||||||
|
integration,
|
||||||
// get integration auth access token
|
integrationAuth,
|
||||||
const access = await getIntegrationAuthAccessHelper({
|
secrets,
|
||||||
integrationAuthId: integration.integrationAuth.toString()
|
accessId: access.accessId === undefined ? null : access.accessId,
|
||||||
});
|
accessToken: access.accessToken
|
||||||
|
});
|
||||||
// sync secrets to integration
|
|
||||||
await syncSecrets({
|
|
||||||
integration,
|
|
||||||
integrationAuth,
|
|
||||||
secrets,
|
|
||||||
accessId: access.accessId,
|
|
||||||
accessToken: access.accessToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to sync secrets to integrations');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,31 +161,19 @@ const syncIntegrationsHelper = async ({
|
|||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @param {String} refreshToken - decrypted refresh token
|
* @param {String} refreshToken - decrypted refresh token
|
||||||
*/
|
*/
|
||||||
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||||
let refreshToken;
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
try {
|
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||||
const integrationAuth = await IntegrationAuth
|
|
||||||
.findById(integrationAuthId)
|
|
||||||
.select('+refreshCiphertext +refreshIV +refreshTag');
|
|
||||||
|
|
||||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
refreshToken = await BotService.decryptSymmetric({
|
const refreshToken = await BotService.decryptSymmetric({
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
workspaceId: integrationAuth.workspace,
|
||||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||||
iv: integrationAuth.refreshIV as string,
|
iv: integrationAuth.refreshIV as string,
|
||||||
tag: integrationAuth.refreshTag as string
|
tag: integrationAuth.refreshTag as string
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
if(err instanceof RequestError)
|
|
||||||
throw err
|
|
||||||
else
|
|
||||||
throw new Error('Failed to get integration refresh token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return refreshToken;
|
return refreshToken;
|
||||||
}
|
}
|
||||||
@@ -204,53 +186,43 @@ const syncIntegrationsHelper = async ({
|
|||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @returns {String} accessToken - decrypted access token
|
* @returns {String} accessToken - decrypted access token
|
||||||
*/
|
*/
|
||||||
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
|
||||||
let accessId;
|
let accessId;
|
||||||
let accessToken;
|
let accessToken;
|
||||||
try {
|
const integrationAuth = await IntegrationAuth
|
||||||
const integrationAuth = await IntegrationAuth
|
.findById(integrationAuthId)
|
||||||
.findById(integrationAuthId)
|
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
||||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
|
|
||||||
|
|
||||||
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
accessToken = await BotService.decryptSymmetric({
|
accessToken = await BotService.decryptSymmetric({
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
workspaceId: integrationAuth.workspace,
|
||||||
ciphertext: integrationAuth.accessCiphertext as string,
|
ciphertext: integrationAuth.accessCiphertext as string,
|
||||||
iv: integrationAuth.accessIV as string,
|
iv: integrationAuth.accessIV as string,
|
||||||
tag: integrationAuth.accessTag as string
|
tag: integrationAuth.accessTag as string
|
||||||
});
|
});
|
||||||
|
|
||||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||||
// there is a access token expiration date
|
// there is a access token expiration date
|
||||||
// and refresh token to exchange with the OAuth2 server
|
// and refresh token to exchange with the OAuth2 server
|
||||||
|
|
||||||
if (integrationAuth.accessExpiresAt < new Date()) {
|
|
||||||
// access token is expired
|
|
||||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
|
||||||
accessToken = await exchangeRefresh({
|
|
||||||
integrationAuth,
|
|
||||||
refreshToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
|
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||||
accessId = await BotService.decryptSymmetric({
|
// access token is expired
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||||
ciphertext: integrationAuth.accessIdCiphertext as string,
|
accessToken = await exchangeRefresh({
|
||||||
iv: integrationAuth.accessIdIV as string,
|
integrationAuth,
|
||||||
tag: integrationAuth.accessIdTag as string
|
refreshToken
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
|
||||||
Sentry.captureException(err);
|
accessId = await BotService.decryptSymmetric({
|
||||||
if(err instanceof RequestError)
|
workspaceId: integrationAuth.workspace,
|
||||||
throw err
|
ciphertext: integrationAuth.accessIdCiphertext as string,
|
||||||
else
|
iv: integrationAuth.accessIdIV as string,
|
||||||
throw new Error('Failed to get integration access token');
|
tag: integrationAuth.accessIdTag as string
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return ({
|
||||||
@@ -267,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
|
|||||||
* @param {String} obj.integrationAuthId - id of integration auth
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
* @param {String} obj.refreshToken - refresh token
|
* @param {String} obj.refreshToken - refresh token
|
||||||
*/
|
*/
|
||||||
const setIntegrationAuthRefreshHelper = async ({
|
export const setIntegrationAuthRefreshHelper = async ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
refreshToken
|
refreshToken
|
||||||
}: {
|
}: {
|
||||||
@@ -275,32 +247,27 @@ const setIntegrationAuthRefreshHelper = async ({
|
|||||||
refreshToken: string;
|
refreshToken: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
let integrationAuth;
|
let integrationAuth = await IntegrationAuth
|
||||||
try {
|
.findById(integrationAuthId);
|
||||||
integrationAuth = await IntegrationAuth
|
|
||||||
.findById(integrationAuthId);
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace,
|
||||||
const obj = await BotService.encryptSymmetric({
|
plaintext: refreshToken
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
});
|
||||||
plaintext: refreshToken
|
|
||||||
});
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
}, {
|
||||||
_id: integrationAuthId
|
refreshCiphertext: obj.ciphertext,
|
||||||
}, {
|
refreshIV: obj.iv,
|
||||||
refreshCiphertext: obj.ciphertext,
|
refreshTag: obj.tag,
|
||||||
refreshIV: obj.iv,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
refreshTag: obj.tag
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
}, {
|
}, {
|
||||||
new: true
|
new: true
|
||||||
});
|
});
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to set integration auth refresh token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrationAuth;
|
return integrationAuth;
|
||||||
}
|
}
|
||||||
@@ -314,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
|
|||||||
* @param {String} obj.accessToken - access token
|
* @param {String} obj.accessToken - access token
|
||||||
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||||
*/
|
*/
|
||||||
const setIntegrationAuthAccessHelper = async ({
|
export const setIntegrationAuthAccessHelper = async ({
|
||||||
integrationAuthId,
|
integrationAuthId,
|
||||||
accessId,
|
accessId,
|
||||||
accessToken,
|
accessToken,
|
||||||
@@ -325,52 +292,38 @@ const setIntegrationAuthAccessHelper = async ({
|
|||||||
accessToken: string;
|
accessToken: string;
|
||||||
accessExpiresAt: Date | undefined;
|
accessExpiresAt: Date | undefined;
|
||||||
}) => {
|
}) => {
|
||||||
let integrationAuth;
|
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
try {
|
|
||||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace,
|
||||||
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
|
plaintext: accessToken
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
});
|
||||||
plaintext: accessToken
|
|
||||||
});
|
let encryptedAccessIdObj;
|
||||||
|
if (accessId) {
|
||||||
let encryptedAccessIdObj;
|
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
||||||
if (accessId) {
|
workspaceId: integrationAuth.workspace,
|
||||||
encryptedAccessIdObj = await BotService.encryptSymmetric({
|
plaintext: accessId
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
});
|
||||||
plaintext: accessId
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
|
||||||
_id: integrationAuthId
|
|
||||||
}, {
|
|
||||||
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
|
||||||
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
|
||||||
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
|
||||||
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
|
||||||
accessIV: encryptedAccessTokenObj.iv,
|
|
||||||
accessTag: encryptedAccessTokenObj.tag,
|
|
||||||
accessExpiresAt
|
|
||||||
}, {
|
|
||||||
new: true
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to save integration auth access token');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
|
||||||
|
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
|
||||||
|
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
|
||||||
|
accessCiphertext: encryptedAccessTokenObj.ciphertext,
|
||||||
|
accessIV: encryptedAccessTokenObj.iv,
|
||||||
|
accessTag: encryptedAccessTokenObj.tag,
|
||||||
|
accessExpiresAt,
|
||||||
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
|
keyEncoding: ENCODING_SCHEME_UTF8
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
return integrationAuth;
|
return integrationAuth;
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
handleOAuthExchangeHelper,
|
|
||||||
syncIntegrationsHelper,
|
|
||||||
getIntegrationAuthRefreshHelper,
|
|
||||||
getIntegrationAuthAccessHelper,
|
|
||||||
setIntegrationAuthRefreshHelper,
|
|
||||||
setIntegrationAuthAccessHelper
|
|
||||||
}
|
}
|
||||||
+24
-33
@@ -1,4 +1,3 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Key, IKey } from '../models';
|
import { Key, IKey } from '../models';
|
||||||
|
|
||||||
interface Key {
|
interface Key {
|
||||||
@@ -18,7 +17,7 @@ interface Key {
|
|||||||
* @param {String} obj.keys.nonce - nonce for encryption
|
* @param {String} obj.keys.nonce - nonce for encryption
|
||||||
* @param {String} obj.keys.userId - id of receiver user
|
* @param {String} obj.keys.userId - id of receiver user
|
||||||
*/
|
*/
|
||||||
const pushKeys = async ({
|
export const pushKeys = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
@@ -27,36 +26,28 @@ const pushKeys = async ({
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
keys: Key[];
|
keys: Key[];
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
// filter out already-inserted keys
|
||||||
// filter out already-inserted keys
|
const keysSet = new Set(
|
||||||
const keysSet = new Set(
|
(
|
||||||
(
|
await Key.find(
|
||||||
await Key.find(
|
{
|
||||||
{
|
workspace: workspaceId
|
||||||
workspace: workspaceId
|
},
|
||||||
},
|
'receiver'
|
||||||
'receiver'
|
)
|
||||||
)
|
).map((k: IKey) => k.receiver.toString())
|
||||||
).map((k: IKey) => k.receiver.toString())
|
);
|
||||||
);
|
|
||||||
|
|
||||||
keys = keys.filter((key) => !keysSet.has(key.userId));
|
keys = keys.filter((key) => !keysSet.has(key.userId));
|
||||||
|
|
||||||
// add new shared keys only
|
// add new shared keys only
|
||||||
await Key.insertMany(
|
await Key.insertMany(
|
||||||
keys.map((k) => ({
|
keys.map((k) => ({
|
||||||
encryptedKey: k.encryptedKey,
|
encryptedKey: k.encryptedKey,
|
||||||
nonce: k.nonce,
|
nonce: k.nonce,
|
||||||
sender: userId,
|
sender: userId,
|
||||||
receiver: k.userId,
|
receiver: k.userId,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
} catch (err) {
|
};
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to push access keys');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export { pushKeys };
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import { Types } from "mongoose";
|
||||||
import { Membership, Key } from '../models';
|
import { Membership, Key } from "../models";
|
||||||
|
import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
@@ -9,54 +10,45 @@ import { Membership, Key } from '../models';
|
|||||||
* @param {String} obj.workspaceId - id of workspace
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
|
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
|
||||||
*/
|
*/
|
||||||
const validateMembership = async ({
|
export const validateMembership = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
}: {
|
}: {
|
||||||
userId: string;
|
userId: Types.ObjectId | string;
|
||||||
workspaceId: string;
|
workspaceId: Types.ObjectId | string;
|
||||||
acceptedRoles: string[];
|
acceptedRoles?: Array<"admin" | "member">;
|
||||||
}) => {
|
}) => {
|
||||||
|
const membership = await Membership.findOne({
|
||||||
let membership;
|
user: userId,
|
||||||
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
|
workspace: workspaceId,
|
||||||
try {
|
}).populate("workspace");
|
||||||
membership = await Membership.findOne({
|
|
||||||
user: userId,
|
if (!membership) {
|
||||||
workspace: workspaceId
|
throw MembershipNotFoundError({
|
||||||
}).populate("workspace");
|
message: "Failed to find workspace membership",
|
||||||
|
});
|
||||||
if (!membership) throw new Error('Failed to find membership');
|
}
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
if (acceptedRoles) {
|
||||||
throw new Error('Failed to validate membership role');
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
}
|
throw BadRequestError({
|
||||||
} catch (err) {
|
message: "Failed authorization for membership role",
|
||||||
Sentry.setUser(null);
|
});
|
||||||
Sentry.captureException(err);
|
}
|
||||||
throw new Error('Failed to validate membership');
|
}
|
||||||
}
|
|
||||||
|
return membership;
|
||||||
return membership;
|
};
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return membership matching criteria specified in query [queryObj]
|
* Return membership matching criteria specified in query [queryObj]
|
||||||
* @param {Object} queryObj - query object
|
* @param {Object} queryObj - query object
|
||||||
* @return {Object} membership - membership
|
* @return {Object} membership - membership
|
||||||
*/
|
*/
|
||||||
const findMembership = async (queryObj: any) => {
|
export const findMembership = async (queryObj: any) => {
|
||||||
let membership;
|
const membership = await Membership.findOne(queryObj);
|
||||||
try {
|
return membership;
|
||||||
membership = await Membership.findOne(queryObj);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to find membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
return membership;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -67,40 +59,33 @@ const findMembership = async (queryObj: any) => {
|
|||||||
* @param {String} obj.workspaceId - id of workspace.
|
* @param {String} obj.workspaceId - id of workspace.
|
||||||
* @param {String[]} obj.roles - roles of users.
|
* @param {String[]} obj.roles - roles of users.
|
||||||
*/
|
*/
|
||||||
const addMemberships = async ({
|
export const addMemberships = async ({
|
||||||
userIds,
|
userIds,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roles
|
roles
|
||||||
}: {
|
}: {
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
const operations = userIds.map((userId, idx) => {
|
||||||
const operations = userIds.map((userId, idx) => {
|
return {
|
||||||
return {
|
updateOne: {
|
||||||
updateOne: {
|
filter: {
|
||||||
filter: {
|
user: userId,
|
||||||
user: userId,
|
workspace: workspaceId,
|
||||||
workspace: workspaceId,
|
role: roles[idx],
|
||||||
role: roles[idx]
|
},
|
||||||
},
|
update: {
|
||||||
update: {
|
user: userId,
|
||||||
user: userId,
|
workspace: workspaceId,
|
||||||
workspace: workspaceId,
|
role: roles[idx],
|
||||||
role: roles[idx]
|
},
|
||||||
},
|
upsert: true,
|
||||||
upsert: true
|
},
|
||||||
}
|
};
|
||||||
};
|
});
|
||||||
});
|
await Membership.bulkWrite(operations as any);
|
||||||
|
|
||||||
await Membership.bulkWrite(operations as any);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to add users to workspace');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -108,33 +93,19 @@ const addMemberships = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.membershipId - id of membership to delete
|
* @param {String} obj.membershipId - id of membership to delete
|
||||||
*/
|
*/
|
||||||
const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||||
let deletedMembership;
|
const deletedMembership = await Membership.findOneAndDelete({
|
||||||
try {
|
_id: membershipId
|
||||||
deletedMembership = await Membership.findOneAndDelete({
|
});
|
||||||
_id: membershipId
|
|
||||||
});
|
|
||||||
|
|
||||||
// delete keys associated with the membership
|
// delete keys associated with the membership
|
||||||
if (deletedMembership?.user) {
|
if (deletedMembership?.user) {
|
||||||
// case: membership had a registered user
|
// case: membership had a registered user
|
||||||
await Key.deleteMany({
|
await Key.deleteMany({
|
||||||
receiver: deletedMembership.user,
|
receiver: deletedMembership.user,
|
||||||
workspace: deletedMembership.workspace
|
workspace: deletedMembership.workspace,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to delete membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
|
||||||
validateMembership,
|
|
||||||
addMemberships,
|
|
||||||
findMembership,
|
|
||||||
deleteMembership
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,40 +1,56 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import { MembershipOrg, Workspace, Membership, Key } from '../models';
|
import {
|
||||||
|
MembershipOrg,
|
||||||
|
Workspace,
|
||||||
|
Membership,
|
||||||
|
Key
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
MembershipOrgNotFoundError,
|
||||||
|
UnauthorizedRequestError
|
||||||
|
} from '../utils/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
||||||
* and has at least one of the roles in [acceptedRoles]
|
* 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 ({
|
export const validateMembershipOrg = async ({
|
||||||
userId,
|
userId,
|
||||||
organizationId,
|
organizationId,
|
||||||
acceptedRoles
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
}: {
|
}: {
|
||||||
userId: string;
|
userId: Types.ObjectId;
|
||||||
organizationId: string;
|
organizationId: Types.ObjectId;
|
||||||
acceptedRoles: string[];
|
acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
|
||||||
|
acceptedStatuses?: Array<'invited' | 'accepted'>;
|
||||||
}) => {
|
}) => {
|
||||||
let membership;
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
try {
|
user: userId,
|
||||||
membership = await MembershipOrg.findOne({
|
organization: organizationId
|
||||||
user: new Types.ObjectId(userId),
|
});
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
});
|
if (!membershipOrg) {
|
||||||
|
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
|
||||||
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');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -43,16 +59,8 @@ const validateMembership = async ({
|
|||||||
* @param {Object} queryObj - query object
|
* @param {Object} queryObj - query object
|
||||||
* @return {Object} membershipOrg - membership
|
* @return {Object} membershipOrg - membership
|
||||||
*/
|
*/
|
||||||
const findMembershipOrg = (queryObj: any) => {
|
export const findMembershipOrg = (queryObj: any) => {
|
||||||
let membershipOrg;
|
const membershipOrg = MembershipOrg.findOne(queryObj);
|
||||||
try {
|
|
||||||
membershipOrg = MembershipOrg.findOne(queryObj);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to find organization membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
return membershipOrg;
|
return membershipOrg;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
|
|||||||
* @param {String} obj.organizationId - id of organization.
|
* @param {String} obj.organizationId - id of organization.
|
||||||
* @param {String[]} obj.roles - roles of users.
|
* @param {String[]} obj.roles - roles of users.
|
||||||
*/
|
*/
|
||||||
const addMembershipsOrg = async ({
|
export const addMembershipsOrg = async ({
|
||||||
userIds,
|
userIds,
|
||||||
organizationId,
|
organizationId,
|
||||||
roles,
|
roles,
|
||||||
@@ -75,33 +83,27 @@ const addMembershipsOrg = async ({
|
|||||||
roles: string[];
|
roles: string[];
|
||||||
statuses: string[];
|
statuses: string[];
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
const operations = userIds.map((userId, idx) => {
|
||||||
const operations = userIds.map((userId, idx) => {
|
return {
|
||||||
return {
|
updateOne: {
|
||||||
updateOne: {
|
filter: {
|
||||||
filter: {
|
user: userId,
|
||||||
user: userId,
|
organization: organizationId,
|
||||||
organization: organizationId,
|
role: roles[idx],
|
||||||
role: roles[idx],
|
status: statuses[idx]
|
||||||
status: statuses[idx]
|
},
|
||||||
},
|
update: {
|
||||||
update: {
|
user: userId,
|
||||||
user: userId,
|
organization: organizationId,
|
||||||
organization: organizationId,
|
role: roles[idx],
|
||||||
role: roles[idx],
|
status: statuses[idx]
|
||||||
status: statuses[idx]
|
},
|
||||||
},
|
upsert: true
|
||||||
upsert: true
|
}
|
||||||
}
|
};
|
||||||
};
|
});
|
||||||
});
|
|
||||||
|
|
||||||
await MembershipOrg.bulkWrite(operations as any);
|
await MembershipOrg.bulkWrite(operations as any);
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to add users to organization');
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -109,55 +111,41 @@ const addMembershipsOrg = async ({
|
|||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.membershipOrgId - id of organization membership to delete
|
* @param {String} obj.membershipOrgId - id of organization membership to delete
|
||||||
*/
|
*/
|
||||||
const deleteMembershipOrg = async ({
|
export const deleteMembershipOrg = async ({
|
||||||
membershipOrgId
|
membershipOrgId
|
||||||
}: {
|
}: {
|
||||||
membershipOrgId: string;
|
membershipOrgId: string;
|
||||||
}) => {
|
}) => {
|
||||||
let deletedMembershipOrg;
|
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
||||||
try {
|
_id: membershipOrgId
|
||||||
deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
|
});
|
||||||
_id: membershipOrgId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
|
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
|
||||||
|
|
||||||
// delete keys associated with organization membership
|
// delete keys associated with organization membership
|
||||||
if (deletedMembershipOrg?.user) {
|
if (deletedMembershipOrg?.user) {
|
||||||
// case: organization membership had a registered user
|
// case: organization membership had a registered user
|
||||||
|
|
||||||
const workspaces = (
|
const workspaces = (
|
||||||
await Workspace.find({
|
await Workspace.find({
|
||||||
organization: deletedMembershipOrg.organization
|
organization: deletedMembershipOrg.organization
|
||||||
})
|
})
|
||||||
).map((w) => w._id.toString());
|
).map((w) => w._id.toString());
|
||||||
|
|
||||||
await Membership.deleteMany({
|
await Membership.deleteMany({
|
||||||
user: deletedMembershipOrg.user,
|
user: deletedMembershipOrg.user,
|
||||||
workspace: {
|
workspace: {
|
||||||
$in: workspaces
|
$in: workspaces
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
await Key.deleteMany({
|
await Key.deleteMany({
|
||||||
receiver: deletedMembershipOrg.user,
|
receiver: deletedMembershipOrg.user,
|
||||||
workspace: {
|
workspace: {
|
||||||
$in: workspaces
|
$in: workspaces
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to delete organization membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
return deletedMembershipOrg;
|
return deletedMembershipOrg;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
|
||||||
validateMembership,
|
|
||||||
findMembershipOrg,
|
|
||||||
addMembershipsOrg,
|
|
||||||
deleteMembershipOrg
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user