Compare commits

..

2 Commits

Author SHA1 Message Date
d83220cdc3 Update README.md 2023-03-22 19:37:51 -07:00
8aa44ba103 Add cli guide to read me 2023-03-22 13:16:37 -07:00
2180 changed files with 64460 additions and 171792 deletions

View File

@ -1,10 +0,0 @@
backend/node_modules
frontend/node_modules
backend/frontend-build
**/node_modules
**/.next
.dockerignore
.git
README.md
.dockerignore
**/Dockerfile

View File

@ -1,12 +1,20 @@
# Keys # Keys
# Required key for platform encryption/decryption ops # Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE= JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
# JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string
# describing a time span (e.g. 60, "2 days", "10h", "7d")
JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME=
# MongoDB # MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
@ -14,9 +22,6 @@ AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
# Required # Required
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
# Redis
REDIS_URL=redis://redis:6379
# Optional credentials for MongoDB container instance and Mongo-Express # Optional credentials for MongoDB container instance and Mongo-Express
MONGO_USERNAME=root MONGO_USERNAME=root
MONGO_PASSWORD=example MONGO_PASSWORD=example
@ -25,12 +30,14 @@ MONGO_PASSWORD=example
# Required # Required
SITE_URL=http://localhost:8080 SITE_URL=http://localhost:8080
# Mail/SMTP # Mail/SMTP
SMTP_HOST= SMTP_HOST= # required
SMTP_PORT= SMTP_USERNAME= # required
SMTP_NAME= SMTP_PASSWORD= # required
SMTP_USERNAME= SMTP_PORT=587
SMTP_PASSWORD= SMTP_SECURE=false
SMTP_FROM_ADDRESS= # required
SMTP_FROM_NAME=Infisical
# Integration # Integration
# Optional only if integration is used # Optional only if integration is used
@ -39,13 +46,11 @@ CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY= CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB= CLIENT_ID_GITHUB=
CLIENT_ID_GITLAB= CLIENT_ID_GITLAB=
CLIENT_ID_BITBUCKET=
CLIENT_SECRET_HEROKU= CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL= CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY= CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB= CLIENT_SECRET_GITHUB=
CLIENT_SECRET_GITLAB= CLIENT_SECRET_GITLAB=
CLIENT_SECRET_BITBUCKET=
CLIENT_SLUG_VERCEL= CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors # Sentry (optional) for monitoring errors
@ -55,13 +60,10 @@ SENTRY_DSN=
# Ignore - Not applicable for self-hosted version # Ignore - Not applicable for self-hosted version
POSTHOG_HOST= POSTHOG_HOST=
POSTHOG_PROJECT_API_KEY= POSTHOG_PROJECT_API_KEY=
STRIPE_SECRET_KEY=
# SSO-specific variables STRIPE_PUBLISHABLE_KEY=
CLIENT_ID_GOOGLE_LOGIN= STRIPE_WEBHOOK_SECRET=
CLIENT_SECRET_GOOGLE_LOGIN= STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM=
CLIENT_ID_GITHUB_LOGIN= STRIPE_PRODUCT_PRO=
CLIENT_SECRET_GITHUB_LOGIN= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
CLIENT_ID_GITLAB_LOGIN=
CLIENT_SECRET_GITLAB_LOGIN=

View File

@ -8,7 +8,7 @@ assignees: ''
--- ---
### Feature description ### Feature description
A clear and concise description of what the feature should be. A clear and concise description of what the the feature should be.
### Why would it be useful? ### Why would it be useful?
Why would this feature be useful for Infisical users? Why would this feature be useful for Infisical users?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,6 +1,6 @@
# Description 📣 # Description 📣
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. --> *Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.*
## Type ✨ ## Type ✨
@ -11,7 +11,7 @@
# Tests 🛠️ # Tests 🛠️
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible --> *Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. You may want to add screenshots when relevant and possible*
```sh ```sh
# Here's some code block to paste some code snippets # Here's some code block to paste some code snippets

View File

@ -6,14 +6,13 @@ services:
restart: unless-stopped restart: unless-stopped
depends_on: depends_on:
- mongo - mongo
image: infisical/infisical:test image: infisical/backend:test
command: npm run start command: npm run start
environment: environment:
- NODE_ENV=production - NODE_ENV=production
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin - MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
- MONGO_USERNAME=test - MONGO_USERNAME=test
- MONGO_PASSWORD=example - MONGO_PASSWORD=example
- ENCRYPTION_KEY=a984ecdf82ec779e55dbcc21303a900f
networks: networks:
- infisical-test - infisical-test

38
.github/values.yaml vendored
View File

@ -1,3 +1,22 @@
frontend:
enabled: true
name: frontend
podAnnotations: {}
deploymentAnnotations:
secrets.infisical.com/auto-reload: "true"
replicaCount: 2
image:
repository: infisical/frontend
tag: "latest"
pullPolicy: Always
kubeSecretRef: managed-secret-frontend
service:
annotations: {}
type: ClusterIP
nodePort: ""
frontendEnvironmentVariables: null
backend: backend:
enabled: true enabled: true
name: backend name: backend
@ -6,7 +25,7 @@ backend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/staging_infisical repository: infisical/backend
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-backend-secret kubeSecretRef: managed-backend-secret
@ -14,15 +33,12 @@ backend:
annotations: {} annotations: {}
type: ClusterIP type: ClusterIP
nodePort: "" nodePort: ""
resources:
limits:
memory: 300Mi
backendEnvironmentVariables: null backendEnvironmentVariables: null
## Mongo DB persistence ## Mongo DB persistence
mongodb: mongodb:
enabled: false enabled: true
persistence: persistence:
enabled: false enabled: false
@ -35,10 +51,16 @@ mongodbConnection:
ingress: ingress:
enabled: true enabled: true
# annotations: annotations:
# kubernetes.io/ingress.class: "nginx" kubernetes.io/ingress.class: "nginx"
# cert-manager.io/issuer: letsencrypt-nginx # cert-manager.io/issuer: letsencrypt-nginx
hostName: gamma.infisical.com ## <- Replace with your own domain hostName: gamma.infisical.com ## <- Replace with your own domain
frontend:
path: /
pathType: Prefix
backend:
path: /api
pathType: Prefix
tls: tls:
[] []
# - secretName: letsencrypt-nginx # - secretName: letsencrypt-nginx

View File

@ -1,99 +0,0 @@
name: Build, Publish and Deploy to Gamma
on: [workflow_dispatch]
jobs:
infisical-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: .
file: Dockerfile.standalone-infisical
tags: infisical/infisical:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- name: 🧪 Test backend image
run: |
./.github/resources/healthcheck.sh infisical-backend-test
- name: ⏻ Shut down backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
file: Dockerfile.standalone-infisical
tags: |
infisical/staging_infisical:${{ steps.commit.outputs.short }}
infisical/staging_infisical:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
gamma-deployment:
name: Deploy to gamma
runs-on: ubuntu-latest
needs: [infisical-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 --wait --install
if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed"
exit 1
else
echo "Helm upgrade was successful"
fi

View File

@ -13,7 +13,6 @@ jobs:
check-be-pr: check-be-pr:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
@ -27,17 +26,17 @@ jobs:
- name: 📦 Install dependencies - name: 📦 Install dependencies
run: npm ci --only-production run: npm ci --only-production
working-directory: backend working-directory: backend
# - name: 🧪 Run tests - name: 🧪 Run tests
# run: npm run test:ci run: npm run test:ci
# working-directory: backend working-directory: backend
# - name: 📁 Upload test results - name: 📁 Upload test results
# uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v3
# if: always() if: always()
# with: with:
# name: be-test-results name: be-test-results
# path: | path: |
# ./backend/reports ./backend/reports
# ./backend/coverage ./backend/coverage
- name: 🏗️ Run build - name: 🏗️ Run build
run: npm run build run: npm run build
working-directory: backend working-directory: backend

View File

@ -2,35 +2,40 @@ name: Check Frontend Pull Request
on: on:
pull_request: pull_request:
types: [opened, synchronize] types: [ opened, synchronize ]
paths: paths:
- "frontend/**" - 'frontend/**'
- "!frontend/README.md" - '!frontend/README.md'
- "!frontend/.*" - '!frontend/.*'
- "frontend/.eslintrc.js" - 'frontend/.eslintrc.js'
jobs: jobs:
check-fe-pr: check-fe-pr:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- name: ☁️ Checkout source -
name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 🔧 Setup Node 16 -
name: 🔧 Setup Node 16
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: "16" node-version: '16'
cache: "npm" cache: 'npm'
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
- name: 📦 Install dependencies -
name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts run: npm ci --only-production --ignore-scripts
working-directory: frontend working-directory: frontend
# - # -
# name: 🧪 Run tests # name: 🧪 Run tests
# run: npm run test:ci # run: npm run test:ci
# working-directory: frontend # working-directory: frontend
- name: 🏗️ Run build -
name: 🏗️ Run build
run: npm run build run: npm run build
working-directory: frontend working-directory: frontend

View File

@ -1,25 +1,14 @@
name: Release production images (frontend, backend) name: Build, Publish and Deploy to Gamma
on: on: [workflow_dispatch]
push:
tags:
- "infisical/v*.*.*"
jobs: jobs:
backend-image: backend-image:
name: Build backend image name: Build backend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: Save commit hashes for tag - name: Save commit hashes for tag
id: commit id: commit
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
@ -39,7 +28,7 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true load: true
context: backend context: backend
tags: infisical/infisical:test tags: infisical/backend:test
- name: ⏻ Spawn backend container and dependencies - name: ⏻ Spawn backend container and dependencies
run: | run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
@ -56,19 +45,15 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true push: true
context: backend context: backend
tags: | tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:${{ steps.commit.outputs.short }} infisical/backend:latest
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
frontend-image: frontend-image:
name: Build frontend image name: Build frontend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: Save commit hashes for tag - name: Save commit hashes for tag
@ -93,7 +78,6 @@ jobs:
tags: infisical/frontend:test tags: infisical/frontend:test
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
- name: ⏻ Spawn frontend container - name: ⏻ Spawn frontend container
run: | run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test docker run -d --rm --name infisical-frontend-test infisical/frontend:test
@ -110,11 +94,45 @@ jobs:
push: true push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend context: frontend
tags: | tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:${{ steps.commit.outputs.short }} infisical/frontend:latest
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
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 }}
NEXT_INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }} 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

View File

@ -1,78 +0,0 @@
name: Release standalone docker image
on:
push:
tags:
- "infisical/v*.*.*"
jobs:
infisical-standalone:
name: Build infisical standalone image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- uses: paulhatch/semantic-version@v5.0.2
id: version
with:
# The prefix to use to identify tags
tag_prefix: "infisical-standalone/v"
# A string which, if present in a git commit, indicates that a change represents a
# major (breaking) change, supports regular expressions wrapped with '/'
major_pattern: "(MAJOR)"
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
minor_pattern: "(MINOR)"
# A string to determine the format of the version output
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
# Optional path to check for changes. If any changes are detected in the path the
# 'changed' output will true. Enter multiple paths separated by spaces.
change_path: "backend,frontend"
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
enable_prerelease_mode: true
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }}
infisical/infisical:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}

View File

@ -4,7 +4,7 @@ on:
push: push:
# run only against tags # run only against tags
tags: tags:
- "infisical-cli/v*.*.*" - "v*"
permissions: permissions:
contents: write contents: write
@ -41,15 +41,13 @@ jobs:
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v4 - uses: goreleaser/goreleaser-action@v4
with: with:
distribution: goreleaser-pro distribution: goreleaser
version: latest version: latest
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }} AUR_KEY: ${{ secrets.AUR_KEY }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli - run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith - name: Publish to CloudSmith

View File

@ -1,16 +1,10 @@
name: Release Docker image for K8 operator name: Release Docker image for K8 operator
on: on: [workflow_dispatch]
push:
tags:
- "infisical-k8-operator/v*.*.*"
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical-k8-operator/}"
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: 🔧 Set up QEMU - name: 🔧 Set up QEMU
@ -32,6 +26,4 @@ jobs:
context: k8-operator context: k8-operator
push: true push: true
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
tags: | tags: infisical/kubernetes-operator:latest
infisical/kubernetes-operator:latest
infisical/kubernetes-operator:${{ steps.extract_version.outputs.version }}

8
.gitignore vendored
View File

@ -2,7 +2,6 @@
node_modules node_modules
.env .env
.env.dev .env.dev
.env.gamma
.env.prod .env.prod
.env.infisical .env.infisical
@ -33,7 +32,7 @@ reports
junit.xml junit.xml
# next.js # next.js
.next/ /.next/
/out/ /out/
# production # production
@ -57,8 +56,3 @@ yarn-error.log*
# Infisical init # Infisical init
.infisical.json .infisical.json
# Editor specific
.vscode/*
frontend-build

View File

@ -11,16 +11,10 @@ before:
- ./cli/scripts/completions.sh - ./cli/scripts/completions.sh
- ./cli/scripts/manpages.sh - ./cli/scripts/manpages.sh
monorepo:
tag_prefix: infisical-cli/
dir: cli
builds: builds:
- id: darwin-build - id: darwin-build
binary: infisical binary: infisical
ldflags: ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags: flags:
- -trimpath - -trimpath
env: env:
@ -38,9 +32,7 @@ builds:
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
binary: infisical binary: infisical
ldflags: ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags: flags:
- -trimpath - -trimpath
goos: goos:
@ -69,10 +61,10 @@ archives:
- goos: windows - goos: windows
format: zip format: zip
files: files:
- ../README* - README*
- ../LICENSE* - LICENSE*
- ../manpages/* - manpages/*
- ../completions/* - completions/*
release: release:
replace_existing_draft: true replace_existing_draft: true
@ -82,7 +74,14 @@ checksum:
name_template: "checksums.txt" name_template: "checksums.txt"
snapshot: snapshot:
name_template: "{{ .Version }}-devel" name_template: "{{ incpatch .Version }}-devel"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
# publishers: # publishers:
# - name: fury.io # - name: fury.io
@ -108,22 +107,6 @@ brews:
zsh_completion.install "completions/infisical.zsh" => "_infisical" zsh_completion.install "completions/infisical.zsh" => "_infisical"
fish_completion.install "completions/infisical.fish" fish_completion.install "completions/infisical.fish"
man1.install "manpages/infisical.1.gz" man1.install "manpages/infisical.1.gz"
- name: 'infisical@{{.Version}}'
tap:
owner: Infisical
name: homebrew-get-cli
commit_author:
name: "Infisical"
email: ai@infisical.com
folder: Formula
homepage: "https://infisical.com"
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
@ -181,7 +164,7 @@ aurs:
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical" install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical" install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish" install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
# man pages # man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz" install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"

View File

@ -1,6 +1,5 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npx lint-staged npx lint-staged
infisical scan git-changes --staged -v

View File

@ -1 +0,0 @@
.github/resources/docker-compose.be-test.yml:generic-api-key:16

View File

@ -1,128 +0,0 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
ARG INTERCOM_ID=intercom-id
FROM node:16-alpine AS base
FROM base AS frontend-dependencies
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM base AS frontend-builder
WORKDIR /app
# Copy dependencies
COPY --from=frontend-dependencies /app/node_modules ./node_modules
# Copy all files
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
ARG INFISICAL_PLATFORM_VERSION
ENV NEXT_PUBLIC_INFISICAL_PLATFORM_VERSION $INFISICAL_PLATFORM_VERSION
# Build
RUN npm run build
# Production image
FROM base AS frontend-runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 non-root-user
RUN mkdir -p /app/.next/cache/images && chown non-root-user:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
COPY --chown=non-root-user:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown non-root-user:nodejs ./public/data
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=non-root-user:nodejs /app/.next/static ./.next/static
USER non-root-user
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM base AS backend-build
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
COPY --chown=non-root-user:nodejs standalone-entrypoint.sh standalone-entrypoint.sh
RUN npm run build
# Production stage
FROM base AS backend-runner
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY --from=backend-build /app .
RUN mkdir frontend-build
# Production stage
FROM base AS production
RUN addgroup --system --gid 1001 nodejs \
&& adduser --system --uid 1001 non-root-user
## set pre baked keys
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID=intercom-id
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
WORKDIR /
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app ./backend/frontend-build
ENV PORT 8080
ENV HTTPS_ENABLED false
ENV NODE_ENV production
ENV STANDALONE_BUILD true
WORKDIR /backend
ENV TELEMETRY_ENABLED true
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js
EXPOSE 8080
USER non-root-user
CMD ["./standalone-entrypoint.sh"]

481
README.md

File diff suppressed because one or more lines are too long

View File

@ -1,13 +1,9 @@
# Security Policy # Security Policy
## Supported versions ## Supported Versions
We always recommend using the latest version of Infisical to ensure you get all security updates. We always recommend using the latest version of Infisical to ensure you get all security updates.
## Reporting vulnerabilities ## Reporting a Vulnerability
Please do not file GitHub issues or post on our public forum for security vulnerabilities, as they are public! Please report security vulnerabilities or concerns to team@infisical.com.
Infisical takes security issues very seriously. If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible.
Note that this security address should be used only for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly.

View File

@ -1,41 +1,12 @@
{ {
"parser": "@typescript-eslint/parser", "parser": "@typescript-eslint/parser",
"plugins": [ "plugins": ["@typescript-eslint"],
"@typescript-eslint",
"unused-imports"
],
"extends": [ "extends": [
"eslint:recommended", "eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended" "plugin:@typescript-eslint/recommended"
], ],
"rules": { "rules": {
"no-empty-function": "off", "no-console": 2
"@typescript-eslint/no-empty-function": "off",
"no-console": 2,
"quotes": [
"error",
"double",
{
"avoidEscape": true
}
],
"comma-dangle": [
"error",
"only-multiline"
],
"@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
"unused-imports/no-unused-vars": [
"warn",
{
"vars": "all",
"varsIgnorePattern": "^_",
"args": "after-used",
"argsIgnorePattern": "^_"
}
],
"sort-imports": 1
} }
} }

View File

@ -1,7 +0,0 @@
{
"singleQuote": false,
"printWidth": 100,
"trailingComma": "none",
"tabWidth": 2,
"semi": true
}

View File

@ -14,20 +14,14 @@ FROM node:16-alpine
WORKDIR /app WORKDIR /app
ENV npm_config_cache /home/node/.npm
COPY package*.json ./ COPY package*.json ./
RUN npm ci --only-production && npm cache clean --force RUN npm ci --only-production
COPY --from=build /app . COPY --from=build /app .
RUN apk add --no-cache bash curl && curl -1sLf \
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
&& apk add infisical=0.8.1 && apk add --no-cache git
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \ HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
CMD node healthcheck.js CMD node healthcheck.js
EXPOSE 4000 EXPOSE 4000
CMD ["node", "build/index.js"] CMD ["npm", "run", "start"]

View File

@ -14,7 +14,7 @@ declare global {
JWT_SIGNUP_LIFETIME: string; JWT_SIGNUP_LIFETIME: string;
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';
VERBOSE_ERROR_OUTPUT: string; VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string; LOKI_HOST: string;
CLIENT_ID_HEROKU: string; CLIENT_ID_HEROKU: string;
@ -39,6 +39,12 @@ declare global {
SMTP_PASSWORD: string; SMTP_PASSWORD: string;
SMTP_FROM_ADDRESS: string; SMTP_FROM_ADDRESS: string;
SMTP_FROM_NAME: string; SMTP_FROM_NAME: string;
STRIPE_PRODUCT_STARTER: string;
STRIPE_PRODUCT_TEAM: string;
STRIPE_PRODUCT_PRO: string;
STRIPE_PUBLISHABLE_KEY: string;
STRIPE_SECRET_KEY: string;
STRIPE_WEBHOOK_SECRET: string;
TELEMETRY_ENABLED: string; TELEMETRY_ENABLED: string;
LICENSE_KEY: string; LICENSE_KEY: string;
} }

View File

@ -1,9 +1,9 @@
export default { export default {
preset: "ts-jest", preset: 'ts-jest',
testEnvironment: "node", testEnvironment: 'node',
collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"], collectCoverageFrom: ['src/*.{js,ts}', '!**/node_modules/**'],
modulePaths: ["<rootDir>/src"], modulePaths: ['<rootDir>/src'],
testMatch: ["<rootDir>/tests/**/*.test.ts"], testMatch: ['<rootDir>/tests/**/*.test.ts'],
setupFiles: ["<rootDir>/test-resources/env-vars.js"], setupFiles: ['<rootDir>/test-resources/env-vars.js'],
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"], setupFilesAfterEnv: ['<rootDir>/tests/setupTests.ts']
}; };

20342
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,85 +1,63 @@
{ {
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.319.0", "@aws-sdk/client-secrets-manager": "^3.267.0",
"@casl/ability": "^6.5.0", "@godaddy/terminus": "^4.11.2",
"@casl/mongoose": "^7.2.1",
"@godaddy/terminus": "^4.12.0",
"@node-saml/passport-saml": "^4.0.4",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/node": "^7.77.0", "@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.48.0", "@sentry/tracing": "^7.19.0",
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"@ucast/mongo2js": "^1.3.4", "await-to-js": "^3.0.0",
"ajv": "^8.12.0", "aws-sdk": "^2.1311.0",
"argon2": "^0.30.3", "axios": "^1.1.3",
"aws-sdk": "^2.1364.0",
"axios": "^1.3.5",
"axios-retry": "^3.4.0", "axios-retry": "^3.4.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.4.0", "bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto-js": "^4.2.0", "crypto-js": "^4.1.1",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"express": "^4.18.1", "express": "^4.18.1",
"express-async-errors": "^3.1.1",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2", "express-validator": "^6.14.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.2.1", "infisical-node": "^1.0.37",
"ioredis": "^5.3.2",
"jmespath": "^0.16.0",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^7.4.1", "mongoose": "^6.7.2",
"mysql2": "^3.6.2",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"ora": "^5.4.1", "posthog-node": "^2.2.2",
"passport": "^0.6.0",
"passport-github": "^1.1.0",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"pg": "^8.11.3",
"pino": "^8.16.1",
"pino-http": "^8.5.1",
"posthog-node": "^2.6.0",
"probot": "^12.3.1",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rate-limit-mongo": "^2.3.2", "request-ip": "^3.3.0",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"swagger-ui-express": "^4.6.2", "stripe": "^10.7.0",
"swagger-autogen": "^2.22.0",
"swagger-ui-express": "^4.6.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3", "typescript": "^4.9.3",
"utility-types": "^3.10.0", "utility-types": "^3.10.0",
"zod": "^3.22.3" "winston": "^3.8.2",
}, "winston-loki": "^6.0.6"
"overrides": {
"rate-limit-mongo": {
"mongodb": "5.8.0"
}
}, },
"name": "infisical-api", "name": "infisical-api",
"version": "1.0.0", "version": "1.0.0",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"start": "node build/index.js", "start": "node build/index.js",
"dev": "nodemon index.js", "dev": "nodemon",
"swagger-autogen": "node ./swagger/index.ts", "swagger-autogen": "node ./swagger/index.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build", "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",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d", "pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest", "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", "test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down" "posttest": "docker compose -f test-resources/docker-compose.test.yml down"
}, },
@ -97,24 +75,16 @@
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1", "@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4", "@posthog/plugin-scaffold": "^1.3.4",
"@swc/core": "^1.3.99",
"@swc/helpers": "^0.5.3",
"@types/bcrypt": "^5.0.0", "@types/bcrypt": "^5.0.0",
"@types/bcryptjs": "^2.4.2", "@types/bcryptjs": "^2.4.2",
"@types/bull": "^4.10.0",
"@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.5.0", "@types/jest": "^29.2.4",
"@types/jmespath": "^0.15.1",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12",
"@types/pg": "^8.10.7",
"@types/picomatch": "^2.3.0",
"@types/pino": "^7.0.5",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1", "@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
@ -122,17 +92,12 @@
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.40.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.26.0", "eslint": "^8.26.0",
"eslint-plugin-unused-imports": "^2.0.0",
"install": "^0.13.0", "install": "^0.13.0",
"jest": "^29.3.1", "jest": "^29.3.1",
"jest-junit": "^15.0.0", "jest-junit": "^15.0.0",
"nodemon": "^2.0.19", "nodemon": "^2.0.19",
"npm": "^8.19.3", "npm": "^8.19.3",
"pino-pretty": "^10.2.3",
"regenerator-runtime": "^0.14.0",
"smee-client": "^1.2.3",
"supertest": "^6.3.3", "supertest": "^6.3.3",
"swagger-autogen": "^2.23.5",
"ts-jest": "^29.0.3", "ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
}, },

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
import ora from "ora";
import nodemailer from "nodemailer";
import { getSmtpHost, getSmtpPort } from "./config";
import { logger } from "./utils/logging";
import mongoose from "mongoose";
import { redisClient } from "./services/RedisService";
type BootstrapOpt = {
transporter: nodemailer.Transporter;
};
export const bootstrap = async ({ transporter }: BootstrapOpt) => {
const spinner = ora().start();
spinner.info("Checking configurations...");
spinner.info("Testing smtp connection");
await transporter
.verify()
.then(async () => {
spinner.succeed("SMTP successfully connected");
})
.catch(async (err) => {
spinner.fail(`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
logger.error(err);
});
spinner.info("Testing mongodb connection");
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
spinner.fail("Mongo DB - Failed to connect");
} else {
spinner.succeed("Mongodb successfully connected");
}
spinner.info("Testing redis connection");
const redisPing = await redisClient?.ping();
if (!redisPing) {
spinner.fail("Redis - Failed to connect");
} else {
spinner.succeed("Redis successfully connected");
}
spinner.stop();
};

View File

@ -1,173 +1,51 @@
import { GITLAB_URL } from "../variables"; import infisical from 'infisical-node';
export const getPort = () => infisical.get('PORT')! || 4000;
import InfisicalClient from "infisical-node"; export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP');
export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!;
export const client = new InfisicalClient({ export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10;
token: process.env.INFISICAL_TOKEN! export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d';
}); export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!;
export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000; export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
export const getEncryptionKey = async () => { export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d';
const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue; export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!;
return secretValue === "" ? undefined : secretValue; export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
}; export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
export const getRootEncryptionKey = async () => { export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue; export const getMongoURL = () => infisical.get('MONGO_URL')!;
return secretValue === "" ? undefined : secretValue; export const getNodeEnv = () => infisical.get('NODE_ENV')!;
}; export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
export const getInviteOnlySignup = async () => export const getLokiHost = () => infisical.get('LOKI_HOST')!;
(await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true"; export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
export const getSaltRounds = async () => export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!;
parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10; export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!;
export const getAuthSecret = async () => export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!;
(await client.getSecret("JWT_AUTH_SECRET")).secretValue ?? export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!;
(await client.getSecret("AUTH_SECRET")).secretValue; export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!;
export const getJwtAuthLifetime = async () => export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!;
(await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d"; export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!;
export const getJwtMfaLifetime = async () => export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!;
(await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m"; export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!;
export const getJwtRefreshLifetime = async () => export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!;
(await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d"; export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!;
export const getJwtServiceSecret = async () => export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!;
(await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1) export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com';
export const getJwtSignupLifetime = async () => export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
(await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m"; export const getSentryDSN = () => infisical.get('SENTRY_DSN')!;
export const getJwtProviderAuthLifetime = async () => export const getSiteURL = () => infisical.get('SITE_URL')!;
(await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m"; export const getSmtpHost = () => infisical.get('SMTP_HOST')!;
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue; export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false;
export const getNodeEnv = async () => export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587;
(await client.getSecret("NODE_ENV")).secretValue || "production"; export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!;
export const getVerboseErrorOutput = async () => export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!;
(await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true; export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!;
export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue; export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical';
export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue; export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!;
export const getClientIdHeroku = async () => export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!;
(await client.getSecret("CLIENT_ID_HEROKU")).secretValue; export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!;
export const getClientIdVercel = async () => export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!;
(await client.getSecret("CLIENT_ID_VERCEL")).secretValue; export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
export const getClientIdNetlify = async () => export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
(await client.getSecret("CLIENT_ID_NETLIFY")).secretValue; export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
export const getClientIdGitHub = async () => export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
(await client.getSecret("CLIENT_ID_GITHUB")).secretValue; export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true
export const getClientIdGitLab = async () =>
(await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
export const getClientIdBitBucket = async () =>
(await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
export const getClientIdGCPSecretManager = async () =>
(await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue;
export const getClientSecretAzure = async () =>
(await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
export const getClientSecretHeroku = async () =>
(await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
export const getClientSecretVercel = async () =>
(await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
export const getClientSecretNetlify = async () =>
(await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
export const getClientSecretGitHub = async () =>
(await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
export const getClientSecretGitLab = async () =>
(await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
export const getClientSecretBitBucket = async () =>
(await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
export const getClientSecretGCPSecretManager = async () =>
(await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
export const getClientSlugVercel = async () =>
(await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
export const getClientIdGoogleLogin = async () =>
(await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
export const getClientSecretGoogleLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
export const getClientIdGitHubLogin = async () =>
(await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
export const getClientSecretGitHubLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
export const getClientIdGitLabLogin = async () =>
(await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
export const getClientSecretGitLabLogin = async () =>
(await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
export const getUrlGitLabLogin = async () =>
(await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
export const getAwsCloudWatchLog = async () => {
const logGroupName =
(await client.getSecret("AWS_CLOUDWATCH_LOG_GROUP_NAME")).secretValue || "infisical-log-stream";
const region = (await client.getSecret("AWS_CLOUDWATCH_LOG_REGION")).secretValue;
const accessKeyId = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID")).secretValue;
const accessKeySecret = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET"))
.secretValue;
const interval = parseInt(
(await client.getSecret("AWS_CLOUDWATCH_LOG_INTERVAL")).secretValue || 1000,
10
);
if (!region || !accessKeyId || !accessKeySecret) return;
return { logGroupName, region, accessKeySecret, accessKeyId, interval };
};
export const getPostHogHost = async () =>
(await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
export const getPostHogProjectApiKey = async () =>
(await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue ||
"phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
export const getSiteURL = async () => (await client.getSecret("SITE_URL")).secretValue;
export const getSmtpHost = async () => (await client.getSecret("SMTP_HOST")).secretValue;
export const getSmtpSecure = async () =>
(await client.getSecret("SMTP_SECURE")).secretValue === "true" || false;
export const getSmtpPort = async () =>
parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587;
export const getSmtpUsername = async () => (await client.getSecret("SMTP_USERNAME")).secretValue;
export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWORD")).secretValue;
export const getSmtpFromAddress = async () =>
(await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
export const getSmtpFromName = async () =>
(await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
export const getSecretScanningWebhookProxy = async () =>
(await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
export const getSecretScanningWebhookSecret = async () =>
(await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
export const getSecretScanningGitAppId = async () =>
(await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
export const getSecretScanningPrivateKey = async () =>
(await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
export const getIsInfisicalCloud = async () =>
(await client.getSecret("INFISICAL_CLOUD")).secretValue === "true";
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
};
export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue;
return secretValue === "" ? undefined : secretValue;
};
export const getLicenseServerUrl = async () =>
(await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
export const getTelemetryEnabled = async () =>
(await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue;
export const getSmtpConfigured = async () =>
(await client.getSecret("SMTP_HOST")).secretValue == "" ||
(await client.getSecret("SMTP_HOST")).secretValue == undefined
? false
: true;
export const getHttpsEnabled = async () => {
if ((await getNodeEnv()) != "production") {
// no https for anything other than prod
return false;
}
if (
(await client.getSecret("HTTPS_ENABLED")).secretValue == undefined ||
(await client.getSecret("HTTPS_ENABLED")).secretValue == ""
) {
// default when no value present
return true;
}
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true;
};

View File

@ -1,24 +1,10 @@
import axios from "axios"; import axios from 'axios';
import axiosRetry from "axios-retry"; import axiosRetry from 'axios-retry';
import {
getLicenseKeyAuthToken,
getLicenseServerKeyAuthToken,
setLicenseKeyAuthToken,
setLicenseServerKeyAuthToken,
} from "./storage";
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl,
} from "./index";
// should have JWT to interact with the license server const axiosInstance = axios.create();
export const licenseServerKeyRequest = axios.create();
export const licenseKeyRequest = axios.create();
export const standardRequest = axios.create();
// add retry functionality to the axios instance // add retry functionality to the axios instance
axiosRetry(standardRequest, { axiosRetry(axiosInstance, {
retries: 3, retries: 3,
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
retryCondition: (error) => { retryCondition: (error) => {
@ -27,98 +13,4 @@ axiosRetry(standardRequest, {
}, },
}); });
export const refreshLicenseServerKeyToken = async () => { export default axiosInstance;
const licenseServerKey = await getLicenseServerKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
{
headers: {
"X-API-KEY": licenseServerKey,
},
}
);
setLicenseServerKeyAuthToken(token);
return token;
}
export const refreshLicenseKeyToken = async () => {
const licenseKey = await getLicenseKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-login`, {},
{
headers: {
"X-API-KEY": licenseKey,
},
}
);
setLicenseKeyAuthToken(token);
return token;
}
licenseServerKeyRequest.interceptors.request.use((config) => {
const token = getLicenseServerKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseServerKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseServerKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseServerKeyRequest(originalRequest);
}
return Promise.reject(err);
});
licenseKeyRequest.interceptors.request.use((config) => {
const token = getLicenseKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseKeyToken();
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
return licenseKeyRequest(originalRequest);
}
return Promise.reject(err);
});

