mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' into snyk-upgrade-168622761b1452230387c1e39953ec92
This commit is contained in:
42
.env.example
42
.env.example
@@ -1,21 +1,19 @@
|
|||||||
# Keys
|
# Keys
|
||||||
# Required keys for platform encryption/decryption ops
|
# Required key for platform encryption/decryption ops
|
||||||
PRIVATE_KEY=replace_with_nacl_sk
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
PUBLIC_KEY=replace_with_nacl_pk
|
|
||||||
ENCRYPTION_KEY=replace_with_lengthy_secure_hex
|
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Required secrets to sign JWT tokens
|
# Required secrets to sign JWT tokens
|
||||||
JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex
|
JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
|
||||||
JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex
|
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
|
||||||
JWT_AUTH_SECRET=replace_with_lengthy_secure_hex
|
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
|
||||||
|
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
|
||||||
|
|
||||||
# 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
|
||||||
# describing a time span (e.g. 60, "2 days", "10h", "7d")
|
# describing a time span (e.g. 60, "2 days", "10h", "7d")
|
||||||
JWT_AUTH_LIFETIME=
|
JWT_AUTH_LIFETIME=
|
||||||
JWT_REFRESH_LIFETIME=
|
JWT_REFRESH_LIFETIME=
|
||||||
JWT_SERVICE_SECRET=
|
|
||||||
JWT_SIGNUP_LIFETIME=
|
JWT_SIGNUP_LIFETIME=
|
||||||
|
|
||||||
# Optional lifetimes for OTP expressed in seconds
|
# Optional lifetimes for OTP expressed in seconds
|
||||||
@@ -33,22 +31,28 @@ MONGO_PASSWORD=example
|
|||||||
|
|
||||||
# Website URL
|
# Website URL
|
||||||
# Required
|
# Required
|
||||||
|
|
||||||
SITE_URL=http://localhost:8080
|
SITE_URL=http://localhost:8080
|
||||||
|
|
||||||
# Mail/SMTP
|
# Mail/SMTP
|
||||||
# Required to send emails
|
SMTP_HOST= # required
|
||||||
# By default, SMTP_HOST is set to smtp.gmail.com
|
SMTP_USERNAME= # required
|
||||||
SMTP_HOST=smtp.gmail.com
|
SMTP_PASSWORD= # required
|
||||||
SMTP_PORT=587
|
SMTP_PORT=587
|
||||||
SMTP_NAME=Team
|
SMTP_SECURE=false
|
||||||
SMTP_USERNAME=team@infisical.com
|
SMTP_FROM_ADDRESS= # required
|
||||||
SMTP_PASSWORD=
|
SMTP_FROM_NAME=Infisical
|
||||||
|
|
||||||
# Integration
|
# Integration
|
||||||
# Optional only if integration is used
|
# Optional only if integration is used
|
||||||
OAUTH_CLIENT_SECRET_HEROKU=
|
CLIENT_ID_HEROKU=
|
||||||
OAUTH_TOKEN_URL_HEROKU=
|
CLIENT_ID_VERCEL=
|
||||||
|
CLIENT_ID_NETLIFY=
|
||||||
|
CLIENT_ID_GITHUB=
|
||||||
|
CLIENT_SECRET_HEROKU=
|
||||||
|
CLIENT_SECRET_VERCEL=
|
||||||
|
CLIENT_SECRET_NETLIFY=
|
||||||
|
CLIENT_SECRET_GITHUB=
|
||||||
|
CLIENT_SLUG_VERCEL=
|
||||||
|
|
||||||
# Sentry (optional) for monitoring errors
|
# Sentry (optional) for monitoring errors
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
@@ -60,7 +64,7 @@ POSTHOG_PROJECT_API_KEY=
|
|||||||
STRIPE_SECRET_KEY=
|
STRIPE_SECRET_KEY=
|
||||||
STRIPE_PUBLISHABLE_KEY=
|
STRIPE_PUBLISHABLE_KEY=
|
||||||
STRIPE_WEBHOOK_SECRET=
|
STRIPE_WEBHOOK_SECRET=
|
||||||
STRIPE_PRODUCT_CARD_AUTH=
|
|
||||||
STRIPE_PRODUCT_PRO=
|
|
||||||
STRIPE_PRODUCT_STARTER=
|
STRIPE_PRODUCT_STARTER=
|
||||||
|
STRIPE_PRODUCT_TEAM=
|
||||||
|
STRIPE_PRODUCT_PRO=
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
node_modules
|
node_modules
|
||||||
built
|
built
|
||||||
healthcheck.js
|
healthcheck.js
|
||||||
|
tailwind.config.js
|
||||||
93
.github/values.yaml
vendored
Normal file
93
.github/values.yaml
vendored
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
#####
|
||||||
|
# INFISICAL K8 DEFAULT VALUES FILE
|
||||||
|
# PLEASE REPLACE VALUES/EDIT AS REQUIRED
|
||||||
|
#####
|
||||||
|
|
||||||
|
nameOverride: ""
|
||||||
|
|
||||||
|
frontend:
|
||||||
|
name: frontend
|
||||||
|
podAnnotations: {}
|
||||||
|
deploymentAnnotations:
|
||||||
|
secrets.infisical.com/auto-reload: "true"
|
||||||
|
replicaCount: 2
|
||||||
|
image:
|
||||||
|
repository: infisical/frontend
|
||||||
|
pullPolicy: Always
|
||||||
|
tag: "latest"
|
||||||
|
kubeSecretRef: managed-secret-frontend
|
||||||
|
service:
|
||||||
|
# type of the frontend service
|
||||||
|
type: ClusterIP
|
||||||
|
# define the nodePort if service type is NodePort
|
||||||
|
# nodePort:
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
backend:
|
||||||
|
name: backend
|
||||||
|
podAnnotations: {}
|
||||||
|
deploymentAnnotations:
|
||||||
|
secrets.infisical.com/auto-reload: "true"
|
||||||
|
replicaCount: 2
|
||||||
|
image:
|
||||||
|
repository: infisical/backend
|
||||||
|
pullPolicy: Always
|
||||||
|
tag: "latest"
|
||||||
|
kubeSecretRef: managed-backend-secret
|
||||||
|
service:
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
mongodb:
|
||||||
|
name: mongodb
|
||||||
|
podAnnotations: {}
|
||||||
|
image:
|
||||||
|
repository: mongo
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
tag: "latest"
|
||||||
|
service:
|
||||||
|
annotations: {}
|
||||||
|
|
||||||
|
# By default the backend will be connected to a Mongo instance in the cluster.
|
||||||
|
# However, it is recommended to add a managed document DB connection string because the DB instance in the cluster does not have persistence yet ( data will be deleted on next deploy).
|
||||||
|
# Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
||||||
|
mongodbConnection: {}
|
||||||
|
# externalMongoDBConnectionString: <>
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
annotations:
|
||||||
|
kubernetes.io/ingress.class: "nginx"
|
||||||
|
hostName: gamma.infisical.com # replace with your domain
|
||||||
|
frontend:
|
||||||
|
path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
path: /api
|
||||||
|
pathType: Prefix
|
||||||
|
tls: []
|
||||||
|
|
||||||
|
|
||||||
|
## Complete Ingress example
|
||||||
|
# ingress:
|
||||||
|
# enabled: true
|
||||||
|
# annotations:
|
||||||
|
# kubernetes.io/ingress.class: "nginx"
|
||||||
|
# cert-manager.io/issuer: letsencrypt-nginx
|
||||||
|
# hostName: k8.infisical.com
|
||||||
|
# frontend:
|
||||||
|
# path: /
|
||||||
|
# pathType: Prefix
|
||||||
|
# backend:
|
||||||
|
# path: /api
|
||||||
|
# pathType: Prefix
|
||||||
|
# tls:
|
||||||
|
# - secretName: letsencrypt-nginx
|
||||||
|
# hosts:
|
||||||
|
# - k8.infisical.com
|
||||||
|
|
||||||
|
###
|
||||||
|
### YOU MUST FILL IN ALL SECRETS BELOW
|
||||||
|
###
|
||||||
|
backendEnvironmentVariables: {}
|
||||||
|
|
||||||
|
frontendEnvironmentVariables: {}
|
||||||
41
.github/workflows/be-test-report.yml
vendored
Normal file
41
.github/workflows/be-test-report.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
name: "Backend Test Report"
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Check Backend Pull Request"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
be-report:
|
||||||
|
name: Backend test report
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: 📁 Download test results
|
||||||
|
id: download-artifact
|
||||||
|
uses: dawidd6/action-download-artifact@v2
|
||||||
|
with:
|
||||||
|
name: be-test-results
|
||||||
|
path: backend
|
||||||
|
workflow: check-be-pull-request.yml
|
||||||
|
workflow_conclusion: success
|
||||||
|
- name: 📋 Publish test results
|
||||||
|
uses: dorny/test-reporter@v1
|
||||||
|
with:
|
||||||
|
name: Test Results
|
||||||
|
path: reports/jest-*.xml
|
||||||
|
reporter: jest-junit
|
||||||
|
working-directory: backend
|
||||||
|
- name: 📋 Publish coverage
|
||||||
|
uses: ArtiomTr/jest-coverage-report-action@v2
|
||||||
|
id: coverage
|
||||||
|
with:
|
||||||
|
output: comment, report-markdown
|
||||||
|
coverage-file: coverage/report.json
|
||||||
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
working-directory: backend
|
||||||
|
- uses: marocchino/sticky-pull-request-comment@v2
|
||||||
|
with:
|
||||||
|
message: ${{ steps.coverage.outputs.report }}
|
||||||
47
.github/workflows/check-be-pull-request.yml
vendored
47
.github/workflows/check-be-pull-request.yml
vendored
@@ -1,41 +1,42 @@
|
|||||||
name: Check Backend Pull Request
|
name: "Check Backend Pull Request"
|
||||||
|
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
types: [ opened, synchronize ]
|
types: [opened, synchronize]
|
||||||
paths:
|
paths:
|
||||||
- 'backend/**'
|
- "backend/**"
|
||||||
- '!backend/README.md'
|
- "!backend/README.md"
|
||||||
- '!backend/.*'
|
- "!backend/.*"
|
||||||
- 'backend/.eslintrc.js'
|
- "backend/.eslintrc.js"
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
check-be-pr:
|
check-be-pr:
|
||||||
name: Check
|
name: Check
|
||||||
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: 🔧 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: backend/package-lock.json
|
cache-dependency-path: backend/package-lock.json
|
||||||
-
|
- name: 📦 Install dependencies
|
||||||
name: 📦 Install dependencies
|
run: npm ci --only-production
|
||||||
run: npm ci --only-production --ignore-scripts
|
|
||||||
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
|
||||||
-
|
uses: actions/upload-artifact@v3
|
||||||
name: 🏗️ Run build
|
if: always()
|
||||||
|
with:
|
||||||
|
name: be-test-results
|
||||||
|
path: |
|
||||||
|
./backend/reports
|
||||||
|
./backend/coverage
|
||||||
|
- name: 🏗️ Run build
|
||||||
run: npm run build
|
run: npm run build
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
|
|||||||
22
.github/workflows/close_inactive_issues.yml
vendored
22
.github/workflows/close_inactive_issues.yml
vendored
@@ -1,22 +0,0 @@
|
|||||||
name: Close inactive issues
|
|
||||||
on:
|
|
||||||
schedule:
|
|
||||||
- cron: "30 1 * * *"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
close-issues:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
issues: write
|
|
||||||
pull-requests: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/stale@v4
|
|
||||||
with:
|
|
||||||
days-before-issue-stale: 30
|
|
||||||
days-before-issue-close: 14
|
|
||||||
stale-issue-label: "stale"
|
|
||||||
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
|
|
||||||
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
|
|
||||||
days-before-pr-stale: -1
|
|
||||||
days-before-pr-close: -1
|
|
||||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
76
.github/workflows/docker-image.yml
vendored
76
.github/workflows/docker-image.yml
vendored
@@ -1,5 +1,4 @@
|
|||||||
name: Push to Docker Hub
|
name: Build, Publish and Deploy to Gamma
|
||||||
|
|
||||||
on: [workflow_dispatch]
|
on: [workflow_dispatch]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -10,8 +9,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 🔧 Set up QEMU
|
- name: Save commit hashes for tag
|
||||||
uses: docker/setup-qemu-action@v2
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
- name: 🔧 Set up Docker Buildx
|
- name: 🔧 Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: 🐋 Login to Docker Hub
|
- name: 🐋 Login to Docker Hub
|
||||||
@@ -19,9 +19,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
- name: 📦 Build backend and export to Docker
|
- name: 📦 Build backend and export to Docker
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
load: true
|
load: true
|
||||||
context: backend
|
context: backend
|
||||||
tags: infisical/backend:test
|
tags: infisical/backend:test
|
||||||
@@ -35,11 +39,14 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||||
- name: 🏗️ Build backend and push
|
- name: 🏗️ Build backend and push
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
push: true
|
push: true
|
||||||
context: backend
|
context: backend
|
||||||
tags: infisical/backend:latest
|
tags: infisical/backend:${{ steps.commit.outputs.short }},
|
||||||
|
infisical/backend:latest
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|
||||||
frontend-image:
|
frontend-image:
|
||||||
@@ -49,8 +56,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: 🔧 Set up QEMU
|
- name: Save commit hashes for tag
|
||||||
uses: docker/setup-qemu-action@v2
|
id: commit
|
||||||
|
uses: pr-mpt/actions-commit-hash@v2
|
||||||
- name: 🔧 Set up Docker Buildx
|
- name: 🔧 Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v2
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: 🐋 Login to Docker Hub
|
- name: 🐋 Login to Docker Hub
|
||||||
@@ -58,10 +66,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
- name: Set up Depot CLI
|
||||||
|
uses: depot/setup-action@v1
|
||||||
- name: 📦 Build frontend and export to Docker
|
- name: 📦 Build frontend and export to Docker
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
load: true
|
load: true
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
|
project: 64mmf0n610
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:test
|
tags: infisical/frontend:test
|
||||||
build-args: |
|
build-args: |
|
||||||
@@ -76,11 +88,51 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
docker stop infisical-frontend-test
|
docker stop infisical-frontend-test
|
||||||
- name: 🏗️ Build frontend and push
|
- name: 🏗️ Build frontend and push
|
||||||
uses: docker/build-push-action@v3
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
|
project: 64mmf0n610
|
||||||
push: true
|
push: true
|
||||||
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
context: frontend
|
context: frontend
|
||||||
tags: infisical/frontend:latest
|
tags: infisical/frontend:${{ steps.commit.outputs.short }},
|
||||||
|
infisical/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 }}
|
||||||
|
gamma-deployment:
|
||||||
|
name: Deploy to gamma
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [frontend-image, backend-image]
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Install Helm
|
||||||
|
uses: azure/setup-helm@v3
|
||||||
|
with:
|
||||||
|
version: v3.10.0
|
||||||
|
- name: Install infisical helm chart
|
||||||
|
run: |
|
||||||
|
helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/'
|
||||||
|
helm repo update
|
||||||
|
- name: Install kubectl
|
||||||
|
uses: azure/setup-kubectl@v3
|
||||||
|
- name: Install doctl
|
||||||
|
uses: digitalocean/action-doctl@v2
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
|
||||||
|
- name: Save DigitalOcean kubeconfig with short-lived credentials
|
||||||
|
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
|
||||||
|
- name: switch to gamma namespace
|
||||||
|
run: kubectl config set-context --current --namespace=gamma
|
||||||
|
- name: test kubectl
|
||||||
|
run: kubectl get ingress
|
||||||
|
- name: Download helm values to file and upgrade gamma deploy
|
||||||
|
run: |
|
||||||
|
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
|
||||||
|
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods
|
||||||
|
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
||||||
|
echo "Helm upgrade failed"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Helm upgrade was successful"
|
||||||
|
fi
|
||||||
19
.github/workflows/release_build.yml
vendored
19
.github/workflows/release_build.yml
vendored
@@ -1,4 +1,4 @@
|
|||||||
name: Go releaser
|
name: Build and release CLI
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -13,25 +13,36 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
goreleaser:
|
goreleaser:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
- run: git fetch --force --tags
|
- run: git fetch --force --tags
|
||||||
|
- run: echo "Ref name ${{github.ref_name}}"
|
||||||
- uses: actions/setup-go@v3
|
- uses: actions/setup-go@v3
|
||||||
with:
|
with:
|
||||||
go-version: '>=1.19.3'
|
go-version: '>=1.19.3'
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: cli/go.sum
|
cache-dependency-path: cli/go.sum
|
||||||
- uses: goreleaser/goreleaser-action@v2
|
- name: libssl1.1 => libssl1.0-dev for OSXCross
|
||||||
|
run: |
|
||||||
|
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
|
||||||
|
sudo apt update && apt-cache policy libssl1.0-dev
|
||||||
|
sudo apt-get install libssl1.0-dev
|
||||||
|
- name: OSXCross for CGO Support
|
||||||
|
run: |
|
||||||
|
mkdir ../../osxcross
|
||||||
|
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
|
||||||
|
- uses: goreleaser/goreleaser-action@v4
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: latest
|
version: latest
|
||||||
args: release --rm-dist
|
args: release --clean
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
|
||||||
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
|
||||||
|
AUR_KEY: ${{ secrets.AUR_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
|
||||||
|
|||||||
@@ -26,13 +26,4 @@ jobs:
|
|||||||
context: k8-operator
|
context: k8-operator
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
tags: infisical/kubernetes-operator:latest
|
tags: infisical/kubernetes-operator:latest
|
||||||
|
|
||||||
- uses: actions/setup-go@v2
|
|
||||||
|
|
||||||
- name: Upload CRD manifest
|
|
||||||
uses: svenstaro/upload-release-action@v2
|
|
||||||
with:
|
|
||||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
file: dist/install-secrets-operator.yaml
|
|
||||||
tag: ${{ github.ref }}
|
|
||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -12,6 +12,8 @@ node_modules
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
/dist
|
/dist
|
||||||
|
/completions/
|
||||||
|
/manpages/
|
||||||
|
|
||||||
# frontend
|
# frontend
|
||||||
|
|
||||||
@@ -25,7 +27,9 @@ node_modules
|
|||||||
.env
|
.env
|
||||||
|
|
||||||
# testing
|
# testing
|
||||||
/coverage
|
coverage
|
||||||
|
reports
|
||||||
|
junit.xml
|
||||||
|
|
||||||
# next.js
|
# next.js
|
||||||
/.next/
|
/.next/
|
||||||
|
|||||||
101
.goreleaser.yaml
101
.goreleaser.yaml
@@ -6,43 +6,76 @@
|
|||||||
# - cd cli && go mod tidy
|
# - cd cli && go mod tidy
|
||||||
# # you may remove this if you don't need go generate
|
# # you may remove this if you don't need go generate
|
||||||
# - cd cli && go generate ./...
|
# - cd cli && go generate ./...
|
||||||
|
before:
|
||||||
|
hooks:
|
||||||
|
- ./cli/scripts/completions.sh
|
||||||
|
- ./cli/scripts/manpages.sh
|
||||||
|
|
||||||
builds:
|
builds:
|
||||||
- env:
|
- id: darwin-build
|
||||||
- CGO_ENABLED=0
|
|
||||||
binary: infisical
|
binary: infisical
|
||||||
id: infisical
|
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=1
|
||||||
|
- CC=/home/runner/work/osxcross/target/bin/o64-clang
|
||||||
|
- CXX=/home/runner/work/osxcross/target/bin/o64-clang++
|
||||||
goos:
|
goos:
|
||||||
- darwin
|
- darwin
|
||||||
|
ignore:
|
||||||
|
- goos: darwin
|
||||||
|
goarch: "386"
|
||||||
|
dir: ./cli
|
||||||
|
|
||||||
|
- id: all-other-builds
|
||||||
|
env:
|
||||||
|
- CGO_ENABLED=0
|
||||||
|
binary: infisical
|
||||||
|
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
|
||||||
|
flags:
|
||||||
|
- -trimpath
|
||||||
|
goos:
|
||||||
- freebsd
|
- freebsd
|
||||||
- linux
|
- linux
|
||||||
- netbsd
|
- netbsd
|
||||||
- openbsd
|
- openbsd
|
||||||
- windows
|
- windows
|
||||||
goarch:
|
goarch:
|
||||||
- 386
|
- "386"
|
||||||
- amd64
|
- amd64
|
||||||
- arm
|
- arm
|
||||||
- arm64
|
- arm64
|
||||||
goarm:
|
goarm:
|
||||||
- 6
|
- "6"
|
||||||
- 7
|
- "7"
|
||||||
ignore:
|
ignore:
|
||||||
- goos: darwin
|
|
||||||
goarch: "386"
|
|
||||||
- goos: windows
|
- goos: windows
|
||||||
goarch: "386"
|
goarch: "386"
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: "386"
|
goarch: "386"
|
||||||
dir: ./cli
|
dir: ./cli
|
||||||
|
|
||||||
|
archives:
|
||||||
|
- format_overrides:
|
||||||
|
- goos: windows
|
||||||
|
format: zip
|
||||||
|
files:
|
||||||
|
- README*
|
||||||
|
- LICENSE*
|
||||||
|
- manpages/*
|
||||||
|
- completions/*
|
||||||
|
|
||||||
release:
|
release:
|
||||||
replace_existing_draft: true
|
replace_existing_draft: true
|
||||||
mode: 'replace'
|
mode: 'replace'
|
||||||
|
|
||||||
checksum:
|
checksum:
|
||||||
name_template: 'checksums.txt'
|
name_template: 'checksums.txt'
|
||||||
|
|
||||||
snapshot:
|
snapshot:
|
||||||
name_template: "{{ incpatch .Version }}"
|
name_template: "{{ incpatch .Version }}-devel"
|
||||||
|
|
||||||
changelog:
|
changelog:
|
||||||
sort: asc
|
sort: asc
|
||||||
filters:
|
filters:
|
||||||
@@ -56,6 +89,7 @@ changelog:
|
|||||||
# - infisical
|
# - infisical
|
||||||
# dir: "{{ dir .ArtifactPath }}"
|
# dir: "{{ dir .ArtifactPath }}"
|
||||||
# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
|
# cmd: curl -F package=@{{ .ArtifactName }} https://{{ .Env.FURY_TOKEN }}@push.fury.io/infisical/
|
||||||
|
|
||||||
brews:
|
brews:
|
||||||
- name: infisical
|
- name: infisical
|
||||||
tap:
|
tap:
|
||||||
@@ -67,21 +101,39 @@ brews:
|
|||||||
folder: Formula
|
folder: Formula
|
||||||
homepage: "https://infisical.com"
|
homepage: "https://infisical.com"
|
||||||
description: "The official Infisical CLI"
|
description: "The official Infisical CLI"
|
||||||
|
install: |-
|
||||||
|
bin.install "infisical"
|
||||||
|
bash_completion.install "completions/infisical.bash" => "infisical"
|
||||||
|
zsh_completion.install "completions/infisical.zsh" => "_infisical"
|
||||||
|
fish_completion.install "completions/infisical.fish"
|
||||||
|
man1.install "manpages/infisical.1.gz"
|
||||||
|
|
||||||
nfpms:
|
nfpms:
|
||||||
- id: infisical
|
- id: infisical
|
||||||
package_name: infisical
|
package_name: infisical
|
||||||
builds:
|
builds:
|
||||||
- infisical
|
- all-other-builds
|
||||||
vendor: Infisical, Inc
|
vendor: Infisical, Inc
|
||||||
homepage: https://infisical.com/
|
homepage: https://infisical.com/
|
||||||
maintainer: Infisical, Inc
|
maintainer: Infisical, Inc
|
||||||
description: The offical Infisical CLI
|
description: The offical Infisical CLI
|
||||||
license: Apache 2.0
|
license: MIT
|
||||||
formats:
|
formats:
|
||||||
- rpm
|
- rpm
|
||||||
- deb
|
- deb
|
||||||
- apk
|
- apk
|
||||||
|
- archlinux
|
||||||
bindir: /usr/bin
|
bindir: /usr/bin
|
||||||
|
contents:
|
||||||
|
- src: ./completions/infisical.bash
|
||||||
|
dst: /etc/bash_completion.d/infisical
|
||||||
|
- src: ./completions/infisical.fish
|
||||||
|
dst: /usr/share/fish/vendor_completions.d/infisical.fish
|
||||||
|
- src: ./completions/infisical.zsh
|
||||||
|
dst: /usr/share/zsh/site-functions/_infisical
|
||||||
|
- src: ./manpages/infisical.1.gz
|
||||||
|
dst: /usr/share/man/man1/infisical.1.gz
|
||||||
|
|
||||||
scoop:
|
scoop:
|
||||||
bucket:
|
bucket:
|
||||||
owner: Infisical
|
owner: Infisical
|
||||||
@@ -91,7 +143,32 @@ scoop:
|
|||||||
email: ai@infisical.com
|
email: ai@infisical.com
|
||||||
homepage: "https://infisical.com"
|
homepage: "https://infisical.com"
|
||||||
description: "The official Infisical CLI"
|
description: "The official Infisical CLI"
|
||||||
license: Apache-2.0
|
license: MIT
|
||||||
|
|
||||||
|
aurs:
|
||||||
|
-
|
||||||
|
name: infisical-bin
|
||||||
|
homepage: "https://infisical.com"
|
||||||
|
description: "The official Infisical CLI"
|
||||||
|
maintainers:
|
||||||
|
- Infisical, Inc <support@infisical.com>
|
||||||
|
license: MIT
|
||||||
|
private_key: '{{ .Env.AUR_KEY }}'
|
||||||
|
git_url: 'ssh://aur@aur.archlinux.org/infisical-bin.git'
|
||||||
|
package: |-
|
||||||
|
# bin
|
||||||
|
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
|
||||||
|
# license
|
||||||
|
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
|
||||||
|
# completions
|
||||||
|
mkdir -p "${pkgdir}/usr/share/bash-completion/completions/"
|
||||||
|
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
|
||||||
|
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.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
|
||||||
|
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
|
||||||
|
# man pages
|
||||||
|
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
|
||||||
# dockers:
|
# dockers:
|
||||||
# - dockerfile: goreleaser.dockerfile
|
# - dockerfile: goreleaser.dockerfile
|
||||||
# goos: linux
|
# goos: linux
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"semi": true,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 80,
|
|
||||||
"useTabs": false
|
|
||||||
}
|
|
||||||
3
Makefile
3
Makefile
@@ -7,6 +7,9 @@ push:
|
|||||||
up-dev:
|
up-dev:
|
||||||
docker-compose -f docker-compose.dev.yml up --build
|
docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
|
i-dev:
|
||||||
|
infisical run -- docker-compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
up-prod:
|
up-prod:
|
||||||
docker-compose -f docker-compose.yml up --build
|
docker-compose -f docker-compose.yml up --build
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
{
|
{
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"plugins": ["@typescript-eslint", "prettier"],
|
"plugins": ["@typescript-eslint"],
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
"plugin:@typescript-eslint/eslint-recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended"
|
||||||
"prettier"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-console": 2
|
"no-console": 2
|
||||||
|
|||||||
@@ -4,12 +4,15 @@ WORKDIR /app
|
|||||||
|
|
||||||
COPY package.json package-lock.json ./
|
COPY package.json package-lock.json ./
|
||||||
|
|
||||||
|
# RUN npm ci --only-production --ignore-scripts
|
||||||
|
# "prepare": "cd .. && npm install"
|
||||||
|
|
||||||
RUN npm ci --only-production
|
RUN npm ci --only-production
|
||||||
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||||
CMD node healthcheck.js
|
CMD node healthcheck.js
|
||||||
|
|
||||||
|
|
||||||
CMD ["npm", "run", "start"]
|
CMD ["npm", "run", "start"]
|
||||||
|
|||||||
19
backend/__tests__/healthcheck.test.ts
Normal file
19
backend/__tests__/healthcheck.test.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { server } from '../src/app';
|
||||||
|
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
|
||||||
|
import supertest from 'supertest';
|
||||||
|
import { setUpHealthEndpoint } from '../src/services/health';
|
||||||
|
|
||||||
|
const requestWithSupertest = supertest(server);
|
||||||
|
describe('Healthcheck endpoint', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
setUpHealthEndpoint(server);
|
||||||
|
});
|
||||||
|
afterAll(async () => {
|
||||||
|
server.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /healthcheck should return OK', async () => {
|
||||||
|
const res = await requestWithSupertest.get('/healthcheck');
|
||||||
|
expect(res.status).toEqual(200);
|
||||||
|
});
|
||||||
|
});
|
||||||
30
backend/environment.d.ts
vendored
30
backend/environment.d.ts
vendored
@@ -3,8 +3,10 @@ export {};
|
|||||||
declare global {
|
declare global {
|
||||||
namespace NodeJS {
|
namespace NodeJS {
|
||||||
interface ProcessEnv {
|
interface ProcessEnv {
|
||||||
|
PORT: string;
|
||||||
EMAIL_TOKEN_LIFETIME: string;
|
EMAIL_TOKEN_LIFETIME: string;
|
||||||
ENCRYPTION_KEY: string;
|
ENCRYPTION_KEY: string;
|
||||||
|
SALT_ROUNDS: string;
|
||||||
JWT_AUTH_LIFETIME: string;
|
JWT_AUTH_LIFETIME: string;
|
||||||
JWT_AUTH_SECRET: string;
|
JWT_AUTH_SECRET: string;
|
||||||
JWT_REFRESH_LIFETIME: string;
|
JWT_REFRESH_LIFETIME: string;
|
||||||
@@ -14,24 +16,36 @@ declare global {
|
|||||||
JWT_SIGNUP_SECRET: string;
|
JWT_SIGNUP_SECRET: string;
|
||||||
MONGO_URL: string;
|
MONGO_URL: string;
|
||||||
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
||||||
OAUTH_CLIENT_SECRET_HEROKU: string;
|
VERBOSE_ERROR_OUTPUT: string;
|
||||||
OAUTH_TOKEN_URL_HEROKU: string;
|
LOKI_HOST: string;
|
||||||
|
CLIENT_ID_HEROKU: string;
|
||||||
|
CLIENT_ID_VERCEL: string;
|
||||||
|
CLIENT_ID_NETLIFY: string;
|
||||||
|
CLIENT_ID_GITHUB: string;
|
||||||
|
CLIENT_SECRET_HEROKU: string;
|
||||||
|
CLIENT_SECRET_VERCEL: string;
|
||||||
|
CLIENT_SECRET_NETLIFY: string;
|
||||||
|
CLIENT_SECRET_GITHUB: string;
|
||||||
|
CLIENT_SLUG_VERCEL: string;
|
||||||
POSTHOG_HOST: string;
|
POSTHOG_HOST: string;
|
||||||
POSTHOG_PROJECT_API_KEY: string;
|
POSTHOG_PROJECT_API_KEY: string;
|
||||||
PRIVATE_KEY: string;
|
|
||||||
PUBLIC_KEY: string;
|
|
||||||
SENTRY_DSN: string;
|
SENTRY_DSN: string;
|
||||||
SITE_URL: string;
|
SITE_URL: string;
|
||||||
SMTP_HOST: string;
|
SMTP_HOST: string;
|
||||||
SMTP_NAME: string;
|
SMTP_SECURE: string;
|
||||||
SMTP_PASSWORD: string;
|
SMTP_PORT: string;
|
||||||
SMTP_USERNAME: string;
|
SMTP_USERNAME: string;
|
||||||
STRIPE_PRODUCT_CARD_AUTH: string;
|
SMTP_PASSWORD: string;
|
||||||
STRIPE_PRODUCT_PRO: string;
|
SMTP_FROM_ADDRESS: string;
|
||||||
|
SMTP_FROM_NAME: string;
|
||||||
STRIPE_PRODUCT_STARTER: string;
|
STRIPE_PRODUCT_STARTER: string;
|
||||||
|
STRIPE_PRODUCT_TEAM: string;
|
||||||
|
STRIPE_PRODUCT_PRO: string;
|
||||||
STRIPE_PUBLISHABLE_KEY: string;
|
STRIPE_PUBLISHABLE_KEY: string;
|
||||||
STRIPE_SECRET_KEY: string;
|
STRIPE_SECRET_KEY: string;
|
||||||
STRIPE_WEBHOOK_SECRET: string;
|
STRIPE_WEBHOOK_SECRET: string;
|
||||||
|
TELEMETRY_ENABLED: string;
|
||||||
|
LICENSE_KEY: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2729
backend/package-lock.json
generated
2729
backend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,9 +2,9 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@godaddy/terminus": "^4.11.2",
|
"@godaddy/terminus": "^4.11.2",
|
||||||
"@sentry/node": "^7.21.1",
|
"@sentry/node": "^7.21.1",
|
||||||
"@sentry/tracing": "^7.19.0",
|
"@sentry/tracing": "^7.21.1",
|
||||||
"@types/crypto-js": "^4.1.1",
|
"@types/crypto-js": "^4.1.1",
|
||||||
"axios": "^1.1.3",
|
"axios": "^1.2.0",
|
||||||
"bigint-conversion": "^2.2.2",
|
"bigint-conversion": "^2.2.2",
|
||||||
"cookie-parser": "^1.4.6",
|
"cookie-parser": "^1.4.6",
|
||||||
"cors": "^2.8.5",
|
"cors": "^2.8.5",
|
||||||
@@ -17,7 +17,7 @@
|
|||||||
"helmet": "^5.1.1",
|
"helmet": "^5.1.1",
|
||||||
"jsonwebtoken": "^8.5.1",
|
"jsonwebtoken": "^8.5.1",
|
||||||
"jsrp": "^0.2.4",
|
"jsrp": "^0.2.4",
|
||||||
"mongoose": "^6.7.2",
|
"mongoose": "^6.7.3",
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.8.0",
|
||||||
"posthog-node": "^2.1.0",
|
"posthog-node": "^2.1.0",
|
||||||
"query-string": "^7.1.3",
|
"query-string": "^7.1.3",
|
||||||
@@ -33,11 +33,15 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "npm run build && node build/index.js",
|
"start": "npm run build && node build/index.js",
|
||||||
"dev": "nodemon",
|
"dev": "nodemon",
|
||||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
|
"swagger-autogen": "node ./swagger/index.ts",
|
||||||
|
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
||||||
"lint": "eslint . --ext .ts",
|
"lint": "eslint . --ext .ts",
|
||||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
"lint-staged": "lint-staged",
|
||||||
"lint-staged": "lint-staged"
|
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
||||||
|
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
|
||||||
|
"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"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -51,25 +55,94 @@
|
|||||||
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
||||||
"description": "",
|
"description": "",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@jest/globals": "^29.3.1",
|
||||||
"@posthog/plugin-scaffold": "^1.3.4",
|
"@posthog/plugin-scaffold": "^1.3.4",
|
||||||
|
"@types/bcrypt": "^5.0.0",
|
||||||
|
"@types/bcryptjs": "^2.4.2",
|
||||||
"@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/jsonwebtoken": "^8.5.9",
|
"@types/jsonwebtoken": "^8.5.9",
|
||||||
|
"@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/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",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||||
"@typescript-eslint/parser": "^5.40.1",
|
"@typescript-eslint/parser": "^5.40.1",
|
||||||
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.26.0",
|
"eslint": "^8.26.0",
|
||||||
"eslint-config-prettier": "^8.5.0",
|
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
|
||||||
"install": "^0.13.0",
|
"install": "^0.13.0",
|
||||||
"jest": "^29.3.1",
|
"jest": "^29.3.1",
|
||||||
|
"jest-junit": "^15.0.0",
|
||||||
"nodemon": "^2.0.19",
|
"nodemon": "^2.0.19",
|
||||||
"npm": "^8.19.3",
|
"npm": "^8.19.3",
|
||||||
"prettier": "^2.7.1",
|
"supertest": "^6.3.3",
|
||||||
|
"ts-jest": "^29.0.3",
|
||||||
"ts-node": "^10.9.1"
|
"ts-node": "^10.9.1"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"preset": "ts-jest",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/*.{js,ts}",
|
||||||
|
"!**/node_modules/**"
|
||||||
|
],
|
||||||
|
"setupFiles": [
|
||||||
|
"<rootDir>/test-resources/env-vars.js"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"jest-junit": {
|
||||||
|
"outputDirectory": "reports",
|
||||||
|
"outputName": "jest-junit.xml",
|
||||||
|
"ancestorSeparator": " › ",
|
||||||
|
"uniqueOutputName": "false",
|
||||||
|
"suiteNameTemplate": "{filepath}",
|
||||||
|
"classNameTemplate": "{classname}",
|
||||||
|
"titleTemplate": "{title}"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@godaddy/terminus": "^4.11.2",
|
||||||
|
"@octokit/rest": "^19.0.5",
|
||||||
|
"@sentry/node": "^7.14.0",
|
||||||
|
"@sentry/tracing": "^7.19.0",
|
||||||
|
"@types/crypto-js": "^4.1.1",
|
||||||
|
"@types/libsodium-wrappers": "^0.7.10",
|
||||||
|
"await-to-js": "^3.0.0",
|
||||||
|
"axios": "^1.1.3",
|
||||||
|
"bcrypt": "^5.1.0",
|
||||||
|
"bigint-conversion": "^2.2.2",
|
||||||
|
"builder-pattern": "^2.2.0",
|
||||||
|
"cookie-parser": "^1.4.6",
|
||||||
|
"cors": "^2.8.5",
|
||||||
|
"crypto-js": "^4.1.1",
|
||||||
|
"dotenv": "^16.0.1",
|
||||||
|
"express": "^4.18.1",
|
||||||
|
"express-rate-limit": "^6.7.0",
|
||||||
|
"express-validator": "^6.14.2",
|
||||||
|
"handlebars": "^4.7.7",
|
||||||
|
"helmet": "^5.1.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
|
"jsonwebtoken": "^9.0.0",
|
||||||
|
"jsrp": "^0.2.4",
|
||||||
|
"libsodium-wrappers": "^0.7.10",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"mongoose": "^6.7.2",
|
||||||
|
"nodemailer": "^6.8.0",
|
||||||
|
"posthog-node": "^2.2.2",
|
||||||
|
"query-string": "^7.1.3",
|
||||||
|
"request-ip": "^3.3.0",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"stripe": "^10.7.0",
|
||||||
|
"swagger-autogen": "^2.22.0",
|
||||||
|
"swagger-ui-express": "^4.6.0",
|
||||||
|
"tweetnacl": "^1.0.3",
|
||||||
|
"tweetnacl-util": "^0.15.1",
|
||||||
|
"typescript": "^4.9.3",
|
||||||
|
"utility-types": "^3.10.0",
|
||||||
|
"winston": "^3.8.2",
|
||||||
|
"winston-loki": "^6.0.6"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4146
backend/spec.json
Normal file
4146
backend/spec.json
Normal file
File diff suppressed because it is too large
Load Diff
138
backend/src/app.ts
Normal file
138
backend/src/app.ts
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
|
||||||
|
|
||||||
|
import express, { Request, Response } from 'express';
|
||||||
|
import helmet from 'helmet';
|
||||||
|
import cors from 'cors';
|
||||||
|
import cookieParser from 'cookie-parser';
|
||||||
|
import dotenv from 'dotenv';
|
||||||
|
import swaggerUi = require('swagger-ui-express');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const swaggerFile = require('../spec.json');
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
|
const requestIp = require('request-ip');
|
||||||
|
|
||||||
|
dotenv.config();
|
||||||
|
import { PORT, NODE_ENV, SITE_URL } from './config';
|
||||||
|
import { apiLimiter } from './helpers/rateLimiter';
|
||||||
|
|
||||||
|
import {
|
||||||
|
workspace as eeWorkspaceRouter,
|
||||||
|
secret as eeSecretRouter,
|
||||||
|
secretSnapshot as eeSecretSnapshotRouter,
|
||||||
|
action as eeActionRouter
|
||||||
|
} from './ee/routes/v1';
|
||||||
|
import {
|
||||||
|
signup as v1SignupRouter,
|
||||||
|
auth as v1AuthRouter,
|
||||||
|
bot as v1BotRouter,
|
||||||
|
organization as v1OrganizationRouter,
|
||||||
|
workspace as v1WorkspaceRouter,
|
||||||
|
membershipOrg as v1MembershipOrgRouter,
|
||||||
|
membership as v1MembershipRouter,
|
||||||
|
key as v1KeyRouter,
|
||||||
|
inviteOrg as v1InviteOrgRouter,
|
||||||
|
user as v1UserRouter,
|
||||||
|
userAction as v1UserActionRouter,
|
||||||
|
secret as v1SecretRouter,
|
||||||
|
serviceToken as v1ServiceTokenRouter,
|
||||||
|
password as v1PasswordRouter,
|
||||||
|
stripe as v1StripeRouter,
|
||||||
|
integration as v1IntegrationRouter,
|
||||||
|
integrationAuth as v1IntegrationAuthRouter
|
||||||
|
} from './routes/v1';
|
||||||
|
import {
|
||||||
|
users as v2UsersRouter,
|
||||||
|
organizations as v2OrganizationsRouter,
|
||||||
|
workspace as v2WorkspaceRouter,
|
||||||
|
secret as v2SecretRouter, // begin to phase out
|
||||||
|
secrets as v2SecretsRouter,
|
||||||
|
serviceTokenData as v2ServiceTokenDataRouter,
|
||||||
|
apiKeyData as v2APIKeyDataRouter,
|
||||||
|
environment as v2EnvironmentRouter,
|
||||||
|
} from './routes/v2';
|
||||||
|
|
||||||
|
import { healthCheck } from './routes/status';
|
||||||
|
|
||||||
|
import { getLogger } from './utils/logger';
|
||||||
|
import { RouteNotFoundError } from './utils/errors';
|
||||||
|
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||||
|
|
||||||
|
// patch async route params to handle Promise Rejections
|
||||||
|
patchRouterParam();
|
||||||
|
|
||||||
|
export const app = express();
|
||||||
|
|
||||||
|
app.enable('trust proxy');
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(
|
||||||
|
cors({
|
||||||
|
credentials: true,
|
||||||
|
origin: SITE_URL
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
app.use(requestIp.mw())
|
||||||
|
|
||||||
|
if (NODE_ENV === 'production') {
|
||||||
|
// enable app-wide rate-limiting + helmet security
|
||||||
|
// in production
|
||||||
|
app.disable('x-powered-by');
|
||||||
|
app.use(apiLimiter);
|
||||||
|
app.use(helmet());
|
||||||
|
}
|
||||||
|
|
||||||
|
// (EE) routes
|
||||||
|
app.use('/api/v1/secret', eeSecretRouter);
|
||||||
|
app.use('/api/v1/secret-snapshot', eeSecretSnapshotRouter);
|
||||||
|
app.use('/api/v1/workspace', eeWorkspaceRouter);
|
||||||
|
app.use('/api/v1/action', eeActionRouter);
|
||||||
|
|
||||||
|
// v1 routes
|
||||||
|
app.use('/api/v1/signup', v1SignupRouter);
|
||||||
|
app.use('/api/v1/auth', v1AuthRouter);
|
||||||
|
app.use('/api/v1/bot', v1BotRouter);
|
||||||
|
app.use('/api/v1/user', v1UserRouter);
|
||||||
|
app.use('/api/v1/user-action', v1UserActionRouter);
|
||||||
|
app.use('/api/v1/organization', v1OrganizationRouter);
|
||||||
|
app.use('/api/v1/workspace', v1WorkspaceRouter);
|
||||||
|
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
|
||||||
|
app.use('/api/v1/membership', v1MembershipRouter);
|
||||||
|
app.use('/api/v1/key', v1KeyRouter);
|
||||||
|
app.use('/api/v1/invite-org', v1InviteOrgRouter);
|
||||||
|
app.use('/api/v1/secret', v1SecretRouter);
|
||||||
|
app.use('/api/v1/service-token', v1ServiceTokenRouter); // deprecated
|
||||||
|
app.use('/api/v1/password', v1PasswordRouter);
|
||||||
|
app.use('/api/v1/stripe', v1StripeRouter);
|
||||||
|
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||||
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
|
|
||||||
|
// v2 routes
|
||||||
|
app.use('/api/v2/users', v2UsersRouter);
|
||||||
|
app.use('/api/v2/organizations', v2OrganizationsRouter);
|
||||||
|
app.use('/api/v2/workspace', v2EnvironmentRouter);
|
||||||
|
app.use('/api/v2/workspace', v2WorkspaceRouter);
|
||||||
|
app.use('/api/v2/secret', v2SecretRouter); // deprecated
|
||||||
|
app.use('/api/v2/secrets', v2SecretsRouter);
|
||||||
|
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
|
||||||
|
app.use('/api/v2/api-key', v2APIKeyDataRouter);
|
||||||
|
|
||||||
|
// api docs
|
||||||
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
||||||
|
|
||||||
|
// Server status
|
||||||
|
app.use('/api', healthCheck)
|
||||||
|
|
||||||
|
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
if (res.headersSent) return next();
|
||||||
|
next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` }))
|
||||||
|
})
|
||||||
|
|
||||||
|
//* Error Handling Middleware (must be after all routing logic)
|
||||||
|
app.use(requestErrorHandler)
|
||||||
|
|
||||||
|
export const server = app.listen(PORT, () => {
|
||||||
|
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
const PORT = process.env.PORT || 4000;
|
const PORT = process.env.PORT || 4000;
|
||||||
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400';
|
const EMAIL_TOKEN_LIFETIME = process.env.EMAIL_TOKEN_LIFETIME! || '86400';
|
||||||
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY!;
|
||||||
|
const SALT_ROUNDS = parseInt(process.env.SALT_ROUNDS!) || 10;
|
||||||
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
const JWT_AUTH_LIFETIME = process.env.JWT_AUTH_LIFETIME! || '10d';
|
||||||
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
|
const JWT_AUTH_SECRET = process.env.JWT_AUTH_SECRET!;
|
||||||
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
|
const JWT_REFRESH_LIFETIME = process.env.JWT_REFRESH_LIFETIME! || '90d';
|
||||||
@@ -10,33 +11,44 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
|
|||||||
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
||||||
const MONGO_URL = process.env.MONGO_URL!;
|
const MONGO_URL = process.env.MONGO_URL!;
|
||||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||||
const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!;
|
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
|
||||||
const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!;
|
const LOKI_HOST = process.env.LOKI_HOST || undefined;
|
||||||
|
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||||
|
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||||
|
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||||
|
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
|
||||||
|
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||||
|
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||||
|
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||||
|
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
|
||||||
|
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
|
||||||
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
||||||
const POSTHOG_PROJECT_API_KEY =
|
const POSTHOG_PROJECT_API_KEY =
|
||||||
process.env.POSTHOG_PROJECT_API_KEY! ||
|
process.env.POSTHOG_PROJECT_API_KEY! ||
|
||||||
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
|
||||||
const PRIVATE_KEY = process.env.PRIVATE_KEY!;
|
|
||||||
const PUBLIC_KEY = process.env.PUBLIC_KEY!;
|
|
||||||
const SENTRY_DSN = process.env.SENTRY_DSN!;
|
const SENTRY_DSN = process.env.SENTRY_DSN!;
|
||||||
const SITE_URL = process.env.SITE_URL!;
|
const SITE_URL = process.env.SITE_URL!;
|
||||||
const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com';
|
const SMTP_HOST = process.env.SMTP_HOST!;
|
||||||
const SMTP_PORT = process.env.SMTP_PORT! || 587;
|
const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false;
|
||||||
const SMTP_NAME = process.env.SMTP_NAME!;
|
const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587;
|
||||||
const SMTP_USERNAME = process.env.SMTP_USERNAME!;
|
const SMTP_USERNAME = process.env.SMTP_USERNAME!;
|
||||||
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
|
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
|
||||||
const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!;
|
const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!;
|
||||||
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
|
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical';
|
||||||
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
|
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
|
||||||
|
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
|
||||||
|
const STRIPE_PRODUCT_TEAM = process.env.STRIPE_PRODUCT_TEAM!;
|
||||||
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
|
||||||
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
|
||||||
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
|
||||||
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
|
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
|
||||||
|
const LICENSE_KEY = process.env.LICENSE_KEY!;
|
||||||
|
|
||||||
export {
|
export {
|
||||||
PORT,
|
PORT,
|
||||||
EMAIL_TOKEN_LIFETIME,
|
EMAIL_TOKEN_LIFETIME,
|
||||||
ENCRYPTION_KEY,
|
ENCRYPTION_KEY,
|
||||||
|
SALT_ROUNDS,
|
||||||
JWT_AUTH_LIFETIME,
|
JWT_AUTH_LIFETIME,
|
||||||
JWT_AUTH_SECRET,
|
JWT_AUTH_SECRET,
|
||||||
JWT_REFRESH_LIFETIME,
|
JWT_REFRESH_LIFETIME,
|
||||||
@@ -46,24 +58,34 @@ export {
|
|||||||
JWT_SIGNUP_SECRET,
|
JWT_SIGNUP_SECRET,
|
||||||
MONGO_URL,
|
MONGO_URL,
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
VERBOSE_ERROR_OUTPUT,
|
||||||
OAUTH_TOKEN_URL_HEROKU,
|
LOKI_HOST,
|
||||||
|
CLIENT_ID_HEROKU,
|
||||||
|
CLIENT_ID_VERCEL,
|
||||||
|
CLIENT_ID_NETLIFY,
|
||||||
|
CLIENT_ID_GITHUB,
|
||||||
|
CLIENT_SECRET_HEROKU,
|
||||||
|
CLIENT_SECRET_VERCEL,
|
||||||
|
CLIENT_SECRET_NETLIFY,
|
||||||
|
CLIENT_SECRET_GITHUB,
|
||||||
|
CLIENT_SLUG_VERCEL,
|
||||||
POSTHOG_HOST,
|
POSTHOG_HOST,
|
||||||
POSTHOG_PROJECT_API_KEY,
|
POSTHOG_PROJECT_API_KEY,
|
||||||
PRIVATE_KEY,
|
|
||||||
PUBLIC_KEY,
|
|
||||||
SENTRY_DSN,
|
SENTRY_DSN,
|
||||||
SITE_URL,
|
SITE_URL,
|
||||||
SMTP_HOST,
|
SMTP_HOST,
|
||||||
SMTP_PORT,
|
SMTP_PORT,
|
||||||
SMTP_NAME,
|
SMTP_SECURE,
|
||||||
SMTP_USERNAME,
|
SMTP_USERNAME,
|
||||||
SMTP_PASSWORD,
|
SMTP_PASSWORD,
|
||||||
STRIPE_PRODUCT_CARD_AUTH,
|
SMTP_FROM_ADDRESS,
|
||||||
STRIPE_PRODUCT_PRO,
|
SMTP_FROM_NAME,
|
||||||
STRIPE_PRODUCT_STARTER,
|
STRIPE_PRODUCT_STARTER,
|
||||||
|
STRIPE_PRODUCT_TEAM,
|
||||||
|
STRIPE_PRODUCT_PRO,
|
||||||
STRIPE_PUBLISHABLE_KEY,
|
STRIPE_PUBLISHABLE_KEY,
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
STRIPE_WEBHOOK_SECRET,
|
STRIPE_WEBHOOK_SECRET,
|
||||||
TELEMETRY_ENABLED
|
TELEMETRY_ENABLED,
|
||||||
|
LICENSE_KEY
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
import { Request, Response } from 'express';
|
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { IntegrationAuth, Integration } from '../models';
|
|
||||||
import { processOAuthTokenRes } from '../helpers/integrationAuth';
|
|
||||||
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
|
||||||
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
|
||||||
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const integrationAuthOauthExchange = async (
|
|
||||||
req: Request,
|
|
||||||
res: Response
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
let clientSecret;
|
|
||||||
|
|
||||||
const { workspaceId, code, integration } = req.body;
|
|
||||||
|
|
||||||
if (!INTEGRATION_SET.has(integration))
|
|
||||||
throw new Error('Failed to validate integration');
|
|
||||||
|
|
||||||
// use correct client secret
|
|
||||||
switch (integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU!,
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: code,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
const integrationAuth = await processOAuthTokenRes({
|
|
||||||
workspaceId,
|
|
||||||
integration,
|
|
||||||
res
|
|
||||||
});
|
|
||||||
|
|
||||||
// create or replace integration
|
|
||||||
const integrationObj = await Integration.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: ENV_DEV,
|
|
||||||
isActive: false,
|
|
||||||
app: null,
|
|
||||||
integration,
|
|
||||||
integrationAuth: integrationAuth._id
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get OAuth2 token'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'Successfully enabled integration authorization'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return list of applications allowed for integration with id [integrationAuthId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
let apps;
|
|
||||||
try {
|
|
||||||
const res = await axios.get('https://api.heroku.com/apps', {
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
|
||||||
Authorization: 'Bearer ' + req.accessToken
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
apps = res.data.map((a: any) => ({
|
|
||||||
name: a.name
|
|
||||||
}));
|
|
||||||
} catch (err) {}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apps
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete integration authorization with id [integrationAuthId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
|
||||||
// TODO: unfinished - disable application via Heroku API and make compatible with other integration types
|
|
||||||
try {
|
|
||||||
const { integrationAuthId } = req.params;
|
|
||||||
|
|
||||||
// TODO: disable application via Heroku API; figure out what authorization id is
|
|
||||||
|
|
||||||
const integrations = JSON.parse(
|
|
||||||
readFileSync('./src/json/integrations.json').toString()
|
|
||||||
);
|
|
||||||
|
|
||||||
let authorizationId;
|
|
||||||
switch (req.integrationAuth.integration) {
|
|
||||||
case 'heroku':
|
|
||||||
authorizationId = integrations.heroku.clientId;
|
|
||||||
}
|
|
||||||
|
|
||||||
// not sure what authorizationId is?
|
|
||||||
// // revoke authorization
|
|
||||||
// const res2 = await axios.delete(
|
|
||||||
// `https://api.heroku.com/oauth/authorizations/${authorizationId}`,
|
|
||||||
// {
|
|
||||||
// headers: {
|
|
||||||
// 'Accept': 'application/vnd.heroku+json; version=3',
|
|
||||||
// 'Authorization': 'Bearer ' + req.accessToken
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// );
|
|
||||||
|
|
||||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
|
||||||
_id: integrationAuthId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (deletedIntegrationAuth) {
|
|
||||||
await Integration.deleteMany({
|
|
||||||
integrationAuth: deletedIntegrationAuth._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to delete integration authorization'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
import { Request, Response } from 'express';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { Integration } from '../models';
|
|
||||||
import { decryptAsymmetric } from '../utils/crypto';
|
|
||||||
import { decryptSecrets } from '../helpers/secret';
|
|
||||||
import { PRIVATE_KEY } from '../config';
|
|
||||||
|
|
||||||
interface Key {
|
|
||||||
encryptedKey: string;
|
|
||||||
nonce: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PushSecret {
|
|
||||||
ciphertextKey: string;
|
|
||||||
ivKey: string;
|
|
||||||
tagKey: string;
|
|
||||||
hashKey: string;
|
|
||||||
ciphertextValue: string;
|
|
||||||
ivValue: string;
|
|
||||||
tagValue: string;
|
|
||||||
hashValue: string;
|
|
||||||
type: 'shared' | 'personal';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return list of all available integrations on Infisical
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getIntegrations = async (req: Request, res: Response) => {
|
|
||||||
let integrations;
|
|
||||||
try {
|
|
||||||
integrations = JSON.parse(
|
|
||||||
readFileSync('./src/json/integrations.json').toString()
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to get integrations'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integrations
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sync secrets [secrets] to integration with id [integrationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const syncIntegration = async (req: Request, res: Response) => {
|
|
||||||
// TODO: unfinished - make more versatile to accomodate for other integrations
|
|
||||||
try {
|
|
||||||
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
|
||||||
const symmetricKey = decryptAsymmetric({
|
|
||||||
ciphertext: key.encryptedKey,
|
|
||||||
nonce: key.nonce,
|
|
||||||
publicKey: req.user.publicKey,
|
|
||||||
privateKey: PRIVATE_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// decrypt secrets with symmetric key
|
|
||||||
const content = decryptSecrets({
|
|
||||||
secrets,
|
|
||||||
key: symmetricKey,
|
|
||||||
format: 'object'
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: make integration work for other integrations as well
|
|
||||||
const res = await axios.patch(
|
|
||||||
`https://api.heroku.com/apps/${req.integration.app}/config-vars`,
|
|
||||||
content,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
|
||||||
Authorization: 'Bearer ' + req.accessToken
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to sync secrets with integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: 'Successfully synced secrets with integration'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change environment or name of integration with id [integrationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const modifyIntegration = async (req: Request, res: Response) => {
|
|
||||||
let integration;
|
|
||||||
try {
|
|
||||||
const { update } = req.body;
|
|
||||||
|
|
||||||
integration = await Integration.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: req.integration._id
|
|
||||||
},
|
|
||||||
update,
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser({ email: req.user.email });
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: 'Failed to modify integration'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integration
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete integration with id [integrationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
|
||||||
let deletedIntegration;
|
|
||||||
try {
|
|
||||||
const { integrationId } = req.params;
|
|
||||||
|
|
||||||
deletedIntegration = await Integration.findOneAndDelete({
|
|
||||||
_id: integrationId
|
|
||||||
});
|
|
||||||
} 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({
|
|
||||||
deletedIntegration
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -4,14 +4,16 @@ import jwt from 'jsonwebtoken';
|
|||||||
import * as Sentry from '@sentry/node';
|
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 } from '../models';
|
import { User } from '../../models';
|
||||||
import { createToken, issueTokens, clearTokens } from '../helpers/auth';
|
import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
|
||||||
import {
|
import {
|
||||||
NODE_ENV,
|
NODE_ENV,
|
||||||
JWT_AUTH_LIFETIME,
|
JWT_AUTH_LIFETIME,
|
||||||
JWT_AUTH_SECRET,
|
JWT_AUTH_SECRET,
|
||||||
JWT_REFRESH_SECRET
|
JWT_REFRESH_SECRET
|
||||||
} from '../config';
|
} from '../../config';
|
||||||
|
import LoginSRPDetail from '../../models/LoginSRPDetail';
|
||||||
|
import { BadRequestError } from '../../utils/errors';
|
||||||
|
|
||||||
declare module 'jsonwebtoken' {
|
declare module 'jsonwebtoken' {
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
||||||
@@ -19,8 +21,6 @@ declare module 'jsonwebtoken' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const clientPublicKeys: any = {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
||||||
* @param req
|
* @param req
|
||||||
@@ -46,13 +46,15 @@ export const login1 = async (req: Request, res: Response) => {
|
|||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
() => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
clientPublicKeys[email] = {
|
|
||||||
clientPublicKey,
|
await LoginSRPDetail.findOneAndReplace({ email: email }, {
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
email: email,
|
||||||
};
|
clientPublicKey: clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
|
}, { upsert: true, returnNewDocument: false })
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serverPublicKey,
|
serverPublicKey,
|
||||||
@@ -85,15 +87,21 @@ export const login2 = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
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 })
|
||||||
|
|
||||||
|
if (!loginSRPDetailFromDB) {
|
||||||
|
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: clientPublicKeys[email].serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(clientPublicKeys[email].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)) {
|
||||||
@@ -170,10 +178,11 @@ export const logout = async (req: Request, res: Response) => {
|
|||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const checkAuth = async (req: Request, res: Response) =>
|
export const checkAuth = async (req: Request, res: Response) => {
|
||||||
res.status(200).send({
|
return res.status(200).send({
|
||||||
message: 'Authenticated'
|
message: 'Authenticated'
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return new token by redeeming refresh token
|
* Return new token by redeeming refresh token
|
||||||
107
backend/src/controllers/v1/botController.ts
Normal file
107
backend/src/controllers/v1/botController.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Bot, BotKey } from '../../models';
|
||||||
|
import { createBot } from '../../helpers/bot';
|
||||||
|
|
||||||
|
interface BotKey {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||||
|
* then create and return a new bot.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||||
|
// -> create a new bot and return it
|
||||||
|
bot = await createBot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot with id [req.bot._id] with active state set to [isActive].
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
// bot state set to active -> share workspace key with bot
|
||||||
|
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to set bot state to active - missing bot key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
||||||
|
_id: req.bot._id
|
||||||
|
}, {
|
||||||
|
isActive
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to update bot active state');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update bot active state'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as authController from './authController';
|
import * as authController from './authController';
|
||||||
|
import * as botController from './botController';
|
||||||
import * as integrationAuthController from './integrationAuthController';
|
import * as integrationAuthController from './integrationAuthController';
|
||||||
import * as integrationController from './integrationController';
|
import * as integrationController from './integrationController';
|
||||||
import * as keyController from './keyController';
|
import * as keyController from './keyController';
|
||||||
@@ -16,6 +17,7 @@ import * as workspaceController from './workspaceController';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
authController,
|
authController,
|
||||||
|
botController,
|
||||||
integrationAuthController,
|
integrationAuthController,
|
||||||
integrationController,
|
integrationController,
|
||||||
keyController,
|
keyController,
|
||||||
173
backend/src/controllers/v1/integrationAuthController.ts
Normal file
173
backend/src/controllers/v1/integrationAuthController.ts
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
IntegrationAuth,
|
||||||
|
Bot
|
||||||
|
} from '../../models';
|
||||||
|
import { INTEGRATION_SET, INTEGRATION_OPTIONS } from '../../variables';
|
||||||
|
import { IntegrationService } from '../../services';
|
||||||
|
import { getApps, revokeAccess } from '../../integrations';
|
||||||
|
|
||||||
|
export const getIntegrationOptions = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
return res.status(200).send({
|
||||||
|
integrationOptions: INTEGRATION_OPTIONS
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const oAuthExchange = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
const { workspaceId, code, integration } = req.body;
|
||||||
|
|
||||||
|
if (!INTEGRATION_SET.has(integration))
|
||||||
|
throw new Error('Failed to validate integration');
|
||||||
|
|
||||||
|
const environments = req.membership.workspace?.environments || [];
|
||||||
|
if(environments.length === 0){
|
||||||
|
throw new Error("Failed to get environments")
|
||||||
|
}
|
||||||
|
|
||||||
|
await IntegrationService.handleOAuthExchange({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code,
|
||||||
|
environment: environments[0].slug,
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get OAuth2 code-token exchange'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully enabled integration authorization'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save integration access token as part of integration [integration] for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const saveIntegrationAccessToken = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
// TODO: refactor
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
workspaceId,
|
||||||
|
accessToken,
|
||||||
|
integration
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
accessToken: string;
|
||||||
|
integration: string;
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
integration
|
||||||
|
}, {
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
integration
|
||||||
|
}, {
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Bot must be enabled to save integration access token');
|
||||||
|
|
||||||
|
// encrypt and save integration access token
|
||||||
|
integrationAuth = await IntegrationService.setIntegrationAuthAccess({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt: undefined
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to save integration access token');
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to save access token for integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
integrationAuth
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
apps = await getApps({
|
||||||
|
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 get integration authorization applications'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
apps
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete integration authorization with id [integrationAuthId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await revokeAccess({
|
||||||
|
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({
|
||||||
|
integrationAuth
|
||||||
|
});
|
||||||
|
}
|
||||||
131
backend/src/controllers/v1/integrationController.ts
Normal file
131
backend/src/controllers/v1/integrationController.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
Workspace,
|
||||||
|
Bot,
|
||||||
|
BotKey
|
||||||
|
} from '../../models';
|
||||||
|
import { EventService } from '../../services';
|
||||||
|
import { eventPushSecrets } from '../../events';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create/initialize an (empty) integration for integration authorization
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createIntegration = async (req: Request, res: Response) => {
|
||||||
|
let integration;
|
||||||
|
try {
|
||||||
|
// initialize new integration after saving integration access token
|
||||||
|
integration = await new Integration({
|
||||||
|
workspace: req.integrationAuth.workspace._id,
|
||||||
|
isActive: false,
|
||||||
|
app: null,
|
||||||
|
environment: req.integrationAuth.workspace?.environments[0].slug,
|
||||||
|
integration: req.integrationAuth.integration,
|
||||||
|
integrationAuth: req.integrationAuth._id
|
||||||
|
}).save();
|
||||||
|
} 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({
|
||||||
|
integration
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change environment or name of integration with id [integrationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateIntegration = async (req: Request, res: Response) => {
|
||||||
|
let integration;
|
||||||
|
|
||||||
|
// TODO: add integration-specific validation to ensure that each
|
||||||
|
// integration has the correct fields populated in [Integration]
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
environment,
|
||||||
|
isActive,
|
||||||
|
app,
|
||||||
|
appId,
|
||||||
|
targetEnvironment,
|
||||||
|
owner, // github-specific integration param
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
integration = await Integration.findOneAndUpdate(
|
||||||
|
{
|
||||||
|
_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);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update integration'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
integration
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete integration with id [integrationId] and deactivate bot if there are
|
||||||
|
* no integrations left
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteIntegration = async (req: Request, res: Response) => {
|
||||||
|
let integration;
|
||||||
|
try {
|
||||||
|
const { integrationId } = req.params;
|
||||||
|
|
||||||
|
integration = await Integration.findOneAndDelete({
|
||||||
|
_id: integrationId
|
||||||
|
});
|
||||||
|
|
||||||
|
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({
|
||||||
|
integration
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Key } from '../models';
|
import { Key } from '../../models';
|
||||||
import { findMembership } from '../helpers/membership';
|
import { findMembership } from '../../helpers/membership';
|
||||||
import { PUBLIC_KEY } from '../config';
|
|
||||||
import { GRANTED } from '../variables';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
||||||
@@ -17,16 +15,6 @@ export const uploadKey = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { key } = req.body;
|
const { key } = req.body;
|
||||||
|
|
||||||
// validate membership of sender
|
|
||||||
const senderMembership = await findMembership({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!senderMembership) {
|
|
||||||
throw new Error('Failed sender membership validation for workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate membership of receiver
|
// validate membership of receiver
|
||||||
const receiverMembership = await findMembership({
|
const receiverMembership = await findMembership({
|
||||||
user: key.userId,
|
user: key.userId,
|
||||||
@@ -37,9 +25,6 @@ export const uploadKey = async (req: Request, res: Response) => {
|
|||||||
throw new Error('Failed receiver membership validation for workspace');
|
throw new Error('Failed receiver membership validation for workspace');
|
||||||
}
|
}
|
||||||
|
|
||||||
receiverMembership.status = GRANTED;
|
|
||||||
await receiverMembership.save();
|
|
||||||
|
|
||||||
await new Key({
|
await new Key({
|
||||||
encryptedKey: key.encryptedKey,
|
encryptedKey: key.encryptedKey,
|
||||||
nonce: key.nonce,
|
nonce: key.nonce,
|
||||||
@@ -94,16 +79,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
return res.status(200).send(resObj);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Return public key of Infisical
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getPublicKeyInfisical = async (req: Request, res: Response) => {
|
|
||||||
return res.status(200).send({
|
|
||||||
publicKey: PUBLIC_KEY
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Membership, MembershipOrg, User, Key } from '../models';
|
import { Membership, MembershipOrg, User, Key, IMembership, Workspace } from '../../models';
|
||||||
import {
|
import {
|
||||||
findMembership,
|
findMembership,
|
||||||
deleteMembership as deleteMember
|
deleteMembership as deleteMember
|
||||||
} from '../helpers/membership';
|
} from '../../helpers/membership';
|
||||||
import { sendMail } from '../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
import { SITE_URL } from '../config';
|
import { SITE_URL } from '../../config';
|
||||||
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables';
|
import { ADMIN, MEMBER, ACCEPTED } from '../../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check that user is a member of workspace with id [workspaceId]
|
* Check that user is a member of workspace with id [workspaceId]
|
||||||
@@ -175,8 +175,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
|||||||
// 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
|
||||||
status: GRANTED
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (inviteeMembership)
|
if (inviteeMembership)
|
||||||
@@ -205,8 +204,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
|||||||
const m = await new Membership({
|
const m = await new Membership({
|
||||||
user: invitee._id,
|
user: invitee._id,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
role: MEMBER,
|
role: MEMBER
|
||||||
status: GRANTED
|
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
@@ -232,4 +230,4 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
|||||||
invitee,
|
invitee,
|
||||||
latestKey
|
latestKey
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||||
import { MembershipOrg, Organization, User, Token } from '../models';
|
import { MembershipOrg, Organization, User, Token } from '../../models';
|
||||||
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg';
|
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
|
||||||
import { checkEmailVerification } from '../helpers/signup';
|
import { checkEmailVerification } from '../../helpers/signup';
|
||||||
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 { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables';
|
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete organization membership with id [membershipOrgId] from organization
|
* Delete organization membership with id [membershipOrgId] from organization
|
||||||
@@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
|||||||
// TODO
|
// TODO
|
||||||
|
|
||||||
let membershipToChangeRole;
|
let membershipToChangeRole;
|
||||||
try {
|
// try {
|
||||||
} catch (err) {
|
// } catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
// Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
// Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
// return res.status(400).send({
|
||||||
message: 'Failed to change organization membership role'
|
// message: 'Failed to change organization membership role'
|
||||||
});
|
// });
|
||||||
}
|
// }
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
membershipOrg: membershipToChangeRole
|
membershipOrg: membershipToChangeRole
|
||||||
@@ -115,13 +115,14 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
if (!membershipOrg) {
|
if (!membershipOrg) {
|
||||||
throw new Error('Failed to validate organization membership');
|
throw new Error('Failed to validate organization membership');
|
||||||
}
|
}
|
||||||
|
|
||||||
invitee = await User.findOne({
|
invitee = await User.findOne({
|
||||||
email: inviteeEmail
|
email: inviteeEmail
|
||||||
});
|
}).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
|
||||||
@@ -218,12 +219,6 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
const { email, code } = req.body;
|
const { email, code } = req.body;
|
||||||
|
|
||||||
user = await User.findOne({ email }).select('+publicKey');
|
user = await User.findOne({ email }).select('+publicKey');
|
||||||
if (user && user?.publicKey) {
|
|
||||||
// case: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: 'Failed email magic link verification for complete account'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
const membershipOrg = await MembershipOrg.findOne({
|
||||||
inviteEmail: email,
|
inviteEmail: email,
|
||||||
@@ -238,6 +233,18 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (user && user?.publicKey) {
|
||||||
|
// case: user has already completed account
|
||||||
|
// membership can be approved and redirected to login/dashboard
|
||||||
|
membershipOrg.status = ACCEPTED;
|
||||||
|
await membershipOrg.save();
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully verified email',
|
||||||
|
user,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
// initialize user account
|
// initialize user account
|
||||||
user = await new User({
|
user = await new User({
|
||||||
@@ -2,11 +2,8 @@ import { Request, Response } from 'express';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
SITE_URL,
|
SITE_URL,
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY
|
||||||
STRIPE_PRODUCT_STARTER,
|
} from '../../config';
|
||||||
STRIPE_PRODUCT_PRO,
|
|
||||||
STRIPE_PRODUCT_CARD_AUTH
|
|
||||||
} from '../config';
|
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
|
|
||||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
@@ -18,23 +15,11 @@ import {
|
|||||||
Organization,
|
Organization,
|
||||||
Workspace,
|
Workspace,
|
||||||
IncidentContactOrg
|
IncidentContactOrg
|
||||||
} from '../models';
|
} from '../../models';
|
||||||
import { createOrganization as create } from '../helpers/organization';
|
import { createOrganization as create } from '../../helpers/organization';
|
||||||
import { addMembershipsOrg } from '../helpers/membershipOrg';
|
import { addMembershipsOrg } from '../../helpers/membershipOrg';
|
||||||
import { OWNER, ACCEPTED } from '../variables';
|
import { OWNER, ACCEPTED } from '../../variables';
|
||||||
|
|
||||||
const productToPriceMap = {
|
|
||||||
starter: STRIPE_PRODUCT_STARTER,
|
|
||||||
pro: STRIPE_PRODUCT_PRO,
|
|
||||||
cardAuth: STRIPE_PRODUCT_CARD_AUTH
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return organizations that user is part of
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizations = async (req: Request, res: Response) => {
|
export const getOrganizations = async (req: Request, res: Response) => {
|
||||||
let organizations;
|
let organizations;
|
||||||
try {
|
try {
|
||||||
@@ -346,7 +331,6 @@ export const createOrganizationPortalSession = async (
|
|||||||
|
|
||||||
if (paymentMethods.data.length < 1) {
|
if (paymentMethods.data.length < 1) {
|
||||||
// case: no payment method on file
|
// case: no payment method on file
|
||||||
productToPriceMap['cardAuth'];
|
|
||||||
session = await stripe.checkout.sessions.create({
|
session = await stripe.checkout.sessions.create({
|
||||||
customer: req.membershipOrg.organization.customerId,
|
customer: req.membershipOrg.organization.customerId,
|
||||||
mode: 'setup',
|
mode: 'setup',
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
// 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, Token, BackupPrivateKey } from '../models';
|
import { User, Token, BackupPrivateKey } from '../../models';
|
||||||
import { checkEmailVerification } from '../helpers/signup';
|
import { checkEmailVerification } from '../../helpers/signup';
|
||||||
import { createToken } from '../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { sendMail } from '../helpers/nodemailer';
|
import { sendMail } from '../../helpers/nodemailer';
|
||||||
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config';
|
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
|
||||||
|
import LoginSRPDetail from '../../models/LoginSRPDetail';
|
||||||
const clientPublicKeys: any = {};
|
import { BadRequestError } from '../../utils/errors';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Password reset step 1: Send email verification link to email [email]
|
* Password reset step 1: Send email verification link to email [email]
|
||||||
@@ -31,7 +32,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
|||||||
error: 'Failed to send email verification for password reset'
|
error: 'Failed to send email verification for password reset'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const token = crypto.randomBytes(16).toString('hex');
|
const token = crypto.randomBytes(16).toString('hex');
|
||||||
|
|
||||||
await Token.findOneAndUpdate(
|
await Token.findOneAndUpdate(
|
||||||
@@ -43,7 +44,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
|||||||
},
|
},
|
||||||
{ upsert: true, new: true }
|
{ upsert: true, new: true }
|
||||||
);
|
);
|
||||||
|
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'passwordReset.handlebars',
|
template: 'passwordReset.handlebars',
|
||||||
subjectLine: 'Infisical password reset',
|
subjectLine: 'Infisical password reset',
|
||||||
@@ -54,15 +55,15 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
|
|||||||
callback_url: SITE_URL + '/password-reset'
|
callback_url: SITE_URL + '/password-reset'
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to send email for account recovery'
|
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: `Sent an email for account recovery to ${email}`
|
||||||
});
|
});
|
||||||
@@ -78,7 +79,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
|||||||
let user, token;
|
let user, token;
|
||||||
try {
|
try {
|
||||||
const { email, code } = req.body;
|
const { email, code } = req.body;
|
||||||
|
|
||||||
user = await User.findOne({ email }).select('+publicKey');
|
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
|
||||||
@@ -92,7 +93,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
|||||||
email,
|
email,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
// generate temporary password-reset token
|
// generate temporary password-reset token
|
||||||
token = createToken({
|
token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
@@ -106,7 +107,7 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
|||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed email verification for password reset'
|
message: 'Failed email verification for password reset'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
@@ -129,7 +130,7 @@ export const srp1 = async (req: Request, res: Response) => {
|
|||||||
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();
|
||||||
@@ -138,13 +139,15 @@ export const srp1 = async (req: Request, res: Response) => {
|
|||||||
salt: user.salt,
|
salt: user.salt,
|
||||||
verifier: user.verifier
|
verifier: user.verifier
|
||||||
},
|
},
|
||||||
() => {
|
async () => {
|
||||||
// generate server-side public key
|
// generate server-side public key
|
||||||
const serverPublicKey = server.getPublicKey();
|
const serverPublicKey = server.getPublicKey();
|
||||||
clientPublicKeys[req.user.email] = {
|
|
||||||
clientPublicKey,
|
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
email: req.user.email,
|
||||||
};
|
clientPublicKey: clientPublicKey,
|
||||||
|
serverBInt: bigintConversion.bigintToBuf(server.bInt),
|
||||||
|
}, { upsert: true, returnNewDocument: false })
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serverPublicKey,
|
serverPublicKey,
|
||||||
@@ -179,17 +182,21 @@ export const changePassword = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
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 })
|
||||||
|
|
||||||
|
if (!loginSRPDetailFromDB) {
|
||||||
|
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: clientPublicKeys[req.user.email].serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(
|
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
||||||
clientPublicKeys[req.user.email].clientPublicKey
|
|
||||||
);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
if (server.checkClientProof(clientProof)) {
|
if (server.checkClientProof(clientProof)) {
|
||||||
@@ -248,16 +255,22 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
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 })
|
||||||
|
|
||||||
|
if (!loginSRPDetailFromDB) {
|
||||||
|
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: clientPublicKeys[req.user.email].serverBInt
|
b: loginSRPDetailFromDB.serverBInt
|
||||||
},
|
},
|
||||||
async () => {
|
async () => {
|
||||||
server.setClientPublicKey(
|
server.setClientPublicKey(
|
||||||
clientPublicKeys[req.user.email].clientPublicKey
|
loginSRPDetailFromDB.clientPublicKey
|
||||||
);
|
);
|
||||||
|
|
||||||
// compare server and client shared keys
|
// compare server and client shared keys
|
||||||
@@ -310,16 +323,16 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
|||||||
backupPrivateKey = await BackupPrivateKey.findOne({
|
backupPrivateKey = await BackupPrivateKey.findOne({
|
||||||
user: req.user._id
|
user: req.user._id
|
||||||
}).select('+encryptedPrivateKey +iv +tag');
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
if (!backupPrivateKey) throw new Error('Failed to find backup private key');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email});
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get backup private key'
|
message: 'Failed to get backup private key'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
backupPrivateKey
|
backupPrivateKey
|
||||||
});
|
});
|
||||||
@@ -347,15 +360,15 @@ export const resetPassword = async (req: Request, res: Response) => {
|
|||||||
{
|
{
|
||||||
new: true
|
new: true
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email});
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get backup private key'
|
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,15 +1,15 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Key } from '../models';
|
import { Key, Secret } from '../../models';
|
||||||
import {
|
import {
|
||||||
pushSecrets as push,
|
v1PushSecrets as push,
|
||||||
pullSecrets as pull,
|
pullSecrets as pull,
|
||||||
reformatPullSecrets
|
reformatPullSecrets
|
||||||
} from '../helpers/secret';
|
} from '../../helpers/secret';
|
||||||
import { pushKeys } from '../helpers/key';
|
import { pushKeys } from '../../helpers/key';
|
||||||
import { ENV_SET } from '../variables';
|
import { eventPushSecrets } from '../../events';
|
||||||
|
import { EventService } from '../../services';
|
||||||
import { postHogClient } from '../services';
|
import { postHogClient } from '../../services';
|
||||||
|
|
||||||
interface PushSecret {
|
interface PushSecret {
|
||||||
ciphertextKey: string;
|
ciphertextKey: string;
|
||||||
@@ -20,6 +20,10 @@ interface PushSecret {
|
|||||||
ivValue: string;
|
ivValue: string;
|
||||||
tagValue: string;
|
tagValue: string;
|
||||||
hashValue: string;
|
hashValue: string;
|
||||||
|
ciphertextComment: string;
|
||||||
|
ivComment: string;
|
||||||
|
tagComment: string;
|
||||||
|
hashComment: string;
|
||||||
type: 'shared' | 'personal';
|
type: 'shared' | 'personal';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +43,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,7 +65,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pushed',
|
event: 'secrets pushed',
|
||||||
@@ -74,6 +80,13 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// trigger event - push secrets
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -103,14 +116,17 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets = await pull({
|
secrets = await pull({
|
||||||
userId: req.user._id.toString(),
|
userId: req.user._id.toString(),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment,
|
||||||
|
channel: channel ? channel : 'cli',
|
||||||
|
ipAddress: req.ip
|
||||||
});
|
});
|
||||||
|
|
||||||
key = await Key.findOne({
|
key = await Key.findOne({
|
||||||
@@ -160,9 +176,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||||
// get (encrypted) secrets from workspace with id [workspaceId]
|
|
||||||
// service token route
|
|
||||||
|
|
||||||
let secrets;
|
let secrets;
|
||||||
let key;
|
let key;
|
||||||
try {
|
try {
|
||||||
@@ -171,14 +184,17 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets = await pull({
|
secrets = await pull({
|
||||||
userId: req.serviceToken.user._id.toString(),
|
userId: req.serviceToken.user._id.toString(),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment,
|
||||||
|
channel: 'cli',
|
||||||
|
ipAddress: req.ip
|
||||||
});
|
});
|
||||||
|
|
||||||
key = {
|
key = {
|
||||||
@@ -192,7 +208,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
// capture secrets pushed event in production
|
// capture secrets pulled event in production
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
distinctId: req.serviceToken.user.email,
|
distinctId: req.serviceToken.user.email,
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
@@ -216,4 +232,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
secrets: reformatPullSecrets({ secrets }),
|
secrets: reformatPullSecrets({ secrets }),
|
||||||
key
|
key
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { ServiceToken } from '../models';
|
import { ServiceToken } from '../../models';
|
||||||
import { createToken } from '../helpers/auth';
|
import { createToken } from '../../helpers/auth';
|
||||||
import { ENV_SET } from '../variables';
|
import { JWT_SERVICE_SECRET } from '../../config';
|
||||||
import { JWT_SERVICE_SECRET } from '../config';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return service token on request
|
* Return service token on request
|
||||||
@@ -11,7 +10,6 @@ import { JWT_SERVICE_SECRET } from '../config';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getServiceToken = async (req: Request, res: Response) => {
|
export const getServiceToken = async (req: Request, res: Response) => {
|
||||||
// get service token
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
serviceToken: req.serviceToken
|
serviceToken: req.serviceToken
|
||||||
});
|
});
|
||||||
@@ -37,7 +35,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
|||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// validate environment
|
// validate environment
|
||||||
if (!ENV_SET.has(environment)) {
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
throw new Error('Failed to validate environment');
|
throw new Error('Failed to validate environment');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,7 +57,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
token = createToken({
|
token = createToken({
|
||||||
payload: {
|
payload: {
|
||||||
serviceTokenId: serviceToken._id.toString()
|
serviceTokenId: serviceToken._id.toString(),
|
||||||
|
workspaceId
|
||||||
},
|
},
|
||||||
expiresIn: expiresIn,
|
expiresIn: expiresIn,
|
||||||
secret: JWT_SERVICE_SECRET
|
secret: JWT_SERVICE_SECRET
|
||||||
@@ -72,4 +72,4 @@ export const createServiceToken = async (req: Request, res: Response) => {
|
|||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
token
|
token
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config';
|
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
|
||||||
import { User, MembershipOrg } from '../models';
|
import { User, MembershipOrg } from '../../models';
|
||||||
import { completeAccount } from '../helpers/user';
|
import { completeAccount } from '../../helpers/user';
|
||||||
import {
|
import {
|
||||||
sendEmailVerification,
|
sendEmailVerification,
|
||||||
checkEmailVerification,
|
checkEmailVerification,
|
||||||
initializeDefaultOrg
|
initializeDefaultOrg
|
||||||
} from '../helpers/signup';
|
} from '../../helpers/signup';
|
||||||
import { issueTokens, createToken } from '../helpers/auth';
|
import { issueTokens, createToken } from '../../helpers/auth';
|
||||||
import { INVITED, ACCEPTED } from '../variables';
|
import { INVITED, ACCEPTED } from '../../variables';
|
||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
||||||
@@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
token = tokens.token;
|
token = tokens.token;
|
||||||
refreshToken = tokens.refreshToken;
|
refreshToken = tokens.refreshToken;
|
||||||
|
|
||||||
|
// sending a welcome email to new users
|
||||||
|
if (process.env.LOOPS_API_KEY) {
|
||||||
|
await axios.post("https://app.loops.so/api/v1/events/send", {
|
||||||
|
"email": email,
|
||||||
|
"eventName": "Sign Up",
|
||||||
|
"firstName": firstName,
|
||||||
|
"lastName": lastName
|
||||||
|
}, {
|
||||||
|
headers: {
|
||||||
|
"Accept": "application/json",
|
||||||
|
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { UserAction } from '../models';
|
import { UserAction } from '../../models';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add user action [action]
|
* Add user action [action]
|
||||||
@@ -7,14 +7,15 @@ import {
|
|||||||
Integration,
|
Integration,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
IUser,
|
IUser,
|
||||||
ServiceToken
|
ServiceToken,
|
||||||
} from '../models';
|
ServiceTokenData
|
||||||
|
} from '../../models';
|
||||||
import {
|
import {
|
||||||
createWorkspace as create,
|
createWorkspace as create,
|
||||||
deleteWorkspace as deleteWork
|
deleteWorkspace as deleteWork
|
||||||
} from '../helpers/workspace';
|
} from '../../helpers/workspace';
|
||||||
import { addMemberships } from '../helpers/membership';
|
import { addMemberships } from '../../helpers/membership';
|
||||||
import { ADMIN, COMPLETED, GRANTED } from '../variables';
|
import { ADMIN } from '../../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return public keys of members of workspace with id [workspaceId]
|
* Return public keys of members of workspace with id [workspaceId]
|
||||||
@@ -32,13 +33,12 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
|||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
}).populate<{ user: IUser }>('user', 'publicKey')
|
}).populate<{ user: IUser }>('user', 'publicKey')
|
||||||
)
|
)
|
||||||
.filter((m) => m.status === COMPLETED || m.status === GRANTED)
|
.map((member) => {
|
||||||
.map((member) => {
|
return {
|
||||||
return {
|
publicKey: member.user.publicKey,
|
||||||
publicKey: member.user.publicKey,
|
userId: member.user._id
|
||||||
userId: member.user._id
|
};
|
||||||
};
|
});
|
||||||
});
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -168,8 +168,7 @@ export const createWorkspace = async (req: Request, res: Response) => {
|
|||||||
await addMemberships({
|
await addMemberships({
|
||||||
userIds: [req.user._id],
|
userIds: [req.user._id],
|
||||||
workspaceId: workspace._id.toString(),
|
workspaceId: workspace._id.toString(),
|
||||||
roles: [ADMIN],
|
roles: [ADMIN]
|
||||||
statuses: [GRANTED]
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
@@ -318,7 +317,7 @@ export const getWorkspaceServiceTokens = async (
|
|||||||
let serviceTokens;
|
let serviceTokens;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
// ?? FIX.
|
||||||
serviceTokens = await ServiceToken.find({
|
serviceTokens = await ServiceToken.find({
|
||||||
user: req.user._id,
|
user: req.user._id,
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
105
backend/src/controllers/v2/apiKeyDataController.ts
Normal file
105
backend/src/controllers/v2/apiKeyDataController.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import {
|
||||||
|
APIKeyData
|
||||||
|
} from '../../models';
|
||||||
|
import {
|
||||||
|
SALT_ROUNDS
|
||||||
|
} from '../../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return API key data for user with id [req.user_id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getAPIKeyData = async (req: Request, res: Response) => {
|
||||||
|
let apiKeyData;
|
||||||
|
try {
|
||||||
|
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({
|
||||||
|
apiKeyData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new API key data for user with id [req.user._id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createAPIKeyData = async (req: Request, res: Response) => {
|
||||||
|
let apiKey, apiKeyData;
|
||||||
|
try {
|
||||||
|
const { name, expiresIn } = req.body;
|
||||||
|
|
||||||
|
const secret = crypto.randomBytes(16).toString('hex');
|
||||||
|
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
|
||||||
|
|
||||||
|
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'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
apiKey,
|
||||||
|
apiKeyData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete API key data with id [apiKeyDataId].
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
||||||
|
let apiKeyData;
|
||||||
|
try {
|
||||||
|
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({
|
||||||
|
apiKeyData
|
||||||
|
});
|
||||||
|
}
|
||||||
261
backend/src/controllers/v2/environmentController.ts
Normal file
261
backend/src/controllers/v2/environmentController.ts
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Secret,
|
||||||
|
ServiceToken,
|
||||||
|
Workspace,
|
||||||
|
Integration,
|
||||||
|
ServiceTokenData,
|
||||||
|
Membership,
|
||||||
|
} from '../../models';
|
||||||
|
import { SecretVersion } from '../../ee/models';
|
||||||
|
import { BadRequestError } from '../../utils/errors';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new workspace environment named [environmentName] under workspace with id
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createWorkspaceEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentName, environmentSlug } = req.body;
|
||||||
|
try {
|
||||||
|
const workspace = await Workspace.findById(workspaceId).exec();
|
||||||
|
if (
|
||||||
|
!workspace ||
|
||||||
|
workspace?.environments.find(
|
||||||
|
({ name, slug }) => slug === environmentSlug || environmentName === name
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully created new environment',
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: {
|
||||||
|
name: environmentName,
|
||||||
|
slug: environmentSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
|
||||||
|
* Old slug [oldEnvironmentSlug] must be provided
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const renameWorkspaceEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
|
||||||
|
try {
|
||||||
|
// user should pass both new slug and env name
|
||||||
|
if (!environmentSlug || !environmentName) {
|
||||||
|
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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully update environment',
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: {
|
||||||
|
name: environmentName,
|
||||||
|
slug: environmentSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteWorkspaceEnvironment = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const { environmentSlug } = req.body;
|
||||||
|
try {
|
||||||
|
// 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 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',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
message: 'Successfully deleted environment',
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: environmentSlug,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const getAllAccessibleEnvironmentsOfWorkspace = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
const workspacesUserIsMemberOf = await Membership.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
user: req.user
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!workspacesUserIsMemberOf) {
|
||||||
|
throw BadRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const accessibleEnvironments: any = []
|
||||||
|
const deniedPermission = workspacesUserIsMemberOf.deniedPermissions
|
||||||
|
|
||||||
|
const relatedWorkspace = await Workspace.findById(workspaceId)
|
||||||
|
if (!relatedWorkspace) {
|
||||||
|
throw BadRequestError()
|
||||||
|
}
|
||||||
|
relatedWorkspace.environments.forEach(environment => {
|
||||||
|
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
|
||||||
|
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
|
||||||
|
if (isReadBlocked) {
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
accessibleEnvironments.push({
|
||||||
|
name: environment.name,
|
||||||
|
slug: environment.slug,
|
||||||
|
isWriteDenied: isWriteBlocked
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
res.json({ accessibleEnvironments })
|
||||||
|
};
|
||||||
19
backend/src/controllers/v2/index.ts
Normal file
19
backend/src/controllers/v2/index.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import * as usersController from './usersController';
|
||||||
|
import * as organizationsController from './organizationsController';
|
||||||
|
import * as workspaceController from './workspaceController';
|
||||||
|
import * as serviceTokenDataController from './serviceTokenDataController';
|
||||||
|
import * as apiKeyDataController from './apiKeyDataController';
|
||||||
|
import * as secretController from './secretController';
|
||||||
|
import * as secretsController from './secretsController';
|
||||||
|
import * as environmentController from './environmentController';
|
||||||
|
|
||||||
|
export {
|
||||||
|
usersController,
|
||||||
|
organizationsController,
|
||||||
|
workspaceController,
|
||||||
|
serviceTokenDataController,
|
||||||
|
apiKeyDataController,
|
||||||
|
secretController,
|
||||||
|
secretsController,
|
||||||
|
environmentController
|
||||||
|
}
|
||||||
296
backend/src/controllers/v2/organizationsController.ts
Normal file
296
backend/src/controllers/v2/organizationsController.ts
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
MembershipOrg,
|
||||||
|
Membership,
|
||||||
|
Workspace
|
||||||
|
} from '../../models';
|
||||||
|
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
|
||||||
|
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return memberships for organization with id [organizationId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getOrganizationMemberships = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return organization memberships'
|
||||||
|
#swagger.description = 'Return organization memberships'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['organizationId'] = {
|
||||||
|
"description": "ID of organization",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"memberships": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/MembershipOrg"
|
||||||
|
},
|
||||||
|
"description": "Memberships of organization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let memberships;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
|
||||||
|
memberships = await MembershipOrg.find({
|
||||||
|
organization: organizationId
|
||||||
|
}).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({
|
||||||
|
memberships
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update role of membership with id [membershipId] to role [role]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const updateOrganizationMembership = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Update organization membership'
|
||||||
|
#swagger.description = 'Update organization membership'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['organizationId'] = {
|
||||||
|
"description": "ID of organization",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['membershipId'] = {
|
||||||
|
"description": "ID of organization membership to update",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Role of organization membership - either owner, admin, or member",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"membership": {
|
||||||
|
$ref: "#/components/schemas/MembershipOrg",
|
||||||
|
"description": "Updated organization membership"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
const { membershipId } = req.params;
|
||||||
|
const { role } = req.body;
|
||||||
|
|
||||||
|
membership = await MembershipOrg.findByIdAndUpdate(
|
||||||
|
membershipId,
|
||||||
|
{
|
||||||
|
role
|
||||||
|
}, {
|
||||||
|
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({
|
||||||
|
membership
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete organization membership with id [membershipId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Delete organization membership'
|
||||||
|
#swagger.description = 'Delete organization membership'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['organizationId'] = {
|
||||||
|
"description": "ID of organization",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['membershipId'] = {
|
||||||
|
"description": "ID of organization membership to delete",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"membership": {
|
||||||
|
$ref: "#/components/schemas/MembershipOrg",
|
||||||
|
"description": "Deleted organization membership"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
const { membershipId } = req.params;
|
||||||
|
|
||||||
|
// delete organization membership
|
||||||
|
membership = await deleteMembershipOrg({
|
||||||
|
membershipOrgId: membershipId
|
||||||
|
});
|
||||||
|
|
||||||
|
await updateSubscriptionOrgQuantity({
|
||||||
|
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({
|
||||||
|
membership
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return workspaces for organization with id [organizationId] that user has
|
||||||
|
* access to
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return projects in organization that user is part of'
|
||||||
|
#swagger.description = 'Return projects in organization that user is part of'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['organizationId'] = {
|
||||||
|
"description": "ID of organization",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaces": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Project"
|
||||||
|
},
|
||||||
|
"description": "Projects of organization"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let workspaces;
|
||||||
|
try {
|
||||||
|
const { organizationId } = req.params;
|
||||||
|
|
||||||
|
const workspacesSet = new Set(
|
||||||
|
(
|
||||||
|
await Workspace.find(
|
||||||
|
{
|
||||||
|
organization: organizationId
|
||||||
|
},
|
||||||
|
'_id'
|
||||||
|
)
|
||||||
|
).map((w) => w._id.toString())
|
||||||
|
);
|
||||||
|
|
||||||
|
workspaces = (
|
||||||
|
await Membership.find({
|
||||||
|
user: req.user._id
|
||||||
|
}).populate('workspace')
|
||||||
|
)
|
||||||
|
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
||||||
|
.map((m) => m.workspace);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get organization workspaces'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
workspaces
|
||||||
|
});
|
||||||
|
}
|
||||||
401
backend/src/controllers/v2/secretController.ts
Normal file
401
backend/src/controllers/v2/secretController.ts
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
import to from "await-to-js";
|
||||||
|
import { Request, Response } from "express";
|
||||||
|
import mongoose, { Types } from "mongoose";
|
||||||
|
import Secret, { ISecret } from "../../models/secret";
|
||||||
|
import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret";
|
||||||
|
const { ValidationError } = mongoose.Error;
|
||||||
|
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
|
||||||
|
import { AnyBulkWriteOperation } from 'mongodb';
|
||||||
|
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
|
||||||
|
import { postHogClient } from '../../services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create secret for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecret = async (req: Request, res: Response) => {
|
||||||
|
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
||||||
|
const { workspaceId, environment } = req.params
|
||||||
|
const sanitizedSecret: SanitizedSecretForCreate = {
|
||||||
|
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
||||||
|
secretKeyIV: secretToCreate.secretKeyIV,
|
||||||
|
secretKeyTag: secretToCreate.secretKeyTag,
|
||||||
|
secretKeyHash: secretToCreate.secretKeyHash,
|
||||||
|
secretValueCiphertext: secretToCreate.secretValueCiphertext,
|
||||||
|
secretValueIV: secretToCreate.secretValueIV,
|
||||||
|
secretValueTag: secretToCreate.secretValueTag,
|
||||||
|
secretValueHash: secretToCreate.secretValueHash,
|
||||||
|
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
|
||||||
|
secretCommentIV: secretToCreate.secretCommentIV,
|
||||||
|
secretCommentTag: secretToCreate.secretCommentTag,
|
||||||
|
secretCommentHash: secretToCreate.secretCommentHash,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type: secretToCreate.type,
|
||||||
|
user: new Types.ObjectId(req.user._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
throw RouteValidationError({ message: error.message, stack: error.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets added',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
secret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create many secrets for workspace wiht id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
|
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
||||||
|
const { workspaceId, environment } = req.params
|
||||||
|
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
|
||||||
|
|
||||||
|
secretsToCreate.forEach(rawSecret => {
|
||||||
|
const safeUpdateFields: SanitizedSecretForCreate = {
|
||||||
|
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
|
||||||
|
secretKeyIV: rawSecret.secretKeyIV,
|
||||||
|
secretKeyTag: rawSecret.secretKeyTag,
|
||||||
|
secretKeyHash: rawSecret.secretKeyHash,
|
||||||
|
secretValueCiphertext: rawSecret.secretValueCiphertext,
|
||||||
|
secretValueIV: rawSecret.secretValueIV,
|
||||||
|
secretValueTag: rawSecret.secretValueTag,
|
||||||
|
secretValueHash: rawSecret.secretValueHash,
|
||||||
|
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
|
||||||
|
secretCommentIV: rawSecret.secretCommentIV,
|
||||||
|
secretCommentTag: rawSecret.secretCommentTag,
|
||||||
|
secretCommentHash: rawSecret.secretCommentHash,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
type: rawSecret.type,
|
||||||
|
user: new Types.ObjectId(req.user._id)
|
||||||
|
}
|
||||||
|
|
||||||
|
sanitizedSecretesToCreate.push(safeUpdateFields)
|
||||||
|
})
|
||||||
|
|
||||||
|
const [bulkCreateError, secrets] = await to(Secret.insertMany(sanitizedSecretesToCreate).then())
|
||||||
|
if (bulkCreateError) {
|
||||||
|
if (bulkCreateError instanceof ValidationError) {
|
||||||
|
throw RouteValidationError({ message: bulkCreateError.message, stack: bulkCreateError.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError({ message: "Unable to process your batch create request. Please try again", stack: bulkCreateError.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets added',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: (secretsToCreate ?? []).length,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
secrets
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete secrets in workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environmentName } = req.params
|
||||||
|
const secretIdsToDelete: string[] = req.body.secretIds
|
||||||
|
|
||||||
|
const [secretIdsUserCanDeleteError, secretIdsUserCanDelete] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||||
|
if (secretIdsUserCanDeleteError) {
|
||||||
|
throw InternalServerError({ message: `Unable to fetch secrets you own: [error=${secretIdsUserCanDeleteError.message}]` })
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
|
||||||
|
const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
|
||||||
|
|
||||||
|
let numSecretsDeleted = 0;
|
||||||
|
secretIdsToDelete.forEach(secretIdToDelete => {
|
||||||
|
if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
|
||||||
|
const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
|
||||||
|
deleteOperationsToPerform.push(deleteOperation)
|
||||||
|
numSecretsDeleted++;
|
||||||
|
} else {
|
||||||
|
throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then())
|
||||||
|
if (bulkDeleteError) {
|
||||||
|
if (bulkDeleteError instanceof ValidationError) {
|
||||||
|
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack })
|
||||||
|
}
|
||||||
|
throw InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: numSecretsDeleted,
|
||||||
|
environment: environmentName,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete secret with id [secretId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecret = async (req: Request, res: Response) => {
|
||||||
|
await Secret.findByIdAndDelete(req._secret._id)
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
workspaceId: req._secret.workspace.toString(),
|
||||||
|
environment: req._secret.environment,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
secret: req._secret
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update secrets for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environmentName } = req.params
|
||||||
|
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
||||||
|
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||||
|
if (secretIdsUserCanModifyError) {
|
||||||
|
throw InternalServerError({ message: "Unable to fetch secrets you own" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
|
||||||
|
const updateOperationsToPerform: any = []
|
||||||
|
|
||||||
|
secretsModificationsRequested.forEach(userModifiedSecret => {
|
||||||
|
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
|
||||||
|
const sanitizedSecret: SanitizedSecretModify = {
|
||||||
|
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
|
||||||
|
secretKeyIV: userModifiedSecret.secretKeyIV,
|
||||||
|
secretKeyTag: userModifiedSecret.secretKeyTag,
|
||||||
|
secretKeyHash: userModifiedSecret.secretKeyHash,
|
||||||
|
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
|
||||||
|
secretValueIV: userModifiedSecret.secretValueIV,
|
||||||
|
secretValueTag: userModifiedSecret.secretValueTag,
|
||||||
|
secretValueHash: userModifiedSecret.secretValueHash,
|
||||||
|
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
|
||||||
|
secretCommentIV: userModifiedSecret.secretCommentIV,
|
||||||
|
secretCommentTag: userModifiedSecret.secretCommentTag,
|
||||||
|
secretCommentHash: userModifiedSecret.secretCommentHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } }
|
||||||
|
updateOperationsToPerform.push(updateOperation)
|
||||||
|
} else {
|
||||||
|
throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const [bulkModificationInfoError, bulkModificationInfo] = await to(Secret.bulkWrite(updateOperationsToPerform).then())
|
||||||
|
if (bulkModificationInfoError) {
|
||||||
|
if (bulkModificationInfoError instanceof ValidationError) {
|
||||||
|
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkModificationInfoError.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
throw InternalServerError()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
||||||
|
environment: environmentName,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a secret within workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateSecret = async (req: Request, res: Response) => {
|
||||||
|
const { workspaceId, environmentName } = req.params
|
||||||
|
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
||||||
|
|
||||||
|
const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
|
||||||
|
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
|
||||||
|
throw BadRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitizedSecret: SanitizedSecretModify = {
|
||||||
|
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
|
||||||
|
secretKeyIV: secretModificationsRequested.secretKeyIV,
|
||||||
|
secretKeyTag: secretModificationsRequested.secretKeyTag,
|
||||||
|
secretKeyHash: secretModificationsRequested.secretKeyHash,
|
||||||
|
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
|
||||||
|
secretValueIV: secretModificationsRequested.secretValueIV,
|
||||||
|
secretValueTag: secretModificationsRequested.secretValueTag,
|
||||||
|
secretValueHash: secretModificationsRequested.secretValueHash,
|
||||||
|
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
|
||||||
|
secretCommentIV: secretModificationsRequested.secretCommentIV,
|
||||||
|
secretCommentTag: secretModificationsRequested.secretCommentTag,
|
||||||
|
secretCommentHash: secretModificationsRequested.secretCommentHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
|
||||||
|
if (error instanceof ValidationError) {
|
||||||
|
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: 1,
|
||||||
|
environment: environmentName,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send(singleModificationUpdate)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secrets for workspace with id [workspaceId], environment [environment] and user
|
||||||
|
* with id [req.user._id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
|
const { environment } = req.query;
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
|
||||||
|
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
|
||||||
|
if (req.user) {
|
||||||
|
userId = req.user._id;
|
||||||
|
userEmail = req.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.serviceTokenData) {
|
||||||
|
userId = req.serviceTokenData.user._id
|
||||||
|
userEmail = req.serviceTokenData.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [err, secrets] = await to(Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
$or: [{ user: userId }, { user: { $exists: false } }],
|
||||||
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
|
}
|
||||||
|
).then())
|
||||||
|
|
||||||
|
if (err) {
|
||||||
|
throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pulled',
|
||||||
|
distinctId: userEmail,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: (secrets ?? []).length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json(secrets)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret with id [secretId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecret = async (req: Request, res: Response) => {
|
||||||
|
// if (postHogClient) {
|
||||||
|
// postHogClient.capture({
|
||||||
|
// event: 'secrets pulled',
|
||||||
|
// distinctId: req.user.email,
|
||||||
|
// properties: {
|
||||||
|
// numberOfSecrets: 1,
|
||||||
|
// workspaceId: req._secret.workspace.toString(),
|
||||||
|
// environment: req._secret.environment,
|
||||||
|
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
||||||
|
// userAgent: req.headers?.['user-agent']
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret: req._secret
|
||||||
|
});
|
||||||
|
}
|
||||||
673
backend/src/controllers/v2/secretsController.ts
Normal file
673
backend/src/controllers/v2/secretsController.ts
Normal file
@@ -0,0 +1,673 @@
|
|||||||
|
import to from 'await-to-js';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { ISecret, Membership, Secret, Workspace } from '../../models';
|
||||||
|
import {
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
SECRET_SHARED,
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
|
} from '../../variables';
|
||||||
|
import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
|
||||||
|
import { EventService } from '../../services';
|
||||||
|
import { eventPushSecrets } from '../../events';
|
||||||
|
import { EESecretService, EELogService } from '../../ee/services';
|
||||||
|
import { postHogClient } from '../../services';
|
||||||
|
import { getChannelFromUserAgent } from '../../utils/posthog';
|
||||||
|
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
|
||||||
|
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create secret(s) for workspace with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const createSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Create new secret(s)'
|
||||||
|
#swagger.description = 'Create one or many secrets for a given project and environment.'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"workspaceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID of project",
|
||||||
|
},
|
||||||
|
"environment": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Environment within project"
|
||||||
|
},
|
||||||
|
"secrets": {
|
||||||
|
$ref: "#/components/schemas/CreateSecret",
|
||||||
|
"description": "Secret(s) to create - object or array of objects"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Newly-created secrets for the given project and environment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||||
|
const { workspaceId, environment } = req.body;
|
||||||
|
|
||||||
|
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||||
|
}
|
||||||
|
|
||||||
|
let toAdd;
|
||||||
|
if (Array.isArray(req.body.secrets)) {
|
||||||
|
// case: create multiple secrets
|
||||||
|
toAdd = req.body.secrets;
|
||||||
|
} else if (typeof req.body.secrets === 'object') {
|
||||||
|
// case: create 1 secret
|
||||||
|
toAdd = [req.body.secrets];
|
||||||
|
}
|
||||||
|
|
||||||
|
const newSecrets = await Secret.insertMany(
|
||||||
|
toAdd.map(({
|
||||||
|
type,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
}: {
|
||||||
|
type: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
}) => {
|
||||||
|
return ({
|
||||||
|
version: 1,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
type,
|
||||||
|
user: type === SECRET_PERSONAL ? req.user : undefined,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
setTimeout(async () => {
|
||||||
|
// trigger event - push secrets
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
// (EE) add secret versions for new secrets
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: newSecrets.map(({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
const addAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_ADD_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
secretIds: newSecrets.map((n) => n._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
addAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId,
|
||||||
|
actions: [addAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets added',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: toAdd.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: newSecrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret(s) for workspace with id [workspaceId], environment [environment] and user
|
||||||
|
* with id [req.user._id]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Read secrets'
|
||||||
|
#swagger.description = 'Read secrets from a project and environment'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['environment'] = {
|
||||||
|
"description": "Environment within project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Secrets for the given project and environment"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const { workspaceId, environment } = req.query;
|
||||||
|
|
||||||
|
let userId = "" // used for getting personal secrets for user
|
||||||
|
let userEmail = "" // used for posthog
|
||||||
|
if (req.user) {
|
||||||
|
userId = req.user._id;
|
||||||
|
userEmail = req.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.serviceTokenData) {
|
||||||
|
userId = req.serviceTokenData.user._id
|
||||||
|
userEmail = req.serviceTokenData.user.email;
|
||||||
|
}
|
||||||
|
|
||||||
|
// none service token case as service tokens are already scoped
|
||||||
|
if (!req.serviceTokenData) {
|
||||||
|
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const [err, secrets] = await to(Secret.find(
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
$or: [
|
||||||
|
{ user: userId },
|
||||||
|
{ user: { $exists: false } }
|
||||||
|
],
|
||||||
|
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
||||||
|
}
|
||||||
|
).then())
|
||||||
|
|
||||||
|
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
|
||||||
|
|
||||||
|
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||||
|
|
||||||
|
const readAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_READ_SECRETS,
|
||||||
|
userId: userId,
|
||||||
|
workspaceId: workspaceId as string,
|
||||||
|
secretIds: secrets.map((n: any) => n._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
readAction && await EELogService.createLog({
|
||||||
|
userId: userId,
|
||||||
|
workspaceId: workspaceId as string,
|
||||||
|
actions: [readAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets pulled',
|
||||||
|
distinctId: userEmail,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update secret(s)
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const updateSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Update secret(s)'
|
||||||
|
#swagger.description = 'Update secret(s)'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
$ref: "#/components/schemas/UpdateSecret",
|
||||||
|
"description": "Secret(s) to update - object or array of objects"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Updated secrets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
|
||||||
|
|
||||||
|
// TODO: move type
|
||||||
|
interface PatchSecret {
|
||||||
|
id: string;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretCommentCiphertext: string;
|
||||||
|
secretCommentIV: string;
|
||||||
|
secretCommentTag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
|
||||||
|
const {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} = secret;
|
||||||
|
|
||||||
|
return ({
|
||||||
|
updateOne: {
|
||||||
|
filter: { _id: new Types.ObjectId(secret.id) },
|
||||||
|
update: {
|
||||||
|
$inc: {
|
||||||
|
version: 1
|
||||||
|
},
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
...((
|
||||||
|
secretCommentCiphertext &&
|
||||||
|
secretCommentIV &&
|
||||||
|
secretCommentTag
|
||||||
|
) ? {
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag
|
||||||
|
} : {}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await Secret.bulkWrite(updateOperationsToPerform);
|
||||||
|
|
||||||
|
const secretModificationsBySecretId: { [key: string]: PatchSecret } = {};
|
||||||
|
req.body.secrets.forEach((secret: PatchSecret) => {
|
||||||
|
secretModificationsBySecretId[secret.id] = secret;
|
||||||
|
});
|
||||||
|
|
||||||
|
const ListOfSecretsBeforeModifications = req.secrets
|
||||||
|
const secretVersions = {
|
||||||
|
secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => {
|
||||||
|
const {
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag,
|
||||||
|
} = secretModificationsBySecretId[secret._id.toString()]
|
||||||
|
|
||||||
|
return ({
|
||||||
|
secret: secret._id,
|
||||||
|
version: secret.version + 1,
|
||||||
|
workspace: secret.workspace,
|
||||||
|
type: secret.type,
|
||||||
|
environment: secret.environment,
|
||||||
|
secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext,
|
||||||
|
secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV,
|
||||||
|
secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag,
|
||||||
|
secretValueCiphertext: secretValueCiphertext ? secretValueCiphertext : secret.secretValueCiphertext,
|
||||||
|
secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV,
|
||||||
|
secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag,
|
||||||
|
secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext,
|
||||||
|
secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV,
|
||||||
|
secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await EESecretService.addSecretVersions(secretVersions);
|
||||||
|
|
||||||
|
|
||||||
|
// group secrets into workspaces so updated secrets can
|
||||||
|
// be logged and snapshotted separately for each workspace
|
||||||
|
const workspaceSecretObj: any = {};
|
||||||
|
req.secrets.forEach((s: any) => {
|
||||||
|
if (s.workspace.toString() in workspaceSecretObj) {
|
||||||
|
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||||
|
} else {
|
||||||
|
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||||
|
// trigger event - push secrets
|
||||||
|
setTimeout(async () => {
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}, 10000);
|
||||||
|
|
||||||
|
const updateAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_UPDATE_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
updateAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
actions: [updateAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets modified',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
|
workspaceId: key,
|
||||||
|
channel: channel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: await Secret.find({
|
||||||
|
_id: {
|
||||||
|
$in: req.secrets.map((secret: ISecret) => secret._id)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete secret(s) with id [workspaceId] and environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const deleteSecrets = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Delete secret(s)'
|
||||||
|
#swagger.description = 'Delete one or many secrets by their ID(s)'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretIds": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "ID(s) of secrets - string or array of strings"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Deleted secrets"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
const channel = getChannelFromUserAgent(req.headers['user-agent'])
|
||||||
|
const toDelete = req.secrets.map((s: any) => s._id);
|
||||||
|
|
||||||
|
await Secret.deleteMany({
|
||||||
|
_id: {
|
||||||
|
$in: toDelete
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await EESecretService.markDeletedSecretVersions({
|
||||||
|
secretIds: toDelete
|
||||||
|
});
|
||||||
|
|
||||||
|
// group secrets into workspaces so deleted secrets can
|
||||||
|
// be logged and snapshotted separately for each workspace
|
||||||
|
const workspaceSecretObj: any = {};
|
||||||
|
req.secrets.forEach((s: any) => {
|
||||||
|
if (s.workspace.toString() in workspaceSecretObj) {
|
||||||
|
workspaceSecretObj[s.workspace.toString()].push(s);
|
||||||
|
} else {
|
||||||
|
workspaceSecretObj[s.workspace.toString()] = [s]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.keys(workspaceSecretObj).forEach(async (key) => {
|
||||||
|
// trigger event - push secrets
|
||||||
|
await EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
});
|
||||||
|
const deleteAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_DELETE_SECRETS,
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
deleteAction && await EELogService.createLog({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: key,
|
||||||
|
actions: [deleteAction],
|
||||||
|
channel,
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: key
|
||||||
|
})
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
postHogClient.capture({
|
||||||
|
event: 'secrets deleted',
|
||||||
|
distinctId: req.user.email,
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: workspaceSecretObj[key].length,
|
||||||
|
environment: workspaceSecretObj[key][0].environment,
|
||||||
|
workspaceId: key,
|
||||||
|
channel: channel,
|
||||||
|
userAgent: req.headers?.['user-agent']
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets: req.secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
114
backend/src/controllers/v2/serviceTokenDataController.ts
Normal file
114
backend/src/controllers/v2/serviceTokenDataController.ts
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
|
import {
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../../models';
|
||||||
|
import {
|
||||||
|
SALT_ROUNDS
|
||||||
|
} from '../../config';
|
||||||
|
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
|
||||||
|
import { ABILITY_READ } from '../../variables/organization';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return service token data associated with service token on request
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getServiceTokenData = async (req: Request, res: Response) => res.status(200).json(req.serviceTokenData);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create new service token data for workspace with id [workspaceId] and
|
||||||
|
* environment [environment].
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createServiceTokenData = async (req: Request, res: Response) => {
|
||||||
|
let serviceToken, serviceTokenData;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
encryptedKey,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
expiresIn
|
||||||
|
} = req.body;
|
||||||
|
|
||||||
|
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
|
||||||
|
if (!hasAccess) {
|
||||||
|
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const secret = crypto.randomBytes(16).toString('hex');
|
||||||
|
const secretHash = await bcrypt.hash(secret, SALT_ROUNDS);
|
||||||
|
|
||||||
|
const expiresAt = new Date();
|
||||||
|
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
||||||
|
|
||||||
|
serviceTokenData = await new ServiceTokenData({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
user: req.user._id,
|
||||||
|
expiresAt,
|
||||||
|
secretHash,
|
||||||
|
encryptedKey,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}).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({
|
||||||
|
serviceToken,
|
||||||
|
serviceTokenData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete service token data with id [serviceTokenDataId].
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
||||||
|
let serviceTokenData;
|
||||||
|
try {
|
||||||
|
const { serviceTokenDataId } = req.params;
|
||||||
|
|
||||||
|
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to delete service token data'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
serviceTokenData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function UnauthorizedRequestError(arg0: { message: string; }) {
|
||||||
|
throw new Error('Function not implemented.');
|
||||||
|
}
|
||||||
109
backend/src/controllers/v2/usersController.ts
Normal file
109
backend/src/controllers/v2/usersController.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
User,
|
||||||
|
MembershipOrg
|
||||||
|
} from '../../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return the current user.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getMe = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = "Retrieve the current user on the request"
|
||||||
|
#swagger.description = "Retrieve the current user on the request"
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"user": {
|
||||||
|
"type": "object",
|
||||||
|
$ref: "#/components/schemas/CurrentUser",
|
||||||
|
"description": "Current user on request"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
user = await User
|
||||||
|
.findById(req.user._id)
|
||||||
|
.select('+publicKey +encryptedPrivateKey +iv +tag');
|
||||||
|
} 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({
|
||||||
|
user
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return organizations that the current user is part of.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getMyOrganizations = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return organizations that current user is part of'
|
||||||
|
#swagger.description = 'Return organizations that current user is part of'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"organizations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Organization"
|
||||||
|
},
|
||||||
|
"description": "Organizations that user is part of"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let organizations;
|
||||||
|
try {
|
||||||
|
organizations = (
|
||||||
|
await MembershipOrg.find({
|
||||||
|
user: req.user._id
|
||||||
|
}).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({
|
||||||
|
organizations
|
||||||
|
});
|
||||||
|
}
|
||||||
470
backend/src/controllers/v2/workspaceController.ts
Normal file
470
backend/src/controllers/v2/workspaceController.ts
Normal file
@@ -0,0 +1,470 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
Workspace,
|
||||||
|
Secret,
|
||||||
|
Membership,
|
||||||
|
MembershipOrg,
|
||||||
|
Integration,
|
||||||
|
IntegrationAuth,
|
||||||
|
Key,
|
||||||
|
IUser,
|
||||||
|
ServiceToken,
|
||||||
|
ServiceTokenData
|
||||||
|
} from '../../models';
|
||||||
|
import {
|
||||||
|
v2PushSecrets as push,
|
||||||
|
pullSecrets as pull,
|
||||||
|
reformatPullSecrets
|
||||||
|
} from '../../helpers/secret';
|
||||||
|
import { pushKeys } from '../../helpers/key';
|
||||||
|
import { postHogClient, EventService } from '../../services';
|
||||||
|
import { eventPushSecrets } from '../../events';
|
||||||
|
|
||||||
|
interface V2PushSecret {
|
||||||
|
type: string; // personal or shared
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretKeyHash: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretValueHash: string;
|
||||||
|
secretCommentCiphertext?: string;
|
||||||
|
secretCommentIV?: string;
|
||||||
|
secretCommentTag?: string;
|
||||||
|
secretCommentHash?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
|
* for environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
||||||
|
// upload (encrypted) secrets to workspace with id [workspaceId]
|
||||||
|
try {
|
||||||
|
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
||||||
|
const { keys, environment, channel } = req.body;
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
// validate environment
|
||||||
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
|
throw new Error('Failed to validate environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitize secrets
|
||||||
|
secrets = secrets.filter(
|
||||||
|
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
|
||||||
|
);
|
||||||
|
|
||||||
|
await push({
|
||||||
|
userId: req.user._id,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secrets,
|
||||||
|
channel: channel ? channel : 'cli',
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
await pushKeys({
|
||||||
|
userId: req.user._id,
|
||||||
|
workspaceId,
|
||||||
|
keys
|
||||||
|
});
|
||||||
|
|
||||||
|
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({
|
||||||
|
message: 'Successfully uploaded workspace secrets'
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return (encrypted) secrets for workspace with id [workspaceId]
|
||||||
|
* for environment [environment]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const pullSecrets = async (req: Request, res: Response) => {
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
const environment: string = req.query.environment as string;
|
||||||
|
const channel: string = req.query.channel as string;
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
let userId;
|
||||||
|
if (req.user) {
|
||||||
|
userId = req.user._id.toString();
|
||||||
|
} else if (req.serviceTokenData) {
|
||||||
|
userId = req.serviceTokenData.user._id
|
||||||
|
}
|
||||||
|
// validate environment
|
||||||
|
const workspaceEnvs = req.membership.workspace.environments;
|
||||||
|
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
||||||
|
throw new Error('Failed to validate environment');
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets = await pull({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
channel: channel ? channel : 'cli',
|
||||||
|
ipAddress: req.ip
|
||||||
|
});
|
||||||
|
|
||||||
|
if (channel !== 'cli') {
|
||||||
|
secrets = reformatPullSecrets({ secrets });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (postHogClient) {
|
||||||
|
// capture secrets pushed event in production
|
||||||
|
postHogClient.capture({
|
||||||
|
distinctId: req.user.email,
|
||||||
|
event: 'secrets pulled',
|
||||||
|
properties: {
|
||||||
|
numberOfSecrets: secrets.length,
|
||||||
|
environment,
|
||||||
|
workspaceId,
|
||||||
|
channel: channel ? channel : 'cli'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} 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({
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return encrypted project key'
|
||||||
|
#swagger.description = 'Return encrypted project key'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/ProjectKey"
|
||||||
|
},
|
||||||
|
"description": "Encrypted project key for the given project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
key = await Key.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
receiver: req.user._id
|
||||||
|
}).populate('sender', '+publicKey');
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
export const getWorkspaceServiceTokenData = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
|
let serviceTokenData;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
serviceTokenData = await ServiceTokenData
|
||||||
|
.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
})
|
||||||
|
.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({
|
||||||
|
serviceTokenData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return memberships for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return project memberships'
|
||||||
|
#swagger.description = 'Return project memberships'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"memberships": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Membership"
|
||||||
|
},
|
||||||
|
"description": "Memberships of project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let memberships;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
memberships = await Membership.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).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({
|
||||||
|
memberships
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update role of membership with id [membershipId] to role [role]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Update project membership'
|
||||||
|
#swagger.description = 'Update project membership'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['membershipId'] = {
|
||||||
|
"description": "ID of project membership to update",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"role": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Role of membership - either admin or member",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"membership": {
|
||||||
|
$ref: "#/components/schemas/Membership",
|
||||||
|
"description": "Updated membership"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
membershipId
|
||||||
|
} = req.params;
|
||||||
|
const { role } = req.body;
|
||||||
|
|
||||||
|
membership = await Membership.findByIdAndUpdate(
|
||||||
|
membershipId,
|
||||||
|
{
|
||||||
|
role
|
||||||
|
}, {
|
||||||
|
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({
|
||||||
|
membership
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete workspace membership with id [membershipId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Delete project membership'
|
||||||
|
#swagger.description = 'Delete project membership'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['membershipId'] = {
|
||||||
|
"description": "ID of project membership to delete",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"membership": {
|
||||||
|
$ref: "#/components/schemas/Membership",
|
||||||
|
"description": "Deleted membership"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
const {
|
||||||
|
membershipId
|
||||||
|
} = req.params;
|
||||||
|
|
||||||
|
membership = await Membership.findByIdAndDelete(membershipId);
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to delete workspace membership');
|
||||||
|
|
||||||
|
await Key.deleteMany({
|
||||||
|
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({
|
||||||
|
membership
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import * as stripeController from './stripeController';
|
|
||||||
|
|
||||||
export {
|
|
||||||
stripeController
|
|
||||||
}
|
|
||||||
31
backend/src/ee/controllers/v1/actionController.ts
Normal file
31
backend/src/ee/controllers/v1/actionController.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Action, SecretVersion } from '../../models';
|
||||||
|
import { ActionNotFoundError } from '../../../utils/errors';
|
||||||
|
|
||||||
|
export const getAction = async (req: Request, res: Response) => {
|
||||||
|
let action;
|
||||||
|
try {
|
||||||
|
const { actionId } = req.params;
|
||||||
|
|
||||||
|
action = await Action
|
||||||
|
.findById(actionId)
|
||||||
|
.populate([
|
||||||
|
'payload.secretVersions.oldSecretVersion',
|
||||||
|
'payload.secretVersions.newSecretVersion'
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!action) throw ActionNotFoundError({
|
||||||
|
message: 'Failed to find action'
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw ActionNotFoundError({
|
||||||
|
message: 'Failed to find action'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
action
|
||||||
|
});
|
||||||
|
}
|
||||||
15
backend/src/ee/controllers/v1/index.ts
Normal file
15
backend/src/ee/controllers/v1/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import * as stripeController from './stripeController';
|
||||||
|
import * as secretController from './secretController';
|
||||||
|
import * as secretSnapshotController from './secretSnapshotController';
|
||||||
|
import * as workspaceController from './workspaceController';
|
||||||
|
import * as actionController from './actionController';
|
||||||
|
import * as membershipController from './membershipController';
|
||||||
|
|
||||||
|
export {
|
||||||
|
stripeController,
|
||||||
|
secretController,
|
||||||
|
secretSnapshotController,
|
||||||
|
workspaceController,
|
||||||
|
actionController,
|
||||||
|
membershipController
|
||||||
|
}
|
||||||
63
backend/src/ee/controllers/v1/membershipController.ts
Normal file
63
backend/src/ee/controllers/v1/membershipController.ts
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
import { Request, Response } from "express";
|
||||||
|
import { Membership, Workspace } from "../../../models";
|
||||||
|
import { IMembershipPermission } from "../../../models/membership";
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
||||||
|
import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization";
|
||||||
|
import { Builder } from "builder-pattern"
|
||||||
|
import _ from "lodash";
|
||||||
|
|
||||||
|
export const denyMembershipPermissions = async (req: Request, res: Response) => {
|
||||||
|
const { membershipId } = req.params;
|
||||||
|
const { permissions } = req.body;
|
||||||
|
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
|
||||||
|
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) {
|
||||||
|
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
|
||||||
|
}
|
||||||
|
|
||||||
|
return Builder<IMembershipPermission>()
|
||||||
|
.environmentSlug(permission.environmentSlug)
|
||||||
|
.ability(permission.ability)
|
||||||
|
.build();
|
||||||
|
})
|
||||||
|
|
||||||
|
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual)
|
||||||
|
|
||||||
|
const membershipToModify = await Membership.findById(membershipId)
|
||||||
|
if (!membershipToModify) {
|
||||||
|
throw BadRequestError({ message: "Unable to locate resource" })
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the user making the request is a admin of this project
|
||||||
|
if (![ADMIN, MEMBER].includes(membershipToModify.role)) {
|
||||||
|
throw UnauthorizedRequestError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if the requested slugs are indeed a part of this related workspace
|
||||||
|
const relatedWorkspace = await Workspace.findById(membershipToModify.workspace)
|
||||||
|
if (!relatedWorkspace) {
|
||||||
|
throw BadRequestError({ message: "Something went wrong when locating the related workspace" })
|
||||||
|
}
|
||||||
|
|
||||||
|
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, 'slug')));
|
||||||
|
|
||||||
|
sanitizedMembershipPermissionsUnique.forEach(permission => {
|
||||||
|
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
|
||||||
|
throw BadRequestError({ message: "Unknown environment slug reference" })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// update the permissions
|
||||||
|
const updatedMembershipWithPermissions = await Membership.findByIdAndUpdate(
|
||||||
|
{ _id: membershipToModify._id },
|
||||||
|
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
|
||||||
|
{ new: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!updatedMembershipWithPermissions) {
|
||||||
|
throw BadRequestError({ message: "The resource has been removed before it can be modified" })
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send({
|
||||||
|
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions
|
||||||
|
})
|
||||||
|
}
|
||||||
230
backend/src/ee/controllers/v1/secretController.ts
Normal file
230
backend/src/ee/controllers/v1/secretController.ts
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Secret } from '../../../models';
|
||||||
|
import { SecretVersion } from '../../models';
|
||||||
|
import { EESecretService } from '../../services';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret versions for secret with id [secretId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getSecretVersions = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return secret versions'
|
||||||
|
#swagger.description = 'Return secret versions'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['secretId'] = {
|
||||||
|
"description": "ID of secret",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['offset'] = {
|
||||||
|
"description": "Number of versions to skip",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['limit'] = {
|
||||||
|
"description": "Maximum number of versions to return",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretVersions": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/SecretVersion"
|
||||||
|
},
|
||||||
|
"description": "Secret versions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let secretVersions;
|
||||||
|
try {
|
||||||
|
const { secretId } = req.params;
|
||||||
|
|
||||||
|
const offset: number = parseInt(req.query.offset 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) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get secret versions'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secretVersions
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Roll back secret with id [secretId] to version [version]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Roll back secret to a version.'
|
||||||
|
#swagger.description = 'Roll back secret to a version.'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['secretId'] = {
|
||||||
|
"description": "ID of secret",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Version of secret to roll back to"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secret": {
|
||||||
|
"type": "object",
|
||||||
|
$ref: "#/components/schemas/Secret",
|
||||||
|
"description": "Secret rolled back to"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let secret;
|
||||||
|
try {
|
||||||
|
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,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
} = oldSecretVersion;
|
||||||
|
|
||||||
|
// update secret
|
||||||
|
secret = await Secret.findByIdAndUpdate(
|
||||||
|
secretId,
|
||||||
|
{
|
||||||
|
$inc: {
|
||||||
|
version: 1
|
||||||
|
},
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
},
|
||||||
|
{
|
||||||
|
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,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId: secret.workspace.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to roll back secret version'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secret
|
||||||
|
});
|
||||||
|
}
|
||||||
33
backend/src/ee/controllers/v1/secretSnapshotController.ts
Normal file
33
backend/src/ee/controllers/v1/secretSnapshotController.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { SecretSnapshot } from '../../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret snapshot with id [secretSnapshotId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
||||||
|
let secretSnapshot;
|
||||||
|
try {
|
||||||
|
const { secretSnapshotId } = req.params;
|
||||||
|
|
||||||
|
secretSnapshot = await SecretSnapshot
|
||||||
|
.findById(secretSnapshotId)
|
||||||
|
.populate('secretVersions');
|
||||||
|
|
||||||
|
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get secret snapshot'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secretSnapshot
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import Stripe from 'stripe';
|
import Stripe from 'stripe';
|
||||||
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config';
|
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
|
||||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
apiVersion: '2022-08-01'
|
apiVersion: '2022-08-01'
|
||||||
});
|
});
|
||||||
434
backend/src/ee/controllers/v1/workspaceController.ts
Normal file
434
backend/src/ee/controllers/v1/workspaceController.ts
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
Secret
|
||||||
|
} from '../../../models';
|
||||||
|
import {
|
||||||
|
SecretSnapshot,
|
||||||
|
Log,
|
||||||
|
SecretVersion,
|
||||||
|
ISecretVersion
|
||||||
|
} from '../../models';
|
||||||
|
import { EESecretService } from '../../services';
|
||||||
|
import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return secret snapshots for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return project secret snapshot ids'
|
||||||
|
#swagger.description = 'Return project secret snapshots ids'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['offset'] = {
|
||||||
|
"description": "Number of secret snapshots to skip",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['limit'] = {
|
||||||
|
"description": "Maximum number of secret snapshots to return",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secretSnapshots": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/SecretSnapshot"
|
||||||
|
},
|
||||||
|
"description": "Project secret snapshots"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let secretSnapshots;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
const offset: number = parseInt(req.query.offset 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) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get secret snapshots'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secretSnapshots
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return count of secret snapshots for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
|
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
|
||||||
|
let count;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
count = await SecretSnapshot.countDocuments({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to count number of secret snapshots'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#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.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.requestBody = {
|
||||||
|
"required": true,
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"version": {
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Version of secret snapshot to roll back to",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"secrets": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Secret"
|
||||||
|
},
|
||||||
|
"description": "Secrets rolled back to"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
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 oldSecretVersionsObj: any = secretSnapshot.secretVersions
|
||||||
|
.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
|
||||||
|
secrets = await Secret.insertMany(
|
||||||
|
secretSnapshot.secretVersions.map((sv) => {
|
||||||
|
const secretId = sv.secret;
|
||||||
|
const {
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash,
|
||||||
|
createdAt
|
||||||
|
} = oldSecretVersionsObj[secretId.toString()];
|
||||||
|
|
||||||
|
return ({
|
||||||
|
_id: secretId,
|
||||||
|
version: latestSecretVersions[secretId.toString()].version + 1,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash,
|
||||||
|
secretCommentCiphertext: '',
|
||||||
|
secretCommentIV: '',
|
||||||
|
secretCommentTag: '',
|
||||||
|
createdAt
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// add secret versions
|
||||||
|
await SecretVersion.insertMany(
|
||||||
|
secrets.map(({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// update secret versions of restored secrets as not deleted
|
||||||
|
await SecretVersion.updateMany({
|
||||||
|
secret: {
|
||||||
|
$in: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
isDeleted: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// take secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to roll back secret snapshot'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
secrets
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return (audit) logs for workspace with id [workspaceId]
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
||||||
|
/*
|
||||||
|
#swagger.summary = 'Return project (audit) logs'
|
||||||
|
#swagger.description = 'Return project (audit) logs'
|
||||||
|
|
||||||
|
#swagger.security = [{
|
||||||
|
"apiKeyAuth": []
|
||||||
|
}]
|
||||||
|
|
||||||
|
#swagger.parameters['workspaceId'] = {
|
||||||
|
"description": "ID of project",
|
||||||
|
"required": true,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['userId'] = {
|
||||||
|
"description": "ID of project member",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['offset'] = {
|
||||||
|
"description": "Number of logs to skip",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['limit'] = {
|
||||||
|
"description": "Maximum number of logs to return",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['sortBy'] = {
|
||||||
|
"description": "Order to sort the logs by",
|
||||||
|
"schema": {
|
||||||
|
"type": "string",
|
||||||
|
"@enum": ["oldest", "recent"]
|
||||||
|
},
|
||||||
|
"required": false
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.parameters['actionNames'] = {
|
||||||
|
"description": "Names of log actions (comma-separated)",
|
||||||
|
"required": false,
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
#swagger.responses[200] = {
|
||||||
|
content: {
|
||||||
|
"application/json": {
|
||||||
|
schema: {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"logs": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
$ref: "#/components/schemas/Log"
|
||||||
|
},
|
||||||
|
"description": "Project logs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
let logs
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
const sortBy: string = req.query.sortBy as string;
|
||||||
|
const userId: string = req.query.userId 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) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get workspace logs'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
logs
|
||||||
|
});
|
||||||
|
}
|
||||||
73
backend/src/ee/helpers/action.ts
Normal file
73
backend/src/ee/helpers/action.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { SecretVersion, Action } from '../models';
|
||||||
|
import {
|
||||||
|
getLatestSecretVersionIds,
|
||||||
|
getLatestNSecretSecretVersionIds
|
||||||
|
} from '../helpers/secretVersion';
|
||||||
|
import { ACTION_UPDATE_SECRETS } from '../../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an (audit) action for secrets including
|
||||||
|
* add, delete, update, and read actions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of action
|
||||||
|
* @param {ObjectId[]} obj.secretIds - ids of relevant secrets
|
||||||
|
* @returns {Action} action - new action
|
||||||
|
*/
|
||||||
|
const createActionSecretHelper = async ({
|
||||||
|
name,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let action;
|
||||||
|
let latestSecretVersions;
|
||||||
|
try {
|
||||||
|
if (name === ACTION_UPDATE_SECRETS) {
|
||||||
|
// case: action is updating secrets
|
||||||
|
// -> add old and new secret versions
|
||||||
|
latestSecretVersions = (await getLatestNSecretSecretVersionIds({
|
||||||
|
secretIds,
|
||||||
|
n: 2
|
||||||
|
}))
|
||||||
|
.map((s) => ({
|
||||||
|
oldSecretVersion: s.versions[0]._id,
|
||||||
|
newSecretVersion: s.versions[1]._id
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
// case: action is adding, deleting, or reading secrets
|
||||||
|
// -> add new secret versions
|
||||||
|
latestSecretVersions = (await getLatestSecretVersionIds({
|
||||||
|
secretIds
|
||||||
|
}))
|
||||||
|
.map((s) => ({
|
||||||
|
newSecretVersion: s.versionId
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
action = await new Action({
|
||||||
|
name,
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId,
|
||||||
|
payload: {
|
||||||
|
secretVersions: latestSecretVersions
|
||||||
|
}
|
||||||
|
}).save();
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create action');
|
||||||
|
}
|
||||||
|
|
||||||
|
return action;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { createActionSecretHelper };
|
||||||
18
backend/src/ee/helpers/checkMembershipPermissions.ts
Normal file
18
backend/src/ee/helpers/checkMembershipPermissions.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import { Membership } from "../../models";
|
||||||
|
|
||||||
|
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
|
||||||
|
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
||||||
|
if (!membershipForWorkspace) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
||||||
|
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: action });
|
||||||
|
|
||||||
|
if (isDisallowed) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
|
|
||||||
/**
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.licenseKey - Infisical license key
|
|
||||||
*/
|
|
||||||
const checkLicenseKey = ({
|
|
||||||
licenseKey
|
|
||||||
}: {
|
|
||||||
licenseKey: string
|
|
||||||
}) => {
|
|
||||||
try {
|
|
||||||
// TODO
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
checkLicenseKey
|
|
||||||
}
|
|
||||||
41
backend/src/ee/helpers/log.ts
Normal file
41
backend/src/ee/helpers/log.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Log,
|
||||||
|
IAction
|
||||||
|
} from '../models';
|
||||||
|
|
||||||
|
const createLogHelper = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
actions,
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
actions: IAction[];
|
||||||
|
channel: string;
|
||||||
|
ipAddress: string;
|
||||||
|
}) => {
|
||||||
|
let log;
|
||||||
|
try {
|
||||||
|
log = await new Log({
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId,
|
||||||
|
actionNames: actions.map((a) => a.name),
|
||||||
|
actions,
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create log');
|
||||||
|
}
|
||||||
|
|
||||||
|
return log;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createLogHelper
|
||||||
|
}
|
||||||
169
backend/src/ee/helpers/secret.ts
Normal file
169
backend/src/ee/helpers/secret.ts
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Secret,
|
||||||
|
ISecret
|
||||||
|
} from '../../models';
|
||||||
|
import {
|
||||||
|
SecretSnapshot,
|
||||||
|
SecretVersion,
|
||||||
|
ISecretVersion
|
||||||
|
} from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* secretsnapshots collection.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId
|
||||||
|
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
||||||
|
*/
|
||||||
|
const takeSecretSnapshotHelper = async ({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let secretSnapshot;
|
||||||
|
try {
|
||||||
|
const secretIds = (await Secret.find({
|
||||||
|
workspace: workspaceId
|
||||||
|
}, '_id')).map((s) => s._id);
|
||||||
|
|
||||||
|
const latestSecretVersions = (await SecretVersion.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
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({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).sort({ version: -1 });
|
||||||
|
|
||||||
|
secretSnapshot = await new 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.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object[]} obj.secretVersions
|
||||||
|
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||||
|
*/
|
||||||
|
const addSecretVersionsHelper = async ({
|
||||||
|
secretVersions
|
||||||
|
}: {
|
||||||
|
secretVersions: ISecretVersion[]
|
||||||
|
}) => {
|
||||||
|
let newSecretVersions;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
const markDeletedSecretVersionsHelper = async ({
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await SecretVersion.updateMany({
|
||||||
|
secret: { $in: secretIds }
|
||||||
|
}, {
|
||||||
|
isDeleted: true
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
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 {
|
||||||
|
takeSecretSnapshotHelper,
|
||||||
|
addSecretVersionsHelper,
|
||||||
|
markDeletedSecretVersionsHelper,
|
||||||
|
initSecretVersioningHelper
|
||||||
|
}
|
||||||
110
backend/src/ee/helpers/secretVersion.ts
Normal file
110
backend/src/ee/helpers/secretVersion.ts
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { SecretVersion } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return latest secret versions for secrets with ids [secretIds]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getLatestSecretVersionIds = async ({
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
interface LatestSecretVersionId {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
version: number;
|
||||||
|
versionId: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
let latestSecretVersionIds: LatestSecretVersionId[];
|
||||||
|
try {
|
||||||
|
latestSecretVersionIds = (await SecretVersion.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
secret: {
|
||||||
|
$in: secretIds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: '$secret',
|
||||||
|
version: { $max: '$version' },
|
||||||
|
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]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @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
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getLatestNSecretSecretVersionIds = async ({
|
||||||
|
secretIds,
|
||||||
|
n
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
n: number;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// TODO: optimize query
|
||||||
|
let latestNSecretVersions;
|
||||||
|
try {
|
||||||
|
latestNSecretVersions = (await SecretVersion.aggregate([
|
||||||
|
{
|
||||||
|
$match: {
|
||||||
|
secret: {
|
||||||
|
$in: secretIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$sort: { version: -1 },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$group: {
|
||||||
|
_id: "$secret",
|
||||||
|
versions: { $push: "$$ROOT" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
$project: {
|
||||||
|
_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 {
|
||||||
|
getLatestSecretVersionIds,
|
||||||
|
getLatestNSecretSecretVersionIds
|
||||||
|
}
|
||||||
7
backend/src/ee/middleware/index.ts
Normal file
7
backend/src/ee/middleware/index.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import requireLicenseAuth from './requireLicenseAuth';
|
||||||
|
import requireSecretSnapshotAuth from './requireSecretSnapshotAuth';
|
||||||
|
|
||||||
|
export {
|
||||||
|
requireLicenseAuth,
|
||||||
|
requireSecretSnapshotAuth
|
||||||
|
}
|
||||||
47
backend/src/ee/middleware/requireSecretSnapshotAuth.ts
Normal file
47
backend/src/ee/middleware/requireSecretSnapshotAuth.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { UnauthorizedRequestError, SecretSnapshotNotFoundError } from '../../utils/errors';
|
||||||
|
import { SecretSnapshot } from '../models';
|
||||||
|
import {
|
||||||
|
validateMembership
|
||||||
|
} from '../../helpers/membership';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate if user on request has proper membership for secret snapshot
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||||
|
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||||
|
* @param {String[]} obj.location - location of [workspaceId] on request (e.g. params, body) for parsing
|
||||||
|
*/
|
||||||
|
const requireSecretSnapshotAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const { secretSnapshotId } = req.params;
|
||||||
|
|
||||||
|
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
|
||||||
|
|
||||||
|
if (!secretSnapshot) {
|
||||||
|
return next(SecretSnapshotNotFoundError({
|
||||||
|
message: 'Failed to find secret snapshot'
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: secretSnapshot.workspace.toString(),
|
||||||
|
acceptedRoles
|
||||||
|
});
|
||||||
|
|
||||||
|
req.secretSnapshot = secretSnapshot as any;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireSecretSnapshotAuth;
|
||||||
46
backend/src/ee/models/action.ts
Normal file
46
backend/src/ee/models/action.ts
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IAction {
|
||||||
|
name: string;
|
||||||
|
user?: Types.ObjectId,
|
||||||
|
workspace?: Types.ObjectId,
|
||||||
|
payload: {
|
||||||
|
secretVersions?: Types.ObjectId[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionSchema = new Schema<IAction>(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace'
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
secretVersions: [{
|
||||||
|
oldSecretVersion: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'SecretVersion'
|
||||||
|
},
|
||||||
|
newSecretVersion: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'SecretVersion'
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Action = model<IAction>('Action', actionSchema);
|
||||||
|
|
||||||
|
export default Action;
|
||||||
15
backend/src/ee/models/index.ts
Normal file
15
backend/src/ee/models/index.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
|
||||||
|
import SecretVersion, { ISecretVersion } from './secretVersion';
|
||||||
|
import Log, { ILog } from './log';
|
||||||
|
import Action, { IAction } from './action';
|
||||||
|
|
||||||
|
export {
|
||||||
|
SecretSnapshot,
|
||||||
|
ISecretSnapshot,
|
||||||
|
SecretVersion,
|
||||||
|
ISecretVersion,
|
||||||
|
Log,
|
||||||
|
ILog,
|
||||||
|
Action,
|
||||||
|
IAction
|
||||||
|
}
|
||||||
59
backend/src/ee/models/log.ts
Normal file
59
backend/src/ee/models/log.ts
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
|
export interface ILog {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
user?: Types.ObjectId;
|
||||||
|
workspace?: Types.ObjectId;
|
||||||
|
actionNames: string[];
|
||||||
|
actions: Types.ObjectId[];
|
||||||
|
channel: string;
|
||||||
|
ipAddress?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logSchema = new Schema<ILog>(
|
||||||
|
{
|
||||||
|
user: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace'
|
||||||
|
},
|
||||||
|
actionNames: {
|
||||||
|
type: [String],
|
||||||
|
enum: [
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_READ_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS
|
||||||
|
],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
actions: [{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Action',
|
||||||
|
required: true
|
||||||
|
}],
|
||||||
|
channel: {
|
||||||
|
type: String,
|
||||||
|
enum: ['web', 'cli', 'auto', 'k8-operator', 'other'],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
ipAddress: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
}, {
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Log = model<ILog>('Log', logSchema);
|
||||||
|
|
||||||
|
export default Log;
|
||||||
33
backend/src/ee/models/secretSnapshot.ts
Normal file
33
backend/src/ee/models/secretSnapshot.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface ISecretSnapshot {
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
version: number;
|
||||||
|
secretVersions: Types.ObjectId[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretVersions: [{
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'SecretVersion',
|
||||||
|
required: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
|
||||||
|
|
||||||
|
export default SecretSnapshot;
|
||||||
100
backend/src/ee/models/secretVersion.ts
Normal file
100
backend/src/ee/models/secretVersion.ts
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
} from '../../variables';
|
||||||
|
|
||||||
|
export interface ISecretVersion {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
secret: Types.ObjectId;
|
||||||
|
version: number;
|
||||||
|
workspace: Types.ObjectId; // new
|
||||||
|
type: string; // new
|
||||||
|
user: Types.ObjectId; // new
|
||||||
|
environment: string; // new
|
||||||
|
isDeleted: boolean;
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretKeyHash: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretValueHash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||||
|
{
|
||||||
|
secret: { // could be deleted
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Secret',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
version: {
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
user: {
|
||||||
|
// user associated with the personal secret
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User'
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isDeleted: { // consider removing field
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyCiphertext: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyIV: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyTag: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretKeyHash: {
|
||||||
|
type: String
|
||||||
|
},
|
||||||
|
secretValueCiphertext: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretValueIV: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretValueTag: {
|
||||||
|
type: String, // symmetric
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
secretValueHash: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
||||||
|
|
||||||
|
export default SecretVersion;
|
||||||
17
backend/src/ee/routes/v1/action.ts
Normal file
17
backend/src/ee/routes/v1/action.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
validateRequest
|
||||||
|
} from '../../../middleware';
|
||||||
|
import { param } from 'express-validator';
|
||||||
|
import { actionController } from '../../controllers/v1';
|
||||||
|
|
||||||
|
// TODO: put into action controller
|
||||||
|
router.get(
|
||||||
|
'/:actionId',
|
||||||
|
param('actionId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
actionController.getAction
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
11
backend/src/ee/routes/v1/index.ts
Normal file
11
backend/src/ee/routes/v1/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import secret from './secret';
|
||||||
|
import secretSnapshot from './secretSnapshot';
|
||||||
|
import workspace from './workspace';
|
||||||
|
import action from './action';
|
||||||
|
|
||||||
|
export {
|
||||||
|
secret,
|
||||||
|
secretSnapshot,
|
||||||
|
workspace,
|
||||||
|
action
|
||||||
|
}
|
||||||
40
backend/src/ee/routes/v1/secret.ts
Normal file
40
backend/src/ee/routes/v1/secret.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireSecretAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../../middleware';
|
||||||
|
import { query, param, body } from 'express-validator';
|
||||||
|
import { secretController } from '../../controllers/v1';
|
||||||
|
import { ADMIN, MEMBER } from '../../../variables';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:secretId/secret-versions',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireSecretAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('secretId').exists().trim(),
|
||||||
|
query('offset').exists().isInt(),
|
||||||
|
query('limit').exists().isInt(),
|
||||||
|
validateRequest,
|
||||||
|
secretController.getSecretVersions
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:secretId/secret-versions/rollback',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireSecretAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('secretId').exists().trim(),
|
||||||
|
body('version').exists().isInt(),
|
||||||
|
secretController.rollbackSecretVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
27
backend/src/ee/routes/v1/secretSnapshot.ts
Normal file
27
backend/src/ee/routes/v1/secretSnapshot.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireSecretSnapshotAuth
|
||||||
|
} from '../../middleware';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../../middleware';
|
||||||
|
import { param, body } from 'express-validator';
|
||||||
|
import { ADMIN, MEMBER } from '../../../variables';
|
||||||
|
import { secretSnapshotController } from '../../controllers/v1';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:secretSnapshotId',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireSecretSnapshotAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('secretSnapshotId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
secretSnapshotController.getSecretSnapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { stripeController } from '../controllers';
|
import { stripeController } from '../../controllers/v1';
|
||||||
|
|
||||||
router.post('/webhook', stripeController.handleWebhook);
|
router.post('/webhook', stripeController.handleWebhook);
|
||||||
|
|
||||||
72
backend/src/ee/routes/v1/workspace.ts
Normal file
72
backend/src/ee/routes/v1/workspace.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../../../middleware';
|
||||||
|
import { param, query, body } from 'express-validator';
|
||||||
|
import { ADMIN, MEMBER } from '../../../variables';
|
||||||
|
import { workspaceController } from '../../controllers/v1';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/secret-snapshots',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
query('offset').exists().isInt(),
|
||||||
|
query('limit').exists().isInt(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceSecretSnapshots
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/secret-snapshots/count',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceSecretSnapshotsCount
|
||||||
|
);
|
||||||
|
|
||||||
|
router.post(
|
||||||
|
'/:workspaceId/secret-snapshots/rollback',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
body('version').exists().isInt(),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.rollbackWorkspaceSecretSnapshot
|
||||||
|
);
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/:workspaceId/logs',
|
||||||
|
requireAuth({
|
||||||
|
acceptedAuthModes: ['jwt', 'apiKey']
|
||||||
|
}),
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER]
|
||||||
|
}),
|
||||||
|
param('workspaceId').exists().trim(),
|
||||||
|
query('offset').exists().isInt(),
|
||||||
|
query('limit').exists().isInt(),
|
||||||
|
query('sortBy'),
|
||||||
|
query('userId'),
|
||||||
|
query('actionNames'),
|
||||||
|
validateRequest,
|
||||||
|
workspaceController.getWorkspaceLogs
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
19
backend/src/ee/services/EELicenseService.ts
Normal file
19
backend/src/ee/services/EELicenseService.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { LICENSE_KEY } from '../../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle Enterprise Edition license actions
|
||||||
|
*/
|
||||||
|
class EELicenseService {
|
||||||
|
|
||||||
|
private readonly _isLicenseValid: boolean;
|
||||||
|
|
||||||
|
constructor(licenseKey: string) {
|
||||||
|
this._isLicenseValid = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isLicenseValid(): boolean {
|
||||||
|
return this._isLicenseValid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new EELicenseService(LICENSE_KEY);
|
||||||
81
backend/src/ee/services/EELogService.ts
Normal file
81
backend/src/ee/services/EELogService.ts
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import {
|
||||||
|
Log,
|
||||||
|
Action,
|
||||||
|
IAction
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
createLogHelper
|
||||||
|
} from '../helpers/log';
|
||||||
|
import {
|
||||||
|
createActionSecretHelper
|
||||||
|
} from '../helpers/action';
|
||||||
|
import EELicenseService from './EELicenseService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle Enterprise Edition log actions
|
||||||
|
*/
|
||||||
|
class EELogService {
|
||||||
|
/**
|
||||||
|
* Create an (audit) log
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user associated with the log
|
||||||
|
* @param {String} obj.workspaceId - id of workspace associated with the log
|
||||||
|
* @param {Action} obj.actions - actions to include in log
|
||||||
|
* @param {String} obj.channel - channel (web/cli/auto) associated with the log
|
||||||
|
* @param {String} obj.ipAddress - ip address associated with the log
|
||||||
|
* @returns {Log} log - new audit log
|
||||||
|
*/
|
||||||
|
static async createLog({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
actions,
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
actions: IAction[];
|
||||||
|
channel: string;
|
||||||
|
ipAddress: string;
|
||||||
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return null;
|
||||||
|
return await createLogHelper({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
actions,
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an (audit) action for secrets including
|
||||||
|
* add, delete, update, and read actions.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of action
|
||||||
|
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||||
|
* @returns {Action} action - new action
|
||||||
|
*/
|
||||||
|
static async createActionSecret({
|
||||||
|
name,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return null;
|
||||||
|
return await createActionSecretHelper({
|
||||||
|
name,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EELogService;
|
||||||
78
backend/src/ee/services/EESecretService.ts
Normal file
78
backend/src/ee/services/EESecretService.ts
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
import { Types } from 'mongoose';
|
||||||
|
import { ISecretVersion } from '../models';
|
||||||
|
import {
|
||||||
|
takeSecretSnapshotHelper,
|
||||||
|
addSecretVersionsHelper,
|
||||||
|
markDeletedSecretVersionsHelper,
|
||||||
|
initSecretVersioningHelper
|
||||||
|
} from '../helpers/secret';
|
||||||
|
import EELicenseService from './EELicenseService';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle Enterprise Edition secret actions
|
||||||
|
*/
|
||||||
|
class EESecretService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
* SecretSnapshot collection.
|
||||||
|
* Requires a valid license key [licenseKey]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId
|
||||||
|
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
|
||||||
|
*/
|
||||||
|
static async takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
|
return await takeSecretSnapshotHelper({ workspaceId });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add secret versions [secretVersions] to the SecretVersion collection.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object[]} obj.secretVersions
|
||||||
|
* @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]
|
||||||
|
* as deleted.
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||||
|
*/
|
||||||
|
static async markDeletedSecretVersions({
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
secretIds: Types.ObjectId[];
|
||||||
|
}) {
|
||||||
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
|
await markDeletedSecretVersionsHelper({
|
||||||
|
secretIds
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize secret versioning by setting previously unversioned
|
||||||
|
* secrets to version 1 and begin populating secret versions.
|
||||||
|
*/
|
||||||
|
static async initSecretVersioning() {
|
||||||
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
|
await initSecretVersioningHelper();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EESecretService;
|
||||||
9
backend/src/ee/services/index.ts
Normal file
9
backend/src/ee/services/index.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import EELicenseService from "./EELicenseService";
|
||||||
|
import EESecretService from "./EESecretService";
|
||||||
|
import EELogService from "./EELogService";
|
||||||
|
|
||||||
|
export {
|
||||||
|
EELicenseService,
|
||||||
|
EESecretService,
|
||||||
|
EELogService
|
||||||
|
}
|
||||||
5
backend/src/events/index.ts
Normal file
5
backend/src/events/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { eventPushSecrets } from "./secret"
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
60
backend/src/events/secret.ts
Normal file
60
backend/src/events/secret.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import {
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
interface PushSecret {
|
||||||
|
ciphertextKey: string;
|
||||||
|
ivKey: string;
|
||||||
|
tagKey: string;
|
||||||
|
hashKey: string;
|
||||||
|
ciphertextValue: string;
|
||||||
|
ivValue: string;
|
||||||
|
tagValue: string;
|
||||||
|
hashValue: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return event for pushing secrets
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to push secrets to
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const eventPushSecrets = ({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
return ({
|
||||||
|
name: EVENT_PUSH_SECRETS,
|
||||||
|
workspaceId,
|
||||||
|
payload: {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return event for pulling secrets
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to pull secrets from
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const eventPullSecrets = ({
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
return ({
|
||||||
|
name: EVENT_PULL_SECRETS,
|
||||||
|
workspaceId,
|
||||||
|
payload: {
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
@@ -1,7 +1,10 @@
|
|||||||
import jwt from 'jsonwebtoken';
|
import jwt from 'jsonwebtoken';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import bcrypt from 'bcrypt';
|
||||||
import {
|
import {
|
||||||
User
|
User,
|
||||||
|
ServiceTokenData,
|
||||||
|
APIKeyData
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import {
|
import {
|
||||||
JWT_AUTH_LIFETIME,
|
JWT_AUTH_LIFETIME,
|
||||||
@@ -9,6 +12,196 @@ import {
|
|||||||
JWT_REFRESH_LIFETIME,
|
JWT_REFRESH_LIFETIME,
|
||||||
JWT_REFRESH_SECRET
|
JWT_REFRESH_SECRET
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
import {
|
||||||
|
AccountNotFoundError,
|
||||||
|
ServiceTokenDataNotFoundError,
|
||||||
|
APIKeyDataNotFoundError,
|
||||||
|
UnauthorizedRequestError,
|
||||||
|
BadRequestError
|
||||||
|
} from '../utils/errors';
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.headers - HTTP request headers object
|
||||||
|
*/
|
||||||
|
const validateAuthMode = ({
|
||||||
|
headers,
|
||||||
|
acceptedAuthModes
|
||||||
|
}: {
|
||||||
|
headers: { [key: string]: string | string[] | undefined },
|
||||||
|
acceptedAuthModes: string[]
|
||||||
|
}) => {
|
||||||
|
// TODO: refactor middleware
|
||||||
|
const apiKey = headers['x-api-key'];
|
||||||
|
const authHeader = headers['authorization'];
|
||||||
|
|
||||||
|
let authTokenType, authTokenValue;
|
||||||
|
if (apiKey === undefined && authHeader === undefined) {
|
||||||
|
// case: no auth or X-API-KEY header present
|
||||||
|
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof apiKey === 'string') {
|
||||||
|
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
|
||||||
|
authTokenType = 'apiKey';
|
||||||
|
authTokenValue = apiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof authHeader === 'string') {
|
||||||
|
// case: treat request authentication type as via Authorization header (i.e. either JWT or service token)
|
||||||
|
const [tokenType, tokenValue] = <[string, string]>authHeader.split(' ', 2) ?? [null, null]
|
||||||
|
if (tokenType === null)
|
||||||
|
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
|
||||||
|
if (tokenType.toLowerCase() !== 'bearer')
|
||||||
|
throw BadRequestError({ message: `The provided authentication type '${tokenType}' is not supported.` });
|
||||||
|
if (tokenValue === null)
|
||||||
|
throw BadRequestError({ message: 'Missing Authorization Body in the request header.' });
|
||||||
|
|
||||||
|
switch (tokenValue.split('.', 1)[0]) {
|
||||||
|
case 'st':
|
||||||
|
authTokenType = 'serviceToken';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
authTokenType = 'jwt';
|
||||||
|
}
|
||||||
|
authTokenValue = tokenValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!authTokenType || !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.' });
|
||||||
|
|
||||||
|
return ({
|
||||||
|
authTokenType,
|
||||||
|
authTokenValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return user payload corresponding to JWT token [authTokenValue]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.authTokenValue - JWT token value
|
||||||
|
* @returns {User} user - user corresponding to JWT token
|
||||||
|
*/
|
||||||
|
const getAuthUserPayload = async ({
|
||||||
|
authTokenValue
|
||||||
|
}: {
|
||||||
|
authTokenValue: string;
|
||||||
|
}) => {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const decodedToken = <jwt.UserIDJwtPayload>(
|
||||||
|
jwt.verify(authTokenValue, JWT_AUTH_SECRET)
|
||||||
|
);
|
||||||
|
|
||||||
|
user = await User.findOne({
|
||||||
|
_id: decodedToken.userId
|
||||||
|
}).select('+publicKey');
|
||||||
|
|
||||||
|
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
|
||||||
|
|
||||||
|
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to authenticate JWT token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return service token data payload corresponding to service token [authTokenValue]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.authTokenValue - service token value
|
||||||
|
* @returns {ServiceTokenData} serviceTokenData - service token data
|
||||||
|
*/
|
||||||
|
const getAuthSTDPayload = async ({
|
||||||
|
authTokenValue
|
||||||
|
}: {
|
||||||
|
authTokenValue: string;
|
||||||
|
}) => {
|
||||||
|
let serviceTokenData;
|
||||||
|
try {
|
||||||
|
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||||
|
|
||||||
|
// TODO: optimize double query
|
||||||
|
serviceTokenData = await ServiceTokenData
|
||||||
|
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
|
||||||
|
|
||||||
|
if (!serviceTokenData) {
|
||||||
|
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
|
||||||
|
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
|
||||||
|
// case: service token expired
|
||||||
|
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to authenticate expired service token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
|
||||||
|
if (!isMatch) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to authenticate service token'
|
||||||
|
});
|
||||||
|
|
||||||
|
serviceTokenData = await ServiceTokenData
|
||||||
|
.findById(TOKEN_IDENTIFIER)
|
||||||
|
.select('+encryptedKey +iv +tag').populate('user');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to authenticate service token'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceTokenData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return API key data payload corresponding to API key [authTokenValue]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.authTokenValue - API key value
|
||||||
|
* @returns {APIKeyData} apiKeyData - API key data
|
||||||
|
*/
|
||||||
|
const getAuthAPIKeyPayload = async ({
|
||||||
|
authTokenValue
|
||||||
|
}: {
|
||||||
|
authTokenValue: string;
|
||||||
|
}) => {
|
||||||
|
let user;
|
||||||
|
try {
|
||||||
|
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
|
||||||
|
|
||||||
|
const apiKeyData = await APIKeyData
|
||||||
|
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
|
||||||
|
.populate('user', '+publicKey');
|
||||||
|
|
||||||
|
if (!apiKeyData) {
|
||||||
|
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
|
||||||
|
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
|
||||||
|
// case: API key expired
|
||||||
|
await APIKeyData.findByIdAndDelete(apiKeyData._id);
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to authenticate expired API key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
|
||||||
|
if (!isMatch) throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to authenticate API key'
|
||||||
|
});
|
||||||
|
|
||||||
|
user = apiKeyData.user;
|
||||||
|
} catch (err) {
|
||||||
|
throw UnauthorizedRequestError({
|
||||||
|
message: 'Failed to authenticate API key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
|
* Return newly issued (JWT) auth and refresh tokens to user with id [userId]
|
||||||
@@ -99,4 +292,12 @@ const createToken = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { createToken, issueTokens, clearTokens };
|
export {
|
||||||
|
validateAuthMode,
|
||||||
|
getAuthUserPayload,
|
||||||
|
getAuthSTDPayload,
|
||||||
|
getAuthAPIKeyPayload,
|
||||||
|
createToken,
|
||||||
|
issueTokens,
|
||||||
|
clearTokens
|
||||||
|
};
|
||||||
|
|||||||
229
backend/src/helpers/bot.ts
Normal file
229
backend/src/helpers/bot.ts
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
BotKey,
|
||||||
|
Secret,
|
||||||
|
ISecret,
|
||||||
|
IUser
|
||||||
|
} from '../models';
|
||||||
|
import {
|
||||||
|
generateKeyPair,
|
||||||
|
encryptSymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
|
decryptAsymmetric
|
||||||
|
} from '../utils/crypto';
|
||||||
|
import { ENCRYPTION_KEY } from '../config';
|
||||||
|
import { SECRET_SHARED } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of bot
|
||||||
|
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||||
|
*/
|
||||||
|
const createBot = async ({
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
bot = await new Bot({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted secrets for workspace with id [workspaceId]
|
||||||
|
* and [environment] using bot
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.environment - environment
|
||||||
|
*/
|
||||||
|
const getSecretsHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
}) => {
|
||||||
|
const content = {} as any;
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const secrets = await Secret.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
type: SECRET_SHARED
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
const secretKey = decryptSymmetric({
|
||||||
|
ciphertext: secret.secretKeyCiphertext,
|
||||||
|
iv: secret.secretKeyIV,
|
||||||
|
tag: secret.secretKeyTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
const secretValue = decryptSymmetric({
|
||||||
|
ciphertext: secret.secretValueCiphertext,
|
||||||
|
iv: secret.secretValueIV,
|
||||||
|
tag: secret.secretValueTag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
content[secretKey] = secretValue;
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot's copy of the workspace key for workspace
|
||||||
|
* with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @returns {String} key - decrypted workspace key
|
||||||
|
*/
|
||||||
|
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const botKey = await BotKey.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
||||||
|
|
||||||
|
if (!botKey) throw new Error('Failed to find bot key');
|
||||||
|
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to find bot');
|
||||||
|
if (!bot.isActive) throw new Error('Bot is not active');
|
||||||
|
|
||||||
|
const privateKeyBot = decryptSymmetric({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
key = decryptAsymmetric({
|
||||||
|
ciphertext: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
publicKey: botKey.sender.publicKey as string,
|
||||||
|
privateKey: privateKeyBot
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get workspace key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
|
* key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {String} obj1.workspaceId - id of workspace
|
||||||
|
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||||
|
*/
|
||||||
|
const encryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
plaintext: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
ciphertext,
|
||||||
|
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
|
||||||
|
* key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
|
* @param {String} obj.iv - iv
|
||||||
|
* @param {String} obj.tag - tag
|
||||||
|
*/
|
||||||
|
const decryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}: {
|
||||||
|
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 {
|
||||||
|
createBot,
|
||||||
|
getSecretsHelper,
|
||||||
|
encryptSymmetricHelper,
|
||||||
|
decryptSymmetricHelper
|
||||||
|
}
|
||||||
34
backend/src/helpers/database.ts
Normal file
34
backend/src/helpers/database.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import mongoose from 'mongoose';
|
||||||
|
import { EESecretService } from '../ee/services';
|
||||||
|
import { getLogger } from '../utils/logger';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize database connection
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.mongoURL - mongo connection string
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const initDatabaseHelper = async ({
|
||||||
|
mongoURL
|
||||||
|
}: {
|
||||||
|
mongoURL: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await mongoose.connect(mongoURL);
|
||||||
|
|
||||||
|
// allow empty strings to pass the required validator
|
||||||
|
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
|
||||||
|
|
||||||
|
getLogger("database").info("Database connection established");
|
||||||
|
|
||||||
|
await EESecretService.initSecretVersioning();
|
||||||
|
} catch (err) {
|
||||||
|
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return mongoose.connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
initDatabaseHelper
|
||||||
|
}
|
||||||
51
backend/src/helpers/event.ts
Normal file
51
backend/src/helpers/event.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { Bot, IBot } from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||||
|
import { IntegrationService } from '../services';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle event [event]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Event} obj.event - an event
|
||||||
|
* @param {String} obj.event.name - name of event
|
||||||
|
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||||
|
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||||
|
*/
|
||||||
|
const handleEventHelper = async ({
|
||||||
|
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 {
|
||||||
|
handleEventHelper
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
Integration,
|
||||||
|
IntegrationAuth,
|
||||||
|
} from '../models';
|
||||||
|
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||||
|
import { BotService } from '../services';
|
||||||
|
import {
|
||||||
|
INTEGRATION_VERCEL,
|
||||||
|
INTEGRATION_NETLIFY
|
||||||
|
} from '../variables';
|
||||||
|
import { UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
import RequestError from '../utils/requestError';
|
||||||
|
|
||||||
|
interface Update {
|
||||||
|
workspace: string;
|
||||||
|
integration: string;
|
||||||
|
teamId?: string;
|
||||||
|
accountId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
|
* named [integration]
|
||||||
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||||
|
* - Add placeholder inactive integration
|
||||||
|
* - Create bot sequence for integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.code - code
|
||||||
|
*/
|
||||||
|
const handleOAuthExchangeHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
environment: string;
|
||||||
|
}) => {
|
||||||
|
let action;
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||||
|
|
||||||
|
// exchange code for access and refresh tokens
|
||||||
|
const res = await exchangeCode({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
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(),
|
||||||
|
accessToken: res.accessToken,
|
||||||
|
accessExpiresAt: res.accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialize new integration after exchange
|
||||||
|
await new Integration({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
app: null,
|
||||||
|
environment,
|
||||||
|
integration,
|
||||||
|
integrationAuth: integrationAuth._id
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
|
* all active integrations for that workspace
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
const syncIntegrationsHelper = async ({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let integrations;
|
||||||
|
try {
|
||||||
|
integrations = await Integration.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
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.toString(),
|
||||||
|
environment: integration.environment
|
||||||
|
});
|
||||||
|
|
||||||
|
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
// get integration auth access token
|
||||||
|
const accessToken = await getIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId: integration.integrationAuth.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// sync secrets to integration
|
||||||
|
await syncSecrets({
|
||||||
|
integration,
|
||||||
|
integrationAuth,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to integrations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted refresh token using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} refreshToken - decrypted refresh token
|
||||||
|
*/
|
||||||
|
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||||
|
let refreshToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
|
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||||
|
|
||||||
|
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
|
refreshToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||||
|
iv: integrationAuth.refreshIV 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 decrypted access token using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @returns {String} accessToken - decrypted access token
|
||||||
|
*/
|
||||||
|
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||||
|
let accessToken;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
|
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
|
||||||
|
|
||||||
|
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
|
||||||
|
|
||||||
|
accessToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
ciphertext: integrationAuth.accessCiphertext as string,
|
||||||
|
iv: integrationAuth.accessIV as string,
|
||||||
|
tag: integrationAuth.accessTag as string
|
||||||
|
});
|
||||||
|
|
||||||
|
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||||
|
// there is a access token expiration date
|
||||||
|
// and refresh token to exchange with the OAuth2 server
|
||||||
|
|
||||||
|
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||||
|
// access token is expired
|
||||||
|
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||||
|
accessToken = await exchangeRefresh({
|
||||||
|
integration: integrationAuth.integration,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
if(err instanceof RequestError)
|
||||||
|
throw err
|
||||||
|
else
|
||||||
|
throw new Error('Failed to get integration access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId] and store it
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.refreshToken - refresh token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthRefreshHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
refreshCiphertext: obj.ciphertext,
|
||||||
|
refreshIV: obj.iv,
|
||||||
|
refreshTag: obj.tag
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to set integration auth refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt access token [accessToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId] and store it along with [accessExpiresAt]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.accessToken - access token
|
||||||
|
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthAccessHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
accessToken: string;
|
||||||
|
accessExpiresAt: Date | undefined;
|
||||||
|
}) => {
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
accessCiphertext: obj.ciphertext,
|
||||||
|
accessIV: obj.iv,
|
||||||
|
accessTag: obj.tag,
|
||||||
|
accessExpiresAt
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to save integration auth access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
handleOAuthExchangeHelper,
|
||||||
|
syncIntegrationsHelper,
|
||||||
|
getIntegrationAuthRefreshHelper,
|
||||||
|
getIntegrationAuthAccessHelper,
|
||||||
|
setIntegrationAuthRefreshHelper,
|
||||||
|
setIntegrationAuthAccessHelper
|
||||||
|
}
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { IntegrationAuth } from '../models';
|
|
||||||
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
|
|
||||||
import { IIntegrationAuth } from '../models';
|
|
||||||
import {
|
|
||||||
ENCRYPTION_KEY,
|
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
|
||||||
} from '../config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
|
||||||
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
|
||||||
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
|
||||||
* @param {String} obj.integration - name of integration (e.g. heroku)
|
|
||||||
* @param {Object} obj.res - response from OAuth2 authorization server
|
|
||||||
*/
|
|
||||||
const processOAuthTokenRes = async ({
|
|
||||||
workspaceId,
|
|
||||||
integration,
|
|
||||||
res
|
|
||||||
}: {
|
|
||||||
workspaceId: string;
|
|
||||||
integration: string;
|
|
||||||
res: any;
|
|
||||||
}): Promise<IIntegrationAuth> => {
|
|
||||||
let integrationAuth;
|
|
||||||
try {
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const {
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.refresh_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.access_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// compute access token expiration date
|
|
||||||
const accessExpiresAt = new Date();
|
|
||||||
accessExpiresAt.setSeconds(
|
|
||||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
|
||||||
);
|
|
||||||
|
|
||||||
// create or replace integration authorization with encrypted tokens
|
|
||||||
// and access token expiration date
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration,
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(
|
|
||||||
'Failed to process OAuth2 authorization server token response'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrationAuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
|
||||||
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
|
||||||
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
|
|
||||||
* @returns {String} access token - new access token
|
|
||||||
*/
|
|
||||||
const getOAuthAccessToken = async ({
|
|
||||||
integrationAuth
|
|
||||||
}: {
|
|
||||||
integrationAuth: IIntegrationAuth;
|
|
||||||
}) => {
|
|
||||||
let accessToken;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
} = integrationAuth;
|
|
||||||
|
|
||||||
if (
|
|
||||||
refreshCiphertext &&
|
|
||||||
refreshIV &&
|
|
||||||
refreshTag &&
|
|
||||||
accessCiphertext &&
|
|
||||||
accessIV &&
|
|
||||||
accessTag &&
|
|
||||||
accessExpiresAt
|
|
||||||
) {
|
|
||||||
if (accessExpiresAt < new Date()) {
|
|
||||||
// case: access token expired
|
|
||||||
// TODO: fetch another access token
|
|
||||||
|
|
||||||
let clientSecret;
|
|
||||||
switch (integrationAuth.integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// record new access token and refresh token
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const refreshToken = decryptSymmetric({
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: make route compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
accessToken = res.data.access_token;
|
|
||||||
|
|
||||||
await processOAuthTokenRes({
|
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
|
||||||
integration: integrationAuth.integration,
|
|
||||||
res
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// case: access token still works
|
|
||||||
accessToken = decryptSymmetric({
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get OAuth2 access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { processOAuthTokenRes, getOAuthAccessToken };
|
|
||||||
|
|||||||
@@ -1,6 +1,46 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Membership, Key } from '../models';
|
import { Membership, Key } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
|
* and has at least one of the roles in [acceptedRoles]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user to validate
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
|
||||||
|
*/
|
||||||
|
const validateMembership = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles,
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
acceptedRoles: string[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let membership;
|
||||||
|
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
|
||||||
|
try {
|
||||||
|
membership = await Membership.findOne({
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate("workspace");
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to find membership');
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate membership role');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to validate 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
|
||||||
@@ -26,18 +66,15 @@ const findMembership = async (queryObj: any) => {
|
|||||||
* @param {String[]} obj.userIds - id of users.
|
* @param {String[]} obj.userIds - id of users.
|
||||||
* @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.
|
||||||
* @param {String[]} obj.statuses - statuses of users.
|
|
||||||
*/
|
*/
|
||||||
const addMemberships = async ({
|
const addMemberships = async ({
|
||||||
userIds,
|
userIds,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roles,
|
roles
|
||||||
statuses
|
|
||||||
}: {
|
}: {
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
roles: string[];
|
roles: string[];
|
||||||
statuses: string[];
|
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
try {
|
try {
|
||||||
const operations = userIds.map((userId, idx) => {
|
const operations = userIds.map((userId, idx) => {
|
||||||
@@ -46,14 +83,12 @@ const addMemberships = async ({
|
|||||||
filter: {
|
filter: {
|
||||||
user: userId,
|
user: userId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
role: roles[idx],
|
role: roles[idx]
|
||||||
status: statuses[idx]
|
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
user: userId,
|
user: userId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
role: roles[idx],
|
role: roles[idx]
|
||||||
status: statuses[idx]
|
|
||||||
},
|
},
|
||||||
upsert: true
|
upsert: true
|
||||||
}
|
}
|
||||||
@@ -97,4 +132,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { addMemberships, findMembership, deleteMembership };
|
export {
|
||||||
|
validateMembership,
|
||||||
|
addMemberships,
|
||||||
|
findMembership,
|
||||||
|
deleteMembership
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,6 +1,42 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
import { MembershipOrg, Workspace, Membership, Key } from '../models';
|
import { MembershipOrg, Workspace, Membership, Key } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user with id [userId] is a member of organization with id [organizationId]
|
||||||
|
* and has at least one of the roles in [acceptedRoles]
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
const validateMembership = async ({
|
||||||
|
userId,
|
||||||
|
organizationId,
|
||||||
|
acceptedRoles
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
organizationId: string;
|
||||||
|
acceptedRoles: string[];
|
||||||
|
}) => {
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
membership = await MembershipOrg.findOne({
|
||||||
|
user: new Types.ObjectId(userId),
|
||||||
|
organization: new Types.ObjectId(organizationId)
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to find organization membership');
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate organization membership role');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to validate organization membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return organization membership matching criteria specified in
|
* Return organization membership matching criteria specified in
|
||||||
* query [queryObj]
|
* query [queryObj]
|
||||||
@@ -84,6 +120,8 @@ const deleteMembershipOrg = async ({
|
|||||||
_id: membershipOrgId
|
_id: membershipOrgId
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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
|
||||||
@@ -117,4 +155,9 @@ const deleteMembershipOrg = async ({
|
|||||||
return deletedMembershipOrg;
|
return deletedMembershipOrg;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { findMembershipOrg, addMembershipsOrg, deleteMembershipOrg };
|
export {
|
||||||
|
validateMembership,
|
||||||
|
findMembershipOrg,
|
||||||
|
addMembershipsOrg,
|
||||||
|
deleteMembershipOrg
|
||||||
|
};
|
||||||
|
|||||||
@@ -2,40 +2,10 @@ import fs from 'fs';
|
|||||||
import path from 'path';
|
import path from 'path';
|
||||||
import handlebars from 'handlebars';
|
import handlebars from 'handlebars';
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import {
|
import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config';
|
||||||
SMTP_HOST,
|
|
||||||
SMTP_PORT,
|
|
||||||
SMTP_NAME,
|
|
||||||
SMTP_USERNAME,
|
|
||||||
SMTP_PASSWORD
|
|
||||||
} from '../config';
|
|
||||||
import SMTPConnection from 'nodemailer/lib/smtp-connection';
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
|
||||||
const mailOpts: SMTPConnection.Options = {
|
let smtpTransporter: nodemailer.Transporter;
|
||||||
host: SMTP_HOST,
|
|
||||||
port: SMTP_PORT as number
|
|
||||||
};
|
|
||||||
if (SMTP_USERNAME && SMTP_PASSWORD) {
|
|
||||||
mailOpts.auth = {
|
|
||||||
user: SMTP_USERNAME,
|
|
||||||
pass: SMTP_PASSWORD
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// create nodemailer transporter
|
|
||||||
const transporter = nodemailer.createTransport(mailOpts);
|
|
||||||
transporter
|
|
||||||
.verify()
|
|
||||||
.then(() => {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureMessage('SMTP - Successfully connected');
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(
|
|
||||||
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
@@ -63,8 +33,8 @@ const sendMail = async ({
|
|||||||
const temp = handlebars.compile(html);
|
const temp = handlebars.compile(html);
|
||||||
const htmlToSend = temp(substitutions);
|
const htmlToSend = temp(substitutions);
|
||||||
|
|
||||||
await transporter.sendMail({
|
await smtpTransporter.sendMail({
|
||||||
from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`,
|
from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`,
|
||||||
to: recipients.join(', '),
|
to: recipients.join(', '),
|
||||||
subject: subjectLine,
|
subject: subjectLine,
|
||||||
html: htmlToSend
|
html: htmlToSend
|
||||||
@@ -75,4 +45,8 @@ const sendMail = async ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export { sendMail };
|
const setTransporter = (transporter: nodemailer.Transporter) => {
|
||||||
|
smtpTransporter = transporter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { sendMail, setTransporter };
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Stripe from 'stripe';
|
|||||||
import {
|
import {
|
||||||
STRIPE_SECRET_KEY,
|
STRIPE_SECRET_KEY,
|
||||||
STRIPE_PRODUCT_STARTER,
|
STRIPE_PRODUCT_STARTER,
|
||||||
|
STRIPE_PRODUCT_TEAM,
|
||||||
STRIPE_PRODUCT_PRO
|
STRIPE_PRODUCT_PRO
|
||||||
} from '../config';
|
} from '../config';
|
||||||
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
const stripe = new Stripe(STRIPE_SECRET_KEY, {
|
||||||
@@ -14,6 +15,7 @@ import { Organization, MembershipOrg } from '../models';
|
|||||||
|
|
||||||
const productToPriceMap = {
|
const productToPriceMap = {
|
||||||
starter: STRIPE_PRODUCT_STARTER,
|
starter: STRIPE_PRODUCT_STARTER,
|
||||||
|
team: STRIPE_PRODUCT_TEAM,
|
||||||
pro: STRIPE_PRODUCT_PRO
|
pro: STRIPE_PRODUCT_PRO
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -55,7 +57,7 @@ const createOrganization = async ({
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email });
|
Sentry.setUser({ email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed to create organization');
|
throw new Error(`Failed to create organization [err=${err}]`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return organization;
|
return organization;
|
||||||
|
|||||||
@@ -1,36 +1,43 @@
|
|||||||
import rateLimit from 'express-rate-limit';
|
import rateLimit from 'express-rate-limit';
|
||||||
|
|
||||||
// 300 requests per 15 minutes
|
// 120 requests per minute
|
||||||
const apiLimiter = rateLimit({
|
const apiLimiter = rateLimit({
|
||||||
windowMs: 15 * 60 * 1000,
|
windowMs: 60 * 1000,
|
||||||
max: 400,
|
max: 240,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false,
|
legacyHeaders: false,
|
||||||
skip: (request) => request.path === '/healthcheck'
|
skip: (request) => {
|
||||||
|
return request.path === '/healthcheck' || request.path === '/api/status'
|
||||||
|
},
|
||||||
|
keyGenerator: (req, res) => {
|
||||||
|
return req.clientIp
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 5 requests per hour
|
// 10 requests per minute
|
||||||
const signupLimiter = rateLimit({
|
const authLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000,
|
windowMs: 60 * 1000,
|
||||||
max: 10,
|
max: 10,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false,
|
||||||
|
keyGenerator: (req, res) => {
|
||||||
|
return req.clientIp
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// 10 requests per hour
|
// 10 requests per hour
|
||||||
const loginLimiter = rateLimit({
|
|
||||||
windowMs: 60 * 60 * 1000,
|
|
||||||
max: 20,
|
|
||||||
standardHeaders: true,
|
|
||||||
legacyHeaders: false
|
|
||||||
});
|
|
||||||
|
|
||||||
// 5 requests per hour
|
|
||||||
const passwordLimiter = rateLimit({
|
const passwordLimiter = rateLimit({
|
||||||
windowMs: 60 * 60 * 1000,
|
windowMs: 60 * 60 * 1000,
|
||||||
max: 10,
|
max: 10,
|
||||||
standardHeaders: true,
|
standardHeaders: true,
|
||||||
legacyHeaders: false
|
legacyHeaders: false,
|
||||||
|
keyGenerator: (req, res) => {
|
||||||
|
return req.clientIp
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export { apiLimiter, signupLimiter, loginLimiter, passwordLimiter };
|
export {
|
||||||
|
apiLimiter,
|
||||||
|
authLimiter,
|
||||||
|
passwordLimiter
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,12 +1,81 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Types } from 'mongoose';
|
||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ISecret
|
ISecret,
|
||||||
|
Membership
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { decryptSymmetric } from '../utils/crypto';
|
import {
|
||||||
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
|
EESecretService,
|
||||||
|
EELogService
|
||||||
|
} from '../ee/services';
|
||||||
|
import {
|
||||||
|
IAction
|
||||||
|
} from '../ee/models';
|
||||||
|
import {
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
ACTION_ADD_SECRETS,
|
||||||
|
ACTION_UPDATE_SECRETS,
|
||||||
|
ACTION_DELETE_SECRETS,
|
||||||
|
ACTION_READ_SECRETS
|
||||||
|
} from '../variables';
|
||||||
|
import _ from 'lodash';
|
||||||
|
import { ABILITY_WRITE } from '../variables/organization';
|
||||||
|
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
|
||||||
|
|
||||||
interface PushSecret {
|
/**
|
||||||
|
* Validate that user with id [userId] can modify secrets with ids [secretIds]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.userId - id of user to validate
|
||||||
|
* @param {Object} obj.secretIds - secret ids
|
||||||
|
* @returns {Secret[]} secrets
|
||||||
|
*/
|
||||||
|
const validateSecrets = async ({
|
||||||
|
userId,
|
||||||
|
secretIds
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
secretIds: string[];
|
||||||
|
}) => {
|
||||||
|
let secrets;
|
||||||
|
try {
|
||||||
|
secrets = await Secret.find({
|
||||||
|
_id: {
|
||||||
|
$in: secretIds.map((secretId: string) => new Types.ObjectId(secretId))
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (secrets.length != secretIds.length) {
|
||||||
|
throw BadRequestError({ message: 'Unable to validate some secrets' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMemberships = await Membership.find({ user: userId })
|
||||||
|
const userMembershipById = _.keyBy(userMemberships, 'workspace');
|
||||||
|
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
|
||||||
|
|
||||||
|
// for each secret check if the secret belongs to a workspace the user is a member of
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
if (workspaceIdsSet.has(secret.workspace.toString())) {
|
||||||
|
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
|
||||||
|
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: ABILITY_WRITE });
|
||||||
|
|
||||||
|
if (isDisallowed) {
|
||||||
|
throw UnauthorizedRequestError({ message: 'You do not have the required permissions to perform this action' });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw BadRequestError({ message: 'You cannot edit secrets of a workspace you are not a member of' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
throw BadRequestError({ message: 'Unable to validate secrets' })
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface V1PushSecret {
|
||||||
ciphertextKey: string;
|
ciphertextKey: string;
|
||||||
ivKey: string;
|
ivKey: string;
|
||||||
tagKey: string;
|
tagKey: string;
|
||||||
@@ -15,14 +84,32 @@ interface PushSecret {
|
|||||||
ivValue: string;
|
ivValue: string;
|
||||||
tagValue: string;
|
tagValue: string;
|
||||||
hashValue: string;
|
hashValue: string;
|
||||||
|
ciphertextComment: string;
|
||||||
|
ivComment: string;
|
||||||
|
tagComment: string;
|
||||||
|
hashComment: string;
|
||||||
type: 'shared' | 'personal';
|
type: 'shared' | 'personal';
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Update {
|
interface V2PushSecret {
|
||||||
[index: string]: string;
|
type: string; // personal or shared
|
||||||
|
secretKeyCiphertext: string;
|
||||||
|
secretKeyIV: string;
|
||||||
|
secretKeyTag: string;
|
||||||
|
secretKeyHash: string;
|
||||||
|
secretValueCiphertext: string;
|
||||||
|
secretValueIV: string;
|
||||||
|
secretValueTag: string;
|
||||||
|
secretValueHash: string;
|
||||||
|
secretCommentCiphertext?: string;
|
||||||
|
secretCommentIV?: string;
|
||||||
|
secretCommentTag?: string;
|
||||||
|
secretCommentHash?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type DecryptSecretType = 'text' | 'object' | 'expanded';
|
interface Update {
|
||||||
|
[index: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Push secrets for user with id [userId] to workspace
|
* Push secrets for user with id [userId] to workspace
|
||||||
@@ -35,92 +122,155 @@ type DecryptSecretType = 'text' | 'object' | 'expanded';
|
|||||||
* @param {String} obj.environment - environment for secrets
|
* @param {String} obj.environment - environment for secrets
|
||||||
* @param {Object[]} obj.secrets - secrets to push
|
* @param {Object[]} obj.secrets - secrets to push
|
||||||
*/
|
*/
|
||||||
const pushSecrets = async ({
|
const v1PushSecrets = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
secrets
|
secrets,
|
||||||
}: {
|
}: {
|
||||||
userId: string;
|
userId: string;
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
secrets: PushSecret[];
|
secrets: V1PushSecret[];
|
||||||
}): Promise<void> => {
|
}): Promise<void> => {
|
||||||
|
// TODO: clean up function and fix up types
|
||||||
try {
|
try {
|
||||||
// construct useful data structures
|
// construct useful data structures
|
||||||
const oldSecrets = await pullSecrets({
|
const oldSecrets = await getSecrets({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment
|
||||||
});
|
});
|
||||||
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => {
|
|
||||||
return { ...accumulator, [s.secretKeyHash]: s };
|
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
|
||||||
}, {});
|
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||||
const newSecretsObj = secrets.reduce((accumulator, s) => {
|
, {});
|
||||||
return { ...accumulator, [s.hashKey]: s };
|
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
|
||||||
}, {});
|
({ ...accumulator, [`${s.type}-${s.hashKey}`]: s })
|
||||||
|
, {});
|
||||||
|
|
||||||
// handle deleting secrets
|
// handle deleting secrets
|
||||||
const toDelete = oldSecrets.filter(
|
const toDelete = oldSecrets
|
||||||
(s: ISecret) => !(s.secretKeyHash in newSecretsObj)
|
.filter(
|
||||||
);
|
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
|
||||||
|
)
|
||||||
|
.map((s) => s._id);
|
||||||
if (toDelete.length > 0) {
|
if (toDelete.length > 0) {
|
||||||
await Secret.deleteMany({
|
await Secret.deleteMany({
|
||||||
_id: { $in: toDelete.map((s) => s._id) }
|
_id: { $in: toDelete }
|
||||||
|
});
|
||||||
|
|
||||||
|
await EESecretService.markDeletedSecretVersions({
|
||||||
|
secretIds: toDelete
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// handle modifying secrets where type or value changed
|
const toUpdate = oldSecrets
|
||||||
const operations = secrets
|
|
||||||
.filter((s) => {
|
.filter((s) => {
|
||||||
if (s.hashKey in oldSecretsObj) {
|
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||||
if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) {
|
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
|
||||||
// case: filter secrets where value changed
|
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
|
||||||
|
// case: filter secrets where value or comment changed
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (s.type !== oldSecretsObj[s.hashKey].type) {
|
if (!s.version) {
|
||||||
// case: filter secrets where type changed
|
// case: filter (legacy) secrets that were not versioned
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
})
|
});
|
||||||
|
|
||||||
|
const operations = toUpdate
|
||||||
.map((s) => {
|
.map((s) => {
|
||||||
|
const {
|
||||||
|
ciphertextValue,
|
||||||
|
ivValue,
|
||||||
|
tagValue,
|
||||||
|
hashValue,
|
||||||
|
ciphertextComment,
|
||||||
|
ivComment,
|
||||||
|
tagComment,
|
||||||
|
hashComment
|
||||||
|
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||||
|
|
||||||
const update: Update = {
|
const update: Update = {
|
||||||
type: s.type,
|
secretValueCiphertext: ciphertextValue,
|
||||||
secretValueCiphertext: s.ciphertextValue,
|
secretValueIV: ivValue,
|
||||||
secretValueIV: s.ivValue,
|
secretValueTag: tagValue,
|
||||||
secretValueTag: s.tagValue,
|
secretValueHash: hashValue,
|
||||||
secretValueHash: s.hashValue
|
secretCommentCiphertext: ciphertextComment,
|
||||||
};
|
secretCommentIV: ivComment,
|
||||||
|
secretCommentTag: tagComment,
|
||||||
|
secretCommentHash: hashComment,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s.version) {
|
||||||
|
// case: (legacy) secret was not versioned
|
||||||
|
update.version = 1;
|
||||||
|
} else {
|
||||||
|
update['$inc'] = {
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (s.type === SECRET_PERSONAL) {
|
if (s.type === SECRET_PERSONAL) {
|
||||||
// attach user assocaited with the personal secret
|
// attach user associated with the personal secret
|
||||||
update['user'] = userId;
|
update['user'] = userId;
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
updateOne: {
|
updateOne: {
|
||||||
filter: {
|
filter: {
|
||||||
workspace: workspaceId,
|
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||||
_id: oldSecretsObj[s.hashKey]._id
|
|
||||||
},
|
},
|
||||||
update
|
update
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
const a = await Secret.bulkWrite(operations as any);
|
await Secret.bulkWrite(operations as any);
|
||||||
|
|
||||||
|
// (EE) add secret versions for updated secrets
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: toUpdate.map(({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
type,
|
||||||
|
secretKeyHash,
|
||||||
|
}) => {
|
||||||
|
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
|
||||||
|
return ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version: version ? version + 1 : 1,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
type: newSecret.type,
|
||||||
|
user: new Types.ObjectId(userId),
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext: newSecret.ciphertextKey,
|
||||||
|
secretKeyIV: newSecret.ivKey,
|
||||||
|
secretKeyTag: newSecret.tagKey,
|
||||||
|
secretKeyHash: newSecret.hashKey,
|
||||||
|
secretValueCiphertext: newSecret.ciphertextValue,
|
||||||
|
secretValueIV: newSecret.ivValue,
|
||||||
|
secretValueTag: newSecret.tagValue,
|
||||||
|
secretValueHash: newSecret.hashValue
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
// handle adding new secrets
|
// handle adding new secrets
|
||||||
const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj));
|
const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj));
|
||||||
|
|
||||||
if (toAdd.length > 0) {
|
if (toAdd.length > 0) {
|
||||||
// add secrets
|
// add secrets
|
||||||
await Secret.insertMany(
|
const newSecrets = await Secret.insertMany(
|
||||||
toAdd.map((s, idx) => {
|
toAdd.map((s, idx) => {
|
||||||
let obj: any = {
|
const obj: any = {
|
||||||
|
version: 1,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
type: toAdd[idx].type,
|
type: toAdd[idx].type,
|
||||||
environment,
|
environment,
|
||||||
@@ -131,7 +281,11 @@ const pushSecrets = async ({
|
|||||||
secretValueCiphertext: s.ciphertextValue,
|
secretValueCiphertext: s.ciphertextValue,
|
||||||
secretValueIV: s.ivValue,
|
secretValueIV: s.ivValue,
|
||||||
secretValueTag: s.tagValue,
|
secretValueTag: s.tagValue,
|
||||||
secretValueHash: s.hashValue
|
secretValueHash: s.hashValue,
|
||||||
|
secretCommentCiphertext: s.ciphertextComment,
|
||||||
|
secretCommentIV: s.ivComment,
|
||||||
|
secretCommentTag: s.tagComment,
|
||||||
|
secretCommentHash: s.hashComment
|
||||||
};
|
};
|
||||||
|
|
||||||
if (toAdd[idx].type === 'personal') {
|
if (toAdd[idx].type === 'personal') {
|
||||||
@@ -141,6 +295,270 @@ const pushSecrets = async ({
|
|||||||
return obj;
|
return obj;
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// (EE) add secret versions for new secrets
|
||||||
|
EESecretService.addSecretVersions({
|
||||||
|
secretVersions: newSecrets.map(({
|
||||||
|
_id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}) => ({
|
||||||
|
_id: new Types.ObjectId(),
|
||||||
|
secret: _id,
|
||||||
|
version,
|
||||||
|
workspace,
|
||||||
|
type,
|
||||||
|
user,
|
||||||
|
environment,
|
||||||
|
isDeleted: false,
|
||||||
|
secretKeyCiphertext,
|
||||||
|
secretKeyIV,
|
||||||
|
secretKeyTag,
|
||||||
|
secretKeyHash,
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to push shared and personal secrets');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push secrets for user with id [userId] to workspace
|
||||||
|
* with id [workspaceId] with environment [environment]. Follow steps:
|
||||||
|
* 1. Handle shared secrets (insert, delete)
|
||||||
|
* 2. handle personal secrets (insert, delete)
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user to push secrets for
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to push to
|
||||||
|
* @param {String} obj.environment - environment for secrets
|
||||||
|
* @param {Object[]} obj.secrets - secrets to push
|
||||||
|
* @param {String} obj.channel - channel (web/cli/auto)
|
||||||
|
* @param {String} obj.ipAddress - ip address of request to push secrets
|
||||||
|
*/
|
||||||
|
const v2PushSecrets = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secrets,
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
secrets: V2PushSecret[];
|
||||||
|
channel: string;
|
||||||
|
ipAddress: string;
|
||||||
|
}): Promise<void> => {
|
||||||
|
// TODO: clean up function and fix up types
|
||||||
|
try {
|
||||||
|
const actions: IAction[] = [];
|
||||||
|
|
||||||
|
// construct useful data structures
|
||||||
|
const oldSecrets = await getSecrets({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
});
|
||||||
|
|
||||||
|
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
|
||||||
|
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||||
|
, {});
|
||||||
|
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
|
||||||
|
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
|
||||||
|
, {});
|
||||||
|
|
||||||
|
// handle deleting secrets
|
||||||
|
const toDelete = oldSecrets
|
||||||
|
.filter(
|
||||||
|
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
|
||||||
|
)
|
||||||
|
.map((s) => s._id);
|
||||||
|
if (toDelete.length > 0) {
|
||||||
|
await Secret.deleteMany({
|
||||||
|
_id: { $in: toDelete }
|
||||||
|
});
|
||||||
|
|
||||||
|
await EESecretService.markDeletedSecretVersions({
|
||||||
|
secretIds: toDelete
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_DELETE_SECRETS,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds: toDelete
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteAction && actions.push(deleteAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toUpdate = oldSecrets
|
||||||
|
.filter((s) => {
|
||||||
|
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
|
||||||
|
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|
||||||
|
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
|
||||||
|
// case: filter secrets where value or comment changed
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s.version) {
|
||||||
|
// case: filter (legacy) secrets that were not versioned
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (toUpdate.length > 0) {
|
||||||
|
const operations = toUpdate
|
||||||
|
.map((s) => {
|
||||||
|
const {
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag,
|
||||||
|
secretCommentHash,
|
||||||
|
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
|
||||||
|
|
||||||
|
const update: Update = {
|
||||||
|
secretValueCiphertext,
|
||||||
|
secretValueIV,
|
||||||
|
secretValueTag,
|
||||||
|
secretValueHash,
|
||||||
|
secretCommentCiphertext,
|
||||||
|
secretCommentIV,
|
||||||
|
secretCommentTag,
|
||||||
|
secretCommentHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!s.version) {
|
||||||
|
// case: (legacy) secret was not versioned
|
||||||
|
update.version = 1;
|
||||||
|
} else {
|
||||||
|
update['$inc'] = {
|
||||||
|
version: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (s.type === SECRET_PERSONAL) {
|
||||||
|
// attach user associated with the personal secret
|
||||||
|
update['user'] = userId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
updateOne: {
|
||||||
|
filter: {
|
||||||
|
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
|
||||||
|
},
|
||||||
|
update
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
await Secret.bulkWrite(operations as any);
|
||||||
|
|
||||||
|
// (EE) add secret versions for updated secrets
|
||||||
|
await EESecretService.addSecretVersions({
|
||||||
|
secretVersions: toUpdate.map((s) => {
|
||||||
|
return ({
|
||||||
|
...newSecretsObj[`${s.type}-${s.secretKeyHash}`],
|
||||||
|
secret: s._id,
|
||||||
|
version: s.version ? s.version + 1 : 1,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
user: s.user,
|
||||||
|
environment: s.environment,
|
||||||
|
isDeleted: false
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const updateAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_UPDATE_SECRETS,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds: toUpdate.map((u) => u._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
updateAction && actions.push(updateAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle adding new secrets
|
||||||
|
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
|
||||||
|
|
||||||
|
if (toAdd.length > 0) {
|
||||||
|
// add secrets
|
||||||
|
const newSecrets = await Secret.insertMany(
|
||||||
|
toAdd.map((s, idx) => ({
|
||||||
|
...s,
|
||||||
|
version: 1,
|
||||||
|
workspace: workspaceId,
|
||||||
|
type: toAdd[idx].type,
|
||||||
|
environment,
|
||||||
|
...(toAdd[idx].type === 'personal' ? { user: userId } : {})
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
// (EE) add secret versions for new secrets
|
||||||
|
EESecretService.addSecretVersions({
|
||||||
|
secretVersions: newSecrets.map((secretDocument) => {
|
||||||
|
return {
|
||||||
|
...secretDocument.toObject(),
|
||||||
|
secret: secretDocument._id,
|
||||||
|
isDeleted: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const addAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_ADD_SECRETS,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds: newSecrets.map((n) => n._id)
|
||||||
|
});
|
||||||
|
addAction && actions.push(addAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// (EE) take a secret snapshot
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId
|
||||||
|
})
|
||||||
|
|
||||||
|
// (EE) create (audit) log
|
||||||
|
if (actions.length > 0) {
|
||||||
|
await EELogService.createLog({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
actions,
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
@@ -150,15 +568,14 @@ const pushSecrets = async ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pull secrets for user with id [userId] for workspace
|
* Get secrets for user with id [userId] for workspace
|
||||||
* with id [workspaceId] with environment [environment]
|
* with id [workspaceId] with environment [environment]
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.userId -id of user to pull secrets for
|
* @param {String} obj.userId -id of user to pull secrets for
|
||||||
* @param {String} obj.workspaceId - id of workspace to pull from
|
* @param {String} obj.workspaceId - id of workspace to pull from
|
||||||
* @param {String} obj.environment - environment for secrets
|
* @param {String} obj.environment - environment for secrets
|
||||||
*
|
|
||||||
*/
|
*/
|
||||||
const pullSecrets = async ({
|
const getSecrets = async ({
|
||||||
userId,
|
userId,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment
|
environment
|
||||||
@@ -168,6 +585,7 @@ const pullSecrets = async ({
|
|||||||
environment: string;
|
environment: string;
|
||||||
}): Promise<ISecret[]> => {
|
}): Promise<ISecret[]> => {
|
||||||
let secrets: any; // TODO: FIX any
|
let secrets: any; // TODO: FIX any
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// get shared workspace secrets
|
// get shared workspace secrets
|
||||||
const sharedSecrets = await Secret.find({
|
const sharedSecrets = await Secret.find({
|
||||||
@@ -195,9 +613,64 @@ const pullSecrets = async ({
|
|||||||
return secrets;
|
return secrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pull secrets for user with id [userId] for workspace
|
||||||
|
* with id [workspaceId] with environment [environment]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId -id of user to pull secrets for
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to pull from
|
||||||
|
* @param {String} obj.environment - environment for secrets
|
||||||
|
* @param {String} obj.channel - channel (web/cli/auto)
|
||||||
|
* @param {String} obj.ipAddress - ip address of request to push secrets
|
||||||
|
*/
|
||||||
|
const pullSecrets = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
channel: string;
|
||||||
|
ipAddress: string;
|
||||||
|
}): Promise<ISecret[]> => {
|
||||||
|
let secrets: any;
|
||||||
|
|
||||||
|
try {
|
||||||
|
secrets = await getSecrets({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
})
|
||||||
|
|
||||||
|
const readAction = await EELogService.createActionSecret({
|
||||||
|
name: ACTION_READ_SECRETS,
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
secretIds: secrets.map((n: any) => n._id)
|
||||||
|
});
|
||||||
|
|
||||||
|
readAction && await EELogService.createLog({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
actions: [readAction],
|
||||||
|
channel,
|
||||||
|
ipAddress
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to pull shared and personal secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return secrets;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Reformat output of pullSecrets() to be compatible with how existing
|
* Reformat output of pullSecrets() to be compatible with how existing
|
||||||
* clients handle secrets
|
* web client handle secrets
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Object} obj.secrets
|
* @param {Object} obj.secrets
|
||||||
*/
|
*/
|
||||||
@@ -222,6 +695,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
|||||||
iv: s.secretValueIV,
|
iv: s.secretValueIV,
|
||||||
tag: s.secretValueTag,
|
tag: s.secretValueTag,
|
||||||
hash: s.secretValueHash
|
hash: s.secretValueHash
|
||||||
|
},
|
||||||
|
secretComment: {
|
||||||
|
workspace: s.workspace,
|
||||||
|
ciphertext: s.secretCommentCiphertext,
|
||||||
|
iv: s.secretCommentIV,
|
||||||
|
tag: s.secretCommentTag,
|
||||||
|
hash: s.secretCommentHash
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -233,71 +713,10 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
|
|||||||
return reformatedSecrets;
|
return reformatedSecrets;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Return decrypted secrets in format [format]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects
|
|
||||||
* @param {String} obj.key - symmetric key to decrypt secret key-value pairs
|
|
||||||
* @param {String} obj.format - desired return format that is either "text," "object," or "expanded"
|
|
||||||
* @return {String|Object} (decrypted) secrets also called the content
|
|
||||||
*/
|
|
||||||
const decryptSecrets = ({
|
|
||||||
secrets,
|
|
||||||
key,
|
|
||||||
format
|
|
||||||
}: {
|
|
||||||
secrets: PushSecret[];
|
|
||||||
key: string;
|
|
||||||
format: DecryptSecretType;
|
|
||||||
}) => {
|
|
||||||
// init content
|
|
||||||
let content: any = format === 'text' ? '' : {};
|
|
||||||
|
|
||||||
// decrypt secrets
|
|
||||||
secrets.forEach((s, idx) => {
|
|
||||||
const secretKey = decryptSymmetric({
|
|
||||||
ciphertext: s.ciphertextKey,
|
|
||||||
iv: s.ivKey,
|
|
||||||
tag: s.tagKey,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
const secretValue = decryptSymmetric({
|
|
||||||
ciphertext: s.ciphertextValue,
|
|
||||||
iv: s.ivValue,
|
|
||||||
tag: s.tagValue,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
|
|
||||||
switch (format) {
|
|
||||||
case 'text':
|
|
||||||
content += secretKey;
|
|
||||||
content += '=';
|
|
||||||
content += secretValue;
|
|
||||||
|
|
||||||
if (idx < secrets.length) {
|
|
||||||
content += '\n';
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'object':
|
|
||||||
content[secretKey] = secretValue;
|
|
||||||
break;
|
|
||||||
case 'expanded':
|
|
||||||
content[secretKey] = {
|
|
||||||
...s,
|
|
||||||
plaintextKey: secretKey,
|
|
||||||
plaintextValue: secretValue
|
|
||||||
};
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return content;
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
pushSecrets,
|
validateSecrets,
|
||||||
|
v1PushSecrets,
|
||||||
|
v2PushSecrets,
|
||||||
pullSecrets,
|
pullSecrets,
|
||||||
reformatPullSecrets,
|
reformatPullSecrets
|
||||||
decryptSecrets
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { createOrganization } from './organization';
|
|||||||
import { addMembershipsOrg } from './membershipOrg';
|
import { addMembershipsOrg } from './membershipOrg';
|
||||||
import { createWorkspace } from './workspace';
|
import { createWorkspace } from './workspace';
|
||||||
import { addMemberships } from './membership';
|
import { addMemberships } from './membership';
|
||||||
import { OWNER, ADMIN, ACCEPTED, GRANTED } from '../variables';
|
import { OWNER, ADMIN, ACCEPTED } from '../variables';
|
||||||
import { sendMail } from '../helpers/nodemailer';
|
import { sendMail } from '../helpers/nodemailer';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,7 +66,7 @@ const checkEmailVerification = async ({
|
|||||||
email,
|
email,
|
||||||
token: code
|
token: code
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!token) throw new Error('Failed to find email verification token');
|
if (!token) throw new Error('Failed to find email verification token');
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
@@ -113,11 +113,10 @@ const initializeDefaultOrg = async ({
|
|||||||
await addMemberships({
|
await addMemberships({
|
||||||
userIds: [user._id.toString()],
|
userIds: [user._id.toString()],
|
||||||
workspaceId: workspace._id.toString(),
|
workspaceId: workspace._id.toString(),
|
||||||
roles: [ADMIN],
|
roles: [ADMIN]
|
||||||
statuses: [GRANTED]
|
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new Error('Failed to initialize default organization and workspace');
|
throw new Error(`Failed to initialize default organization and workspace [err=${err}]`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
|
Bot,
|
||||||
Membership,
|
Membership,
|
||||||
Key,
|
Key,
|
||||||
Secret
|
Secret
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import { createBot } from '../helpers/bot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a workspace with name [name] in organization with id [organizationId]
|
* Create a workspace with name [name] in organization with id [organizationId]
|
||||||
|
* and a bot for it.
|
||||||
* @param {String} name - name of workspace to create.
|
* @param {String} name - name of workspace to create.
|
||||||
* @param {String} organizationId - id of organization to create workspace in
|
* @param {String} organizationId - id of organization to create workspace in
|
||||||
* @param {Object} workspace - new workspace
|
* @param {Object} workspace - new workspace
|
||||||
@@ -21,10 +24,16 @@ const createWorkspace = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
|
// create workspace
|
||||||
workspace = await new Workspace({
|
workspace = await new Workspace({
|
||||||
name,
|
name,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
|
const bot = await createBot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspaceId: workspace._id.toString()
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -43,6 +52,9 @@ const createWorkspace = async ({
|
|||||||
const deleteWorkspace = async ({ id }: { id: string }) => {
|
const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||||
try {
|
try {
|
||||||
await Workspace.deleteOne({ _id: id });
|
await Workspace.deleteOne({ _id: id });
|
||||||
|
await Bot.deleteOne({
|
||||||
|
workspace: id
|
||||||
|
});
|
||||||
await Membership.deleteMany({
|
await Membership.deleteMany({
|
||||||
workspace: id
|
workspace: id
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,127 +1,28 @@
|
|||||||
/* eslint-disable no-console */
|
|
||||||
import http from 'http';
|
|
||||||
import express from 'express';
|
|
||||||
import helmet from 'helmet';
|
|
||||||
import cors from 'cors';
|
|
||||||
import cookieParser from 'cookie-parser';
|
|
||||||
import mongoose from 'mongoose';
|
|
||||||
import dotenv from 'dotenv';
|
import dotenv from 'dotenv';
|
||||||
|
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL } from './config';
|
import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
|
||||||
import { apiLimiter } from './helpers/rateLimiter';
|
import { server } from './app';
|
||||||
import { createTerminus } from '@godaddy/terminus';
|
import { DatabaseService } from './services';
|
||||||
|
import { setUpHealthEndpoint } from './services/health';
|
||||||
|
import { initSmtp } from './services/smtp';
|
||||||
|
import { setTransporter } from './helpers/nodemailer';
|
||||||
|
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
|
||||||
|
|
||||||
const app = express();
|
DatabaseService.initDatabase(MONGO_URL);
|
||||||
|
|
||||||
Sentry.init({
|
setUpHealthEndpoint(server);
|
||||||
dsn: SENTRY_DSN,
|
|
||||||
tracesSampleRate: 1.0,
|
|
||||||
debug: NODE_ENV === 'production' ? false : true,
|
|
||||||
environment: NODE_ENV
|
|
||||||
});
|
|
||||||
|
|
||||||
import {
|
setTransporter(initSmtp());
|
||||||
signup as signupRouter,
|
|
||||||
auth as authRouter,
|
|
||||||
organization as organizationRouter,
|
|
||||||
workspace as workspaceRouter,
|
|
||||||
membershipOrg as membershipOrgRouter,
|
|
||||||
membership as membershipRouter,
|
|
||||||
key as keyRouter,
|
|
||||||
inviteOrg as inviteOrgRouter,
|
|
||||||
user as userRouter,
|
|
||||||
userAction as userActionRouter,
|
|
||||||
secret as secretRouter,
|
|
||||||
serviceToken as serviceTokenRouter,
|
|
||||||
password as passwordRouter,
|
|
||||||
stripe as stripeRouter,
|
|
||||||
integration as integrationRouter,
|
|
||||||
integrationAuth as integrationAuthRouter
|
|
||||||
} from './routes';
|
|
||||||
|
|
||||||
const connectWithRetry = () => {
|
if (NODE_ENV !== 'test') {
|
||||||
mongoose
|
Sentry.init({
|
||||||
.connect(MONGO_URL)
|
dsn: SENTRY_DSN,
|
||||||
.then(() => console.log('Successfully connected to DB'))
|
tracesSampleRate: 1.0,
|
||||||
.catch((e) => {
|
debug: NODE_ENV === 'production' ? false : true,
|
||||||
console.log('Failed to connect to DB ', e);
|
environment: NODE_ENV
|
||||||
setTimeout(() => {
|
});
|
||||||
console.log(e);
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
return mongoose.connection;
|
|
||||||
};
|
|
||||||
|
|
||||||
const dbConnection = connectWithRetry();
|
|
||||||
|
|
||||||
app.enable('trust proxy');
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(
|
|
||||||
cors({
|
|
||||||
credentials: true,
|
|
||||||
origin: SITE_URL
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
if (NODE_ENV === 'production') {
|
|
||||||
// enable app-wide rate-limiting + helmet security
|
|
||||||
// in production
|
|
||||||
app.disable('x-powered-by');
|
|
||||||
app.use(apiLimiter);
|
|
||||||
app.use(helmet());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
app.use(express.json());
|
createTestUserForDevelopment()
|
||||||
|
|
||||||
// routers
|
|
||||||
app.use('/api/v1/signup', signupRouter);
|
|
||||||
app.use('/api/v1/auth', authRouter);
|
|
||||||
app.use('/api/v1/user', userRouter);
|
|
||||||
app.use('/api/v1/user-action', userActionRouter);
|
|
||||||
app.use('/api/v1/organization', organizationRouter);
|
|
||||||
app.use('/api/v1/workspace', workspaceRouter);
|
|
||||||
app.use('/api/v1/membership-org', membershipOrgRouter);
|
|
||||||
app.use('/api/v1/membership', membershipRouter);
|
|
||||||
app.use('/api/v1/key', keyRouter);
|
|
||||||
app.use('/api/v1/invite-org', inviteOrgRouter);
|
|
||||||
app.use('/api/v1/secret', secretRouter);
|
|
||||||
app.use('/api/v1/service-token', serviceTokenRouter);
|
|
||||||
app.use('/api/v1/password', passwordRouter);
|
|
||||||
app.use('/api/v1/stripe', stripeRouter);
|
|
||||||
app.use('/api/v1/integration', integrationRouter);
|
|
||||||
app.use('/api/v1/integration-auth', integrationAuthRouter);
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
|
||||||
|
|
||||||
const onSignal = () => {
|
|
||||||
console.log('Server is starting clean-up');
|
|
||||||
return Promise.all([
|
|
||||||
() => {
|
|
||||||
dbConnection.close(() => {
|
|
||||||
console.info('Database connection closed');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const healthCheck = () => {
|
|
||||||
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
|
|
||||||
return Promise
|
|
||||||
.resolve
|
|
||||||
// optionally include a resolve value to be included as
|
|
||||||
// info in the health check response
|
|
||||||
();
|
|
||||||
};
|
|
||||||
|
|
||||||
createTerminus(server, {
|
|
||||||
healthChecks: {
|
|
||||||
'/healthcheck': healthCheck,
|
|
||||||
onSignal
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
console.log('Listening on PORT ' + PORT);
|
|
||||||
});
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user