View File

@ -1,24 +0,0 @@
import { IServerConfig, ServerConfig } from "../models/serverConfig";
let serverConfig: IServerConfig;
export const serverConfigInit = async () => {
const cfg = await ServerConfig.findOne({});
if (!cfg) {
const cfg = new ServerConfig();
await cfg.save();
serverConfig = cfg;
} else {
serverConfig = cfg;
}
return serverConfig;
};
export const getServerConfig = () => serverConfig;
export const updateServerConfig = async (data: Partial<IServerConfig>) => {
const cfg = await ServerConfig.findByIdAndUpdate(serverConfig._id, data, { new: true });
if (!cfg) throw new Error("Failed to update server config");
serverConfig = cfg;
return serverConfig;
};

View File

@ -1,30 +0,0 @@
const MemoryLicenseServerKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const MemoryLicenseKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken,
};
};
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;

View File

@ -1,100 +0,0 @@
import { Request, Response } from "express";
import { getHttpsEnabled } from "../../config";
import { getServerConfig, updateServerConfig as setServerConfig } from "../../config/serverConfig";
import { initializeDefaultOrg, issueAuthTokens } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import { User } from "../../models";
import { TelemetryService } from "../../services";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/admin";
export const getServerConfigInfo = (_req: Request, res: Response) => {
const config = getServerConfig();
return res.send({ config });
};
export const updateServerConfig = async (req: Request, res: Response) => {
const {
body: { allowSignUp }
} = await validateRequest(reqValidator.UpdateServerConfigV1, req);
const config = await setServerConfig({ allowSignUp });
return res.send({ config });
};
export const adminSignUp = async (req: Request, res: Response) => {
const cfg = getServerConfig();
if (cfg.initialized) throw UnauthorizedRequestError({ message: "Admin has been created" });
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.SignupV1, req);
let user = await User.findOne({ email });
if (user) throw BadRequestError({ message: "User already exist" });
user = new User({
email,
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier,
superAdmin: true
});
await user.save();
await initializeDefaultOrg({ organizationName: "Admin Org", user });
await setServerConfig({ initialized: true });
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const token = tokens.token;
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "admin initialization",
properties: {
email: user.email,
lastName,
firstName
}
});
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
return res.status(200).send({
message: "Successfully set up admin account",
user,
token
});
};

View File

@ -1,34 +1,29 @@
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import jwt from "jsonwebtoken"; import { Request, Response } from 'express';
import * as bigintConversion from "bigint-conversion"; import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp"); const jsrp = require('jsrp');
import { LoginSRPDetail, TokenVersion, User } from "../../models"; import { User, LoginSRPDetail } from '../../models';
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth"; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from "../../helpers/user"; import { checkUserDevice } from '../../helpers/user';
import { AuthTokenType } from "../../variables";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { import {
getAuthSecret, ACTION_LOGIN,
getHttpsEnabled, ACTION_LOGOUT
getJwtAuthLifetime } from '../../variables';
} from "../../config"; import { BadRequestError } from '../../utils/errors';
import { ActorType } from "../../ee/models"; import { EELogService } from '../../ee/services';
import { validateRequest } from "../../helpers/validation"; import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import * as reqValidator from "../../validation/auth"; import {
getNodeEnv,
getJwtRefreshSecret,
getJwtAuthLifetime,
getJwtAuthSecret
} from '../../config';
declare module "jsonwebtoken" { declare module 'jsonwebtoken' {
export interface AuthnJwtPayload extends jwt.JwtPayload {
authTokenType: AuthTokenType;
}
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
refreshVersion?: number;
}
export interface ServiceRefreshTokenJwtPayload extends jwt.JwtPayload {
serviceTokenDataId: string;
authTokenType: string;
tokenVersion: number;
} }
} }
@ -39,42 +34,47 @@ declare module "jsonwebtoken" {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
const { try {
body: { email, clientPublicKey } const {
} = await validateRequest(reqValidator.Login1V1, req); email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select("+salt +verifier"); }).select('+salt +verifier');
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace( await LoginSRPDetail.findOneAndReplace({ email: email }, {
{ email: email },
{
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt) serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, }, { upsert: true, returnNewDocument: false })
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@ -85,75 +85,84 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
const { try {
body: { email, clientProof } const { email, clientProof } = req.body;
} = await validateRequest(reqValidator.Login2V1, req); const user = await User.findOne({
email
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
const user = await User.findOne({ if (!user) throw new Error('Failed to find user');
email
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
if (!user) throw new Error("Failed to find user"); const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }); if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
}
if (!loginSRPDetailFromDB) { const server = new jsrp.server();
return BadRequestError( server.init(
Error( {
"It looks like some details from the first login are not found. Please try login one again" salt: user.salt,
) verifier: user.verifier,
); b: loginSRPDetailFromDB.serverBInt
} },
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
const server = new jsrp.server(); // compare server and client shared keys
server.init( if (server.checkClientProof(clientProof)) {
{ // issue tokens
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys await checkUserDevice({
if (server.checkClientProof(clientProof)) { user,
// issue tokens ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
});
await checkUserDevice({ const tokens = await issueAuthTokens({ userId: user._id.toString() });
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
const tokens = await issueAuthTokens({ // store (refresh) token in httpOnly cookie
userId: user._id, res.cookie('jid', tokens.refreshToken, {
ip: req.realIP, httpOnly: true,
userAgent: req.headers["user-agent"] ?? "" path: '/',
}); sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
});
// store (refresh) token in httpOnly cookie const loginAction = await EELogService.createAction({
res.cookie("jid", tokens.refreshToken, { name: ACTION_LOGIN,
httpOnly: true, userId: user._id
path: "/", });
sameSite: "strict",
secure: await getHttpsEnabled() loginAction && await EELogService.createLog({
}); userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
}
// return (access) token in response return res.status(400).send({
return res.status(200).send({ message: 'Failed to authenticate. Try again?'
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}); });
} }
);
return res.status(400).send({ } catch (err) {
message: "Failed to authenticate. Try again?" Sentry.setUser(null);
}); Sentry.captureException(err);
} return res.status(400).send({
); message: 'Failed to authenticate. Try again?'
});
}
}; };
/** /**
@ -163,38 +172,41 @@ export const login2 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const logout = async (req: Request, res: Response) => { export const logout = async (req: Request, res: Response) => {
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { try {
await clearTokens(req.authData.tokenVersionId); await clearTokens({
userId: req.user._id.toString()
});
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to logout'
});
} }
// clear httpOnly cookie
res.cookie("jid", "", {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: (await getHttpsEnabled()) as boolean
});
return res.status(200).send({ return res.status(200).send({
message: "Successfully logged out." message: 'Successfully logged out.'
});
};
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany(
{
user: req.user._id
},
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions."
}); });
}; };
@ -206,64 +218,52 @@ export const revokeAllSessions = async (req: Request, res: Response) => {
*/ */
export const checkAuth = async (req: Request, res: Response) => { export const checkAuth = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
message: "Authenticated" message: 'Authenticated'
}); });
}; }
/** /**
* Return new JWT access token by first validating the refresh token * Return new token by redeeming refresh token
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getNewToken = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => {
try {
const refreshToken = req.cookies.jid;
const refreshToken = req.cookies.jid; if (!refreshToken) {
throw new Error('Failed to find token in request cookies');
}
if (!refreshToken) const decodedToken = <jwt.UserIDJwtPayload>(
throw BadRequestError({ jwt.verify(refreshToken, getJwtRefreshSecret())
message: "Failed to find refresh token in request cookies" );
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const token = createToken({
payload: {
userId: decodedToken.userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
}); });
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getAuthSecret()); return res.status(200).send({
token
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError();
const user = await User.findOne({
_id: decodedToken.userId
}).select("+publicKey +refreshVersion +accessVersion");
if (!user) throw new Error("Failed to authenticate unfound user");
if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account");
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion)
throw UnauthorizedRequestError({
message: "Failed to validate refresh token"
}); });
} catch (err) {
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) Sentry.setUser(null);
throw BadRequestError({ Sentry.captureException(err);
message: "Failed to validate refresh token" return res.status(400).send({
message: 'Invalid request'
}); });
}
const token = createToken({
payload: {
authTokenType: AuthTokenType.ACCESS_TOKEN,
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
token
});
};
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}; };

View File

@ -1,20 +1,11 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { Types } from "mongoose"; import * as Sentry from '@sentry/node';
import { Bot, BotKey } from "../../models"; import { Bot, BotKey } from '../../models';
import { createBot } from "../../helpers/bot"; import { createBot } from '../../helpers/bot';
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/bot";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError } from "../../utils/errors";
interface BotKey { interface BotKey {
encryptedKey: string; encryptedKey: string;
nonce: string; nonce: string;
} }
/** /**
@ -25,35 +16,33 @@ interface BotKey {
* @returns * @returns
*/ */
export const getBotByWorkspaceId = async (req: Request, res: Response) => { export const getBotByWorkspaceId = async (req: Request, res: Response) => {
const { let bot;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req); const { workspaceId } = req.params;
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan( bot = await Bot.findOne({
ProjectPermissionActions.Read, workspace: workspaceId
ProjectPermissionSub.Integrations });
);
if (!bot) {
let bot = await Bot.findOne({ // case: bot doesn't exist for workspace with id [workspaceId]
workspace: workspaceId // -> create a new bot and return it
}); bot = await createBot({
name: 'Infisical Bot',
if (!bot) { workspaceId
// case: bot doesn't exist for workspace with id [workspaceId] });
// -> create a new bot and return it }
bot = await createBot({ } catch (err) {
name: "Infisical Bot", Sentry.setUser({ email: req.user.email });
workspaceId: new Types.ObjectId(workspaceId) Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get bot for workspace'
});
}
return res.status(200).send({
bot
}); });
}
return res.status(200).send({
bot
});
}; };
/** /**
@ -63,73 +52,56 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const setBotActiveState = async (req: Request, res: Response) => { export const setBotActiveState = async (req: Request, res: Response) => {
const { let bot;
body: { botKey, isActive }, try {
params: { botId } const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
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
});
}
const bot = await Bot.findById(botId); bot = await Bot.findOneAndUpdate({
if (!bot) { _id: req.bot._id
throw BadRequestError({ message: "Bot not found" }); }, {
} isActive
const userId = req.user._id; }, {
new: true
const { permission } = await getAuthDataProjectPermissions({ });
authData: req.authData,
workspaceId: bot.workspace if (!bot) throw new Error('Failed to update bot active state');
});
} catch (err) {
ForbiddenError.from(permission).throwUnlessCan( Sentry.setUser({ email: req.user.email });
ProjectPermissionActions.Edit, Sentry.captureException(err);
ProjectPermissionSub.Integrations return res.status(400).send({
); message: 'Failed to update bot active state'
});
if (isActive) { }
// bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) { return res.status(200).send({
return res.status(400).send({ bot
message: "Failed to set bot state to active - missing bot key"
});
}
await BotKey.findOneAndUpdate(
{
workspace: bot.workspace
},
{
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: userId,
bot: bot._id,
workspace: bot.workspace
},
{
upsert: true,
new: true
}
);
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: bot._id
}); });
}
const updatedBot = await Bot.findOneAndUpdate(
{
_id: bot._id
},
{
isActive
},
{
new: true
}
);
if (!updatedBot) throw new Error("Failed to update bot active state");
return res.status(200).send({
bot
});
}; };

View File

@ -1,41 +1,35 @@
import * as authController from "./authController"; import * as authController from './authController';
import * as botController from "./botController"; 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';
import * as membershipController from "./membershipController"; import * as membershipController from './membershipController';
import * as membershipOrgController from "./membershipOrgController"; import * as membershipOrgController from './membershipOrgController';
import * as organizationController from "./organizationController"; import * as organizationController from './organizationController';
import * as passwordController from "./passwordController"; import * as passwordController from './passwordController';
import * as secretController from "./secretController"; import * as secretController from './secretController';
import * as serviceTokenController from "./serviceTokenController"; import * as serviceTokenController from './serviceTokenController';
import * as signupController from "./signupController"; import * as signupController from './signupController';
import * as userActionController from "./userActionController"; import * as stripeController from './stripeController';
import * as userController from "./userController"; import * as userActionController from './userActionController';
import * as workspaceController from "./workspaceController"; import * as userController from './userController';
import * as secretScanningController from "./secretScanningController"; import * as workspaceController from './workspaceController';
import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController";
import * as adminController from "./adminController";
export { export {
authController, authController,
botController, botController,
integrationAuthController, integrationAuthController,
integrationController, integrationController,
keyController, keyController,
membershipController, membershipController,
membershipOrgController, membershipOrgController,
organizationController, organizationController,
passwordController, passwordController,
secretController, secretController,
serviceTokenController, serviceTokenController,
signupController, signupController,
userActionController, stripeController,
userController, userActionController,
workspaceController, userController,
secretScanningController, workspaceController
webhookController,
secretImpsController,
adminController
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,21 +1,11 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { Types } from "mongoose"; import { Types } from 'mongoose';
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models"; import * as Sentry from '@sentry/node';
import { EventService } from "../../services"; import {
import { eventStartIntegration } from "../../events"; Integration
import { getFolderByPath } from "../../services/FolderService"; } from '../../models';
import { BadRequestError } from "../../utils/errors"; import { EventService } from '../../services';
import { EEAuditLogService } from "../../ee/services"; import { eventPushSecrets } from '../../events';
import { EventType } from "../../ee/models";
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/integration";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* Create/initialize an (empty) integration for integration authorization * Create/initialize an (empty) integration for integration authorization
@ -24,118 +14,57 @@ import { ForbiddenError } from "@casl/ability";
* @returns * @returns
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
const { let integration;
body: {
try {
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
integration = await new Integration({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive, isActive,
sourceEnvironment,
secretPath,
app, app,
path, appId,
appId, targetEnvironment,
owner, owner,
region, path,
scope, region,
targetService, integration: req.integrationAuth.integration,
targetServiceId, integrationAuth: new Types.ObjectId(integrationAuthId)
integrationAuthId, }).save();
targetEnvironment,
targetEnvironmentId, if (integration) {
metadata // trigger event - push secrets
} EventService.handleEvent({
} = await validateRequest(reqValidator.CreateIntegrationV1, req); event: eventPushSecrets({
workspaceId: integration.workspace.toString()
const integrationAuth = await IntegrationAuth.findById(integrationAuthId) })
.populate<{ workspace: IWorkspace }>("workspace") });
.select( }
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
);
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" }); } catch (err) {
Sentry.setUser({ email: req.user.email });
const { permission } = await getAuthDataProjectPermissions({ Sentry.captureException(err);
authData: req.authData, return res.status(400).send({
workspaceId: integrationAuth.workspace._id message: 'Failed to create integration'
}); });
}
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Folder path doesn't exist"
});
}
}
// TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token
const integration = await new Integration({
workspace: integrationAuth.workspace._id,
environment: sourceEnvironment,
isActive,
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
scope,
secretPath,
integration: integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId),
metadata
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
}; };
@ -146,162 +75,85 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
let integration;
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
const { try {
body: { const {
environment, environment,
isActive, isActive,
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
secretPath } = req.body;
},
params: { integrationId }
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
const integration = await Integration.findById(integrationId); integration = await Integration.findOneAndUpdate(
if (!integration) throw BadRequestError({ message: "Integration not found" }); {
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
const { permission } = await getAuthDataProjectPermissions({ if (integration) {
authData: req.authData, // trigger event - push secrets
workspaceId: integration.workspace EventService.handleEvent({
}); event: eventPushSecrets({
workspaceId: integration.workspace.toString(),
ForbiddenError.from(permission).throwUnlessCan( }),
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
const folders = await Folder.findOne({
workspace: integration.workspace,
environment
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist"
}); });
} }
} } catch (err) {
Sentry.setUser({ email: req.user.email });
const updatedIntegration = await Integration.findOneAndUpdate( Sentry.captureException(err);
{ return res.status(400).send({
_id: integration._id message: "Failed to update integration",
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
secretPath
},
{
new: true
}
);
if (updatedIntegration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventStartIntegration({
workspaceId: updatedIntegration.workspace,
environment
})
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration: updatedIntegration integration,
}); });
}; };
/** /**
* Delete integration with id [integrationId] * Delete integration with id [integrationId] and deactivate bot if there are
* no integrations left
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { export const deleteIntegration = async (req: Request, res: Response) => {
const { let integration;
params: { integrationId } try {
} = await validateRequest(reqValidator.DeleteIntegrationV1, req); const { integrationId } = req.params;
const integration = await Integration.findById(integrationId); integration = await Integration.findOneAndDelete({
if (!integration) throw BadRequestError({ message: "Integration not found" }); _id: integrationId,
});
const { permission } = await getAuthDataProjectPermissions({ if (!integration) throw new Error("Failed to find integration");
authData: req.authData, } catch (err) {
workspaceId: integration.workspace Sentry.setUser({ email: req.user.email });
}); Sentry.captureException(err);
return res.status(400).send({
ForbiddenError.from(permission).throwUnlessCan( message: "Failed to delete integration",
ProjectPermissionActions.Delete, });
ProjectPermissionSub.Integrations }
);
const deletedIntegration = await Integration.findOneAndDelete({
_id: integrationId
});
if (!deletedIntegration) throw new Error("Failed to find integration");
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_INTEGRATION,
metadata: {
integrationId: integration._id.toString(),
integration: integration.integration,
environment: integration.environment,
secretPath: integration.secretPath,
url: integration.url,
app: integration.app,
appId: integration.appId,
targetEnvironment: integration.targetEnvironment,
targetEnvironmentId: integration.targetEnvironmentId,
targetService: integration.targetService,
targetServiceId: integration.targetServiceId,
path: integration.path,
region: integration.region
}
},
{
workspaceId: integration.workspace
}
);
return res.status(200).send({ return res.status(200).send({
integration integration,
}); });
}; };
// Will trigger sync for all integrations within the given env and workspace id
export const manualSync = async (req: Request, res: Response) => {
const {
body: { workspaceId, environment }
} = await validateRequest(reqValidator.ManualSyncV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Integrations
);
syncSecretsToActiveIntegrationsQueue({
workspaceId,
environment
});
res.status(200).send();
};

View File

@ -1,17 +1,7 @@
import { Types } from "mongoose"; import { Request, Response } from 'express';
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import { Key } from "../../models"; import { Key } from '../../models';
import { findMembership } from "../../helpers/membership"; import { findMembership } from '../../helpers/membership';
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/key";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* 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
@ -21,42 +11,38 @@ import { ForbiddenError } from "@casl/ability";
* @returns * @returns
*/ */
export const uploadKey = async (req: Request, res: Response) => { export const uploadKey = async (req: Request, res: Response) => {
const { try {
params: { workspaceId }, const { workspaceId } = req.params;
body: { key } const { key } = req.body;
} = await validateRequest(reqValidator.UploadKeyV1, req);
const { permission } = await getAuthDataProjectPermissions({ // validate membership of receiver
authData: req.authData, const receiverMembership = await findMembership({
workspaceId: new Types.ObjectId(workspaceId) user: key.userId,
}); workspace: workspaceId
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
// validate membership of receiver if (!receiverMembership) {
const receiverMembership = await findMembership({ throw new Error('Failed receiver membership validation for workspace');
user: key.userId, }
workspace: workspaceId
});
if (!receiverMembership) { await new Key({
throw new Error("Failed receiver membership validation for workspace"); encryptedKey: key.encryptedKey,
} nonce: key.nonce,
sender: req.user._id,
receiver: key.userId,
workspace: workspaceId
}).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload key to workspace'
});
}
await new Key({ return res.status(200).send({
encryptedKey: key.encryptedKey, message: 'Successfully uploaded key to workspace'
nonce: key.nonce, });
sender: req.user._id,
receiver: key.userId,
workspace: workspaceId
}).save();
return res.status(200).send({
message: "Successfully uploaded key to workspace"
});
}; };
/** /**
@ -66,36 +52,31 @@ export const uploadKey = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getLatestKey = async (req: Request, res: Response) => { export const getLatestKey = async (req: Request, res: Response) => {
const { let latestKey;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetLatestKeyV1, req); const { workspaceId } = req.params;
// get latest key // get latest key
const latestKey = await Key.find({ latestKey = await Key.find({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.limit(1) .limit(1)
.populate("sender", "+publicKey"); .populate('sender', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get latest key'
});
}
const resObj: any = {}; const resObj: any = {};
if (latestKey.length > 0) { if (latestKey.length > 0) {
resObj["latestKey"] = latestKey[0]; resObj['latestKey'] = latestKey[0];
await EEAuditLogService.createAuditLog( }
req.authData,
{
type: EventType.GET_WORKSPACE_KEY,
metadata: {
keyId: latestKey[0]._id.toString()
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
}
return res.status(200).send(resObj); return res.status(200).send(resObj);
}; };

View File

@ -1,22 +1,13 @@
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import { Types } from "mongoose"; import { Request, Response } from 'express';
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models"; import { Membership, MembershipOrg, User, Key } from '../../models';
import { EventType, Role } from "../../ee/models";
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
import { sendMail } from "../../helpers/nodemailer";
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, VIEWER } from "../../variables";
import { getSiteURL } from "../../config";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/membership";
import { import {
ProjectPermissionActions, findMembership,
ProjectPermissionSub, deleteMembership as deleteMember
getAuthDataProjectPermissions } from '../../helpers/membership';
} from "../../ee/services/ProjectRoleService"; import { sendMail } from '../../helpers/nodemailer';
import { ForbiddenError } from "@casl/ability"; import { ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { BadRequestError } from "../../utils/errors"; import { getSiteURL } from '../../config';
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
/** /**
* Check that user is a member of workspace with id [workspaceId] * Check that user is a member of workspace with id [workspaceId]
@ -25,23 +16,29 @@ import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
* @returns * @returns
*/ */
export const validateMembership = async (req: Request, res: Response) => { export const validateMembership = async (req: Request, res: Response) => {
const { try {
params: { workspaceId } const { workspaceId } = req.params;
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
// validate membership // validate membership
const membership = await findMembership({ const membership = await findMembership({
user: req.user._id, user: req.user._id,
workspace: workspaceId workspace: workspaceId
}); });
if (!membership) { if (!membership) {
throw new Error("Failed to validate membership"); throw new Error('Failed to validate membership');
} }
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed workspace connection check'
});
}
return res.status(200).send({ return res.status(200).send({
message: "Workspace membership confirmed" message: 'Workspace membership confirmed'
}); });
}; };
/** /**
@ -51,51 +48,52 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteMembership = async (req: Request, res: Response) => { export const deleteMembership = async (req: Request, res: Response) => {
const { let deletedMembership;
params: { membershipId } try {
} = await validateRequest(reqValidator.DeleteMembershipV1, req); const { membershipId } = req.params;
// check if membership to delete exists // check if membership to delete exists
const membershipToDelete = await Membership.findOne({ const membershipToDelete = await Membership.findOne({
_id: membershipId _id: membershipId
}).populate<{ user: IUser }>("user"); }).populate('user');
if (!membershipToDelete) { if (!membershipToDelete) {
throw new Error("Failed to delete workspace membership that doesn't exist"); throw new Error(
} "Failed to delete workspace membership that doesn't exist"
);
const { permission } = await getAuthDataProjectPermissions({ }
authData: req.authData,
workspaceId: membershipToDelete.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Member
);
// delete workspace membership // check if user is a member and admin of the workspace
const deletedMembership = await deleteMember({ // whose membership we wish to delete
membershipId: membershipToDelete._id.toString() const membership = await Membership.findOne({
}); user: req.user._id,
workspace: membershipToDelete.workspace
});
await EEAuditLogService.createAuditLog( if (!membership) {
req.authData, throw new Error('Failed to validate workspace membership');
{ }
type: EventType.REMOVE_WORKSPACE_MEMBER,
metadata: {
userId: membershipToDelete.user._id.toString(),
email: membershipToDelete.user.email
}
},
{
workspaceId: membershipToDelete.workspace
}
);
return res.status(200).send({ if (membership.role !== ADMIN) {
deletedMembership // user is not an admin member of the workspace
}); throw new Error('Insufficient role for deleting workspace membership');
}
// delete workspace membership
deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete membership'
});
}
return res.status(200).send({
deletedMembership
});
}; };
/** /**
@ -105,81 +103,53 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeMembershipRole = async (req: Request, res: Response) => { export const changeMembershipRole = async (req: Request, res: Response) => {
const { let membershipToChangeRole;
body: { role }, try {
params: { membershipId } const { membershipId } = req.params;
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req); const { role } = req.body;
// validate target membership if (![ADMIN, MEMBER].includes(role)) {
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>( throw new Error('Failed to validate role');
"user" }
);
if (!membershipToChangeRole) { // validate target membership
throw new Error("Failed to find membership to change role"); membershipToChangeRole = await findMembership({
} _id: membershipId
});
const { permission } = await getAuthDataProjectPermissions({ if (!membershipToChangeRole) {
authData: req.authData, throw new Error('Failed to find membership to change role');
workspaceId: membershipToChangeRole.workspace }
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Member
);
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role); // check if user is a member and admin of target membership's
if (isCustomRole) { // workspace
const wsRole = await Role.findOne({ const membership = await findMembership({
slug: role, user: req.user._id,
isOrgRole: false, workspace: membershipToChangeRole.workspace
workspace: membershipToChangeRole.workspace });
});
if (!wsRole) throw BadRequestError({ message: "Role not found" });
const membership = await Membership.findByIdAndUpdate(membershipId, {
role: CUSTOM,
customRole: wsRole
});
return res.status(200).send({
membership
});
}
const membership = await Membership.findByIdAndUpdate( if (!membership) {
membershipId, throw new Error('Failed to validate membership');
{ }
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
}
);
await EEAuditLogService.createAuditLog( if (membership.role !== ADMIN) {
req.authData, // user is not an admin member of the workspace
{ throw new Error('Insufficient role for changing member roles');
type: EventType.UPDATE_USER_WORKSPACE_ROLE, }
metadata: {
userId: membershipToChangeRole.user._id.toString(),
email: membershipToChangeRole.user.email,
oldRole: membershipToChangeRole.role,
newRole: role
}
},
{
workspaceId: membershipToChangeRole.workspace
}
);
return res.status(200).send({ membershipToChangeRole.role = role;
membership await membershipToChangeRole.save();
}); } catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change membership role'
});
}
return res.status(200).send({
membership: membershipToChangeRole
});
}; };
/** /**
@ -189,91 +159,75 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToWorkspace = async (req: Request, res: Response) => { export const inviteUserToWorkspace = async (req: Request, res: Response) => {
const { let invitee, latestKey;
params: { workspaceId }, try {
body: { email } const { workspaceId } = req.params;
} = await validateRequest(InviteUserToWorkspaceV1, req); const { email }: { email: string } = req.body;
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
const invitee = await User.findOne({ invitee = await User.findOne({
email email
}).select("+publicKey"); }).select('+publicKey');
if (!invitee || !invitee?.publicKey) throw new Error("Failed to validate invitee"); if (!invitee || !invitee?.publicKey)
throw new Error('Failed to validate invitee');
// validate invitee's workspace membership - ensure member isn't // validate invitee's workspace membership - ensure member isn't
// already a member of the workspace // already a member of the workspace
const inviteeMembership = await Membership.findOne({ const inviteeMembership = await Membership.findOne({
user: invitee._id, user: invitee._id,
workspace: workspaceId workspace: workspaceId
}).populate<{ user: IUser }>("user"); });
if (inviteeMembership) throw new Error("Failed to add existing member of workspace"); if (inviteeMembership)
throw new Error('Failed to add existing member of workspace');
const workspace = await Workspace.findById(workspaceId); // validate invitee's organization membership - ensure that only
if (!workspace) throw new Error("Failed to find workspace"); // (accepted) organization members can be added to the workspace
// validate invitee's organization membership - ensure that only const membershipOrg = await MembershipOrg.findOne({
// (accepted) organization members can be added to the workspace user: invitee._id,
const membershipOrg = await MembershipOrg.findOne({ organization: req.membership.workspace.organization,
user: invitee._id, status: ACCEPTED
organization: workspace.organization, });
status: ACCEPTED
});
if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership"); if (!membershipOrg)
throw new Error("Failed to validate invitee's organization membership");
// get latest key // get latest key
const latestKey = await Key.findOne({ latestKey = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.populate("sender", "+publicKey"); .populate('sender', '+publicKey');
// create new workspace membership // create new workspace membership
await new Membership({ const m = await new Membership({
user: invitee._id, user: invitee._id,
workspace: workspaceId, workspace: workspaceId,
role: MEMBER role: MEMBER
}).save(); }).save();
await sendMail({ await sendMail({
template: "workspaceInvitation.handlebars", template: 'workspaceInvitation.handlebars',
subjectLine: "Infisical workspace invitation", subjectLine: 'Infisical workspace invitation',
recipients: [invitee.email], recipients: [invitee.email],
substitutions: { substitutions: {
inviterFirstName: req.user.firstName, inviterFirstName: req.user.firstName,
inviterEmail: req.user.email, inviterEmail: req.user.email,
workspaceName: workspace.name, workspaceName: req.membership.workspace.name,
callback_url: (await getSiteURL()) + "/login" callback_url: getSiteURL() + '/login'
} }
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to invite user to workspace'
});
}
await EEAuditLogService.createAuditLog( return res.status(200).send({
req.authData, invitee,
{ latestKey
type: EventType.ADD_WORKSPACE_MEMBER, });
metadata: { };
userId: invitee._id.toString(),
email: invitee.email
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
invitee,
latestKey
});
};

View File

@ -1,29 +1,13 @@
import { Types } from "mongoose"; import { Request, Response } from 'express';
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import { MembershipOrg, Organization, User } from "../../models"; import { MembershipOrg, Organization, User } from '../../models';
import { SSOConfig } from "../../ee/models"; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg"; import { createToken } from '../../helpers/auth';
import { createToken } from "../../helpers/auth"; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { updateSubscriptionOrgQuantity } from "../../helpers/organization"; import { sendMail } from '../../helpers/nodemailer';
import { sendMail } from "../../helpers/nodemailer"; import { TokenService } from '../../services';
import { TokenService } from "../../services"; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { EELicenseService } from "../../ee/services"; import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
import * as reqValidator from "../../validation/membershipOrg";
import {
getAuthSecret,
getJwtSignupLifetime,
getSiteURL,
getSmtpConfigured
} from "../../config";
import { validateUserEmail } from "../../validation";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@ -31,39 +15,55 @@ import { ForbiddenError } from "@casl/ability";
* @param res * @param res
* @returns * @returns
*/ */
export const deleteMembershipOrg = async (req: Request, _res: Response) => { export const deleteMembershipOrg = async (req: Request, res: Response) => {
const { let membershipOrgToDelete;
params: { membershipOrgId } try {
} = await validateRequest(reqValidator.DelOrgMembershipv1, req); const { membershipOrgId } = req.params;
// check if organization membership to delete exists // check if organization membership to delete exists
const membershipOrgToDelete = await MembershipOrg.findOne({ membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId _id: membershipOrgId
}).populate("user"); }).populate('user');
if (!membershipOrgToDelete) { if (!membershipOrgToDelete) {
throw new Error("Failed to delete organization membership that doesn't exist"); throw new Error(
} "Failed to delete organization membership that doesn't exist"
);
}
const { permission, membership: membershipOrg } = await getUserOrgPermissions( // check if user is a member and admin of the organization
req.user._id, // whose membership we wish to delete
membershipOrgToDelete.organization.toString() const membershipOrg = await MembershipOrg.findOne({
); user: req.user._id,
ForbiddenError.from(permission).throwUnlessCan( organization: membershipOrgToDelete.organization
OrgPermissionActions.Delete, });
OrgPermissionSubjects.Member
);
// delete organization membership if (!membershipOrg) {
await deleteMemberFromOrg({ throw new Error('Failed to validate organization membership');
membershipOrgId: membershipOrgToDelete._id.toString() }
});
await updateSubscriptionOrgQuantity({ if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
organizationId: membershipOrg.organization.toString() // user is not an admin member of the organization
}); throw new Error('Insufficient role for deleting organization membership');
}
return membershipOrgToDelete; // delete organization membership
const deletedMembershipOrg = await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString()
});
await updateSubscriptionOrgQuantity({
organizationId: membershipOrg.organization.toString()
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return membershipOrgToDelete;
}; };
/** /**
@ -73,14 +73,22 @@ export const deleteMembershipOrg = async (req: Request, _res: Response) => {
* @returns * @returns
*/ */
export const changeMembershipOrgRole = async (req: Request, res: Response) => { export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// change role for (target) organization membership with id // change role for (target) organization membership with id
// [membershipOrgId] // [membershipOrgId]
let membershipToChangeRole; let membershipToChangeRole;
// try {
// } catch (err) {
// Sentry.setUser({ email: req.user.email });
// Sentry.captureException(err);
// return res.status(400).send({
// message: 'Failed to change organization membership role'
// });
// }
return res.status(200).send({ return res.status(200).send({
membershipOrg: membershipToChangeRole membershipOrg: membershipToChangeRole
}); });
}; };
/** /**
@ -91,128 +99,109 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let inviteeMembershipOrg, completeInviteLink; let invitee, inviteeMembershipOrg, completeInviteLink;
const { try {
body: { inviteeEmail, organizationId } const { organizationId, inviteeEmail } = req.body;
} = await validateRequest(reqValidator.InviteUserToOrgv1, req); const host = req.headers.host;
const siteUrl = `${req.protocol}://${host}`;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); // validate membership
ForbiddenError.from(permission).throwUnlessCan( const membershipOrg = await MembershipOrg.findOne({
OrgPermissionActions.Create, user: req.user._id,
OrgPermissionSubjects.Member organization: organizationId
); });
const host = req.headers.host; if (!membershipOrg) {
const siteUrl = `${req.protocol}://${host}`; throw new Error('Failed to validate organization membership');
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId)); }
const ssoConfig = await SSOConfig.findOne({ invitee = await User.findOne({
organization: new Types.ObjectId(organizationId) email: inviteeEmail
}); }).select('+publicKey');
if (ssoConfig && ssoConfig.isActive) { if (invitee) {
// case: SAML SSO is enabled for the organization // case: invitee is an existing user
return res.status(400).send({
message: "Failed to invite member due to SAML SSO configured for organization"
});
}
if (plan.memberLimit !== null) { inviteeMembershipOrg = await MembershipOrg.findOne({
// case: limit imposed on number of members allowed user: invitee._id,
organization: organizationId
});
if (plan.membersUsed >= plan.memberLimit) { if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
// case: number of members used exceeds the number of members allowed throw new Error(
return res.status(400).send({ 'Failed to invite an existing member of the organization'
message: );
"Failed to invite member due to member limit reached. Upgrade plan to invite more members." }
});
}
}
const invitee = await User.findOne({ if (!inviteeMembershipOrg) {
email: inviteeEmail await new MembershipOrg({
}).select("+publicKey"); user: invitee,
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: invitee?.publicKey ? ACCEPTED : INVITED
}).save();
}
} else {
// check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail,
organization: organizationId
});
if (invitee) { if (!inviteeMembershipOrg) {
// case: invitee is an existing user // case: invitee has never been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({ await new MembershipOrg({
user: invitee._id, inviteEmail: inviteeEmail,
organization: organizationId organization: organizationId,
}); role: MEMBER,
status: INVITED
}).save();
}
}
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { const organization = await Organization.findOne({ _id: organizationId });
throw new Error("Failed to invite an existing member of the organization");
}
if (!inviteeMembershipOrg) { if (organization) {
await new MembershipOrg({ const token = await TokenService.createToken({
user: invitee, type: TOKEN_EMAIL_ORG_INVITATION,
inviteEmail: inviteeEmail, email: inviteeEmail,
organization: organizationId, organizationId: organization._id
role: MEMBER, });
status: INVITED
}).save();
}
} else {
// check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail,
organization: organizationId
});
if (!inviteeMembershipOrg) { await sendMail({
// case: invitee has never been invited before template: 'organizationInvitation.handlebars',
subjectLine: 'Infisical organization invitation',
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
token,
callback_url: getSiteURL() + '/signupinvite'
}
});
// validate that email is not disposable if (!getSmtpConfigured()) {
validateUserEmail(inviteeEmail); completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
}
}
await new MembershipOrg({ await updateSubscriptionOrgQuantity({ organizationId });
inviteEmail: inviteeEmail, } catch (err) {
organization: organizationId, Sentry.setUser(null);
role: MEMBER, Sentry.captureException(err);
status: INVITED return res.status(400).send({
}).save(); message: 'Failed to send organization invite'
} });
} }
const organization = await Organization.findOne({ _id: organizationId }); return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
if (organization) { completeInviteLink
const token = await TokenService.createToken({ });
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
await sendMail({
template: "organizationInvitation.handlebars",
subjectLine: "Infisical organization invitation",
recipients: [inviteeEmail],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
organizationId: organization._id.toString(),
token,
callback_url: (await getSiteURL()) + "/signupinvite"
}
});
if (!(await getSmtpConfigured())) {
completeInviteLink = `${
siteUrl + "/signupinvite"
}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`;
}
}
await updateSubscriptionOrgQuantity({ organizationId });
return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`,
completeInviteLink
});
}; };
/** /**
@ -223,65 +212,65 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const verifyUserToOrganization = async (req: Request, res: Response) => { export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user; let user, token;
try {
const { email, code } = req.body;
const { user = await User.findOne({ email }).select('+publicKey');
body: { organizationId, email, code }
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
user = await User.findOne({ email }).select("+publicKey"); const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED
});
const membershipOrg = await MembershipOrg.findOne({ if (!membershipOrg)
inviteEmail: email, throw new Error('Failed to find any invitations for email');
status: INVITED,
organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) throw new Error("Failed to find any invitations for email"); await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email,
organizationId: membershipOrg.organization,
token: code
});
await TokenService.validateToken({ if (user && user?.publicKey) {
type: TOKEN_EMAIL_ORG_INVITATION, // case: user has already completed account
email, // membership can be approved and redirected to login/dashboard
organizationId: membershipOrg.organization, membershipOrg.status = ACCEPTED;
token: code await membershipOrg.save();
});
if (user && user?.publicKey) { return res.status(200).send({
// case: user has already completed account message: 'Successfully verified email',
// membership can be approved and redirected to login/dashboard user,
membershipOrg.status = ACCEPTED; });
await membershipOrg.save(); }
await updateSubscriptionOrgQuantity({ if (!user) {
organizationId // initialize user account
}); user = await new User({
email
}).save();
}
return res.status(200).send({ // generate temporary signup token
message: "Successfully verified email", token = createToken({
user payload: {
}); userId: user._id.toString()
} },
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email magic link verification for organization invitation'
});
}
if (!user) { return res.status(200).send({
// initialize user account message: 'Successfully verified email',
user = await new User({ user,
email token
}).save(); });
}
// generate temporary signup token
const token = createToken({
payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfully verified email",
user,
token
});
}; };

View File

@ -1,35 +1,79 @@
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { import {
IncidentContactOrg, Membership,
Membership, MembershipOrg,
MembershipOrg, Organization,
Organization, Workspace,
Workspace IncidentContactOrg
} from "../../models"; } from '../../models';
import { getLicenseServerUrl, getSiteURL } from "../../config"; import { createOrganization as create } from '../../helpers/organization';
import { licenseServerKeyRequest } from "../../config/request"; import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { validateRequest } from "../../helpers/validation"; import { OWNER, ACCEPTED } from '../../variables';
import * as reqValidator from "../../validation/organization"; import _ from 'lodash';
import { ACCEPTED } from "../../variables"; import { getStripeSecretKey, getSiteURL } from '../../config';
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { OrganizationNotFoundError } from "../../utils/errors";
import { ForbiddenError } from "@casl/ability";
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {
const organizations = ( let organizations;
await MembershipOrg.find({ try {
user: req.user._id, organizations = (
status: ACCEPTED await MembershipOrg.find({
}).populate("organization") user: req.user._id
).map((m) => m.organization); }).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organizations'
});
}
return res.status(200).send({ return res.status(200).send({
organizations organizations
}); });
};
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
let organization;
try {
const { organizationName } = req.body;
if (organizationName.length < 1) {
throw new Error('Organization names must be at least 1-character long');
}
// create organization and add user as member
organization = await create({
email: req.user.email,
name: organizationName
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [OWNER],
statuses: [ACCEPTED]
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create organization'
});
}
return res.status(200).send({
organization
});
}; };
/** /**
@ -39,23 +83,20 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => {
const { let organization;
params: { organizationId } try {
} = await validateRequest(reqValidator.GetOrgv1, req); organization = req.membershipOrg.organization;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to find organization'
});
}
// ensure user has membership return res.status(200).send({
await getUserOrgPermissions(req.user._id, organizationId); organization
});
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
return res.status(200).send({
organization
});
}; };
/** /**
@ -65,23 +106,24 @@ export const getOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganizationMembers = async (req: Request, res: Response) => { export const getOrganizationMembers = async (req: Request, res: Response) => {
const { let users;
params: { organizationId } try {
} = await validateRequest(reqValidator.GetOrgMembersv1, req); const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); users = await MembershipOrg.find({
ForbiddenError.from(permission).throwUnlessCan( organization: organizationId
OrgPermissionActions.Read, }).populate('user', '+publicKey');
OrgPermissionSubjects.Member } catch (err) {
); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization members'
});
}
const users = await MembershipOrg.find({ return res.status(200).send({
organization: organizationId users
}).populate("user", "+publicKey"); });
return res.status(200).send({
users
});
}; };
/** /**
@ -90,39 +132,43 @@ export const getOrganizationMembers = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationWorkspaces = async (req: Request, res: Response) => { export const getOrganizationWorkspaces = async (
const { req: Request,
params: { organizationId } res: Response
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req); ) => {
let workspaces;
try {
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const workspacesSet = new Set(
ForbiddenError.from(permission).throwUnlessCan( (
OrgPermissionActions.Read, await Workspace.find(
OrgPermissionSubjects.Workspace {
); organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
const workspacesSet = new Set( workspaces = (
( await Membership.find({
await Workspace.find( user: req.user._id
{ }).populate('workspace')
organization: organizationId )
}, .filter((m) => workspacesSet.has(m.workspace._id.toString()))
"_id" .map((m) => m.workspace);
) } catch (err) {
).map((w) => w._id.toString()) Sentry.setUser({ email: req.user.email });
); Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get my workspaces'
});
}
const workspaces = ( return res.status(200).send({
await Membership.find({ workspaces
user: req.user._id });
}).populate("workspace")
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
return res.status(200).send({
workspaces
});
}; };
/** /**
@ -132,33 +178,34 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
* @returns * @returns
*/ */
export const changeOrganizationName = async (req: Request, res: Response) => { export const changeOrganizationName = async (req: Request, res: Response) => {
const { let organization;
params: { organizationId }, try {
body: { name } const { organizationId } = req.params;
} = await validateRequest(reqValidator.ChangeOrgNamev1, req); const { name } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); organization = await Organization.findOneAndUpdate(
ForbiddenError.from(permission).throwUnlessCan( {
OrgPermissionActions.Edit, _id: organizationId
OrgPermissionSubjects.Settings },
); {
name
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change organization name'
});
}
const organization = await Organization.findOneAndUpdate( return res.status(200).send({
{ message: 'Successfully changed organization name',
_id: organizationId organization
}, });
{
name
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed organization name",
organization
});
}; };
/** /**
@ -167,24 +214,28 @@ export const changeOrganizationName = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => { export const getOrganizationIncidentContacts = async (
const { req: Request,
params: { organizationId } res: Response
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req); ) => {
let incidentContactsOrg;
try {
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); incidentContactsOrg = await IncidentContactOrg.find({
ForbiddenError.from(permission).throwUnlessCan( organization: organizationId
OrgPermissionActions.Read, });
OrgPermissionSubjects.IncidentAccount } catch (err) {
); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization incident contacts'
});
}
const incidentContactsOrg = await IncidentContactOrg.find({ return res.status(200).send({
organization: organizationId incidentContactsOrg
}); });
return res.status(200).send({
incidentContactsOrg
});
}; };
/** /**
@ -193,27 +244,31 @@ export const getOrganizationIncidentContacts = async (req: Request, res: Respons
* @param res * @param res
* @returns * @returns
*/ */
export const addOrganizationIncidentContact = async (req: Request, res: Response) => { export const addOrganizationIncidentContact = async (
const { req: Request,
params: { organizationId }, res: Response
body: { email } ) => {
} = await validateRequest(reqValidator.CreateOrgIncideContact, req); let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
ForbiddenError.from(permission).throwUnlessCan( { email, organization: organizationId },
OrgPermissionActions.Create, { email, organization: organizationId },
OrgPermissionSubjects.IncidentAccount { upsert: true, new: true }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to add incident contact for organization'
});
}
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( return res.status(200).send({
{ email, organization: organizationId }, incidentContactOrg
{ email, organization: organizationId }, });
{ upsert: true, new: true }
);
return res.status(200).send({
incidentContactOrg
});
}; };
/** /**
@ -222,137 +277,151 @@ export const addOrganizationIncidentContact = async (req: Request, res: Response
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => { export const deleteOrganizationIncidentContact = async (
const { req: Request,
params: { organizationId }, res: Response
body: { email } ) => {
} = await validateRequest(reqValidator.DelOrgIncideContact, req); let incidentContactOrg;
try {
const { organizationId } = req.params;
const { email } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
ForbiddenError.from(permission).throwUnlessCan( email,
OrgPermissionActions.Delete, organization: organizationId
OrgPermissionSubjects.IncidentAccount });
); } catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization incident contact'
});
}
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ return res.status(200).send({
email, message: 'Successfully deleted organization incident contact',
organization: organizationId incidentContactOrg
}); });
return res.status(200).send({
message: "Successfully deleted organization incident contact",
incidentContactOrg
});
}; };
/** /**
* Redirect user to billing portal or add card page depending on * Redirect user to (stripe) billing portal or add card page depending on
* if there is a card on file * if there is a card on file
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createOrganizationPortalSession = async (req: Request, res: Response) => { export const createOrganizationPortalSession = async (
const { req: Request,
params: { organizationId } res: Response
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req); ) => {
let session;
try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); // check if there is a payment method on file
ForbiddenError.from(permission).throwUnlessCan( const paymentMethods = await stripe.paymentMethods.list({
OrgPermissionActions.Edit, customer: req.membershipOrg.organization.customerId,
OrgPermissionSubjects.Billing type: 'card'
); });
const organization = await Organization.findById(organizationId); if (paymentMethods.data.length < 1) {
if (!organization) { // case: no payment method on file
throw OrganizationNotFoundError({ session = await stripe.checkout.sessions.create({
message: "Failed to find organization" customer: req.membershipOrg.organization.customerId,
}); mode: 'setup',
} payment_method_types: ['card'],
success_url: getSiteURL() + '/dashboard',
cancel_url: getSiteURL() + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.membershipOrg.organization.customerId,
return_url: getSiteURL() + '/dashboard'
});
}
const { return res.status(200).send({ url: session.url });
data: { pmtMethods } } catch (err) {
} = await licenseServerKeyRequest.get( Sentry.setUser({ email: req.user.email });
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${ Sentry.captureException(err);
organization.customerId return res.status(400).send({
}/billing-details/payment-methods` message: 'Failed to redirect to organization billing portal'
); });
}
if (pmtMethods.length < 1) {
// case: organization has no payment method on file
// -> redirect to add payment method portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{
success_url: (await getSiteURL()) + "/dashboard",
cancel_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
} else {
// case: organization has payment method on file
// -> redirect to billing portal
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/billing-portal`,
{
return_url: (await getSiteURL()) + "/dashboard"
}
);
return res.status(200).send({ url });
}
}; };
/**
* Return organization subscriptions
* @param req
* @param res
* @returns
*/
export const getOrganizationSubscriptions = async (
req: Request,
res: Response
) => {
let subscriptions;
try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
subscriptions = await stripe.subscriptions.list({
customer: req.membershipOrg.organization.customerId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization subscriptions'
});
}
return res.status(200).send({
subscriptions
});
};
/** /**
* Given a org id, return the projects each member of the org belongs to * Given a org id, return the projects each member of the org belongs to
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => { export const getOrganizationMembersAndTheirWorkspaces = async (
const { req: Request,
params: { organizationId } res: Response
} = await validateRequest(reqValidator.GetOrgMembersv1, req); ) => {
const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const workspacesSet = (
ForbiddenError.from(permission).throwUnlessCan( await Workspace.find(
OrgPermissionActions.Read, {
OrgPermissionSubjects.Member organization: organizationId
); },
ForbiddenError.from(permission).throwUnlessCan( '_id'
OrgPermissionActions.Read, )
OrgPermissionSubjects.Workspace ).map((w) => w._id.toString());
);
const workspacesSet = ( const memberships = (
await Workspace.find( await Membership.find({
{ workspace: { $in: workspacesSet }
organization: organizationId }).populate('workspace')
}, );
"_id" const userToWorkspaceIds: any = {};
)
).map((w) => w._id.toString());
const memberships = await Membership.find({ memberships.forEach(membership => {
workspace: { $in: workspacesSet } const user = membership.user.toString();
}).populate("workspace"); if (userToWorkspaceIds[user]) {
const userToWorkspaceIds: any = {}; userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
memberships.forEach((membership) => { return res.json(userToWorkspaceIds);
const user = membership.user.toString(); };
if (userToWorkspaceIds[user]) {
userToWorkspaceIds[user].push(membership.workspace);
} else {
userToWorkspaceIds[user] = [membership.workspace];
}
});
return res.json(userToWorkspaceIds);
};

View File

@ -1,106 +1,113 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require("jsrp"); const jsrp = require('jsrp');
import * as bigintConversion from "bigint-conversion"; import * as bigintConversion from 'bigint-conversion';
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models"; import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { clearTokens, createToken, sendMail } from "../../helpers"; import { createToken } from '../../helpers/auth';
import { TokenService } from "../../services"; import { sendMail } from '../../helpers/nodemailer';
import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables"; import { TokenService } from '../../services';
import { BadRequestError } from "../../utils/errors"; import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables';
import { import { BadRequestError } from '../../utils/errors';
getAuthSecret, import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config';
getHttpsEnabled,
getJwtSignupLifetime,
getSiteURL
} from "../../config";
import { ActorType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/** /**
* Password reset step 1: Send email verification link to email [email] * Password reset step 1: Send email verification link to email [email]
* for account recovery. * for account recovery.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const emailPasswordReset = async (req: Request, res: Response) => { export const emailPasswordReset = async (req: Request, res: Response) => {
const { let email: string;
body: { email } try {
} = await validateRequest(reqValidator.EmailPasswordResetV1, req); email = req.body.email;
const user = await User.findOne({ email }).select("+publicKey"); const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(200).send({ return res.status(403).send({
message: "If an account exists with this email, a password reset link has been sent" error: 'Failed to send email verification for password reset'
}); });
} }
const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email
});
await sendMail({
template: 'passwordReset.handlebars',
subjectLine: 'Infisical password reset',
recipients: [email],
substitutions: {
email,
token,
callback_url: getSiteURL() + '/password-reset'
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send email for account recovery'
});
}
const token = await TokenService.createToken({ return res.status(200).send({
type: TOKEN_EMAIL_PASSWORD_RESET, message: `Sent an email for account recovery to ${email}`
email });
}); }
await sendMail({
template: "passwordReset.handlebars",
subjectLine: "Infisical password reset",
recipients: [email],
substitutions: {
email,
token,
callback_url: (await getSiteURL()) + "/password-reset"
}
});
return res.status(200).send({
message: "If an account exists with this email, a password reset link has been sent"
});
};
/** /**
* Password reset step 2: Verify email verification link sent to email [email] * Password reset step 2: Verify email verification link sent to email [email]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const emailPasswordResetVerify = async (req: Request, res: Response) => { export const emailPasswordResetVerify = async (req: Request, res: Response) => {
const { let user, token;
body: { email, code } try {
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req); const { email, code } = req.body;
const 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
// hasn't even completed their account // hasn't even completed their account
return res.status(403).send({ return res.status(403).send({
error: "Failed email verification for password reset" error: 'Failed email verification for password reset'
}); });
} }
await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET,
email,
token: code
});
await TokenService.validateToken({ // generate temporary password-reset token
type: TOKEN_EMAIL_PASSWORD_RESET, token = createToken({
email, payload: {
token: code userId: user._id.toString()
}); },
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed email verification for password reset'
});
}
// generate temporary password-reset token return res.status(200).send({
const token = createToken({ message: 'Successfully verified email',
payload: { user,
authTokenType: AuthTokenType.SIGNUP_TOKEN, token
userId: user._id.toString() });
}, }
expiresIn: await getJwtSignupLifetime(),
secret: await getAuthSecret()
});
return res.status(200).send({
message: "Successfully verified email",
user,
token
});
};
/** /**
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol * Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
@ -109,43 +116,44 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const srp1 = async (req: Request, res: Response) => { export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol // return salt, serverPublicKey as part of first step of SRP protocol
const { try {
body: { clientPublicKey } const { clientPublicKey } = req.body;
} = await validateRequest(reqValidator.Srp1V1, req); const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
const user = await User.findOne({ if (!user) throw new Error('Failed to find user');
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user"); const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
const server = new jsrp.server(); await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
server.init( email: req.user.email,
{ clientPublicKey: clientPublicKey,
salt: user.salt, serverBInt: bigintConversion.bigintToBuf(server.bInt),
verifier: user.verifier }, { upsert: true, returnNewDocument: false })
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace( return res.status(200).send({
{ email: req.user.email }, serverPublicKey,
{ salt: user.salt
email: req.user.email, });
clientPublicKey: clientPublicKey, }
serverBInt: bigintConversion.bigintToBuf(server.bInt) );
}, } catch (err) {
{ upsert: true, returnNewDocument: false } Sentry.setUser({ email: req.user.email });
); Sentry.captureException(err);
return res.status(400).send({
return res.status(200).send({ error: 'Failed to start change password process'
serverPublicKey, });
salt: user.salt }
});
}
);
}; };
/** /**
@ -157,91 +165,80 @@ export const srp1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changePassword = async (req: Request, res: Response) => { export const changePassword = async (req: Request, res: Response) => {
const { try {
body: { const {
clientProof, clientProof,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier verifier
} } = req.body;
} = await validateRequest(reqValidator.ChangePasswordV1, req);
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select("+salt +verifier"); }).select('+salt +verifier');
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }); const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) { if (!loginSRPDetailFromDB) {
return BadRequestError( return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
Error( }
"It looks like some details from the first login are not found. Please try login one again"
)
);
}
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// change password // change password
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true new: true
} }
); );
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) { return res.status(200).send({
await clearTokens(req.authData.tokenVersionId); message: 'Successfully changed password'
} });
}
// clear httpOnly cookie return res.status(400).send({
error: 'Failed to change password. Try again?'
res.cookie("jid", "", { });
httpOnly: true, }
path: "/", );
sameSite: "strict", } catch (err) {
secure: (await getHttpsEnabled()) as boolean Sentry.setUser({ email: req.user.email });
}); Sentry.captureException(err);
return res.status(400).send({
return res.status(200).send({ error: 'Failed to change password. Try again?'
message: "Successfully changed password" });
}); }
}
return res.status(400).send({
error: "Failed to change password. Try again?"
});
}
);
}; };
/** /**
@ -251,120 +248,141 @@ export const changePassword = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createBackupPrivateKey = async (req: Request, res: Response) => { export const createBackupPrivateKey = async (req: Request, res: Response) => {
// create/change backup private key // create/change backup private key
// requires verifying [clientProof] as part of second step of SRP protocol // requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1 // as initiated in /srp1
const {
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
const user = await User.findOne({
email: req.user.email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user"); try {
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
req.body;
const user = await User.findOne({
email: req.user.email
}).select('+salt +verifier');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }); if (!user) throw new Error('Failed to find user');
if (!loginSRPDetailFromDB) { const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
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(); if (!loginSRPDetailFromDB) {
server.init( return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
{ }
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys const server = new jsrp.server();
if (server.checkClientProof(clientProof)) { server.init(
// create new or replace backup private key {
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt
},
async () => {
server.setClientPublicKey(
loginSRPDetailFromDB.clientPublicKey
);
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate( // compare server and client shared keys
{ user: req.user._id }, if (server.checkClientProof(clientProof)) {
{ // create new or replace backup private key
user: req.user._id,
encryptedPrivateKey,
iv,
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select("+user, encryptedPrivateKey");
// issue tokens const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
return res.status(200).send({ { user: req.user._id },
message: "Successfully updated backup private key", {
backupPrivateKey user: req.user._id,
}); encryptedPrivateKey,
} iv,
tag,
salt,
verifier
},
{ upsert: true, new: true }
).select('+user, encryptedPrivateKey');
return res.status(400).send({ // issue tokens
message: "Failed to update backup private key" return res.status(200).send({
}); message: 'Successfully updated backup private key',
} backupPrivateKey
); });
}
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
}; };
/** /**
* Return backup private key for user * Return backup private key for user
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getBackupPrivateKey = async (req: Request, res: Response) => { export const getBackupPrivateKey = async (req: Request, res: Response) => {
const backupPrivateKey = await BackupPrivateKey.findOne({ let backupPrivateKey;
user: req.user._id try {
}).select("+encryptedPrivateKey +iv +tag"); backupPrivateKey = await BackupPrivateKey.findOne({
user: req.user._id
}).select('+encryptedPrivateKey +iv +tag');
if (!backupPrivateKey) throw new Error("Failed to find backup private key"); if (!backupPrivateKey) throw new Error('Failed to find backup private key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({ return res.status(200).send({
backupPrivateKey backupPrivateKey
}); });
}; }
export const resetPassword = async (req: Request, res: Response) => { export const resetPassword = async (req: Request, res: Response) => {
const { try {
body: { const {
encryptedPrivateKey, protectedKey,
protectedKeyTag, protectedKeyIV,
protectedKey, protectedKeyTag,
protectedKeyIV, encryptedPrivateKey,
salt, encryptedPrivateKeyIV,
verifier, encryptedPrivateKeyTag,
encryptedPrivateKeyIV, salt,
encryptedPrivateKeyTag verifier,
} } = req.body;
} = await validateRequest(reqValidator.ResetPasswordV1, req);
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully reset password" message: 'Successfully reset password'
}); });
}; }

View File

@ -1,30 +1,30 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { Types } from "mongoose"; import * as Sentry from '@sentry/node';
import { Key } from "../../models"; import { Key, Secret } from '../../models';
import { import {
pullSecrets as pull, v1PushSecrets as push,
v1PushSecrets as push, pullSecrets as pull,
reformatPullSecrets reformatPullSecrets
} from "../../helpers/secret"; } from '../../helpers/secret';
import { pushKeys } from "../../helpers/key"; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from "../../events"; import { eventPushSecrets } from '../../events';
import { EventService } from "../../services"; import { EventService } from '../../services';
import { TelemetryService } from "../../services"; import { getPostHogClient } from '../../services';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
tagKey: string; tagKey: string;
hashKey: string; hashKey: string;
ciphertextValue: string; ciphertextValue: string;
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string; ciphertextComment: string;
ivComment: string; ivComment: string;
tagComment: string; tagComment: string;
hashComment: string; hashComment: string;
type: "shared" | "personal"; type: 'shared' | 'personal';
} }
/** /**
@ -35,59 +35,70 @@ interface PushSecret {
* @returns * @returns
*/ */
export const pushSecrets = async (req: Request, res: Response) => { export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment try {
const workspaceEnvs = req.membership.workspace.environments; const postHogClient = getPostHogClient();
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { let { secrets }: { secrets: PushSecret[] } = req.body;
throw new Error("Failed to validate environment"); const { keys, environment, channel } = req.body;
} const { workspaceId } = req.params;
// sanitize secrets // validate environment
secrets = secrets.filter((s: PushSecret) => s.ciphertextKey !== "" && s.ciphertextValue !== ""); const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment');
}
await push({ // sanitize secrets
userId: req.user._id, secrets = secrets.filter(
workspaceId, (s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
environment, );
secrets
});
await pushKeys({ await push({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
keys environment,
}); secrets
});
if (postHogClient) { await pushKeys({
postHogClient.capture({ userId: req.user._id,
event: "secrets pushed", workspaceId,
distinctId: req.user.email, keys
properties: { });
numberOfSecrets: secrets.length,
environment,
workspaceId, if (postHogClient) {
channel: channel ? channel : "cli" postHogClient.capture({
} event: 'secrets pushed',
}); distinctId: req.user.email,
} properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId
environment, })
secretPath: "/" });
})
});
return res.status(200).send({ } catch (err) {
message: "Successfully uploaded workspace secrets" 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'
});
}; };
/** /**
@ -98,56 +109,64 @@ export const pushSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
let key;
try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
const postHogClient = await TelemetryService.getPostHogClient(); // validate environment
const environment: string = req.query.environment as string; const workspaceEnvs = req.membership.workspace.environments;
const channel: string = req.query.channel as string; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
const { workspaceId } = req.params; throw new Error('Failed to validate environment');
}
// validate environment secrets = await pull({
const workspaceEnvs = req.membership.workspace.environments; userId: req.user._id.toString(),
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { workspaceId,
throw new Error("Failed to validate environment"); environment,
} channel: channel ? channel : 'cli',
ipAddress: req.ip
});
secrets = await pull({ key = await Key.findOne({
userId: req.user._id.toString(), workspace: workspaceId,
workspaceId, receiver: req.user._id
environment, })
channel: channel ? channel : "cli", .sort({ createdAt: -1 })
ipAddress: req.realIP .populate('sender', '+publicKey');
});
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
const key = await Key.findOne({ if (postHogClient) {
workspace: workspaceId, // capture secrets pushed event in production
receiver: req.user._id postHogClient.capture({
}) distinctId: req.user.email,
.sort({ createdAt: -1 }) event: 'secrets pulled',
.populate("sender", "+publicKey"); 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'
});
}
if (channel !== "cli") { return res.status(200).send({
secrets = reformatPullSecrets({ secrets }); secrets,
} key
});
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: "secrets pulled",
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : "cli"
}
});
}
return res.status(200).send({
secrets,
key
});
}; };
/** /**
@ -159,51 +178,61 @@ 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) => {
const postHogClient = await TelemetryService.getPostHogClient(); let secrets;
const environment: string = req.query.environment as string; let key;
const channel: string = req.query.channel as string; try {
const { workspaceId } = req.params; const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment"); throw new Error('Failed to validate environment');
} }
const secrets = await pull({ secrets = await pull({
userId: req.serviceToken.user._id.toString(), userId: req.serviceToken.user._id.toString(),
workspaceId, workspaceId,
environment, environment,
channel: "cli", channel: 'cli',
ipAddress: req.realIP ipAddress: req.ip
}); });
const key = { key = {
encryptedKey: req.serviceToken.encryptedKey, encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce, nonce: req.serviceToken.nonce,
sender: { sender: {
publicKey: req.serviceToken.publicKey publicKey: req.serviceToken.publicKey
}, },
receiver: req.serviceToken.user, receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace workspace: req.serviceToken.workspace
}; };
if (postHogClient) { if (postHogClient) {
// capture secrets pulled 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',
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : "cli" channel: channel ? channel : 'cli'
} }
}); });
} }
} catch (err) {
Sentry.setUser({ email: req.serviceToken.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
secrets: reformatPullSecrets({ secrets }), secrets: reformatPullSecrets({ secrets }),
key key
}); });
}; };

View File

@ -1,721 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import {
BadRequestError,
ResourceNotFoundError,
UnauthorizedRequestError
} from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretImports";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError, subject } from "@casl/ability";
export const createSecretImp = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create secret import'
#swagger.description = 'Create secret import'
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of workspace where to create secret import",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to create secret import",
"example": "dev"
},
"directory": {
"type": "string",
"description": "Path where to create secret import like / or /foo/bar. Default is /",
"example": "/foo/bar"
},
"secretImport": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Slug of environment to import from",
"example": "development"
},
"secretPath": {
"type": "string",
"description": "Path where to import from like / or /foo/bar.",
"example": "/user/oauth"
}
}
}
},
"required": ["workspaceId", "environment", "directory", "secretImport"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully created secret import"
}
},
"description": "Confirmation of secret import creation"
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Secret import already exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
#swagger.responses[404] = {
description: "Resource Not Found. For example, 'Failed to find folder'"
}
*/
const {
body: { workspaceId, environment, directory, secretImport }
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/")
throw ResourceNotFoundError({ message: "Failed to find folder" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
const doc = new SecretImport({
workspace: workspaceId,
environment,
folderId,
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
});
await doc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: doc._id.toString(),
folderId: doc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: doc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
}
const doesImportExist = importSecDoc.imports.find(
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
);
if (doesImportExist) {
throw BadRequestError({ message: "Secret import already exist" });
}
importSecDoc.imports.push({
environment: secretImport.environment,
secretPath: secretImport.secretPath
});
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment,
importToSecretPath: directory
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully created secret import" });
};
// to keep the ordering, you must pass all the imports in here not the only updated one
// this is because the order decide which import gets overriden
/**
* Update secret import
* @param req
* @param res
* @returns
*/
export const updateSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update secret import'
#swagger.description = 'Update secret import'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of secret import to update',
required: true,
type: 'string',
example: 'import12345'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImports": {
"type": "array",
"description": "List of secret imports to update to",
"items": {
"type": "object",
"properties": {
"environment": {
"type": "string",
"description": "Slug of environment to import from",
"example": "dev"
},
"secretPath": {
"type": "string",
"description": "Path where to import secrets from like / or /foo/bar",
"example": "/foo/bar"
}
},
"required": ["environment", "secretPath"]
}
}
},
"required": ["secretImports"]
}
}
}
}
#swagger.responses[200] = {
description: 'Successfully updated the secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully updated secret import"
}
}
}
}
}
}
#swagger.responses[400] = {
description: 'Bad Request - Import not found',
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
body: { secretImports },
params: { id }
} = await validateRequest(reqValidator.UpdateSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token permission check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// non token entry check
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
const orderBefore = importSecDoc.imports;
importSecDoc.imports = secretImports;
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SECRET_IMPORT,
metadata: {
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath,
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
orderBefore,
orderAfter: secretImports
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully updated secret import" });
};
/**
* Delete secret import
* @param req
* @param res
* @returns
*/
export const deleteSecretImport = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete secret import'
#swagger.description = 'Delete secret import'
#swagger.parameters['id'] = {
in: 'path',
description: 'ID of parent secret import document from which to delete secret import',
required: true,
type: 'string',
example: '12345abcde'
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImportEnv": {
"type": "string",
"description": "Slug of environment of import to delete",
"example": "someWorkspaceId"
},
"secretImportPath": {
"type": "string",
"description": "Path like / or /foo/bar of import to delete",
"example": "production"
}
},
"required": ["id", "secretImportEnv", "secretImportPath"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"example": "successfully delete secret import"
}
},
"description": "Confirmation of secret import deletion"
}
}
}
}
*/
const {
params: { id },
body: { secretImportEnv, secretImportPath }
} = await validateRequest(reqValidator.DeleteSecretImportV1, req);
const importSecDoc = await SecretImport.findById(id);
if (!importSecDoc) {
throw BadRequestError({ message: "Import not found" });
}
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
importSecDoc.imports = importSecDoc.imports.filter(
({ environment, secretPath }) =>
!(environment === secretImportEnv && secretPath === secretImportPath)
);
await importSecDoc.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SECRET_IMPORT,
metadata: {
secretImportId: importSecDoc._id.toString(),
folderId: importSecDoc.folderId.toString(),
importFromEnvironment: secretImportEnv,
importFromSecretPath: secretImportPath,
importToEnvironment: importSecDoc.environment,
importToSecretPath: secretPath
}
},
{
workspaceId: importSecDoc.workspace
}
);
return res.status(200).json({ message: "successfully delete secret import" });
};
/**
* Get secret imports
* @param req
* @param res
* @returns
*/
export const getSecretImports = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get secret imports'
#swagger.description = 'Get secret imports'
#swagger.parameters['workspaceId'] = {
in: 'query',
description: 'ID of workspace where to get secret imports from',
required: true,
type: 'string',
example: 'workspace12345'
}
#swagger.parameters['environment'] = {
in: 'query',
description: 'Slug of environment where to get secret imports from',
required: true,
type: 'string',
example: 'production'
}
#swagger.parameters['directory'] = {
in: 'query',
description: 'Path where to get secret imports from like / or /foo/bar. Default is /',
required: false,
type: 'string',
example: 'folder12345'
}
#swagger.responses[200] = {
description: 'Successfully retrieved secret import',
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretImport": {
$ref: '#/definitions/SecretImport'
}
}
}
}
}
}
#swagger.responses[403] = {
description: 'Forbidden access due to insufficient permissions',
}
#swagger.responses[401] = {
description: 'Unauthorized access due to invalid token or scope',
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secretImport: {} });
}
return res.status(200).json({ secretImport: importSecDoc });
};
/**
* Get all secret imports
* @param req
* @param res
* @returns
*/
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({
workspace: workspaceId,
environment,
folderId
});
if (!importSecDoc) {
return res.status(200).json({ secrets: [] });
}
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
permissionCheckFn = (env: string, secPath: string) =>
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: importSecDoc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
permissionCheckFn = (env: string, secPath: string) =>
permission.can(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: env,
secretPath: secPath
})
);
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.GET_SECRET_IMPORTS,
metadata: {
environment,
secretImportId: importSecDoc._id.toString(),
folderId,
numberOfImports: importSecDoc.imports.length
}
},
{
workspaceId: importSecDoc.workspace
}
);
const secrets = await getAllImportedSecrets(
workspaceId,
environment,
folderId,
permissionCheckFn
);
return res.status(200).json({ secrets });
};

View File

@ -1,183 +0,0 @@
import { Request, Response } from "express";
import {
GitAppInstallationSession,
GitAppOrganizationInstallation,
GitRisks
} from "../../ee/models";
import crypto from "crypto";
import { Types } from "mongoose";
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
import {
STATUS_RESOLVED_FALSE_POSITIVE,
STATUS_RESOLVED_NOT_REVOKED,
STATUS_RESOLVED_REVOKED
} from "../../ee/models/gitRisks";
import { ProbotOctokit } from "probot";
import { Organization } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/secretScanning";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
export const createInstallationSession = async (req: Request, res: Response) => {
const sessionId = crypto.randomBytes(16).toString("hex");
const {
params: { organizationId }
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.SecretScanning
);
await GitAppInstallationSession.findByIdAndUpdate(
organization,
{
organization: organization.id,
sessionId: sessionId,
user: new Types.ObjectId(req.user._id)
},
{ upsert: true }
).lean();
res.send({
sessionId: sessionId
});
};
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
const {
body: { sessionId, installationId }
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
const installationSession = await GitAppInstallationSession.findOneAndDelete({
sessionId: sessionId
});
if (!installationSession) {
throw UnauthorizedRequestError();
}
const { permission } = await getUserOrgPermissions(
req.user._id,
installationSession.organization.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate(
{
organizationId: installationSession.organization
},
{
installationId: installationId,
organizationId: installationSession.organization,
user: installationSession.user
},
{
upsert: true
}
).lean();
const octokit = new ProbotOctokit({
auth: {
appId: await getSecretScanningGitAppId(),
privateKey: await getSecretScanningPrivateKey(),
installationId: installationId.toString()
}
});
const {
data: { repositories }
} = await octokit.apps.listReposAccessibleToInstallation();
for (const repository of repositories) {
scanGithubFullRepoForSecretLeaks({
organizationId: installationSession.organization.toString(),
installationId,
repository: { id: repository.id, fullName: repository.full_name }
});
}
res.json(installationLink);
};
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
const { organizationId } = req.params;
try {
const appInstallation = await GitAppOrganizationInstallation.findOne({
organizationId: organizationId
}).lean();
if (!appInstallation) {
res.json({
appInstallationComplete: false
});
}
res.json({
appInstallationComplete: true
});
} catch {
res.json({
appInstallationComplete: false
});
}
};
export const getRisksForOrganization = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.SecretScanning
);
const risks = await GitRisks.find({ organization: organizationId })
.sort({ createdAt: -1 })
.lean();
res.json({
risks: risks
});
};
export const updateRisksStatus = async (req: Request, res: Response) => {
const {
params: { organizationId, riskId },
body: { status }
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.SecretScanning
);
const isRiskResolved =
status == STATUS_RESOLVED_FALSE_POSITIVE ||
status == STATUS_RESOLVED_REVOKED ||
status == STATUS_RESOLVED_NOT_REVOKED
? true
: false;
const risk = await GitRisks.findByIdAndUpdate(riskId, {
status: status,
isResolved: isRiskResolved
}).lean();
res.json(risk);
};

View File

@ -1,680 +0,0 @@
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { EventType, FolderVersion } from "../../ee/models";
import { EEAuditLogService, EESecretService } from "../../ee/services";
import { isValidScope } from "../../helpers/secrets";
import { validateRequest } from "../../helpers/validation";
import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder";
import {
appendFolder,
getAllFolderIds,
getFolderByPath,
getFolderWithPathFromId,
validateFolderName
} from "../../services/FolderService";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders";
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Create folder'
#swagger.description = 'Create folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where to create folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to create folder",
"example": "production"
},
"folderName": {
"type": "string",
"description": "Name of folder to create",
"example": "my_folder"
},
"directory": {
"type": "string",
"description": "Path where to create folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment", "folderName"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folder": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of folder",
"example": "someFolderId"
},
"name": {
"type": "string",
"description": "Name of folder",
"example": "my_folder"
},
"version": {
"type": "number",
"description": "Version of folder",
"example": 1
}
},
"description": "Details of created folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req);
if (!validateFolderName(folderName)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
// token check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// user check
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
// space has no folders initialized
if (!folders) {
const folder = new Folder({
workspace: workspaceId,
environment,
nodes: {
id: "root",
name: "root",
version: 1,
children: []
}
});
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: child.id,
folderName,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder: { id: child.id, name: folderName } });
}
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
if (!hasCreated) return res.json({ folder: child });
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parent
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: child.id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_FOLDER,
metadata: {
environment,
folderId: child.id,
folderName,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({ folder: child });
};
/**
* Update folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const updateFolderById = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Update folder'
#swagger.description = 'Update folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['folderName'] = {
"description": "Name of folder to update",
"required": true,
"type": "string"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of workspace where to update folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to update folder",
"example": "production"
},
"name": {
"type": "string",
"description": "Name of folder to update to",
"example": "updated_folder_name"
},
"directory": {
"type": "string",
"description": "Path where to update folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment", "name"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully updated folder"
},
"folder": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of updated folder",
"example": "updated_folder_name"
},
"id": {
"type": "string",
"description": "ID of created folder",
"example": "abc123"
}
},
"description": "Details of the updated folder"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
body: { workspaceId, environment, name, directory },
params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes"
});
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const folder = parentFolder.children.find(({ name }) => name === folderName);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
const oldFolderName = folder.name;
parentFolder.version += 1;
folder.name = name;
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_FOLDER,
metadata: {
environment,
folderId: folder.id,
oldFolderName,
newFolderName: name,
folderPath
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.json({
message: "Successfully updated folder",
folder: { name: folder.name, id: folder.id }
});
};
/**
* Delete folder with id [folderId]
* @param req
* @param res
* @returns
*/
export const deleteFolder = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Delete folder'
#swagger.description = 'Delete folder'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['folderName'] = {
"description": "Name of folder to delete",
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"type": "string",
"description": "ID of the workspace where to delete folder",
"example": "someWorkspaceId"
},
"environment": {
"type": "string",
"description": "Slug of environment where to delete folder",
"example": "production"
},
"directory": {
"type": "string",
"description": "Path where to delete folder like / or /foo/bar. Default is /",
"example": "/foo/bar"
}
},
"required": ["workspaceId", "environment"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "successfully deleted folders"
},
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "ID of deleted folder",
"example": "abc123"
},
"name": {
"type": "string",
"description": "Name of deleted folder",
"example": "someFolderName"
}
}
},
"description": "List of IDs and names of deleted folders"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. Reasons can include 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
params: { folderName },
body: { environment, workspaceId, directory }
} = await validateRequest(reqValidator.DeleteFolderV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) throw ERR_FOLDER_NOT_FOUND;
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
const deletedFolder = parentFolder.children.splice(index, 1)[0];
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder
});
await folderVersion.save();
if (delFolderIds.length) {
await Secret.deleteMany({
folder: { $in: delFolderIds.map(({ id }) => id) },
workspace: workspaceId,
environment
});
}
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId: parentFolder.id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_FOLDER,
metadata: {
environment,
folderId: deletedFolder.id,
folderName: deletedFolder.name,
folderPath: directory
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
/**
* Get folders for workspace with id [workspaceId] and environment [environment]
* considering directory/path [directory]
* @param req
* @param res
* @returns
*/
export const getFolders = async (req: Request, res: Response) => {
/*
#swagger.summary = 'Get folders'
#swagger.description = 'Get folders'
#swagger.security = [{
"apiKeyAuth": [],
"bearerAuth": []
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of the workspace where to get folders from",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['environment'] = {
"description": "Slug of environment where to get folders from",
"required": true,
"type": "string",
"in": "query"
}
#swagger.parameters['directory'] = {
"description": "Path where to get fodlers from like / or /foo/bar. Default is /",
"required": false,
"type": "string",
"in": "query"
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"folders": {
"type": "array",
"items": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "someFolderId"
},
"name": {
"type": "string",
"example": "someFolderName"
}
}
},
"description": "List of folders"
}
}
}
}
}
}
#swagger.responses[400] = {
description: "Bad Request. For instance, 'The folder doesn't exist'"
}
#swagger.responses[401] = {
description: "Unauthorized request. For example, 'Folder Permission Denied'"
}
*/
const {
query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// check that user is a member of the workspace
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
}
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
return res.send({ folders: [], dir: [] });
}
const folder = getFolderByPath(folders.nodes, directory);
return res.send({
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
});
};

View File

@ -1,7 +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 { getJwtServiceSecret } from "../../config"; import { getJwtServiceSecret } from '../../config';
/** /**
* Return service token on request * Return service token on request
@ -11,7 +11,7 @@ import { getJwtServiceSecret } from "../../config";
*/ */
export const getServiceToken = async (req: Request, res: Response) => { export const getServiceToken = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
serviceToken: req.serviceToken, serviceToken: req.serviceToken
}); });
}; };
@ -31,13 +31,13 @@ export const createServiceToken = async (req: Request, res: Response) => {
expiresIn, expiresIn,
publicKey, publicKey,
encryptedKey, encryptedKey,
nonce, nonce
} = req.body; } = req.body;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment"); throw new Error('Failed to validate environment');
} }
// compute access token expiration date // compute access token expiration date
@ -52,24 +52,24 @@ export const createServiceToken = async (req: Request, res: Response) => {
expiresAt, expiresAt,
publicKey, publicKey,
encryptedKey, encryptedKey,
nonce, nonce
}).save(); }).save();
token = createToken({ token = createToken({
payload: { payload: {
serviceTokenId: serviceToken._id.toString(), serviceTokenId: serviceToken._id.toString(),
workspaceId, workspaceId
}, },
expiresIn: expiresIn, expiresIn: expiresIn,
secret: await getJwtServiceSecret(), secret: getJwtServiceSecret()
}); });
} catch (err) { } catch (err) {
return res.status(400).send({ return res.status(400).send({
message: "Failed to create service token", message: 'Failed to create service token'
}); });
} }
return res.status(200).send({ return res.status(200).send({
token, token
}); });
}; };

View File

@ -1,16 +1,13 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { AuthMethod, User } from "../../models"; import * as Sentry from '@sentry/node';
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup"; import { User } from '../../models';
import { createToken } from "../../helpers/auth";
import { import {
getAuthSecret, sendEmailVerification,
getJwtSignupLifetime, checkEmailVerification,
getSmtpConfigured } from '../../helpers/signup';
} from "../../config"; import { createToken } from '../../helpers/auth';
import { validateUserEmail } from "../../validation"; import { BadRequestError } from '../../utils/errors';
import { validateRequest } from "../../helpers/validation"; import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import * as reqValidator from "../../validation/auth";
import { AuthTokenType } from "../../variables";
/** /**
* 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
@ -20,28 +17,40 @@ import { AuthTokenType } from "../../variables";
* @returns * @returns
*/ */
export const beginEmailSignup = async (req: Request, res: Response) => { export const beginEmailSignup = async (req: Request, res: Response) => {
const { let email: string;
body: { email } try {
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req); email = req.body.email;
// validate that email is not disposable if (getInviteOnlySignup()) {
validateUserEmail(email); // Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({})
if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
}
}
const user = await User.findOne({ email }).select("+publicKey"); const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: "Failed to send email verification code for complete account" error: 'Failed to send email verification code for complete account'
}); });
} }
// send send verification email // send send verification email
await sendEmailVerification({ email }); await sendEmailVerification({ email });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to send email verification code'
});
}
return res.status(200).send({ return res.status(200).send({
message: `Sent an email verification code to ${email}` message: `Sent an email verification code to ${email}`
}); });
}; };
/** /**
@ -52,48 +61,52 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const verifyEmailSignup = async (req: Request, res: Response) => { export const verifyEmailSignup = async (req: Request, res: Response) => {
let user; let user, token;
const { try {
body: { email, code } const { email, code } = req.body;
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
// initialize user account // initialize user account
user = await User.findOne({ email }).select("+publicKey"); user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: "Failed email verification for complete user" error: 'Failed email verification for complete user'
}); });
} }
// verify email // verify email
if (await getSmtpConfigured()) { if (getSmtpConfigured()) {
await checkEmailVerification({ await checkEmailVerification({
email, email,
code code
}); });
} }
if (!user) { if (!user) {
user = await new User({ user = await new User({
email, email
authMethods: [AuthMethod.EMAIL] }).save();
}).save(); }
}
// generate temporary signup token // generate temporary signup token
const token = createToken({ token = createToken({
payload: { payload: {
authTokenType: AuthTokenType.SIGNUP_TOKEN, userId: user._id.toString()
userId: user._id.toString() },
}, expiresIn: getJwtSignupLifetime(),
expiresIn: await getJwtSignupLifetime(), secret: getJwtSignupSecret()
secret: await getAuthSecret() });
}); } catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email verification'
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfuly verified email", message: 'Successfuly verified email',
user, user,
token token
}); });
}; };

View File

@ -0,0 +1,41 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
/**
* Handle service provisioning/un-provisioning via Stripe
* @param req
* @param res
* @returns
*/
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
// check request for valid stripe signature
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
req.body,
sig,
getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) {
case '':
break;
default:
}
return res.json({ received: true });
};

View File

@ -1,7 +1,6 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { validateRequest } from "../../helpers/validation"; import * as Sentry from '@sentry/node';
import { UserAction } from "../../models"; import { UserAction } from '../../models';
import * as reqValidator from "../../validation/action";
/** /**
* Add user action [action] * Add user action [action]
@ -10,27 +9,35 @@ import * as reqValidator from "../../validation/action";
* @returns * @returns
*/ */
export const addUserAction = async (req: Request, res: Response) => { export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id] // add/record new action [action] for user with id [req.user._id]
const {
body: { action }
} = await validateRequest(reqValidator.AddUserActionV1, req);
const userAction = await UserAction.findOneAndUpdate( let userAction;
{ try {
user: req.user._id, const { action } = req.body;
action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
return res.status(200).send({ userAction = await UserAction.findOneAndUpdate(
message: "Successfully recorded user action", {
userAction user: req.user._id,
}); action
},
{ user: req.user._id, action },
{
new: true,
upsert: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to record user action'
});
}
return res.status(200).send({
message: 'Successfully recorded user action',
userAction
});
}; };
/** /**
@ -40,17 +47,24 @@ export const addUserAction = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getUserAction = async (req: Request, res: Response) => { export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id] // get user action [action] for user with id [req.user._id]
const { let userAction;
query: { action } try {
} = await validateRequest(reqValidator.GetUserActionV1, req); const action: string = req.query.action as string;
const userAction = await UserAction.findOne({ userAction = await UserAction.findOne({
user: req.user._id, user: req.user._id,
action action
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get user action'
});
}
return res.status(200).send({ return res.status(200).send({
userAction userAction
}); });
}; };

View File

@ -1,4 +1,4 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
/** /**
* Return user on request * Return user on request
@ -8,6 +8,6 @@ import { Request, Response } from "express";
*/ */
export const getUser = async (req: Request, res: Response) => { export const getUser = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
user: req.user, user: req.user
}); });
}; };

View File

@ -1,268 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
import { Webhook } from "../../models";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
import { EEAuditLogService } from "../../ee/services";
import { EventType } from "../../ee/models";
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8
} from "../../variables";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/webhooks";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
export const createWebhook = async (req: Request, res: Response) => {
const {
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.CreateWebhookV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Webhooks
);
const webhook = new Webhook({
workspace: workspaceId,
environment,
secretPath,
url: webhookUrl
});
if (webhookSecretKey) {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
if (rootEncryptionKey) {
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
webhook.algorithm = ALGORITHM_AES_256_GCM;
webhook.keyEncoding = ENCODING_SCHEME_BASE64;
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: webhookSecretKey,
key: encryptionKey
});
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
webhook.algorithm = ALGORITHM_AES_256_GCM;
webhook.keyEncoding = ENCODING_SCHEME_UTF8;
}
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment,
secretPath,
webhookUrl,
isDisabled: false
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
webhook,
message: "successfully created webhook"
});
};
export const updateWebhook = async (req: Request, res: Response) => {
const {
body: { isDisabled },
params: { webhookId }
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Webhooks
);
if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled;
}
await webhook.save();
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_WEBHOOK_STATUS,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
webhook,
message: "successfully updated webhook"
});
};
export const deleteWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
let webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.Webhooks
);
webhook = await Webhook.findByIdAndDelete(webhookId);
if (!webhook) {
throw ResourceNotFoundError({ message: "Webhook not found!!" });
}
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_WEBHOOK,
metadata: {
webhookId: webhook._id.toString(),
environment: webhook.environment,
secretPath: webhook.secretPath,
webhookUrl: webhook.url,
isDisabled: webhook.isDisabled
}
},
{
workspaceId: webhook.workspace
}
);
return res.status(200).send({
message: "successfully removed webhook"
});
};
export const testWebhook = async (req: Request, res: Response) => {
const {
params: { webhookId }
} = await validateRequest(reqValidator.TestWebhookV1, req);
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: webhook.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload(
"test",
webhook.workspace.toString(),
webhook.environment,
webhook.secretPath
)
);
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "success",
lastRunErrorMessage: null
});
} catch (err) {
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "failed",
lastRunErrorMessage: (err as Error).message
});
return res.status(400).send({
message: "Failed to receive response",
error: (err as Error).message
});
}
return res.status(200).send({
message: "Successfully received response"
});
};
export const listWebhooks = async (req: Request, res: Response) => {
const {
query: { environment, workspaceId, secretPath }
} = await validateRequest(reqValidator.ListWebhooksV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Webhooks
);
const optionalFilters: Record<string, string> = {};
if (environment) optionalFilters.environment = environment as string;
if (secretPath) optionalFilters.secretPath = secretPath as string;
const webhooks = await Webhook.find({
workspace: new Types.ObjectId(workspaceId as string),
...optionalFilters
});
return res.status(200).send({
webhooks
});
};

View File

@ -1,32 +1,21 @@
import { Types } from "mongoose";
import { Request, Response } from "express"; import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { import {
IUser, Workspace,
Membership,
MembershipOrg,
Integration, Integration,
IntegrationAuth, IntegrationAuth,
Membership, IUser,
Organization,
ServiceToken, ServiceToken,
Workspace ServiceTokenData,
} from "../../models"; } from "../../models";
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace"; import {
import { EELicenseService } from "../../ee/services"; createWorkspace as create,
deleteWorkspace as deleteWork,
} from "../../helpers/workspace";
import { addMemberships } from "../../helpers/membership"; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables"; import { ADMIN } from "../../variables";
import { OrganizationNotFoundError } from "../../utils/errors";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
/** /**
* Return public keys of members of workspace with id [workspaceId] * Return public keys of members of workspace with id [workspaceId]
@ -35,33 +24,30 @@ import {
* @returns * @returns
*/ */
export const getWorkspacePublicKeys = async (req: Request, res: Response) => { export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
const { let publicKeys;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req); const { workspaceId } = req.params;
const { permission } = await getAuthDataProjectPermissions({ publicKeys = (
authData: req.authData, await Membership.find({
workspaceId: new Types.ObjectId(workspaceId) workspace: workspaceId,
}); }).populate<{ user: IUser }>("user", "publicKey")
).map((member) => {
ForbiddenError.from(permission).throwUnlessCan( return {
ProjectPermissionActions.Read, publicKey: member.user.publicKey,
ProjectPermissionSub.Member userId: member.user._id,
); };
});
const publicKeys = ( } catch (err) {
await Membership.find({ Sentry.setUser({ email: req.user.email });
workspace: workspaceId Sentry.captureException(err);
}).populate<{ user: IUser }>("user", "publicKey") return res.status(400).send({
).map((member) => { message: "Failed to get workspace member public keys",
return { });
publicKey: member.user.publicKey, }
userId: member.user._id
};
});
return res.status(200).send({ return res.status(200).send({
publicKeys publicKeys,
}); });
}; };
@ -72,26 +58,23 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const { let users;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req); const { workspaceId } = req.params;
const { permission } = await getAuthDataProjectPermissions({ users = await Membership.find({
authData: req.authData, workspace: workspaceId,
workspaceId: new Types.ObjectId(workspaceId) }).populate("user", "+publicKey");
}); } catch (err) {
Sentry.setUser({ email: req.user.email });
ForbiddenError.from(permission).throwUnlessCan( Sentry.captureException(err);
ProjectPermissionActions.Read, return res.status(400).send({
ProjectPermissionSub.Member message: "Failed to get workspace members",
); });
}
const users = await Membership.find({
workspace: workspaceId
}).populate("user", "+publicKey");
return res.status(200).send({ return res.status(200).send({
users users,
}); });
}; };
@ -102,14 +85,23 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaces = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => {
const workspaces = ( let workspaces;
await Membership.find({ try {
user: req.user._id workspaces = (
}).populate("workspace") await Membership.find({
).map((m) => m.workspace); user: req.user._id,
}).populate("workspace")
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspaces",
});
}
return res.status(200).send({ return res.status(200).send({
workspaces workspaces,
}); });
}; };
@ -120,16 +112,23 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspace = async (req: Request, res: Response) => { export const getWorkspace = async (req: Request, res: Response) => {
const { let workspace;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetWorkspaceV1, req); const { workspaceId } = req.params;
const workspace = await Workspace.findOne({ workspace = await Workspace.findOne({
_id: workspaceId _id: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace",
});
}
return res.status(200).send({ return res.status(200).send({
workspace workspace,
}); });
}; };
@ -141,54 +140,45 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
const { let workspace;
body: { organizationId, workspaceName } try {
} = await validateRequest(reqValidator.CreateWorkspaceV1, req); const { workspaceName, organizationId } = req.body;
const organization = await Organization.findById(organizationId); // validate organization membership
if (!organization) { const membershipOrg = await MembershipOrg.findOne({
throw OrganizationNotFoundError({ user: req.user._id,
message: "Failed to find organization" organization: organizationId,
});
if (!membershipOrg) {
throw new Error("Failed to validate organization membership");
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to create workspace",
}); });
} }
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Workspace
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message:
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
});
}
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId: new Types.ObjectId(organizationId)
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN]
});
return res.status(200).send({ return res.status(200).send({
workspace workspace,
}); });
}; };
@ -199,27 +189,23 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteWorkspace = async (req: Request, res: Response) => { export const deleteWorkspace = async (req: Request, res: Response) => {
const { try {
params: { workspaceId } const { workspaceId } = req.params;
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
const { permission } = await getAuthDataProjectPermissions({ // delete workspace
authData: req.authData, await deleteWork({
workspaceId: new Types.ObjectId(workspaceId) id: workspaceId,
}); });
} catch (err) {
ForbiddenError.from(permission).throwUnlessCan( Sentry.setUser({ email: req.user.email });
ProjectPermissionActions.Delete, Sentry.captureException(err);
ProjectPermissionSub.Workspace return res.status(400).send({
); message: "Failed to delete workspace",
});
// delete workspace }
const workspace = await deleteWork({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({ return res.status(200).send({
workspace message: "Successfully deleted workspace",
}); });
}; };
@ -230,36 +216,33 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeWorkspaceName = async (req: Request, res: Response) => { export const changeWorkspaceName = async (req: Request, res: Response) => {
const { let workspace;
params: { workspaceId }, try {
body: { name } const { workspaceId } = req.params;
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req); const { name } = req.body;
const { permission } = await getAuthDataProjectPermissions({ workspace = await Workspace.findOneAndUpdate(
authData: req.authData, {
workspaceId: new Types.ObjectId(workspaceId) _id: workspaceId,
}); },
{
ForbiddenError.from(permission).throwUnlessCan( name,
ProjectPermissionActions.Edit, },
ProjectPermissionSub.Workspace {
); new: true,
}
const workspace = await Workspace.findOneAndUpdate( );
{ } catch (err) {
_id: workspaceId Sentry.setUser({ email: req.user.email });
}, Sentry.captureException(err);
{ return res.status(400).send({
name message: "Failed to change workspace name",
}, });
{ }
new: true
}
);
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed workspace name", message: "Successfully changed workspace name",
workspace workspace,
}); });
}; };
@ -270,26 +253,23 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrations = async (req: Request, res: Response) => { export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
const { let integrations;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req); const { workspaceId } = req.params;
const { permission } = await getAuthDataProjectPermissions({ integrations = await Integration.find({
authData: req.authData, workspace: workspaceId,
workspaceId: new Types.ObjectId(workspaceId) });
}); } catch (err) {
Sentry.setUser({ email: req.user.email });
ForbiddenError.from(permission).throwUnlessCan( Sentry.captureException(err);
ProjectPermissionActions.Read, return res.status(400).send({
ProjectPermissionSub.Integrations message: "Failed to get workspace integrations",
); });
}
const integrations = await Integration.find({
workspace: workspaceId
});
return res.status(200).send({ return res.status(200).send({
integrations integrations,
}); });
}; };
@ -299,27 +279,27 @@ export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => { export const getWorkspaceIntegrationAuthorizations = async (
const { req: Request,
params: { workspaceId } res: Response
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req); ) => {
let authorizations;
try {
const { workspaceId } = req.params;
const { permission } = await getAuthDataProjectPermissions({ authorizations = await IntegrationAuth.find({
authData: req.authData, workspace: workspaceId,
workspaceId: new Types.ObjectId(workspaceId) });
}); } catch (err) {
Sentry.setUser({ email: req.user.email });
ForbiddenError.from(permission).throwUnlessCan( Sentry.captureException(err);
ProjectPermissionActions.Read, return res.status(400).send({
ProjectPermissionSub.Integrations message: "Failed to get workspace integration authorizations",
); });
}
const authorizations = await IntegrationAuth.find({
workspace: workspaceId
});
return res.status(200).send({ return res.status(200).send({
authorizations authorizations,
}); });
}; };
@ -329,28 +309,27 @@ export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: R
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => { export const getWorkspaceServiceTokens = async (
const { req: Request,
params: { workspaceId } res: Response
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req); ) => {
let serviceTokens;
const { permission } = await getAuthDataProjectPermissions({ try {
authData: req.authData, const { workspaceId } = req.params;
workspaceId: new Types.ObjectId(workspaceId) // ?? FIX.
}); serviceTokens = await ServiceToken.find({
user: req.user._id,
ForbiddenError.from(permission).throwUnlessCan( workspace: workspaceId,
ProjectPermissionActions.Read, });
ProjectPermissionSub.ServiceTokens } catch (err) {
); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
// ?? FIX. return res.status(400).send({
const serviceTokens = await ServiceToken.find({ message: "Failed to get workspace service tokens",
user: req.user._id, });
workspace: workspaceId }
});
return res.status(200).send({ return res.status(200).send({
serviceTokens serviceTokens,
}); });
}; };

View File

@ -0,0 +1,103 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
APIKeyData
} from '../../models';
import { getSaltRounds } from '../../config';
/**
* Return API key data for user with id [req.user_id]
* @param req
* @param res
* @returns
*/
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, getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({
name,
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id);
if (!apiKeyData) throw new Error('Failed to find API key data');
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to API key data'
});
}
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
});
}

View File

@ -1,20 +1,28 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from "express"; import { Request, Response } from 'express';
import jwt from "jsonwebtoken"; import jwt from 'jsonwebtoken';
import * as bigintConversion from "bigint-conversion"; import * as Sentry from '@sentry/node';
const jsrp = require("jsrp"); import * as bigintConversion from 'bigint-conversion';
import { LoginSRPDetail, User } from "../../models"; const jsrp = require('jsrp');
import { createToken, issueAuthTokens } from "../../helpers/auth"; import { User, LoginSRPDetail } from '../../models';
import { checkUserDevice } from "../../helpers/user"; import { issueAuthTokens, createToken } from '../../helpers/auth';
import { sendMail } from "../../helpers/nodemailer"; import { checkUserDevice } from '../../helpers/user';
import { TokenService } from "../../services"; import { sendMail } from '../../helpers/nodemailer';
import { BadRequestError, InternalServerError } from "../../utils/errors"; import { TokenService } from '../../services';
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables"; import { EELogService } from '../../ee/services';
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config"; import { BadRequestError, InternalServerError } from '../../utils/errors';
import { validateRequest } from "../../helpers/validation"; import {
import * as reqValidator from "../../validation/auth"; TOKEN_EMAIL_MFA,
ACTION_LOGIN
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtMfaLifetime,
getJwtMfaSecret
} from '../../config';
declare module "jsonwebtoken" { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
} }
@ -27,40 +35,47 @@ declare module "jsonwebtoken" {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body; try {
const {
email,
clientPublicKey
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select("+salt +verifier"); }).select('+salt +verifier');
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace( await LoginSRPDetail.findOneAndReplace({ email: email }, {
{ email: email },
{
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt) serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, }, { upsert: true, returnNewDocument: false });
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@ -71,205 +86,223 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
if (!req.headers["user-agent"]) try {
throw InternalServerError({ message: "User-Agent header is required" });
const { email, clientProof } = req.body; if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const user = await User.findOne({
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user"); const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }); if (!user) throw new Error('Failed to find user');
if (!loginSRPDetail) { const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
return BadRequestError(Error("Failed to find login details for SRP"));
}
const server = new jsrp.server(); if (!loginSRPDetail) {
server.init( return BadRequestError(Error("Failed to find login details for SRP"))
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getAuthSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// case: user does not have MFA enabled
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
} }
);
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: getJwtMfaLifetime(),
secret: getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(response);
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
}; };
/** /**
* Send MFA token to email [email] * Send MFA token to email [email]
* @param req * @param req
* @param res * @param res
*/ */
export const sendMfaToken = async (req: Request, res: Response) => { export const sendMfaToken = async (req: Request, res: Response) => {
const code = await TokenService.createToken({ try {
type: TOKEN_EMAIL_MFA, const { email } = req.body;
email: req.user.email
});
// send MFA code [code] to [email] const code = await TokenService.createToken({
await sendMail({ type: TOKEN_EMAIL_MFA,
template: "emailMfa.handlebars", email
subjectLine: "Infisical MFA code", });
recipients: [req.user.email],
substitutions: { // send MFA code [code] to [email]
code await sendMail({
} template: 'emailMfa.handlebars',
}); subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully sent new MFA code" message: 'Successfully sent new MFA code'
}); });
}; }
/** /**
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the * Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
* MFA token [mfaToken] is valid * MFA token [mfaToken] is valid
* @param req * @param req
* @param res * @param res
*/ */
export const verifyMfaToken = async (req: Request, res: Response) => { export const verifyMfaToken = async (req: Request, res: Response) => {
const { const { email, mfaToken } = req.body;
body: { mfaToken }
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email: req.user.email, email,
token: mfaToken token: mfaToken
}); });
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email
}).select( }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user"); if (!user) throw new Error('Failed to find user');
await LoginSRPDetail.deleteOne({ userId: user.id });
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.realIP, ip: req.ip,
userAgent: req.headers["user-agent"] ?? "" userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({ userId: user._id.toString() });
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: "/", path: '/',
sameSite: "strict", sameSite: 'strict',
secure: await getHttpsEnabled() secure: getNodeEnv() === 'production' ? true : false
}); });
interface VerifyMfaTokenRes { interface VerifyMfaTokenRes {
@ -303,7 +336,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
encryptedPrivateKey: user.encryptedPrivateKey as string, encryptedPrivateKey: user.encryptedPrivateKey as string,
iv: user.iv as string, iv: user.iv as string,
tag: user.tag as string tag: user.tag as string
}; }
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) { if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
resObj.protectedKey = user.protectedKey; resObj.protectedKey = user.protectedKey;
@ -311,5 +344,17 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
resObj.protectedKeyTag = user.protectedKeyTag; resObj.protectedKeyTag = user.protectedKeyTag;
} }
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(resObj); return res.status(200).send(resObj);
}; }

View File

@ -1,236 +1,61 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { Types } from "mongoose"; import * as Sentry from '@sentry/node';
import { import {
Folder,
Integration,
Membership,
Secret, Secret,
ServiceToken, ServiceToken,
Workspace,
Integration,
ServiceTokenData, ServiceTokenData,
Workspace Membership,
} from "../../models"; } from '../../models';
import { EventType, SecretVersion } from "../../ee/models"; import { SecretVersion } from '../../ee/models';
import { EEAuditLogService, EELicenseService } from "../../ee/services"; import { BadRequestError } from '../../utils/errors';
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors"; import _ from 'lodash';
import { validateRequest } from "../../helpers/validation"; import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import * as reqValidator from "../../validation/environments";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { SecretImport } from "../../models";
import { Webhook } from "../../models";
/** /**
* Create new workspace environment named [environmentName] * Create new workspace environment named [environmentName] under workspace with id
* with slug [environmentSlug] under workspace with id
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createWorkspaceEnvironment = async (req: Request, res: Response) => { export const createWorkspaceEnvironment = async (
/* req: Request,
#swagger.summary = 'Create environment' res: Response
#swagger.description = 'Create environment' ) => {
const { workspaceId } = req.params;
#swagger.security = [{ const { environmentName, environmentSlug } = req.body;
"apiKeyAuth": [], try {
}] const workspace = await Workspace.findById(workspaceId).exec();
if (
#swagger.parameters['workspaceId'] = { !workspace ||
"description": "ID of workspace where to create environment", workspace?.environments.find(
"required": true, ({ name, slug }) => slug === environmentSlug || environmentName === name
"type": "string", )
"in": "path" ) {
} throw new Error('Failed to create workspace environment');
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of the environment to create",
"example": "development"
},
"environmentSlug": {
"type": "string",
"description": "Slug of environment to create",
"example": "dev-environment"
}
},
"required": ["environmentName", "environmentSlug"]
}
}
}
}
#swagger.responses[200] = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"message": {
"type": "string",
"description": "Sucess message",
"example": "Successfully created environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was created",
"example": "abc123"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of created environment",
"example": "Staging"
},
"slug": {
"type": "string",
"description": "Slug of created environment",
"example": "staging"
}
}
}
},
"description": "Details of the created environment"
}
}
}
} }
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug }
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({ workspace?.environments.push({
authData: req.authData, name: environmentName,
workspaceId: new Types.ObjectId(workspaceId) slug: environmentSlug.toLowerCase(),
}); });
await workspace.save();
ForbiddenError.from(permission).throwUnlessCan( } catch (err) {
ProjectPermissionActions.Create, Sentry.setUser({ email: req.user.email });
ProjectPermissionSub.Environments Sentry.captureException(err);
); return res.status(400).send({
message: 'Failed to create new workspace environment',
const workspace = await Workspace.findById(workspaceId).exec(); });
if (!workspace) throw WorkspaceNotFoundError();
const plan = await EELicenseService.getPlan(workspace.organization);
if (plan.environmentLimit !== null) {
// case: limit imposed on number of environments allowed
if (workspace.environments.length >= plan.environmentLimit) {
// case: number of environments used exceeds the number of environments allowed
return res.status(400).send({
message:
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
});
}
} }
if (
!workspace ||
workspace?.environments.find(
({ name, slug }) => slug === environmentSlug || environmentName === name
)
) {
throw new Error("Failed to create workspace environment");
}
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase()
});
await workspace.save();
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_ENVIRONMENT,
metadata: {
name: environmentName,
slug: environmentSlug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({ return res.status(200).send({
message: "Successfully created new environment", message: 'Successfully created new environment',
workspace: workspaceId, workspace: workspaceId,
environment: { environment: {
name: environmentName, name: environmentName,
slug: environmentSlug slug: environmentSlug,
} },
});
};
/**
* Swaps the ordering of two environments in the database. This is purely for aesthetic purposes.
* @param req
* @param res
* @returns
*/
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw BadRequestError({ message: "Couldn't load workspace" });
}
const environmentIndex = workspace.environments.findIndex(
(env) => env.name === environmentName && env.slug === environmentSlug
);
const otherEnvironmentIndex = workspace.environments.findIndex(
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
);
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
}
// swap the order of the environments
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
workspace.environments[otherEnvironmentIndex],
workspace.environments[environmentIndex]
];
await workspace.save();
return res.status(200).send({
message: "Successfully reordered environments",
workspace: workspaceId
}); });
}; };
@ -241,210 +66,88 @@ export const reorderWorkspaceEnvironments = async (req: Request, res: Response)
* @param res * @param res
* @returns * @returns
*/ */
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => { export const renameWorkspaceEnvironment = async (
/* req: Request,
#swagger.summary = 'Update environment' res: Response
#swagger.description = 'Update environment' ) => {
const { workspaceId } = req.params;
#swagger.security = [{ const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
"apiKeyAuth": [], try {
}] // user should pass both new slug and env name
if (!environmentSlug || !environmentName) {
#swagger.parameters['workspaceId'] = { throw new Error('Invalid environment given.');
"description": "ID of workspace where to update environment",
"required": true,
"type": "string",
"in": "path"
} }
#swagger.requestBody = { // atomic update the env to avoid conflict
content: { const workspace = await Workspace.findById(workspaceId).exec();
"application/json": { if (!workspace) {
"schema": { throw new Error('Failed to create workspace environment');
"type": "object",
"properties": {
"environmentName": {
"type": "string",
"description": "Name of environment to update to",
"example": "Staging-Renamed"
},
"environmentSlug": {
"type": "string",
"description": "Slug of environment to update to",
"example": "staging-renamed"
},
"oldEnvironmentSlug": {
"type": "string",
"description": "Current slug of environment",
"example": "staging-old"
}
},
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
}
}
}
} }
#swagger.responses[200] = { const isEnvExist = workspace.environments.some(
content: { ({ name, slug }) =>
"application/json": { slug !== oldEnvironmentSlug &&
"schema": { (name === environmentName || slug === environmentSlug)
"type": "object", );
"properties": { if (isEnvExist) {
"message": { throw new Error('Invalid environment given');
"type": "string",
"description": "Success message",
"example": "Successfully update environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was updated",
"example": "abc123"
},
"environment": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name of updated environment",
"example": "Staging-Renamed"
},
"slug": {
"type": "string",
"description": "Slug of updated environment",
"example": "staging-renamed"
}
}
}
},
"description": "Details of the renamed environment"
}
}
}
} }
*/
const {
params: { workspaceId },
body: { environmentName, environmentSlug, oldEnvironmentSlug }
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({ const envIndex = workspace?.environments.findIndex(
authData: req.authData, ({ slug }) => slug === oldEnvironmentSlug
workspaceId: new Types.ObjectId(workspaceId) );
}); if (envIndex === -1) {
throw new Error('Invalid environment given');
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.Environments
);
// 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");
}
const oldEnvironment = workspace.environments[envIndex];
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,
"scopes.environment": oldEnvironmentSlug
},
{ $set: { "scopes.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Folder.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretImport.updateMany(
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
{ $set: { "imports.$[element].environment": environmentSlug } },
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
);
await Webhook.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
);
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_ENVIRONMENT,
metadata: {
oldName: oldEnvironment.name,
newName: environmentName,
oldSlug: oldEnvironment.slug,
newSlug: environmentSlug.toLowerCase()
}
},
{
workspaceId: workspace._id
} }
);
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({ return res.status(200).send({
message: "Successfully update environment", message: 'Successfully update environment',
workspace: workspaceId, workspace: workspaceId,
environment: { environment: {
name: environmentName, name: environmentName,
slug: environmentSlug slug: environmentSlug,
} },
}); });
}; };
@ -454,151 +157,106 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
* @param res * @param res
* @returns * @returns
*/ */
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => { export const deleteWorkspaceEnvironment = async (
/* req: Request,
#swagger.summary = 'Delete environment' res: Response
#swagger.description = 'Delete environment' ) => {
const { workspaceId } = req.params;
#swagger.security = [{ const { environmentSlug } = req.body;
"apiKeyAuth": [] try {
}] // atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
#swagger.parameters['workspaceId'] = { if (!workspace) {
"description": "ID of workspace where to delete environment", throw new Error('Failed to create workspace environment');
"required": true,
"type": "string",
"in": "path"
}
#swagger.requestBody = {
content: {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environmentSlug": {
"type": "string",
"description": "Slug of environment to delete",
"example": "dev"
}
},
"required": ["environmentSlug"]
}
}
}
} }
#swagger.responses[200] = { const envIndex = workspace?.environments.findIndex(
content: { ({ slug }) => slug === environmentSlug
"application/json": { );
"schema": { if (envIndex === -1) {
"type": "object", throw new Error('Invalid environment given');
"properties": {
"message": {
"type": "string",
"description": "Success message",
"example": "Successfully deleted environment"
},
"workspace": {
"type": "string",
"description": "ID of workspace where environment was deleted",
"example": "abc123"
},
"environment": {
"type": "string",
"description": "Slug of deleted environment",
"example": "dev"
}
},
"description": "Response after deleting an environment from a workspace"
}
}
}
} }
*/
const {
params: { workspaceId },
body: { environmentSlug }
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
const { permission } = await getAuthDataProjectPermissions({ workspace.environments.splice(envIndex, 1);
authData: req.authData, await workspace.save();
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan( // clean up
ProjectPermissionActions.Delete, await Secret.deleteMany({
ProjectPermissionSub.Environments 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 } } }
)
// atomic update the env to avoid conflict } catch (err) {
const workspace = await Workspace.findById(workspaceId).exec(); Sentry.setUser({ email: req.user.email });
if (!workspace) { Sentry.captureException(err);
throw new Error("Failed to create workspace environment"); return res.status(400).send({
message: 'Failed to delete workspace environment',
});
} }
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
if (envIndex === -1) {
throw new Error("Invalid environment given");
}
const oldEnvironment = workspace.environments[envIndex];
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,
// });
const result = await ServiceTokenData.updateMany(
{ workspace: workspaceId },
{ $pull: { scopes: { environment: environmentSlug } } }
);
if (result.modifiedCount > 0) {
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
}
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
);
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_ENVIRONMENT,
metadata: {
name: oldEnvironment.name,
slug: oldEnvironment.slug
}
},
{
workspaceId: workspace._id
}
);
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted environment", message: 'Successfully deleted environment',
workspace: workspaceId, workspace: workspaceId,
environment: environmentSlug 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 && isWriteBlocked) {
return
} else {
accessibleEnvironments.push({
name: environment.name,
slug: environment.slug,
isWriteDenied: isWriteBlocked,
isReadDenied: isReadBlocked
})
}
})
res.json({ accessibleEnvironments })
};

View File

@ -1,25 +1,25 @@
import * as authController from "./authController"; import * as authController from './authController';
import * as signupController from "./signupController"; import * as signupController from './signupController';
import * as usersController from "./usersController"; import * as usersController from './usersController';
import * as organizationsController from "./organizationsController"; import * as organizationsController from './organizationsController';
import * as workspaceController from "./workspaceController"; import * as workspaceController from './workspaceController';
import * as serviceTokenDataController from "./serviceTokenDataController"; import * as serviceTokenDataController from './serviceTokenDataController';
import * as secretController from "./secretController"; import * as apiKeyDataController from './apiKeyDataController';
import * as secretsController from "./secretsController"; import * as secretController from './secretController';
import * as environmentController from "./environmentController"; import * as secretsController from './secretsController';
import * as tagController from "./tagController"; import * as environmentController from './environmentController';
import * as membershipController from "./membershipController"; import * as tagController from './tagController';
export { export {
authController, authController,
signupController, signupController,
usersController, usersController,
organizationsController, organizationsController,
workspaceController, workspaceController,
serviceTokenDataController, serviceTokenDataController,
secretController, apiKeyDataController,
secretsController, secretController,
environmentController, secretsController,
tagController, environmentController,
membershipController tagController
}; }

View File

@ -1,107 +0,0 @@
import { ForbiddenError } from "@casl/ability";
import { Request, Response } from "express";
import { Types } from "mongoose";
import { getSiteURL } from "../../config";
import { EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../ee/services/ProjectRoleService";
import { sendMail } from "../../helpers";
import { validateRequest } from "../../helpers/validation";
import { IUser, Key, Membership, MembershipOrg, Workspace } from "../../models";
import { BadRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/membership";
import { ACCEPTED, MEMBER } from "../../variables";
export const addUserToWorkspace = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { members }
} = await validateRequest(reqValidator.AddUserToWorkspaceV2, req);
// check workspace
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw new Error("Failed to find workspace");
// check permission
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.Member
);
// validate members are part of the organization
const orgMembers = await MembershipOrg.find({
status: ACCEPTED,
_id: { $in: members.map(({ orgMembershipId }) => orgMembershipId) },
organization: workspace.organization
})
.populate<{ user: IUser }>("user")
.select({ _id: 1, user: 1 })
.lean();
if (orgMembers.length !== members.length)
throw BadRequestError({ message: "Org member not found" });
const existingMember = await Membership.find({
workspace: workspaceId,
user: { $in: orgMembers.map(({ user }) => user) }
});
if (existingMember?.length)
throw BadRequestError({ message: "Some users are already part of workspace" });
await Membership.insertMany(
orgMembers.map(({ user }) => ({ user: user._id, workspace: workspaceId, role: MEMBER }))
);
const encKeyGroupedByOrgMemberId = members.reduce<Record<string, (typeof members)[number]>>(
(prev, curr) => ({ ...prev, [curr.orgMembershipId]: curr }),
{}
);
await Key.insertMany(
orgMembers.map(({ user, _id: id }) => ({
encryptedKey: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedKey,
nonce: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedNonce,
sender: req.user._id,
receiver: user._id,
workspace: workspaceId
}))
);
await sendMail({
template: "workspaceInvitation.handlebars",
subjectLine: "Infisical workspace invitation",
recipients: orgMembers.map(({ user }) => user.email),
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: workspace.name,
callback_url: (await getSiteURL()) + "/login"
}
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
metadata: orgMembers.map(({ user }) => ({
userId: user._id.toString(),
email: user.email
}))
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
success: true,
data: orgMembers
});
};

View File

@ -1,32 +1,20 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { Types } from "mongoose"; import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, Workspace } from "../../models"; import {
import { Role } from "../../ee/models"; MembershipOrg,
import { deleteMembershipOrg } from "../../helpers/membershipOrg"; Membership,
import { Workspace
createOrganization as create, } from '../../models';
deleteOrganization, import { deleteMembershipOrg } from '../../helpers/membershipOrg';
updateSubscriptionOrgQuantity import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
} from "../../helpers/organization";
import { addMembershipsOrg } from "../../helpers/membershipOrg";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { ACCEPTED, ADMIN, CUSTOM } from "../../variables";
import * as reqValidator from "../../validation/organization";
import { validateRequest } from "../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../ee/services/RoleService";
import { ForbiddenError } from "@casl/ability";
/** /**
* Return memberships for organization with id [organizationId] * Return memberships for organization with id [organizationId]
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationMemberships = async (req: Request, res: Response) => { export const getOrganizationMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return organization memberships' #swagger.summary = 'Return organization memberships'
#swagger.description = 'Return organization memberships' #swagger.description = 'Return organization memberships'
@ -59,32 +47,33 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
} }
} }
*/ */
const { let memberships;
params: { organizationId } try {
} = await validateRequest(reqValidator.GetOrgMembersv2, req); const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); memberships = await MembershipOrg.find({
ForbiddenError.from(permission).throwUnlessCan( organization: organizationId
OrgPermissionActions.Read, }).populate('user', '+publicKey');
OrgPermissionSubjects.Member } catch (err) {
); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
const memberships = await MembershipOrg.find({ return res.status(400).send({
organization: organizationId message: 'Failed to get organization memberships'
}).populate("user", "+publicKey"); });
}
return res.status(200).send({
memberships return res.status(200).send({
}); memberships
}; });
}
/** /**
* Update role of membership with id [membershipId] to role [role] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
* @param res * @param res
*/ */
export const updateOrganizationMembership = async (req: Request, res: Response) => { export const updateOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update organization membership' #swagger.summary = 'Update organization membership'
#swagger.description = 'Update organization membership' #swagger.description = 'Update organization membership'
@ -137,58 +126,40 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
const { let membership;
params: { organizationId, membershipId }, try {
body: { role } const { membershipId } = req.params;
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req); const { role } = req.body;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan( membership = await MembershipOrg.findByIdAndUpdate(
OrgPermissionActions.Edit, membershipId,
OrgPermissionSubjects.Member {
); role
}, {
const isCustomRole = !["admin", "member"].includes(role); new: true
if (isCustomRole) { }
const orgRole = await Role.findOne({ slug: role, isOrgRole: true }); );
if (!orgRole) throw BadRequestError({ message: "Role not found" }); } catch (err) {
Sentry.setUser({ email: req.user.email });
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, { Sentry.captureException(err);
role: CUSTOM, return res.status(400).send({
customRole: orgRole message: 'Failed to update organization membership'
}); });
return res.status(200).send({
membership
});
}
const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
{
$set: {
role
},
$unset: {
customRole: 1
}
},
{
new: true
} }
);
return res.status(200).send({
return res.status(200).send({ membership
membership });
}); }
};
/** /**
* Delete organization membership with id [membershipId] * Delete organization membership with id [membershipId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteOrganizationMembership = async (req: Request, res: Response) => { export const deleteOrganizationMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete organization membership' #swagger.summary = 'Delete organization membership'
#swagger.description = 'Delete organization membership' #swagger.description = 'Delete organization membership'
@ -224,37 +195,39 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
const { let membership;
params: { organizationId, membershipId } try {
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req); const { membershipId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan( // delete organization membership
OrgPermissionActions.Delete, membership = await deleteMembershipOrg({
OrgPermissionSubjects.Member membershipOrgId: membershipId
); });
// delete organization membership await updateSubscriptionOrgQuantity({
const membership = await deleteMembershipOrg({ organizationId: membership.organization.toString()
membershipOrgId: membershipId });
}); } catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
await updateSubscriptionOrgQuantity({ return res.status(200).send({
organizationId: membership.organization.toString() membership
}); });
}
return res.status(200).send({
membership
});
};
/** /**
* Return workspaces for organization with id [organizationId] that user has * Return workspaces for organization with id [organizationId] that user has
* access to * access to
* @param req * @param req
* @param res * @param res
*/ */
export const getOrganizationWorkspaces = async (req: Request, res: Response) => { export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return projects in organization that user is part of' #swagger.summary = 'Return projects in organization that user is part of'
#swagger.description = 'Return projects in organization that user is part of' #swagger.description = 'Return projects in organization that user is part of'
@ -287,93 +260,37 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
} }
} }
*/ */
const { let workspaces;
params: { organizationId } try {
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req); const { organizationId } = req.params;
const { permission } = await getUserOrgPermissions(req.user._id, organizationId); const workspacesSet = new Set(
ForbiddenError.from(permission).throwUnlessCan( (
OrgPermissionActions.Read, await Workspace.find(
OrgPermissionSubjects.Workspace {
); organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
const workspacesSet = new Set( workspaces = (
( await Membership.find({
await Workspace.find( user: req.user._id
{ }).populate('workspace')
organization: organizationId )
}, .filter((m) => workspacesSet.has(m.workspace._id.toString()))
"_id" .map((m) => m.workspace);
) } catch (err) {
).map((w) => w._id.toString()) Sentry.setUser({ email: req.user.email });
); Sentry.captureException(err);
return res.status(400).send({
const workspaces = ( message: 'Failed to get organization workspaces'
await Membership.find({ });
user: req.user._id }
}).populate("workspace")
) return res.status(200).send({
.filter((m) => workspacesSet.has(m.workspace._id.toString())) workspaces
.map((m) => m.workspace); });
}
return res.status(200).send({
workspaces
});
};
/**
* Create new organization named [organizationName]
* and add user as owner
* @param req
* @param res
* @returns
*/
export const createOrganization = async (req: Request, res: Response) => {
const {
body: { name }
} = await validateRequest(reqValidator.CreateOrgv2, req);
// create organization and add user as member
const organization = await create({
email: req.user.email,
name
});
await addMembershipsOrg({
userIds: [req.user._id.toString()],
organizationId: organization._id.toString(),
roles: [ADMIN],
statuses: [ACCEPTED]
});
return res.status(200).send({
organization
});
};
/**
* Delete organization with id [organizationId]
* @param req
* @param res
*/
export const deleteOrganizationById = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.DeleteOrgv2, req);
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: new Types.ObjectId(organizationId),
role: ADMIN
});
if (!membershipOrg) throw UnauthorizedRequestError();
const organization = await deleteOrganization({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send({
organization
});
};

View File

@ -1,35 +1,23 @@
import to from "await-to-js";
import { Request, Response } from "express"; import { Request, Response } from "express";
import mongoose, { Types } from "mongoose"; import mongoose, { Types } from "mongoose";
import { import Secret, { ISecret } from "../../models/secret";
CreateSecretRequestBody, import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCreate, SanitizedSecretModify } from "../../types/secret";
ModifySecretRequestBody,
SanitizedSecretForCreate,
SanitizedSecretModify
} from "../../types/secret";
const { ValidationError } = mongoose.Error; const { ValidationError } = mongoose.Error;
import { import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
ValidationError as RouteValidationError, import { AnyBulkWriteOperation } from 'mongodb';
UnauthorizedRequestError import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
} from "../../utils/errors"; import { getPostHogClient } from '../../services';
import {
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
SECRET_PERSONAL,
SECRET_SHARED
} from "../../variables";
import { TelemetryService } from "../../services";
import { Secret, User } from "../../models";
import { AccountNotFoundError } from "../../utils/errors";
/** /**
* Create secret for workspace with id [workspaceId] and environment [environment] * Create secret for workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret; const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params; const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = { const sanitizedSecret: SanitizedSecretForCreate = {
secretKeyCiphertext: secretToCreate.secretKeyCiphertext, secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
secretKeyIV: secretToCreate.secretKeyIV, secretKeyIV: secretToCreate.secretKeyIV,
@ -46,44 +34,46 @@ export const createSecret = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type: secretToCreate.type, type: secretToCreate.type,
user: new Types.ObjectId(req.user._id), user: new Types.ObjectId(req.user._id)
algorithm: ALGORITHM_AES_256_GCM, }
keyEncoding: ENCODING_SCHEME_UTF8
};
const secret = await new Secret(sanitizedSecret).save();
const [error, secret] = await to(Secret.create(sanitizedSecret).then())
if (error instanceof ValidationError) {
throw RouteValidationError({ message: error.message, stack: error.stack })
}
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets added", event: 'secrets added',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId, workspaceId,
environment, environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.["user-agent"] userAgent: req.headers?.['user-agent']
} }
}); });
} }
res.status(200).send({ res.status(200).send({
secret secret
}); })
}; }
/** /**
* Create many secrets for workspace with id [workspaceId] and environment [environment] * Create many secrets for workspace wiht id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
*/ */
export const createSecrets = async (req: Request, res: Response) => { export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params; const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []; const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
secretsToCreate.forEach((rawSecret) => { secretsToCreate.forEach(rawSecret => {
const safeUpdateFields: SanitizedSecretForCreate = { const safeUpdateFields: SanitizedSecretForCreate = {
secretKeyCiphertext: rawSecret.secretKeyCiphertext, secretKeyCiphertext: rawSecret.secretKeyCiphertext,
secretKeyIV: rawSecret.secretKeyIV, secretKeyIV: rawSecret.secretKeyIV,
@ -100,132 +90,141 @@ export const createSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type: rawSecret.type, type: rawSecret.type,
user: new Types.ObjectId(req.user._id), user: new Types.ObjectId(req.user._id)
algorithm: ALGORITHM_AES_256_GCM, }
keyEncoding: ENCODING_SCHEME_UTF8
};
sanitizedSecretesToCreate.push(safeUpdateFields); sanitizedSecretesToCreate.push(safeUpdateFields)
}); })
const secrets = await Secret.insertMany(sanitizedSecretesToCreate); 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) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets added", event: 'secrets added',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: (secretsToCreate ?? []).length, numberOfSecrets: (secretsToCreate ?? []).length,
workspaceId, workspaceId,
environment, environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.["user-agent"] userAgent: req.headers?.['user-agent']
} }
}); });
} }
res.status(200).send({ res.status(200).send({
secrets secrets
}); })
}; }
/** /**
* Delete secrets in workspace with id [workspaceId] and environment [environment] * Delete secrets in workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
*/ */
export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params; const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds; const secretIdsToDelete: string[] = req.body.secretIds
const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }); 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( const secretsUserCanDeleteSet: Set<string> = new Set(secretIdsUserCanDelete.map(objectId => objectId._id.toString()));
secretIdsUserCanDelete.map((objectId) => objectId._id.toString()) const deleteOperationsToPerform: AnyBulkWriteOperation<ISecret>[] = []
);
// Filter out IDs that user can delete and then map them to delete operations let numSecretsDeleted = 0;
const deleteOperationsToPerform = secretIdsToDelete secretIdsToDelete.forEach(secretIdToDelete => {
.filter(secretIdToDelete => { if (secretsUserCanDeleteSet.has(secretIdToDelete)) {
if (!secretsUserCanDeleteSet.has(secretIdToDelete)) { const deleteOperation = { deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } } }
throw RouteValidationError({ deleteOperationsToPerform.push(deleteOperation)
message: "You cannot delete secrets that you do not have access to" numSecretsDeleted++;
}); } else {
} throw RouteValidationError({ message: "You cannot delete secrets that you do not have access to" })
return true; }
}) })
.map(secretIdToDelete => ({
deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
}));
const numSecretsDeleted = deleteOperationsToPerform.length; const [bulkDeleteError, bulkDelete] = await to(Secret.bulkWrite(deleteOperationsToPerform).then())
if (bulkDeleteError) {
await Secret.bulkWrite(deleteOperationsToPerform); if (bulkDeleteError instanceof ValidationError) {
throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: bulkDeleteError.stack })
}
throw InternalServerError()
}
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets deleted", event: 'secrets deleted',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: numSecretsDeleted, numberOfSecrets: numSecretsDeleted,
environment: environmentName, environment: environmentName,
workspaceId, workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.["user-agent"] userAgent: req.headers?.['user-agent']
} }
}); });
} }
res.status(200).send(); res.status(200).send()
}; }
/** /**
* Delete secret with id [secretId] * Delete secret with id [secretId]
* @param req * @param req
* @param res * @param res
*/ */
export const deleteSecret = async (req: Request, res: Response) => { export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id); await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets deleted", event: 'secrets deleted',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
workspaceId: req._secret.workspace.toString(), workspaceId: req._secret.workspace.toString(),
environment: req._secret.environment, environment: req._secret.environment,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.["user-agent"] userAgent: req.headers?.['user-agent']
} }
}); });
} }
res.status(200).send({ res.status(200).send({
secret: req._secret secret: req._secret
}); })
}; }
/** /**
* Update secrets for workspace with id [workspaceId] and environment [environment] * Update secrets for workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateSecrets = async (req: Request, res: Response) => { export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params; const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }); 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( const secretsUserCanModifySet: Set<string> = new Set(secretIdsUserCanModify.map(objectId => objectId._id.toString()));
secretIdsUserCanModify.map((objectId) => objectId._id.toString()) const updateOperationsToPerform: any = []
);
const updateOperationsToPerform: any = [];
secretsModificationsRequested.forEach((userModifiedSecret) => { secretsModificationsRequested.forEach(userModifiedSecret => {
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) { if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
const sanitizedSecret: SanitizedSecretModify = { const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext, secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
@ -239,54 +238,57 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext, secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
secretCommentIV: userModifiedSecret.secretCommentIV, secretCommentIV: userModifiedSecret.secretCommentIV,
secretCommentTag: userModifiedSecret.secretCommentTag, secretCommentTag: userModifiedSecret.secretCommentTag,
secretCommentHash: userModifiedSecret.secretCommentHash secretCommentHash: userModifiedSecret.secretCommentHash,
}; }
const updateOperation = { const updateOperation = { updateOne: { filter: { _id: userModifiedSecret._id, workspace: workspaceId }, update: { $inc: { version: 1 }, $set: sanitizedSecret } } }
updateOne: { updateOperationsToPerform.push(updateOperation)
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
update: { $inc: { version: 1 }, $set: sanitizedSecret }
}
};
updateOperationsToPerform.push(updateOperation);
} else { } else {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({ message: "You do not have permission to modify one or more of the requested secrets" })
message: "You do not have permission to modify one or more of the requested secrets"
});
} }
}); })
await Secret.bulkWrite(updateOperationsToPerform); 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) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets modified", event: 'secrets modified',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: (secretsModificationsRequested ?? []).length, numberOfSecrets: (secretsModificationsRequested ?? []).length,
environment: environmentName, environment: environmentName,
workspaceId, workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.["user-agent"] userAgent: req.headers?.['user-agent']
} }
}); });
} }
return res.status(200).send(); return res.status(200).send()
}; }
/** /**
* Update a secret within workspace with id [workspaceId] and environment [environment] * Update a secret within workspace with id [workspaceId] and environment [environment]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateSecret = async (req: Request, res: Response) => { export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params; const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }); const [secretIdUserCanModifyError, secretIdUserCanModify] = await to(Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
if (secretIdUserCanModifyError && !secretIdUserCanModify) {
throw BadRequestError()
}
const sanitizedSecret: SanitizedSecretModify = { const sanitizedSecret: SanitizedSecretModify = {
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext, secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
@ -300,103 +302,90 @@ export const updateSecret = async (req: Request, res: Response) => {
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext, secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
secretCommentIV: secretModificationsRequested.secretCommentIV, secretCommentIV: secretModificationsRequested.secretCommentIV,
secretCommentTag: secretModificationsRequested.secretCommentTag, secretCommentTag: secretModificationsRequested.secretCommentTag,
secretCommentHash: secretModificationsRequested.secretCommentHash secretCommentHash: secretModificationsRequested.secretCommentHash,
}; }
const singleModificationUpdate = await Secret.updateOne( const [error, singleModificationUpdate] = await to(Secret.updateOne({ _id: secretModificationsRequested._id, workspace: workspaceId }, { $inc: { version: 1 }, $set: sanitizedSecret }).then())
{ _id: secretModificationsRequested._id, workspace: workspaceId }, if (error instanceof ValidationError) {
{ $inc: { version: 1 }, $set: sanitizedSecret } throw RouteValidationError({ message: "Unable to apply modifications, please try again", stack: error.stack })
) }
.catch((error) => {
if (error instanceof ValidationError) {
throw RouteValidationError({
message: "Unable to apply modifications, please try again",
stack: error.stack
});
}
throw error;
});
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets modified", event: 'secrets modified',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: 1, numberOfSecrets: 1,
environment: environmentName, environment: environmentName,
workspaceId, workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.["user-agent"] userAgent: req.headers?.['user-agent']
} }
}); });
} }
return res.status(200).send(singleModificationUpdate); return res.status(200).send(singleModificationUpdate)
}; }
/** /**
* Return secrets for workspace with id [workspaceId], environment [environment] and user * Return secrets for workspace with id [workspaceId], environment [environment] and user
* with id [req.user._id] * with id [req.user._id]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = getPostHogClient();
const { environment } = req.query; const { environment } = req.query;
const { workspaceId } = req.params; const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
let userEmail: string | undefined = undefined; // used for posthog let userEmail: Types.ObjectId | undefined = undefined // used for posthog
if (req.user) { if (req.user) {
userId = req.user._id; userId = req.user._id;
userEmail = req.user.email; userEmail = req.user.email;
} }
if (req.serviceTokenData) { if (req.serviceTokenData) {
userId = req.serviceTokenData.user; userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
const user = await User.findById(req.serviceTokenData.user, "email");
if (!user) throw AccountNotFoundError();
userEmail = user.email;
} }
const secrets = await Secret.find({ const [err, secrets] = await to(Secret.find(
workspace: workspaceId, {
environment, workspace: workspaceId,
$or: [{ user: userId }, { user: { $exists: false } }], environment,
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] } $or: [{ user: userId }, { user: { $exists: false } }],
}) type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
.catch((err) => { }
throw RouteValidationError({ ).then())
message: "Failed to get secrets, please try again",
stack: err.stack if (err) {
}); throw RouteValidationError({ message: "Failed to get secrets, please try again", stack: err.stack })
}) }
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets pulled", event: 'secrets pulled',
distinctId: userEmail, distinctId: userEmail,
properties: { properties: {
numberOfSecrets: (secrets ?? []).length, numberOfSecrets: (secrets ?? []).length,
environment, environment,
workspaceId, workspaceId,
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli", channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
userAgent: req.headers?.["user-agent"] userAgent: req.headers?.['user-agent']
} }
}); });
} }
return res.json(secrets); return res.json(secrets)
}; }
/** /**
* Return secret with id [secretId] * Return secret with id [secretId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSecret = async (req: Request, res: Response) => { export const getSecret = async (req: Request, res: Response) => {
// if (postHogClient) { // if (postHogClient) {
@ -416,4 +405,4 @@ export const getSecret = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
secret: req._secret secret: req._secret
}); });
}; }

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,22 @@
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import crypto from "crypto"; import { Request, Response } from 'express';
import bcrypt from "bcrypt"; import crypto from 'crypto';
import { ServiceTokenData } from "../../models"; import bcrypt from 'bcrypt';
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import { ActorType, EventType } from "../../ee/models";
import { EEAuditLogService } from "../../ee/services";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/serviceTokenData";
import { import {
ProjectPermissionActions, ServiceTokenData
ProjectPermissionSub, } from '../../models';
getAuthDataProjectPermissions import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
} from "../../ee/services/ProjectRoleService"; import { ABILITY_READ } from '../../variables/organization';
import { ForbiddenError } from "@casl/ability"; import { getSaltRounds } from '../../config';
import { Types } from "mongoose";
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getServiceTokenData = async (req: Request, res: Response) => { export const getServiceTokenData = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return Infisical Token data' #swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data' #swagger.description = 'Return Infisical Token data'
@ -36,158 +29,115 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"serviceTokenData": { "serviceTokenData": {
"type": "object", "type": "object",
$ref: "#/components/schemas/ServiceTokenData", $ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token" "description": "Details of service token"
} }
} }
} }
} }
} }
} }
*/ */
if (!(req.authData.authPayload instanceof ServiceTokenData)) return res.status(200).json(req.serviceTokenData);
throw BadRequestError({ }
message: "Failed accepted client validation for service token data"
});
const serviceTokenData = await ServiceTokenData.findById(req.authData.authPayload._id)
.select("+encryptedKey +iv +tag")
.populate("user")
.lean();
return res.status(200).json(serviceTokenData);
};
/** /**
* Create new service token data for workspace with id [workspaceId] and * Create new service token data for workspace with id [workspaceId] and
* environment [environment]. * environment [environment].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData; let serviceToken, serviceTokenData;
const { try {
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv } const {
} = await validateRequest(reqValidator.CreateServiceTokenV2, req); name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
const { permission } = await getAuthDataProjectPermissions({ const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
authData: req.authData, if (!hasAccess) {
workspaceId: new Types.ObjectId(workspaceId) throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}); }
ForbiddenError.from(permission).throwUnlessCan( const secret = crypto.randomBytes(16).toString('hex');
ProjectPermissionActions.Create, const secretHash = await bcrypt.hash(secret, getSaltRounds());
ProjectPermissionSub.ServiceTokens
);
const secret = crypto.randomBytes(16).toString("hex"); const expiresAt = new Date();
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
let expiresAt; serviceTokenData = await new ServiceTokenData({
if (expiresIn) { name,
expiresAt = new Date(); workspace: workspaceId,
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); environment,
} user: req.user._id,
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
let user; // return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (req.authData.actor.type === ActorType.USER) { if (!serviceTokenData) throw new Error('Failed to find service token data');
user = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({ serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
name,
workspace: workspaceId,
user,
scopes,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data } catch (err) {
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id); Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
if (!serviceTokenData) throw new Error("Failed to find service token data"); return res.status(400).send({
message: 'Failed to create service token data'
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`; });
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SERVICE_TOKEN,
metadata: {
name,
scopes
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
} }
);
return res.status(200).send({ return res.status(200).send({
serviceToken, serviceToken,
serviceTokenData serviceTokenData
}); });
}; }
/** /**
* Delete service token data with id [serviceTokenDataId]. * Delete service token data with id [serviceTokenDataId].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteServiceTokenData = async (req: Request, res: Response) => { export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { let serviceTokenData;
params: { serviceTokenDataId } try {
} = await validateRequest(reqValidator.DeleteServiceTokenV2, req); const { serviceTokenDataId } = req.params;
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId); serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
const { permission } = await getAuthDataProjectPermissions({ } catch (err) {
authData: req.authData, Sentry.setUser({ email: req.user.email });
workspaceId: serviceTokenData.workspace Sentry.captureException(err);
}); return res.status(400).send({
message: 'Failed to delete service token data'
ForbiddenError.from(permission).throwUnlessCan( });
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens
);
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData)
return res.status(200).send({
message: "Failed to delete service token"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SERVICE_TOKEN,
metadata: {
name: serviceTokenData.name,
scopes: serviceTokenData?.scopes
}
},
{
workspaceId: serviceTokenData.workspace
} }
);
return res.status(200).send({ return res.status(200).send({
serviceTokenData serviceTokenData
}); });
}; }
function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
}

View File

@ -1,14 +1,14 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { MembershipOrg, User } from "../../models"; import * as Sentry from '@sentry/node';
import { completeAccount } from "../../helpers/user"; import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import { import {
initializeDefaultOrg, initializeDefaultOrg
} from "../../helpers/signup"; } from '../../helpers/signup';
import { issueAuthTokens } from "../../helpers/auth"; import { issueAuthTokens } from '../../helpers/auth';
import { ACCEPTED, INVITED } from "../../variables"; import { INVITED, ACCEPTED } from '../../variables';
import { standardRequest } from "../../config/request"; import request from '../../config/request';
import { getHttpsEnabled, getLoopsApiKey } from "../../config"; import { getNodeEnv, getLoopsApiKey } from '../../config';
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
@ -18,136 +18,129 @@ import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
* @returns * @returns
*/ */
export const completeAccountSignup = async (req: Request, res: Response) => { export const completeAccountSignup = async (req: Request, res: Response) => {
let user; let user, token, refreshToken;
const { try {
email, const {
firstName, email,
lastName, firstName,
protectedKey, lastName,
protectedKeyIV, protectedKey,
protectedKeyTag, protectedKeyIV,
publicKey, protectedKeyTag,
encryptedPrivateKey, publicKey,
encryptedPrivateKeyIV, encryptedPrivateKey,
encryptedPrivateKeyTag, encryptedPrivateKeyIV,
salt, encryptedPrivateKeyTag,
verifier, salt,
organizationName, verifier,
}: { organizationName
email: string; }: {
firstName: string; email: string;
lastName: string; firstName: string;
protectedKey: string; lastName: string;
protectedKeyIV: string; protectedKey: string;
protectedKeyTag: string; protectedKeyIV: string;
publicKey: string; protectedKeyTag: string;
encryptedPrivateKey: string; publicKey: string;
encryptedPrivateKeyIV: string; encryptedPrivateKey: string;
encryptedPrivateKeyTag: string; encryptedPrivateKeyIV: string;
salt: string; encryptedPrivateKeyTag: string;
verifier: string; salt: string;
organizationName: string; verifier: string;
} = req.body; organizationName: string;
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
if (!user || (user && user?.publicKey)) { // complete setting up user's account
// case 1: user doesn't exist. user = await completeAccount({
// case 2: user has already completed account userId: user._id.toString(),
return res.status(403).send({ firstName,
error: "Failed to complete account for complete user", lastName,
}); encryptionVersion: 2,
} protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
// complete setting up user's account if (!user)
user = await completeAccount({ throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
});
if (!user) // initialize default organization and workspace
throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null await initializeDefaultOrg({
organizationName,
user
});
// initialize default organization and workspace // update organization membership statuses that are
await initializeDefaultOrg({ // invited to completed with user attached
organizationName, await MembershipOrg.updateMany(
user, {
}); inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// update organization membership statuses that are // issue tokens
// invited to completed with user attached const tokens = await issueAuthTokens({
const membershipsToUpdate = await MembershipOrg.find({ userId: user._id.toString()
inviteEmail: email, });
status: INVITED,
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString(),
});
});
// update organization membership statuses that are token = tokens.token;
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED,
},
{
user,
status: ACCEPTED,
}
);
// issue tokens // sending a welcome email to new users
const tokens = await issueAuthTokens({ if (getLoopsApiKey()) {
userId: user._id, await request.post("https://app.loops.so/api/v1/events/send", {
ip: req.realIP, "email": email,
userAgent: req.headers["user-agent"] ?? "", "eventName": "Sign Up",
}); "firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + getLoopsApiKey()
},
});
}
const token = tokens.token; // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
// sending a welcome email to new users httpOnly: true,
if (await getLoopsApiKey()) { path: '/',
await standardRequest.post("https://app.loops.so/api/v1/events/send", { sameSite: 'strict',
"email": email, secure: getNodeEnv() === 'production' ? true : false
"eventName": "Sign Up", });
"firstName": firstName, } catch (err) {
"lastName": lastName, Sentry.setUser(null);
}, { Sentry.captureException(err);
headers: { return res.status(400).send({
"Accept": "application/json", message: 'Failed to complete account setup'
"Authorization": "Bearer " + (await getLoopsApiKey()), });
}, }
});
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
});
return res.status(200).send({ return res.status(200).send({
message: "Successfully set up account", message: 'Successfully set up account',
user, user,
token, token
}); });
}; };
@ -159,104 +152,99 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const completeAccountInvite = async (req: Request, res: Response) => { export const completeAccountInvite = async (req: Request, res: Response) => {
let user; let user, token, refreshToken;
const { try {
email, const {
firstName, email,
lastName, firstName,
protectedKey, lastName,
protectedKeyIV, protectedKey,
protectedKeyTag, protectedKeyIV,
publicKey, protectedKeyTag,
encryptedPrivateKey, publicKey,
encryptedPrivateKeyIV, encryptedPrivateKey,
encryptedPrivateKeyTag, encryptedPrivateKeyIV,
salt, encryptedPrivateKeyTag,
verifier, salt,
} = req.body; verifier
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: "Failed to complete account for complete user", error: 'Failed to complete account for complete user'
}); });
} }
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email, inviteEmail: email,
status: INVITED, status: INVITED
}); });
if (!membershipOrg) throw new Error("Failed to find invitations for email"); if (!membershipOrg) throw new Error('Failed to find invitations for email');
// complete setting up user's account // complete setting up user's account
user = await completeAccount({ user = await completeAccount({
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier, verifier
}); });
if (!user) if (!user)
throw new Error("Failed to complete account for non-existent user"); throw new Error('Failed to complete account for non-existent user');
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED,
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString(),
});
});
await MembershipOrg.updateMany( // update organization membership statuses that are
{ // invited to completed with user attached
inviteEmail: email, await MembershipOrg.updateMany(
status: INVITED, {
}, inviteEmail: email,
{ status: INVITED
user, },
status: ACCEPTED, {
} user,
); status: ACCEPTED
}
);
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id, userId: user._id.toString()
ip: req.realIP, });
userAgent: req.headers["user-agent"] ?? "",
});
const token = tokens.token; token = tokens.token;
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled(),
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully set up account", message: 'Successfully set up account',
user, user,
token, token
}); });
}; };

View File

@ -1,92 +1,72 @@
import { ForbiddenError } from "@casl/ability"; import { Request, Response } from 'express';
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import { Types } from "mongoose"; import { Types } from 'mongoose';
import { Secret, Tag } from "../../models";
import { BadRequestError } from "../../utils/errors";
import { validateRequest } from "../../helpers/validation";
import { import {
ProjectPermissionActions, Membership, Secret,
ProjectPermissionSub, } from '../../models';
getAuthDataProjectPermissions import Tag, { ITag } from '../../models/tag';
} from "../../ee/services/ProjectRoleService"; import { Builder } from "builder-pattern"
import * as reqValidator from "../../validation/tags"; import to from 'await-to-js';
import { BadRequestError, UnauthorizedRequestError } from '../../utils/errors';
import { MongoError } from 'mongodb';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
export const createWorkspaceTag = async (req: Request, res: Response) => { export const createWorkspaceTag = async (req: Request, res: Response) => {
const { const { workspaceId } = req.params
body: { name, slug }, const { name, slug } = req.body
params: { workspaceId } const sanitizedTagToCreate = Builder<ITag>()
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req); .name(name)
.workspace(new Types.ObjectId(workspaceId))
.slug(slug)
.user(new Types.ObjectId(req.user._id))
.build();
const { permission } = await getAuthDataProjectPermissions({ const [err, createdTag] = await to(Tag.create(sanitizedTagToCreate))
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan( if (err) {
ProjectPermissionActions.Create, if ((err as MongoError).code === 11000) {
ProjectPermissionSub.Tags throw BadRequestError({ message: "Tags must be unique in a workspace" })
); }
const tagToCreate = { throw err
name, }
workspace: new Types.ObjectId(workspaceId),
slug,
user: new Types.ObjectId(req.user._id)
};
const createdTag = await new Tag(tagToCreate).save(); res.json(createdTag)
}
res.json(createdTag);
};
export const deleteWorkspaceTag = async (req: Request, res: Response) => { export const deleteWorkspaceTag = async (req: Request, res: Response) => {
const { const { tagId } = req.params
params: { tagId }
} = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req);
const tagFromDB = await Tag.findById(tagId); const tagFromDB = await Tag.findById(tagId)
if (!tagFromDB) { if (!tagFromDB) {
throw BadRequestError(); throw BadRequestError()
} }
const { permission } = await getAuthDataProjectPermissions({ // can only delete if the request user is one that belongs to the same workspace as the tag
authData: req.authData, const membership = await Membership.findOne({
workspaceId: tagFromDB.workspace user: req.user,
}); workspace: tagFromDB.workspace
});
ForbiddenError.from(permission).throwUnlessCan( if (!membership) {
ProjectPermissionActions.Delete, UnauthorizedRequestError({ message: 'Failed to validate membership' });
ProjectPermissionSub.Tags }
);
const result = await Tag.findByIdAndDelete(tagId); const result = await Tag.findByIdAndDelete(tagId);
// remove the tag from secrets // remove the tag from secrets
await Secret.updateMany({ tags: { $in: [tagId] } }, { $pull: { tags: tagId } }); await Secret.updateMany(
{ tags: { $in: [tagId] } },
{ $pull: { tags: tagId } }
);
res.json(result); res.json(result);
}; }
export const getWorkspaceTags = async (req: Request, res: Response) => { export const getWorkspaceTags = async (req: Request, res: Response) => {
const { const { workspaceId } = req.params
params: { workspaceId } const workspaceTags = await Tag.find({ workspace: workspaceId })
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req); return res.json({
workspaceTags
const { permission } = await getAuthDataProjectPermissions({ })
authData: req.authData, }
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.Tags
);
const workspaceTags = await Tag.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.json({
workspaceTags
});
};

View File

@ -1,115 +1,105 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { Types } from "mongoose"; import * as Sentry from '@sentry/node';
import crypto from "crypto"; import {
import bcrypt from "bcrypt"; User,
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models"; MembershipOrg
import { getSaltRounds } from "../../config"; } from '../../models';
import { validateRequest } from "../../helpers/validation";
import { deleteUser } from "../../helpers/user"; /**
import * as reqValidator from "../../validation"; * 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('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get current user'
});
}
return res.status(200).send({
user
});
}
/** /**
* Update the current user's MFA-enabled status [isMfaEnabled]. * Update the current user's MFA-enabled status [isMfaEnabled].
* Note: Infisical currently only supports email-based 2FA only; this will expand to * Note: Infisical currently only supports email-based 2FA only; this will expand to
* include SMS and authenticator app modes of authentication in the future. * include SMS and authenticator app modes of authentication in the future.
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateMyMfaEnabled = async (req: Request, res: Response) => { export const updateMyMfaEnabled = async (req: Request, res: Response) => {
const { let user;
body: { isMfaEnabled } try {
} = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req); const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled;
if (isMfaEnabled) {
// TODO: adapt this route/controller
// to work for different forms of MFA
req.user.mfaMethods = ['email'];
} else {
req.user.mfaMethods = [];
}
req.user.isMfaEnabled = isMfaEnabled; await req.user.save();
if (isMfaEnabled) { user = req.user;
// TODO: adapt this route/controller } catch (err) {
// to work for different forms of MFA Sentry.setUser({ email: req.user.email });
req.user.mfaMethods = ["email"]; Sentry.captureException(err);
} else { return res.status(400).send({
req.user.mfaMethods = []; message: "Failed to update current user's MFA status"
} });
await req.user.save();
const user = req.user;
return res.status(200).send({
user
});
};
/**
* Update name of the current user to [firstName, lastName].
* @param req
* @param res
* @returns
*/
export const updateName = async (req: Request, res: Response) => {
const {
body: { lastName, firstName }
} = await validateRequest(reqValidator.UpdateNameV2, req);
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
firstName,
lastName: lastName ?? ""
},
{
new: true
} }
);
return res.status(200).send({
return res.status(200).send({ user
user
});
};
/**
* Update auth method of the current user to [authMethods]
* @param req
* @param res
* @returns
*/
export const updateAuthMethods = async (req: Request, res: Response) => {
const {
body: { authMethods }
} = await validateRequest(reqValidator.UpdateAuthMethodsV2, req);
const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
);
if (hasSamlEnabled) {
return res.status(400).send({
message: "Failed to update user authentication method because SAML SSO is enforced"
}); });
} }
const user = await User.findByIdAndUpdate(
req.user._id.toString(),
{
authMethods
},
{
new: true
}
);
return res.status(200).send({
user
});
};
/** /**
* Return organizations that the current user is part of. * Return organizations that the current user is part of.
* @param req * @param req
* @param res * @param res
*/ */
export const getMyOrganizations = async (req: Request, res: Response) => { export const getMyOrganizations = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return organizations that current user is part of' #swagger.summary = 'Return organizations that current user is part of'
#swagger.description = 'Return organizations that current user is part of' #swagger.description = 'Return organizations that current user is part of'
@ -136,179 +126,22 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
} }
} }
*/ */
const organizations = ( let organizations;
await MembershipOrg.find({ try {
user: req.user._id organizations = (
}).populate("organization") await MembershipOrg.find({
).map((m) => m.organization); 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({ return res.status(200).send({
organizations organizations
}); });
};
/**
* Return API keys belonging to current user.
* @param req
* @param res
* @returns
*/
export const getMyAPIKeys = async (req: Request, res: Response) => {
const apiKeyData = await APIKeyData.find({
user: req.user._id
});
return res.status(200).send(apiKeyData);
};
/**
* Create new API key for current user.
* @param req
* @param res
* @returns
*/
export const createAPIKey = async (req: Request, res: Response) => {
const {
body: { name, expiresIn }
} = await validateRequest(reqValidator.CreateApiKeyV2, req);
const secret = crypto.randomBytes(16).toString("hex");
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
let apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
if (!apiKeyData) throw new Error("Failed to find API key data");
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
return res.status(200).send({
apiKey,
apiKeyData
});
};
/**
* Delete API key with id [apiKeyDataId] belonging to current user
* @param req
* @param res
*/
export const deleteAPIKey = async (req: Request, res: Response) => {
const {
params: { apiKeyDataId }
} = await validateRequest(reqValidator.DeleteApiKeyV2, req);
const apiKeyData = await APIKeyData.findOneAndDelete({
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
});
return res.status(200).send({
apiKeyData
});
};
/**
* Return active sessions (TokenVersion) belonging to user
* @param req
* @param res
* @returns
*/
export const getMySessions = async (req: Request, res: Response) => {
const tokenVersions = await TokenVersion.find({
user: req.user._id
});
return res.status(200).send(tokenVersions);
};
/**
* Revoke all active sessions belong to user
* @param req
* @param res
* @returns
*/
export const deleteMySessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany(
{
user: req.user._id
},
{
$inc: {
refreshVersion: 1,
accessVersion: 1
}
}
);
return res.status(200).send({
message: "Successfully revoked all sessions"
});
};
/**
* Return 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"
}
}
}
}
}
}
*/
const user = await User.findById(req.user._id).select(
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
);
return res.status(200).send({
user
});
};
/**
* Delete the current user.
* @param req
* @param res
*/
export const deleteMe = async (req: Request, res: Response) => {
const user = await deleteUser({
userId: req.user._id
});
return res.status(200).send({
user
});
} }

View File

@ -1,39 +1,41 @@
import { Request, Response } from "express"; import { Request, Response } from 'express';
import { Types } from "mongoose"; import * as Sentry from '@sentry/node';
import { Key, Membership, ServiceTokenData, Workspace } from "../../models"; import { Types } from 'mongoose';
import { import {
pullSecrets as pull, Workspace,
v2PushSecrets as push, Secret,
reformatPullSecrets Membership,
} from "../../helpers/secret"; MembershipOrg,
import { pushKeys } from "../../helpers/key"; Integration,
import { EventService, TelemetryService } from "../../services"; IntegrationAuth,
import { eventPushSecrets } from "../../events"; Key,
import { EEAuditLogService } from "../../ee/services"; IUser,
import { EventType } from "../../ee/models"; ServiceToken,
import { validateRequest } from "../../helpers/validation"; ServiceTokenData
import * as reqValidator from "../../validation"; } from '../../models';
import { import {
ProjectPermissionActions, v2PushSecrets as push,
ProjectPermissionSub, pullSecrets as pull,
getAuthDataProjectPermissions reformatPullSecrets
} from "../../ee/services/ProjectRoleService"; } from '../../helpers/secret';
import { ForbiddenError } from "@casl/ability"; import { pushKeys } from '../../helpers/key';
import { getPostHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
secretKeyHash: string; secretKeyHash: string;
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
} }
/** /**
@ -44,63 +46,70 @@ interface V2PushSecret {
* @returns * @returns
*/ */
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient(); try {
let { secrets }: { secrets: V2PushSecret[] } = req.body; const postHogClient = getPostHogClient();
const { keys, environment, channel } = req.body; let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { workspaceId } = req.params; const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment"); throw new Error('Failed to validate environment');
} }
// sanitize secrets // sanitize secrets
secrets = secrets.filter( secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== "" && s.secretValueCiphertext !== "" (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
); );
await push({ await push({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, environment,
secrets, secrets,
channel: channel ? channel : "cli", channel: channel ? channel : 'cli',
ipAddress: req.realIP ipAddress: req.ip
}); });
await pushKeys({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
keys keys
}); });
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: "secrets pushed", event: 'secrets pushed',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : "cli" channel: channel ? channel : 'cli'
} }
}); });
} }
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId
environment, })
secretPath: "/" });
})
});
return res.status(200).send({ } catch (err) {
message: "Successfully uploaded workspace secrets" 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'
});
}; };
/** /**
@ -111,57 +120,65 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
const postHogClient = await TelemetryService.getPostHogClient(); try {
const environment: string = req.query.environment as string; const postHogClient = getPostHogClient();
const channel: string = req.query.channel as string; const environment: string = req.query.environment as string;
const { workspaceId } = req.params; const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let userId; let userId;
if (req.user) { if (req.user) {
userId = req.user._id.toString(); userId = req.user._id.toString();
} else if (req.serviceTokenData) { } else if (req.serviceTokenData) {
userId = req.serviceTokenData.user.toString(); userId = req.serviceTokenData.user._id
} }
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error("Failed to validate environment"); throw new Error('Failed to validate environment');
} }
secrets = await pull({ secrets = await pull({
userId, userId,
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : "cli", channel: channel ? channel : 'cli',
ipAddress: req.realIP ipAddress: req.ip
}); });
if (channel !== "cli") { if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets }); secrets = reformatPullSecrets({ secrets });
} }
if (postHogClient) { if (postHogClient) {
// capture secrets pushed event in production // capture secrets pushed event in production
postHogClient.capture({ postHogClient.capture({
distinctId: req.user.email, distinctId: req.user.email,
event: "secrets pulled", event: 'secrets pulled',
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : "cli" channel: channel ? channel : 'cli'
} }
}); });
} }
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
secrets secrets
}); });
}; };
export const getWorkspaceKey = async (req: Request, res: Response) => { export const getWorkspaceKey = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return encrypted project key' #swagger.summary = 'Return encrypted project key'
#swagger.description = 'Return encrypted project key' #swagger.description = 'Return encrypted project key'
@ -189,53 +206,61 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
} }
} }
*/ */
const { let key;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetWorkspaceKeyV2, req); const { workspaceId } = req.params;
const key = await Key.findOne({ key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}).populate("sender", "+publicKey"); }).populate('sender', '+publicKey');
if (!key) throw new Error("Failed to find workspace key"); if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace key'
});
}
await EEAuditLogService.createAuditLog( return res.status(200).json(key);
req.authData, }
{ export const getWorkspaceServiceTokenData = async (
type: EventType.GET_WORKSPACE_KEY, req: Request,
metadata: { res: Response
keyId: key._id.toString() ) => {
} let serviceTokenData;
}, try {
{ const { workspaceId } = req.params;
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).json(key); serviceTokenData = await ServiceTokenData
}; .find({
workspace: workspaceId
})
.select('+encryptedKey +iv +tag');
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => { } catch (err) {
const { workspaceId } = req.params; Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
});
}
const serviceTokenData = await ServiceTokenData.find({ return res.status(200).send({
workspace: workspaceId serviceTokenData
}).select("+encryptedKey +iv +tag"); });
}
return res.status(200).send({
serviceTokenData
});
};
/** /**
* Return memberships for workspace with id [workspaceId] * Return memberships for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return project memberships' #swagger.summary = 'Return project memberships'
#swagger.description = 'Return project memberships' #swagger.description = 'Return project memberships'
@ -268,37 +293,34 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
} }
} }
*/ */
const { let memberships;
params: { workspaceId } try {
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req); const { workspaceId } = req.params;
const { permission } = await getAuthDataProjectPermissions({ memberships = await Membership.find({
authData: req.authData, workspace: workspaceId
workspaceId: new Types.ObjectId(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'
});
}
ForbiddenError.from(permission).throwUnlessCan( return res.status(200).send({
ProjectPermissionActions.Read, memberships
ProjectPermissionSub.Member });
); }
const memberships = await Membership.find({
workspace: workspaceId
}).populate("user", "+publicKey");
return res.status(200).send({
memberships
});
};
/** /**
* Update role of membership with id [membershipId] to role [role] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateWorkspaceMembership = async (req: Request, res: Response) => { export const updateWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update project membership' #swagger.summary = 'Update project membership'
#swagger.description = 'Update project membership' #swagger.description = 'Update project membership'
@ -351,44 +373,42 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
const { let membership;
params: { workspaceId, membershipId }, try {
body: { role } const {
} = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req); membershipId
} = req.params;
const { permission } = await getAuthDataProjectPermissions({ const { role } = req.body;
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId) membership = await Membership.findByIdAndUpdate(
}); membershipId,
{
ForbiddenError.from(permission).throwUnlessCan( role
ProjectPermissionActions.Edit, }, {
ProjectPermissionSub.Member new: true
); }
);
const membership = await Membership.findByIdAndUpdate( } catch (err) {
membershipId, Sentry.setUser({ email: req.user.email });
{ Sentry.captureException(err);
role return res.status(400).send({
}, message: 'Failed to update workspace membership'
{ });
new: true }
}
); return res.status(200).send({
membership
return res.status(200).send({ });
membership }
});
};
/** /**
* Delete workspace membership with id [membershipId] * Delete workspace membership with id [membershipId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteWorkspaceMembership = async (req: Request, res: Response) => { export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete project membership' #swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership' #swagger.description = 'Delete project membership'
@ -424,33 +444,32 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
const { let membership;
params: { workspaceId, membershipId } try {
} = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req); const {
membershipId
const { permission } = await getAuthDataProjectPermissions({ } = req.params;
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId) membership = await Membership.findByIdAndDelete(membershipId);
});
if (!membership) throw new Error('Failed to delete workspace membership');
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, await Key.deleteMany({
ProjectPermissionSub.Member receiver: membership.user,
); workspace: membership.workspace
});
const membership = await Membership.findByIdAndDelete(membershipId); } catch (err) {
Sentry.setUser({ email: req.user.email });
if (!membership) throw new Error("Failed to delete workspace membership"); Sentry.captureException(err);
return res.status(400).send({
await Key.deleteMany({ message: 'Failed to delete workspace membership'
receiver: membership.user, });
workspace: membership.workspace }
});
return res.status(200).send({
return res.status(200).send({ membership
membership });
}); }
};
/** /**
* Change autoCapitilzation Rule of workspace * Change autoCapitilzation Rule of workspace
@ -459,35 +478,33 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns * @returns
*/ */
export const toggleAutoCapitalization = async (req: Request, res: Response) => { export const toggleAutoCapitalization = async (req: Request, res: Response) => {
const { let workspace;
params: { workspaceId }, try {
body: { autoCapitalization } const { workspaceId } = req.params;
} = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req); const { autoCapitalization } = req.body;
const { permission } = await getAuthDataProjectPermissions({ workspace = await Workspace.findOneAndUpdate(
authData: req.authData, {
workspaceId: new Types.ObjectId(workspaceId) _id: workspaceId
}); },
{
autoCapitalization
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change autoCapitalization setting'
});
}
ForbiddenError.from(permission).throwUnlessCan( return res.status(200).send({
ProjectPermissionActions.Edit, message: 'Successfully changed autoCapitalization setting',
ProjectPermissionSub.Settings workspace
); });
const workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
autoCapitalization
},
{
new: true
}
);
return res.status(200).send({
message: "Successfully changed autoCapitalization setting",
workspace
});
}; };

View File

@ -1,224 +0,0 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from "express";
import jwt from "jsonwebtoken";
import * as bigintConversion from "bigint-conversion";
const jsrp = require("jsrp");
import { LoginSRPDetail, User } from "../../models";
import { createToken, issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { checkUserDevice } from "../../helpers/user";
import { sendMail } from "../../helpers/nodemailer";
import { TokenService } from "../../services";
import { BadRequestError, InternalServerError } from "../../utils/errors";
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
import { AuthMethod } from "../../models/user";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
declare module "jsonwebtoken" {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthMethod;
isUserCompleted: boolean;
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
const {
body: { email, clientPublicKey, providerAuthToken }
} = await validateRequest(reqValidator.Login1V3, req);
const user = await User.findOne({
email
}).select("+salt +verifier");
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace(
{
email: email
},
{
email,
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt)
},
{ upsert: true, returnNewDocument: false }
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
if (!req.headers["user-agent"])
throw InternalServerError({ message: "User-Agent header is required" });
const {
body: { email, providerAuthToken, clientProof }
} = await validateRequest(reqValidator.Login2V3, req);
const user = await User.findOne({
email
}).select(
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
);
if (!user) throw new Error("Failed to find user");
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
await validateProviderAuthToken({
email,
providerAuthToken
});
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"));
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
authTokenType: AuthTokenType.MFA_TOKEN,
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getAuthSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: "emailMfa.handlebars",
subjectLine: "Infisical MFA code",
recipients: [user.email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
};
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV;
response.protectedKeyTag = user.protectedKeyTag;
}
return res.status(200).send(response);
}
return res.status(400).send({
message: "Failed to authenticate. Try again?"
});
}
);
};

View File

@ -1,13 +0,0 @@
import * as usersController from "./usersController";
import * as secretsController from "./secretsController";
import * as workspacesController from "./workspacesController";
import * as authController from "./authController";
import * as signupController from "./signupController";
export {
usersController,
authController,
secretsController,
signupController,
workspacesController
}

File diff suppressed because it is too large Load Diff

View File

@ -1,193 +0,0 @@
import jwt from "jsonwebtoken";
import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { MembershipOrg, User } from "../../models";
import { completeAccount } from "../../helpers/user";
import { initializeDefaultOrg } from "../../helpers/signup";
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
import { ACCEPTED, AuthTokenType, INVITED } from "../../variables";
import { standardRequest } from "../../config/request";
import { getAuthSecret, getHttpsEnabled, getLoopsApiKey } from "../../config";
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import { TelemetryService } from "../../services";
import { AuthMethod } from "../../models";
import { validateRequest } from "../../helpers/validation";
import * as reqValidator from "../../validation/auth";
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
* @param req
* @param res
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token;
try {
const {
body: {
email,
publicKey,
salt,
lastName,
verifier,
firstName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
organizationName,
providerAuthToken,
attributionSource,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag
}
} = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: "Failed to complete account for complete user"
});
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>(
req.headers["authorization"]?.split(" ", 2)
) ?? [null, null];
if (AUTH_TOKEN_TYPE === null) {
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
throw BadRequestError({
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
});
}
if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: "Missing Authorization Body in the request header"
});
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError();
if (decodedToken.userId !== user.id) throw UnauthorizedRequestError();
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
);
if (!hasSamlEnabled) {
// TODO: modify this part
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
}
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers["user-agent"] ?? ""
});
token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post(
"https://app.loops.so/api/v1/events/send",
{
email: email,
eventName: "Sign Up",
firstName: firstName,
lastName: lastName
},
{
headers: {
Accept: "application/json",
Authorization: "Bearer " + (await getLoopsApiKey())
}
}
);
}
// store (refresh) token in httpOnly cookie
res.cookie("jid", tokens.refreshToken, {
httpOnly: true,
path: "/",
sameSite: "strict",
secure: await getHttpsEnabled()
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "User Signed Up",
distinctId: email,
properties: {
email,
...(attributionSource ? { attributionSource } : {})
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to complete account setup"
});
}
return res.status(200).send({
message: "Successfully set up account",
user,
token
});
};

View File

@ -1,18 +0,0 @@
import { Request, Response } from "express";
import { APIKeyDataV2 } from "../../models";
/**
* Return API keys belonging to current user.
* @param req
* @param res
* @returns
*/
export const getMyAPIKeys = async (req: Request, res: Response) => {
const apiKeyData = await APIKeyDataV2.find({
user: req.user._id
});
return res.status(200).send({
apiKeyData
});
}

View File

@ -1,156 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../helpers/validation";
import { Membership, Secret, ServiceTokenDataV3, User } from "../../models";
import { SecretService } from "../../services";
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
import { UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/workspace";
/**
* Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed
* @param req
* @param res
* @returns
*/
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req);
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex: {
$exists: false
}
});
return res.status(200).send(secretsWithoutBlindIndex === 0);
};
/**
* Get all secrets for workspace with id [workspaceId]
*/
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req);
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
const secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secrets
});
};
/**
* Update blind indices for secrets in workspace with id [workspaceId]
* @param req
* @param res
*/
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
const {
params: { workspaceId },
body: { secretsToUpdate }
} = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (req.authData.authPayload instanceof User) {
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
if (membership.role !== "admin")
throw UnauthorizedRequestError({ message: "User must be an admin" });
}
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
// update secret blind indices
const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName,
salt
});
return {
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
};
})
);
await Secret.bulkWrite(operations);
return res.status(200).send({
message: "Successfully named workspace secrets"
});
};
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.GetWorkspaceServiceTokenDataV3, req);
const serviceTokenData = await ServiceTokenDataV3.find({
workspace: new Types.ObjectId(workspaceId)
}).populate("customRole");
return res.status(200).send({
serviceTokenData
});
}

File diff suppressed because it is too large Load Diff

View 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
});
}

View File

@ -1,32 +0,0 @@
import { Request, Response } from "express";
import { EELicenseService } from "../../services";
import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/cloudProducts";
/**
* Return available cloud product information.
* Note: Nicely formatted to easily construct a table from
* @param req
* @param res
* @returns
*/
export const getCloudProducts = async (req: Request, res: Response) => {
const {
query: { "billing-cycle": billingCycle }
} = await validateRequest(reqValidator.GetCloudProductsV1, req);
if (EELicenseService.instanceType === "cloud") {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
}
return res.status(200).send({
head: [],
rows: []
});
};

View File

@ -1,29 +1,15 @@
import * as secretController from "./secretController"; import * as stripeController from './stripeController';
import * as secretSnapshotController from "./secretSnapshotController"; import * as secretController from './secretController';
import * as organizationsController from "./organizationsController"; import * as secretSnapshotController from './secretSnapshotController';
import * as ssoController from "./ssoController"; import * as workspaceController from './workspaceController';
import * as usersController from "./usersController"; import * as actionController from './actionController';
import * as workspaceController from "./workspaceController"; import * as membershipController from './membershipController';
import * as membershipController from "./membershipController";
import * as cloudProductsController from "./cloudProductsController";
import * as roleController from "./roleController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
import * as secretRotationProviderController from "./secretRotationProviderController";
import * as secretRotationController from "./secretRotationController";
export { export {
secretController, stripeController,
secretSnapshotController, secretController,
organizationsController, secretSnapshotController,
ssoController, workspaceController,
usersController, actionController,
workspaceController, membershipController
membershipController, }
cloudProductsController,
roleController,
secretApprovalPolicyController,
secretApprovalRequestController,
secretRotationProviderController,
secretRotationController
};

View File

@ -1,25 +1,23 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { IUser, Membership, Workspace } from "../../../models"; import { Membership, Workspace } from "../../../models";
import { EventType } from "../../../ee/models";
import { IMembershipPermission } from "../../../models/membership"; import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ADMIN, MEMBER } from "../../../variables/organization"; import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables"; import { Builder } from "builder-pattern"
import _ from "lodash"; import _ from "lodash";
import { EEAuditLogService } from "../../services";
export const denyMembershipPermissions = async (req: Request, res: Response) => { export const denyMembershipPermissions = async (req: Request, res: Response) => {
const { membershipId } = req.params; const { membershipId } = req.params;
const { permissions } = req.body; const { permissions } = req.body;
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => { const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) { 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" }) throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
} }
return { return Builder<IMembershipPermission>()
environmentSlug: permission.environmentSlug, .environmentSlug(permission.environmentSlug)
ability: permission.ability .ability(permission.ability)
} .build();
}) })
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual) const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual)
@ -40,7 +38,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
throw BadRequestError({ message: "Something went wrong when locating the related workspace" }) throw BadRequestError({ message: "Something went wrong when locating the related workspace" })
} }
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, "slug"))); const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, 'slug')));
sanitizedMembershipPermissionsUnique.forEach(permission => { sanitizedMembershipPermissionsUnique.forEach(permission => {
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) { if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
@ -53,34 +51,13 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
{ _id: membershipToModify._id }, { _id: membershipToModify._id },
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } }, { $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
{ new: true } { new: true }
).populate<{ user: IUser }>("user"); )
if (!updatedMembershipWithPermissions) { if (!updatedMembershipWithPermissions) {
throw BadRequestError({ message: "The resource has been removed before it can be modified" }) throw BadRequestError({ message: "The resource has been removed before it can be modified" })
} }
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
metadata: {
userId: updatedMembershipWithPermissions.user._id.toString(),
email: updatedMembershipWithPermissions.user.email,
deniedPermissions: updatedMembershipWithPermissions.deniedPermissions.map(({
environmentSlug,
ability
}) => ({
environmentSlug,
ability
}))
}
},
{
workspaceId: updatedMembershipWithPermissions.workspace
}
);
res.send({ res.send({
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions, permissionsDenied: updatedMembershipWithPermissions.deniedPermissions
}) })
} }

View File

@ -1,505 +0,0 @@
import { Types } from "mongoose";
import { Request, Response } from "express";
import { getLicenseServerUrl } from "../../../config";
import { licenseServerKeyRequest } from "../../../config/request";
import { EELicenseService } from "../../services";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/organization";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
import { Organization } from "../../../models";
import { OrganizationNotFoundError } from "../../../utils/errors";
export const getOrganizationPlansTable = async (req: Request, res: Response) => {
const {
query: { billingCycle },
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
};
/**
* Return the organization current plan's feature set
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
const {
query: { workspaceId },
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const plan = await EELicenseService.getPlan(
new Types.ObjectId(organizationId),
new Types.ObjectId(workspaceId)
);
return res.status(200).send({
plan
});
};
/**
* Return checkout url for pro trial
* @param req
* @param res
* @returns
*/
export const startOrganizationTrial = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { success_url }
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Billing
);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/session/trial`,
{
success_url
}
);
EELicenseService.delPlan(new Types.ObjectId(organizationId));
return res.status(200).send({
url
});
};
/**
* Return the organization's current plan's billing info
* @param req
* @param res
* @returns
*/
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/billing`
);
return res.status(200).send(data);
};
/**
* Return the organization's current plan's feature table
* @param req
* @param res
* @returns
*/
export const getOrganizationPlanTable = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/cloud-plan/table`
);
return res.status(200).send(data);
};
export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`
);
return res.status(200).send(data);
};
export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { name, email }
} = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details`,
{
...(name ? { name } : {}),
...(email ? { email } : {})
}
);
return res.status(200).send(data);
};
/**
* Return the organization's payment methods on file
*/
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { pmtMethods }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`
);
return res.status(200).send(pmtMethods);
};
/**
* Return URL to add payment method for organization
*/
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { success_url, cancel_url }
} = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { url }
} = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods`,
{
success_url,
cancel_url
}
);
return res.status(200).send({
url
});
};
/**
* Delete payment method with id [pmtMethodId] for organization
* @param req
* @param res
* @returns
*/
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const {
params: { organizationId, pmtMethodId }
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/payment-methods/${pmtMethodId}`
);
return res.status(200).send(data);
};
/**
* Return the organization's tax ids on file
*/
export const getOrganizationTaxIds = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgTaxIdsv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { tax_ids }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids`
);
return res.status(200).send(tax_ids);
};
/**
* Add tax id to organization
*/
export const addOrganizationTaxId = async (req: Request, res: Response) => {
const {
params: { organizationId },
body: { type, value }
} = await validateRequest(reqValidator.CreateOrgTaxId, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids`,
{
type,
value
}
);
return res.status(200).send(data);
};
/**
* Delete tax id with id [taxId] from organization tax ids on file
* @param req
* @param res
* @returns
*/
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
const {
params: { organizationId, taxId }
} = await validateRequest(reqValidator.DelOrgTaxIdv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Delete,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/billing-details/tax-ids/${taxId}`
);
return res.status(200).send(data);
};
/**
* Return organization's invoices on file
* @param req
* @param res
* @returns
*/
export const getOrganizationInvoices = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgInvoicesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { invoices }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/invoices`
);
return res.status(200).send(invoices);
};
/**
* Return organization's licenses on file
* @param req
* @param res
* @returns
*/
export const getOrganizationLicenses = async (req: Request, res: Response) => {
const {
params: { organizationId }
} = await validateRequest(reqValidator.GetOrgLicencesv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Billing
);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization"
});
}
const {
data: { licenses }
} = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
organization.customerId
}/licenses`
);
return res.status(200).send(licenses);
};

View File

@ -1,267 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Membership, User } from "../../../models";
import {
CreateRoleSchema,
DeleteRoleSchema,
GetRoleSchema,
GetUserPermission,
GetUserProjectPermission,
UpdateRoleSchema
} from "../../validation/role";
import {
ProjectPermissionActions,
ProjectPermissionSub,
adminProjectPermissions,
getAuthDataProjectPermissions,
memberProjectPermissions,
viewerProjectPermission
} from "../../services/ProjectRoleService";
import {
OrgPermissionActions,
OrgPermissionSubjects,
adminPermissions,
getUserOrgPermissions,
memberPermissions
} from "../../services/RoleService";
import { BadRequestError } from "../../../utils/errors";
import { Role } from "../../models";
import { validateRequest } from "../../../helpers/validation";
import { packRules } from "@casl/ability/extra";
export const createRole = async (req: Request, res: Response) => {
const {
body: { workspaceId, name, description, slug, permissions, orgId }
} = await validateRequest(CreateRoleSchema, req);
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "user doesn't have the permission." });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the permission." });
}
}
const existingRole = await Role.findOne({ organization: orgId, workspace: workspaceId, slug });
if (existingRole) {
throw BadRequestError({ message: "Role already exist" });
}
const role = new Role({
organization: orgId,
workspace: workspaceId,
isOrgRole,
name,
slug,
permissions,
description
});
await role.save();
res.status(200).json({
message: "Successfully created role",
data: {
role
}
});
};
export const updateRole = async (req: Request, res: Response) => {
const {
params: { id },
body: { name, description, slug, permissions, workspaceId, orgId }
} = await validateRequest(UpdateRoleSchema, req);
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
}
if (slug) {
const existingRole = await Role.findOne({
organization: orgId,
slug,
isOrgRole,
workspace: workspaceId
});
if (existingRole && existingRole.id !== id) {
throw BadRequestError({ message: "Role already exist" });
}
}
const role = await Role.findByIdAndUpdate(
id,
{ name, description, slug, permissions },
{ returnDocument: "after" }
);
if (!role) {
throw BadRequestError({ message: "Role not found" });
}
res.status(200).json({
message: "Successfully updated role",
data: {
role
}
});
};
export const deleteRole = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(DeleteRoleSchema, req);
const role = await Role.findById(id);
if (!role) {
throw BadRequestError({ message: "Role not found" });
}
const isOrgRole = !role.workspace;
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, role.organization.toString());
if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: role.workspace
});
if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
}
await Role.findByIdAndDelete(role.id);
res.status(200).json({
message: "Successfully deleted role",
data: {
role
}
});
};
export const getRoles = async (req: Request, res: Response) => {
const {
query: { workspaceId, orgId }
} = await validateRequest(GetRoleSchema, req);
const isOrgRole = !workspaceId;
if (isOrgRole) {
const { permission } = await getUserOrgPermissions(req.user.id, orgId);
if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) {
throw BadRequestError({ message: "User doesn't have the org permission." });
}
} else {
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) {
throw BadRequestError({ message: "User doesn't have the workspace permission." });
}
}
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
// as this is shared between org and workspace switch the rule set based on it
const roles = [
{
_id: "admin",
name: "Admin",
slug: "admin",
description: "Complete administration access over the organization",
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
},
{
_id: "member",
name: isOrgRole ? "Member" : "Developer",
slug: "member",
description: "Non-administrative role in an organization",
permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules
},
// viewer role only for project level
...(isOrgRole
? []
: [
{
_id: "viewer",
name: "Viewer",
slug: "viewer",
description: "Non-administrative role in an organization",
permissions: viewerProjectPermission.rules
}
]),
...customRoles
];
res.status(200).json({
message: "Successfully fetched role list",
data: {
roles
}
});
};
export const getUserPermissions = async (req: Request, res: Response) => {
const {
params: { orgId }
} = await validateRequest(GetUserPermission, req);
const { permission, membership } = await getUserOrgPermissions(req.user._id, orgId);
res.status(200).json({
data: {
permissions: packRules(permission.rules),
membership
}
});
};
export const getUserWorkspacePermissions = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(GetUserProjectPermission, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
let membership;
if (req.authData.authPayload instanceof User) {
membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
})
}
res.status(200).json({
data: {
permissions: packRules(permission.rules),
membership
}
});
};

View File

@ -1,143 +0,0 @@
import { Types } from "mongoose";
import { ForbiddenError, subject } from "@casl/ability";
import { Request, Response } from "express";
import { nanoid } from "nanoid";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { validateRequest } from "../../../helpers/validation";
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../validation/secretApproval";
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
body: { approvals, secretPath, approvers, environment, workspaceId, name }
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretApproval
);
const secretApproval = new SecretApprovalPolicy({
workspace: workspaceId,
name: name ?? `${environment}-${nanoid(3)}`,
secretPath,
environment,
approvals,
approvers
});
await secretApproval.save();
return res.send({
approval: secretApproval
});
};
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
body: { approvals, approvers, secretPath, name },
params: { id }
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
const secretApproval = await SecretApprovalPolicy.findById(id);
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: secretApproval.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SecretApproval
);
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
approvals,
approvers,
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
});
return res.send({
approval: updatedDoc
});
};
export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.DeleteSecretApprovalRule, req);
const secretApproval = await SecretApprovalPolicy.findById(id);
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: secretApproval.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretApproval
);
const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id);
return res.send({
approval: deletedDoc
});
};
export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretApproval
);
const doc = await SecretApprovalPolicy.find({ workspace: workspaceId });
return res.send({
approvals: doc
});
};
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
const {
query: { workspaceId, environment, secretPath }
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
);
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
return res.send({ policy: secretApprovalPolicy });
};

View File

@ -1,366 +0,0 @@
import { Request, Response } from "express";
import { validateRequest } from "../../../helpers/validation";
import { Folder, Membership, User } from "../../../models";
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
import * as reqValidator from "../../validation/secretApprovalRequest";
import { getFolderWithPathFromId } from "../../../services/FolderService";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
import { Types } from "mongoose";
import { EEAuditLogService } from "../../services";
import { EventType } from "../../models";
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
const approvalRequestCount = await SecretApprovalRequest.aggregate([
{
$match: {
workspace: new Types.ObjectId(workspaceId)
}
},
{
$lookup: {
from: SecretApprovalPolicy.collection.name,
localField: "policy",
foreignField: "_id",
as: "policy"
}
},
{ $unwind: "$policy" },
...(membership.role !== "admin"
? [
{
$match: {
$or: [
{ committer: new Types.ObjectId(membership.id) },
{ "policy.approvers": new Types.ObjectId(membership.id) }
]
}
}
]
: []),
{
$group: {
_id: "$status",
count: { $sum: 1 }
}
}
]);
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
return res.send({
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
});
};
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
const {
query: { status, committer, workspaceId, environment, limit, offset }
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: new Types.ObjectId(workspaceId)
});
if (!membership) throw UnauthorizedRequestError();
const query = {
workspace: new Types.ObjectId(workspaceId),
environment,
committer: committer ? new Types.ObjectId(committer) : undefined,
status
};
// to strip of undefined in query we use es6 spread to ignore those fields
Object.entries(query).forEach(
([key, value]) => value === undefined && delete query[key as keyof typeof query]
);
const approvalRequests = await SecretApprovalRequest.aggregate([
{
$match: query
},
{ $sort: { createdAt: -1 } },
{
$lookup: {
from: SecretApprovalPolicy.collection.name,
localField: "policy",
foreignField: "_id",
as: "policy"
}
},
{ $unwind: "$policy" },
...(membership.role !== "admin"
? [
{
$match: {
$or: [
{ committer: new Types.ObjectId(membership.id) },
{ "policy.approvers": new Types.ObjectId(membership.id) }
]
}
}
]
: []),
{ $skip: offset },
{ $limit: limit }
]);
if (!approvalRequests.length) return res.send({ approvals: [] });
const unqiueEnvs = environment ?? {
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
};
const approvalRootFolders = await Folder.find({
workspace: workspaceId,
environment: unqiueEnvs
}).lean();
const formatedApprovals = approvalRequests.map((el) => {
let secretPath = "/";
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
if (folders) {
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
}
return { ...el, secretPath };
});
return res.send({
approvals: formatedApprovals
});
};
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
.populate<{ policy: ISecretApprovalPolicy }>("policy")
.populate({
path: "commits.secretVersion",
populate: {
path: "tags"
}
})
.populate("commits.secret", "version")
.populate("commits.newVersion.tags")
.lean();
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
// allow to fetch only if its admin or is the committer or approver
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find(
(approverId) => approverId.toString() === membership._id.toString()
)
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
let secretPath = "/";
const approvalRootFolders = await Folder.findOne({
workspace: secretApprovalRequest.workspace,
environment: secretApprovalRequest.environment
}).lean();
if (approvalRootFolders) {
secretPath =
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
?.folderPath || "/";
}
return res.send({
approval: { ...secretApprovalRequest, secretPath }
});
};
export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
const reviewerPos = secretApprovalRequest.reviewers.findIndex(
({ member }) => member.toString() === membership._id.toString()
);
if (reviewerPos !== -1) {
secretApprovalRequest.reviewers[reviewerPos].status = status;
} else {
secretApprovalRequest.reviewers.push({ member: membership._id, status });
}
await secretApprovalRequest.save();
return res.send({ status });
};
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
{}
);
const hasMinApproval =
secretApprovalRequest.policy.approvals <=
secretApprovalRequest.policy.approvers.filter(
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
).length;
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
const approval = await performSecretApprovalRequestMerge(
id,
req.authData,
membership._id.toString()
);
return res.send({ approval });
};
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
const {
body: { status },
params: { id }
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
policy: ISecretApprovalPolicy;
}>("policy");
if (!secretApprovalRequest)
throw BadRequestError({ message: "Secret approval request not found" });
if (!(req.authData.authPayload instanceof User)) return;
const membership = await Membership.findOne({
user: req.authData.authPayload._id,
workspace: secretApprovalRequest.workspace
});
if (!membership) throw UnauthorizedRequestError();
if (
membership.role !== "admin" &&
secretApprovalRequest.committer !== membership.id &&
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
) {
throw UnauthorizedRequestError({ message: "User has no access" });
}
if (secretApprovalRequest.hasMerged)
throw BadRequestError({ message: "Approval request has been merged" });
if (secretApprovalRequest.status === "close" && status === "close")
throw BadRequestError({ message: "Approval request is already closed" });
if (secretApprovalRequest.status === "open" && status === "open")
throw BadRequestError({ message: "Approval request is already open" });
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
id,
{ status, statusChangeBy: membership._id },
{ new: true }
);
if (status === "close") {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_CLOSED,
metadata: {
closedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
} else {
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.SECRET_APPROVAL_REOPENED,
metadata: {
reopenedBy: membership._id.toString(),
secretApprovalRequestId: id,
secretApprovalRequestSlug: secretApprovalRequest.slug
}
},
{
workspaceId: secretApprovalRequest.workspace
}
);
}
return res.send({ approval: updatedRequest });
};

View File

@ -1,25 +1,16 @@
import { ForbiddenError, subject } from "@casl/ability"; import { Request, Response } from 'express';
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import { validateRequest } from "../../../helpers/validation"; import { Secret } from '../../../models';
import { Folder, Secret } from "../../../models"; import { SecretVersion } from '../../models';
import { import { EESecretService } from '../../services';
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../../validation";
import { SecretVersion } from "../../models";
import { EESecretService } from "../../services";
import { getFolderWithPathFromId } from "../../../services/FolderService";
/** /**
* Return secret versions for secret with id [secretId] * Return secret versions for secret with id [secretId]
* @param req * @param req
* @param res * @param res
*/ */
export const getSecretVersions = async (req: Request, res: Response) => { export const getSecretVersions = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return secret versions' #swagger.summary = 'Return secret versions'
#swagger.description = 'Return secret versions' #swagger.description = 'Return secret versions'
@ -64,46 +55,41 @@ export const getSecretVersions = async (req: Request, res: Response) => {
} }
} }
*/ */
const { let secretVersions;
params: { secretId }, try {
query: { offset, limit } const { secretId } = req.params;
} = await validateRequest(reqValidator.GetSecretVersionsV1, req);
const secret = await Secret.findById(secretId); const offset: number = parseInt(req.query.offset as string);
if (!secret) { const limit: number = parseInt(req.query.limit as string);
throw BadRequestError({ message: "Failed to find secret" });
} secretVersions = await SecretVersion.find({
secret: secretId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
const { permission } = await getAuthDataProjectPermissions({ } catch (err) {
authData: req.authData, Sentry.setUser({ email: req.user.email });
workspaceId: secret.workspace Sentry.captureException(err);
}); return res.status(400).send({
message: 'Failed to get secret versions'
ForbiddenError.from(permission).throwUnlessCan( });
ProjectPermissionActions.Read, }
ProjectPermissionSub.SecretRollback
); return res.status(200).send({
secretVersions
const secretVersions = await SecretVersion.find({ });
secret: secretId }
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
return res.status(200).send({
secretVersions
});
};
/** /**
* Roll back secret with id [secretId] to version [version] * Roll back secret with id [secretId] to version [version]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const rollbackSecretVersion = async (req: Request, res: Response) => { export const rollbackSecretVersion = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Roll back secret to a version.' #swagger.summary = 'Roll back secret to a version.'
#swagger.description = 'Roll back secret to a version.' #swagger.description = 'Roll back secret to a version.'
@ -151,119 +137,88 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
} }
} }
*/ */
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,
secretValueCiphertext,
secretValueIV,
secretValueTag,
} = oldSecretVersion;
// update secret
secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1
},
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
},
{
new: true
}
);
if (!secret) throw new Error('Failed to find and update secret');
const { // add new secret version
params: { secretId }, await new SecretVersion({
body: { version } secret: secretId,
} = await validateRequest(reqValidator.RollbackSecretVersionV1, req); version: secret.version,
workspace,
const toBeUpdatedSec = await Secret.findById(secretId); type,
if (!toBeUpdatedSec) { user,
throw BadRequestError({ message: "Failed to find secret" }); environment,
} isDeleted: false,
secretKeyCiphertext,
const { permission } = await getAuthDataProjectPermissions({ secretKeyIV,
authData: req.authData, secretKeyTag,
workspaceId: toBeUpdatedSec.workspace secretValueCiphertext,
}); secretValueIV,
secretValueTag
ForbiddenError.from(permission).throwUnlessCan( }).save();
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRollback // take secret snapshot
); await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace.toString()
// validate secret version });
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId, } catch (err) {
version Sentry.setUser({ email: req.user.email });
}).select("+secretBlindIndex"); Sentry.captureException(err);
return res.status(400).send({
if (!oldSecretVersion) throw new Error("Failed to find secret version"); message: 'Failed to roll back secret version'
});
const { }
workspace,
type, return res.status(200).send({
user, secret
environment, });
secretBlindIndex, }
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
folder,
keyEncoding
} = oldSecretVersion;
let secretPath = "/";
const folders = await Folder.findOne({ workspace, environment });
if (folders)
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
);
// update secret
const secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1
},
workspace,
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folderId: folder,
algorithm,
keyEncoding
},
{
new: true
}
);
if (!secret) throw new Error("Failed to find and update secret");
// add new secret version
await new SecretVersion({
secret: secretId,
version: secret.version,
workspace,
type,
user,
environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folder,
algorithm,
keyEncoding
}).save();
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace,
environment,
folderId: folder
});
return res.status(200).send({
secret
});
};

View File

@ -1,110 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../validation/secretRotation";
import * as secretRotationService from "../../secretRotation/service";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
export const createSecretRotation = async (req: Request, res: Response) => {
const {
body: {
provider,
customProvider,
interval,
outputs,
secretPath,
environment,
workspaceId,
inputs
}
} = await validateRequest(reqValidator.createSecretRotationV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.SecretRotation
);
const secretRotation = await secretRotationService.createSecretRotation({
workspaceId,
inputs,
environment,
secretPath,
outputs,
interval,
customProvider,
provider
});
return res.send({ secretRotation });
};
export const restartSecretRotations = async (req: Request, res: Response) => {
const {
body: { id }
} = await validateRequest(reqValidator.restartSecretRotationV1, req);
const doc = await secretRotationService.getSecretRotationById({ id });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: doc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.SecretRotation
);
const secretRotation = await secretRotationService.restartSecretRotation({ id });
return res.send({ secretRotation });
};
export const deleteSecretRotations = async (req: Request, res: Response) => {
const {
params: { id }
} = await validateRequest(reqValidator.removeSecretRotationV1, req);
const doc = await secretRotationService.getSecretRotationById({ id });
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: doc.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.SecretRotation
);
const secretRotations = await secretRotationService.deleteSecretRotation({ id });
return res.send({ secretRotations });
};
export const getSecretRotations = async (req: Request, res: Response) => {
const {
query: { workspaceId }
} = await validateRequest(reqValidator.getSecretRotationV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation
);
const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId);
return res.send({ secretRotations });
};

View File

@ -1,33 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../validation/secretRotationProvider";
import * as secretRotationProviderService from "../../secretRotation/service";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
export const getProviderTemplates = async (req: Request, res: Response) => {
const {
params: { workspaceId }
} = await validateRequest(reqValidator.getSecretRotationProvidersV1, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRotation
);
const rotationProviderList = await secretRotationProviderService.getProviderTemplate({
workspaceId
});
return res.send(rotationProviderList);
};

View File

@ -1,62 +1,39 @@
import { ForbiddenError } from "@casl/ability"; import { Request, Response } from 'express';
import { Request, Response } from "express"; import * as Sentry from '@sentry/node';
import { validateRequest } from "../../../helpers/validation"; import { SecretSnapshot } from '../../models';
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import * as reqValidator from "../../../validation/secretSnapshot";
import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models";
/** /**
* Return secret snapshot with id [secretSnapshotId] * Return secret snapshot with id [secretSnapshotId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSecretSnapshot = async (req: Request, res: Response) => { export const getSecretSnapshot = async (req: Request, res: Response) => {
const { let secretSnapshot;
params: { secretSnapshotId } try {
} = await validateRequest(reqValidator.GetSecretSnapshotV1, req); const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) secretSnapshot = await SecretSnapshot
.lean() .findById(secretSnapshotId)
.populate<{ secretVersions: ISecretVersion[] }>({ .populate({
path: "secretVersions", path: 'secretVersions',
populate: { populate: {
path: "tags", path: 'tags',
model: "Tag" model: 'Tag',
} }
}) });
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
} catch (err) {
const { permission } = await getAuthDataProjectPermissions({ Sentry.setUser({ email: req.user.email });
authData: req.authData, Sentry.captureException(err);
workspaceId: secretSnapshot.workspace return res.status(400).send({
}); message: 'Failed to get secret snapshot'
});
ForbiddenError.from(permission).throwUnlessCan( }
ProjectPermissionActions.Read,
ProjectPermissionSub.SecretRollback return res.status(200).send({
); secretSnapshot
});
const folderId = secretSnapshot.folderId; }
// to show only the folder required secrets
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
({ folder }) => folder === folderId
);
secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map(
({ id, name }) => ({
id,
name
})
) as any;
return res.status(200).send({
secretSnapshot
});
};

View File

@ -1,259 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { BotOrgService } from "../../../services";
import { SSOConfig } from "../../models";
import { AuthMethod, MembershipOrg, User } from "../../../models";
import { getSSOConfigHelper } from "../../helpers/organizations";
import { client } from "../../../config";
import { ResourceNotFoundError } from "../../../utils/errors";
import { getSiteURL } from "../../../config";
import { EELicenseService } from "../../services";
import * as reqValidator from "../../../validation/sso";
import { validateRequest } from "../../../helpers/validation";
import {
OrgPermissionActions,
OrgPermissionSubjects,
getUserOrgPermissions
} from "../../services/RoleService";
import { ForbiddenError } from "@casl/ability";
/**
* Redirect user to appropriate SSO endpoint after successful authentication
* to finish inputting their master key for logging in or signing up
* @param req
* @param res
* @returns
*/
export const redirectSSO = async (req: Request, res: Response) => {
if (req.isUserCompleted) {
return res.redirect(
`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
}
return res.redirect(
`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
);
};
/**
* Return organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const getSSOConfig = async (req: Request, res: Response) => {
const {
query: { organizationId }
} = await validateRequest(reqValidator.GetSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Read,
OrgPermissionSubjects.Sso
);
const data = await getSSOConfigHelper({
organizationId: new Types.ObjectId(organizationId)
});
return res.status(200).send(data);
};
/**
* Update organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const updateSSOConfig = async (req: Request, res: Response) => {
const {
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
} = await validateRequest(reqValidator.UpdateSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Edit,
OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO)
return res.status(400).send({
message:
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
});
interface PatchUpdate {
authProvider?: string;
isActive?: boolean;
encryptedEntryPoint?: string;
entryPointIV?: string;
entryPointTag?: string;
encryptedIssuer?: string;
issuerIV?: string;
issuerTag?: string;
encryptedCert?: string;
certIV?: string;
certTag?: string;
}
const update: PatchUpdate = {};
if (authProvider) {
update.authProvider = authProvider;
}
if (isActive !== undefined) {
update.isActive = isActive;
}
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
if (entryPoint) {
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
update.encryptedEntryPoint = encryptedEntryPoint;
update.entryPointIV = entryPointIV;
update.entryPointTag = entryPointTag;
}
if (issuer) {
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
update.encryptedIssuer = encryptedIssuer;
update.issuerIV = issuerIV;
update.issuerTag = issuerTag;
}
if (cert) {
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
update.encryptedCert = encryptedCert;
update.certIV = certIV;
update.certTag = certTag;
}
const ssoConfig = await SSOConfig.findOneAndUpdate(
{
organization: new Types.ObjectId(organizationId)
},
update,
{
new: true
}
);
if (!ssoConfig)
throw ResourceNotFoundError({
message: "Failed to find SSO config to update"
});
if (update.isActive !== undefined) {
const membershipOrgs = await MembershipOrg.find({
organization: new Types.ObjectId(organizationId)
}).select("user");
if (update.isActive) {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authMethods: [ssoConfig.authProvider]
}
);
} else {
await User.updateMany(
{
_id: {
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
}
},
{
authMethods: [AuthMethod.EMAIL]
}
);
}
}
return res.status(200).send(ssoConfig);
};
/**
* Create organization SAML SSO configuration
* @param req
* @param res
* @returns
*/
export const createSSOConfig = async (req: Request, res: Response) => {
const {
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
} = await validateRequest(reqValidator.CreateSsoConfigv1, req);
const { permission } = await getUserOrgPermissions(req.user._id, organizationId);
ForbiddenError.from(permission).throwUnlessCan(
OrgPermissionActions.Create,
OrgPermissionSubjects.Sso
);
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
if (!plan.samlSSO)
return res.status(400).send({
message:
"Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
});
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
const {
ciphertext: encryptedEntryPoint,
iv: entryPointIV,
tag: entryPointTag
} = client.encryptSymmetric(entryPoint, key);
const {
ciphertext: encryptedIssuer,
iv: issuerIV,
tag: issuerTag
} = client.encryptSymmetric(issuer, key);
const {
ciphertext: encryptedCert,
iv: certIV,
tag: certTag
} = client.encryptSymmetric(cert, key);
const ssoConfig = await new SSOConfig({
organization: new Types.ObjectId(organizationId),
authProvider,
isActive,
encryptedEntryPoint,
entryPointIV,
entryPointTag,
encryptedIssuer,
issuerIV,
issuerTag,
encryptedCert,
certIV,
certTag
}).save();
return res.status(200).send(ssoConfig);
};

View File

@ -0,0 +1,41 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express';
import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
/**
* Handle service provisioning/un-provisioning via Stripe
* @param req
* @param res
* @returns
*/
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent(
req.body,
sig,
getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) {
case '':
break;
default:
}
return res.json({ received: true });
};

View File

@ -1,13 +0,0 @@
import { Request, Response } from "express";
/**
* Return the ip address of the current user
* @param req
* @param res
* @returns
*/
export const getMyIp = (req: Request, res: Response) => {
return res.status(200).send({
ip: req.authData.ipAddress
});
}

File diff suppressed because it is too large Load Diff

View File

@ -1,101 +0,0 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { APIKeyDataV2 } from "../../../models/apiKeyDataV2";
import { validateRequest } from "../../../helpers/validation";
import { BadRequestError } from "../../../utils/errors";
import * as reqValidator from "../../../validation";
import { createToken } from "../../../helpers";
import { AuthTokenType } from "../../../variables";
import { getAuthSecret } from "../../../config";
/**
* Create API key data v2
* @param req
* @param res
*/
export const createAPIKeyData = async (req: Request, res: Response) => {
const {
body: {
name
}
} = await validateRequest(reqValidator.CreateAPIKeyV3, req);
const apiKeyData = await new APIKeyDataV2({
name,
user: req.user._id,
usageCount: 0,
}).save();
const apiKey = createToken({
payload: {
authTokenType: AuthTokenType.API_KEY,
apiKeyDataId: apiKeyData._id.toString(),
userId: req.user._id.toString()
},
secret: await getAuthSecret()
});
return res.status(200).send({
apiKeyData,
apiKey
});
}
/**
* Update API key data v2 with id [apiKeyDataId]
* @param req
* @param res
*/
export const updateAPIKeyData = async (req: Request, res: Response) => {
const {
params: { apiKeyDataId },
body: {
name,
}
} = await validateRequest(reqValidator.UpdateAPIKeyV3, req);
const apiKeyData = await APIKeyDataV2.findOneAndUpdate(
{
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
},
{
name
},
{
new: true
}
);
if (!apiKeyData) throw BadRequestError({
message: "Failed to update API key"
});
return res.status(200).send({
apiKeyData
});
}
/**
* Delete API key data v2 with id [apiKeyDataId]
* @param req
* @param res
*/
export const deleteAPIKeyData = async (req: Request, res: Response) => {
const {
params: { apiKeyDataId }
} = await validateRequest(reqValidator.DeleteAPIKeyV3, req);
const apiKeyData = await APIKeyDataV2.findOneAndDelete({
_id: new Types.ObjectId(apiKeyDataId),
user: req.user._id
});
if (!apiKeyData) throw BadRequestError({
message: "Failed to delete API key"
});
return res.status(200).send({
apiKeyData
});
}

View File

@ -1,7 +0,0 @@
import * as serviceTokenDataController from "./serviceTokenDataController";
import * as apiKeyDataController from "./apiKeyDataController";
export {
serviceTokenDataController,
apiKeyDataController
}

View File

@ -1,469 +0,0 @@
import jwt from "jsonwebtoken";
import { Request, Response } from "express";
import { Types } from "mongoose";
import {
IServiceTokenDataV3,
IUser,
ServiceTokenDataV3,
ServiceTokenDataV3Key,
Workspace
} from "../../../models";
import { IServiceTokenV3TrustedIp } from "../../../models/serviceTokenDataV3";
import {
ActorType,
EventType,
Role
} from "../../models";
import { validateRequest } from "../../../helpers/validation";
import * as reqValidator from "../../../validation/serviceTokenDataV3";
import { createToken } from "../../../helpers/auth";
import {
ProjectPermissionActions,
ProjectPermissionSub,
getAuthDataProjectPermissions
} from "../../services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability";
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../../utils/errors";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { EEAuditLogService, EELicenseService } from "../../services";
import { getAuthSecret } from "../../../config";
import { ADMIN, AuthTokenType, CUSTOM, MEMBER, VIEWER } from "../../../variables";
/**
* Return project key for service token V3
* @param req
* @param res
*/
export const getServiceTokenDataKey = async (req: Request, res: Response) => {
const key = await ServiceTokenDataV3Key.findOne({
serviceTokenData: (req.authData.authPayload as IServiceTokenDataV3)._id
}).populate<{ sender: IUser }>("sender", "publicKey");
if (!key) throw ResourceNotFoundError({
message: "Failed to find project key for service token"
});
const { _id, workspace, encryptedKey, nonce, sender: { publicKey } } = key;
return res.status(200).send({
key: {
_id,
workspace,
encryptedKey,
publicKey,
nonce
}
});
}
/**
* Return access and refresh token as per refresh operation
* @param req
* @param res
*/
export const refreshToken = async (req: Request, res: Response) => {
const {
body: {
refresh_token
}
} = await validateRequest(reqValidator.RefreshTokenV3, req);
const decodedToken = <jwt.ServiceRefreshTokenJwtPayload>(
jwt.verify(refresh_token, await getAuthSecret())
);
if (decodedToken.authTokenType !== AuthTokenType.SERVICE_REFRESH_TOKEN) throw UnauthorizedRequestError();
let serviceTokenData = await ServiceTokenDataV3.findOne({
_id: new Types.ObjectId(decodedToken.serviceTokenDataId),
isActive: true
});
if (!serviceTokenData) throw UnauthorizedRequestError();
if (decodedToken.tokenVersion !== serviceTokenData.tokenVersion) {
// raise alarm
throw UnauthorizedRequestError();
}
const response: {
refresh_token?: string;
access_token: string;
expires_in: number;
token_type: string;
} = {
refresh_token,
access_token: "",
expires_in: 0,
token_type: "Bearer"
};
if (serviceTokenData.isRefreshTokenRotationEnabled) {
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
$inc: {
tokenVersion: 1
}
},
{
new: true
}
);
if (!serviceTokenData) throw BadRequestError();
response.refresh_token = createToken({
payload: {
serviceTokenDataId: serviceTokenData._id.toString(),
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
tokenVersion: serviceTokenData.tokenVersion
},
secret: await getAuthSecret()
});
}
response.access_token = createToken({
payload: {
serviceTokenDataId: serviceTokenData._id.toString(),
authTokenType: AuthTokenType.SERVICE_ACCESS_TOKEN,
tokenVersion: serviceTokenData.tokenVersion
},
expiresIn: serviceTokenData.accessTokenTTL,
secret: await getAuthSecret()
});
response.expires_in = serviceTokenData.accessTokenTTL;
await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenData._id,
{
refreshTokenLastUsed: new Date(),
$inc: { refreshTokenUsageCount: 1 }
},
{
new: true
}
);
return res.status(200).send(response);
}
/**
* Create service token data V3
* @param req
* @param res
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
const {
body: {
name,
workspaceId,
publicKey,
role,
trustedIps,
expiresIn,
accessTokenTTL,
isRefreshTokenRotationEnabled,
encryptedKey, // for ServiceTokenDataV3Key
nonce, // for ServiceTokenDataV3Key
}
} = await validateRequest(reqValidator.CreateServiceTokenV3, req);
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: new Types.ObjectId(workspaceId)
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
ProjectPermissionSub.ServiceTokens
);
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
let customRole;
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: workspace._id
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
const reformattedTrustedIps = trustedIps.map((trustedIp) => {
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
message: "Failed to add IP access range to service token due to plan restriction. Upgrade plan to add IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(trustedIp.ipAddress);
});
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user;
if (req.authData.actor.type === ActorType.USER) {
user = req.authData.authPayload._id;
}
const isActive = true;
const serviceTokenData = await new ServiceTokenDataV3({
name,
user,
workspace: new Types.ObjectId(workspaceId),
publicKey,
refreshTokenUsageCount: 0,
accessTokenUsageCount: 0,
tokenVersion: 1,
trustedIps: reformattedTrustedIps,
role: isCustomRole ? CUSTOM : role,
customRole,
isActive,
expiresAt,
accessTokenTTL,
isRefreshTokenRotationEnabled
}).save();
await new ServiceTokenDataV3Key({
encryptedKey,
nonce,
sender: req.user._id,
serviceTokenData: serviceTokenData._id,
workspace: new Types.ObjectId(workspaceId)
}).save();
const refreshToken = createToken({
payload: {
serviceTokenDataId: serviceTokenData._id.toString(),
authTokenType: AuthTokenType.SERVICE_REFRESH_TOKEN,
tokenVersion: serviceTokenData.tokenVersion
},
secret: await getAuthSecret()
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.CREATE_SERVICE_TOKEN_V3, // TODO: update
metadata: {
name,
isActive,
role,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
},
{
workspaceId: new Types.ObjectId(workspaceId)
}
);
return res.status(200).send({
serviceTokenData,
refreshToken
});
}
/**
* Update service token V3 data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
*/
export const updateServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId },
body: {
name,
isActive,
role,
trustedIps,
expiresIn,
accessTokenTTL,
isRefreshTokenRotationEnabled
}
} = await validateRequest(reqValidator.UpdateServiceTokenV3, req);
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
if (!serviceTokenData) throw ResourceNotFoundError({
message: "Service token not found"
});
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
ProjectPermissionSub.ServiceTokens
);
const workspace = await Workspace.findById(serviceTokenData.workspace);
if (!workspace) throw BadRequestError({ message: "Workspace not found" });
let customRole;
if (role) {
const isCustomRole = ![ADMIN, MEMBER, VIEWER].includes(role);
if (isCustomRole) {
customRole = await Role.findOne({
slug: role,
isOrgRole: false,
workspace: workspace._id
});
if (!customRole) throw BadRequestError({ message: "Role not found" });
}
}
const plan = await EELicenseService.getPlan(workspace.organization);
// validate trusted ips
let reformattedTrustedIps;
if (trustedIps) {
reformattedTrustedIps = trustedIps.map((trustedIp) => {
if (!plan.ipAllowlisting && trustedIp.ipAddress !== "0.0.0.0/0") return res.status(400).send({
message: "Failed to update IP access range to service token due to plan restriction. Upgrade plan to update IP access range."
});
const isValidIPOrCidr = isValidIpOrCidr(trustedIp.ipAddress);
if (!isValidIPOrCidr) return res.status(400).send({
message: "The IP is not a valid IPv4, IPv6, or CIDR block"
});
return extractIPDetails(trustedIp.ipAddress);
});
}
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
serviceTokenData = await ServiceTokenDataV3.findByIdAndUpdate(
serviceTokenDataId,
{
name,
isActive,
role: customRole ? CUSTOM : role,
...(customRole ? {
customRole
} : {}),
...(role && !customRole ? { // non-custom role
$unset: {
customRole: 1
}
} : {}),
trustedIps: reformattedTrustedIps,
expiresAt,
accessTokenTTL,
isRefreshTokenRotationEnabled
},
{
new: true
}
);
if (!serviceTokenData) throw BadRequestError({
message: "Failed to update service token"
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.UPDATE_SERVICE_TOKEN_V3,
metadata: {
name: serviceTokenData.name,
isActive,
role,
trustedIps: reformattedTrustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
}
/**
* Delete service token data with id [serviceTokenDataId]
* @param req
* @param res
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const {
params: { serviceTokenDataId }
} = await validateRequest(reqValidator.DeleteServiceTokenV3, req);
let serviceTokenData = await ServiceTokenDataV3.findById(serviceTokenDataId);
if (!serviceTokenData) throw ResourceNotFoundError({
message: "Service token not found"
});
const { permission } = await getAuthDataProjectPermissions({
authData: req.authData,
workspaceId: serviceTokenData.workspace
});
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
ProjectPermissionSub.ServiceTokens
);
serviceTokenData = await ServiceTokenDataV3.findByIdAndDelete(serviceTokenDataId);
if (!serviceTokenData) throw BadRequestError({
message: "Failed to delete service token"
});
await ServiceTokenDataV3Key.findOneAndDelete({
serviceTokenData: serviceTokenData._id
});
await EEAuditLogService.createAuditLog(
req.authData,
{
type: EventType.DELETE_SERVICE_TOKEN_V3,
metadata: {
name: serviceTokenData.name,
isActive: serviceTokenData.isActive,
role: serviceTokenData.role,
trustedIps: serviceTokenData.trustedIps as Array<IServiceTokenV3TrustedIp>,
expiresAt: serviceTokenData.expiresAt
}
},
{
workspaceId: serviceTokenData.workspace
}
);
return res.status(200).send({
serviceTokenData
});
}

View File

@ -0,0 +1,202 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Action } from '../models';
import {
getLatestSecretVersionIds,
getLatestNSecretSecretVersionIds
} from '../helpers/secretVersion';
import {
ACTION_LOGIN,
ACTION_LOGOUT,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_DELETE_SECRETS,
ACTION_UPDATE_SECRETS,
} from '../../variables';
/**
* Create an (audit) action for updating secrets
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
* @returns {Action} action - new action
*/
const createActionUpdateSecret = async ({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
let action;
try {
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
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 update secret action');
}
return action;
}
/**
* Create an (audit) action for creating, reading, and deleting
* secrets
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {Types.ObjectId} obj.secretIds - ids of relevant secrets
* @returns {Action} action - new action
*/
const createActionSecret = async ({
name,
userId,
workspaceId,
secretIds
}: {
name: string;
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
let action;
try {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
const 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 create/read/delete secret action');
}
return action;
}
/**
* Create an (audit) action for user with id [userId]
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action
* @returns
*/
const createActionUser = ({
name,
userId
}: {
name: string;
userId: Types.ObjectId;
}) => {
let action;
try {
action = new Action({
name,
user: userId
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create user action');
}
return action;
}
/**
* Create an (audit) action.
* @param {Object} obj
* @param {Object} obj.name - name of action
* @param {Types.ObjectId} obj.userId - id of user associated with action
* @param {Types.ObjectId} obj.workspaceId - id of workspace associated with action
* @param {Types.ObjectId[]} obj.secretIds - ids of secrets associated with action
*/
const createActionHelper = async ({
name,
userId,
workspaceId,
secretIds,
}: {
name: string;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) => {
let action;
try {
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionUser({
name,
userId
});
break;
case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS:
case ACTION_DELETE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionSecret({
name,
userId,
workspaceId,
secretIds
});
break;
case ACTION_UPDATE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionUpdateSecret({
name,
userId,
workspaceId,
secretIds
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action');
}
return action;
}
export {
createActionHelper
};

View File

@ -1,9 +1,8 @@
import { Types } from "mongoose";
import _ from "lodash"; import _ from "lodash";
import { Membership } from "../../models"; import { Membership } from "../../models";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables"; import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization";
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => { export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
@ -19,15 +18,15 @@ export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId
return true return true
} }
export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => { export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
} }
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
// case: you have write only if read is blocked and write is not // case: you have write only if read is blocked and write is not
if (isReadDisallowed && !isWriteDisallowed) { if (isReadDisallowed && !isWriteDisallowed) {
@ -37,15 +36,15 @@ export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceI
return false return false
} }
export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => { export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return true return true
} }
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE });
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS }); const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ });
if (isReadBlocked && isWriteDisallowed) { if (isReadBlocked && isWriteDisallowed) {
return true return true

Some files were not shown because too many files have changed in this diff Show More