Merge branch 'main' into fix/typo

This commit is contained in:
Maidul Islam
2023-06-14 14:47:22 -04:00
committed by GitHub
1020 changed files with 68578 additions and 35598 deletions
+2
View File
@@ -0,0 +1,2 @@
backend/node_modules
frontend/node_modules
+13 -9
View File
@@ -1,5 +1,6 @@
# Keys # Keys
# Required key for platform encryption/decryption ops # Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218 ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT # JWT
@@ -8,6 +9,7 @@ JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200 JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_PROVIDER_AUTH_SECRET=f32f716d70a42c5703f4656015e76201
# JWT lifetime # JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string # Optional lifetimes for JWT tokens expressed in seconds or a string
@@ -15,6 +17,7 @@ JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
JWT_AUTH_LIFETIME= JWT_AUTH_LIFETIME=
JWT_REFRESH_LIFETIME= JWT_REFRESH_LIFETIME=
JWT_SIGNUP_LIFETIME= JWT_SIGNUP_LIFETIME=
JWT_PROVIDER_AUTH_LIFETIME=
# MongoDB # MongoDB
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref # Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
@@ -30,14 +33,12 @@ MONGO_PASSWORD=example
# Required # Required
SITE_URL=http://localhost:8080 SITE_URL=http://localhost:8080
# Mail/SMTP # Mail/SMTP
SMTP_HOST= # required SMTP_HOST=
SMTP_USERNAME= # required SMTP_PORT=
SMTP_PASSWORD= # required SMTP_NAME=
SMTP_PORT=587 SMTP_USERNAME=
SMTP_SECURE=false SMTP_PASSWORD=
SMTP_FROM_ADDRESS= # required
SMTP_FROM_NAME=Infisical
# Integration # Integration
# Optional only if integration is used # Optional only if integration is used
@@ -66,4 +67,7 @@ STRIPE_WEBHOOK_SECRET=
STRIPE_PRODUCT_STARTER= STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM= STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO= STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY= NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
CLIENT_ID_GOOGLE=
CLIENT_SECRET_GOOGLE=
Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

@@ -13,6 +13,7 @@ services:
- MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin - MONGO_URL=mongodb://test:example@mongo:27017/?authSource=admin
- MONGO_USERNAME=test - MONGO_USERNAME=test
- MONGO_PASSWORD=example - MONGO_PASSWORD=example
- ENCRYPTION_KEY=a984ecdf82ec779e55dbcc21303a900f
networks: networks:
- infisical-test - infisical-test
+2 -2
View File
@@ -6,7 +6,7 @@ frontend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/frontend repository: infisical/staging_deployment_frontend
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-secret-frontend kubeSecretRef: managed-secret-frontend
@@ -25,7 +25,7 @@ backend:
secrets.infisical.com/auto-reload: "true" secrets.infisical.com/auto-reload: "true"
replicaCount: 2 replicaCount: 2
image: image:
repository: infisical/backend repository: infisical/staging_deployment_backend
tag: "latest" tag: "latest"
pullPolicy: Always pullPolicy: Always
kubeSecretRef: managed-backend-secret kubeSecretRef: managed-backend-secret
@@ -0,0 +1,118 @@
name: Release production images (frontend, backend)
on:
push:
tags:
- "infisical/v*.*.*"
jobs:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
load: true
context: backend
tags: infisical/backend:test
- name: ⏻ Spawn backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
- name: 🧪 Test backend image
run: |
./.github/resources/healthcheck.sh infisical-backend-test
- name: ⏻ Shut down backend container and dependencies
run: |
docker compose -f .github/resources/docker-compose.be-test.yml down
- name: 🏗️ Build backend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: |
infisical/backend:${{ steps.commit.outputs.short }}
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build frontend and export to Docker
uses: depot/build-push-action@v1
with:
load: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610
context: frontend
tags: infisical/frontend:test
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container
run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test
- name: 🧪 Test frontend image
run: |
./.github/resources/healthcheck.sh infisical-frontend-test
- name: ⏻ Shut down frontend container
run: |
docker stop infisical-frontend-test
- name: 🏗️ Build frontend and push
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -5,10 +5,15 @@ jobs:
backend-image: backend-image:
name: Build backend image name: Build backend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: Save commit hashes for tag - name: Save commit hashes for tag
id: commit id: commit
uses: pr-mpt/actions-commit-hash@v2 uses: pr-mpt/actions-commit-hash@v2
@@ -45,14 +50,14 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true push: true
context: backend context: backend
tags: infisical/backend:${{ steps.commit.outputs.short }}, tags: |
infisical/backend:latest infisical/staging_deployment_backend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_backend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
frontend-image: frontend-image:
name: Build frontend image name: Build frontend image
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
@@ -75,12 +80,12 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
project: 64mmf0n610 project: 64mmf0n610
context: frontend context: frontend
tags: infisical/frontend:test tags: infisical/staging_deployment_frontend:test
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
- name: ⏻ Spawn frontend container - name: ⏻ Spawn frontend container
run: | run: |
docker run -d --rm --name infisical-frontend-test infisical/frontend:test docker run -d --rm --name infisical-frontend-test infisical/staging_deployment_frontend:test
- name: 🧪 Test frontend image - name: 🧪 Test frontend image
run: | run: |
./.github/resources/healthcheck.sh infisical-frontend-test ./.github/resources/healthcheck.sh infisical-frontend-test
@@ -94,8 +99,9 @@ jobs:
push: true push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }} token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend context: frontend
tags: infisical/frontend:${{ steps.commit.outputs.short }}, tags: |
infisical/frontend:latest infisical/staging_deployment_frontend:${{ steps.commit.outputs.short }}
infisical/staging_deployment_frontend:latest
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
build-args: | build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }} POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@@ -122,17 +128,17 @@ jobs:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }} token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials - name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179 run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
- name: switch to gamma namespace - name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma run: kubectl config set-context --current --namespace=gamma
- name: test kubectl - name: test kubectl
run: kubectl get ingress run: kubectl get ingress
- name: Download helm values to file and upgrade gamma deploy - name: Download helm values to file and upgrade gamma deploy
run: | run: |
wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml wget https://raw.githubusercontent.com/Infisical/infisical/main/.github/values.yaml
helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --recreate-pods helm upgrade infisical infisical-helm-charts/infisical --values values.yaml --wait
if [[ $(helm status infisical) == *"FAILED"* ]]; then if [[ $(helm status infisical) == *"FAILED"* ]]; then
echo "Helm upgrade failed" echo "Helm upgrade failed"
exit 1 exit 1
else else
echo "Helm upgrade was successful" echo "Helm upgrade was successful"
fi fi
+12 -11
View File
@@ -13,6 +13,7 @@ jobs:
check-be-pr: check-be-pr:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- name: ☁️ Checkout source - name: ☁️ Checkout source
@@ -26,17 +27,17 @@ jobs:
- name: 📦 Install dependencies - name: 📦 Install dependencies
run: npm ci --only-production run: npm ci --only-production
working-directory: backend working-directory: backend
- name: 🧪 Run tests # - name: 🧪 Run tests
run: npm run test:ci # run: npm run test:ci
working-directory: backend # working-directory: backend
- name: 📁 Upload test results # - name: 📁 Upload test results
uses: actions/upload-artifact@v3 # uses: actions/upload-artifact@v3
if: always() # if: always()
with: # with:
name: be-test-results # name: be-test-results
path: | # path: |
./backend/reports # ./backend/reports
./backend/coverage # ./backend/coverage
- name: 🏗️ Run build - name: 🏗️ Run build
run: npm run build run: npm run build
working-directory: backend working-directory: backend
+12 -17
View File
@@ -2,40 +2,35 @@ name: Check Frontend Pull Request
on: on:
pull_request: pull_request:
types: [ opened, synchronize ] types: [opened, synchronize]
paths: paths:
- 'frontend/**' - "frontend/**"
- '!frontend/README.md' - "!frontend/README.md"
- '!frontend/.*' - "!frontend/.*"
- 'frontend/.eslintrc.js' - "frontend/.eslintrc.js"
jobs: jobs:
check-fe-pr: check-fe-pr:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- - name: ☁️ Checkout source
name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- - name: 🔧 Setup Node 16
name: 🔧 Setup Node 16
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: "16"
cache: 'npm' cache: "npm"
cache-dependency-path: frontend/package-lock.json cache-dependency-path: frontend/package-lock.json
- - name: 📦 Install dependencies
name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts run: npm ci --only-production --ignore-scripts
working-directory: frontend working-directory: frontend
# - # -
# name: 🧪 Run tests # name: 🧪 Run tests
# run: npm run test:ci # run: npm run test:ci
# working-directory: frontend # working-directory: frontend
- - name: 🏗️ Run build
name: 🏗️ Run build
run: npm run build run: npm run build
working-directory: frontend working-directory: frontend
@@ -0,0 +1,68 @@
name: Release standalone docker image
on: [workflow_dispatch]
jobs:
infisical-standalone:
name: Build infisical standalone image
runs-on: ubuntu-latest
steps:
- name: ☁️ Checkout source
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- uses: paulhatch/[email protected]
id: version
with:
# The prefix to use to identify tags
tag_prefix: "infisical-standalone/v"
# A string which, if present in a git commit, indicates that a change represents a
# major (breaking) change, supports regular expressions wrapped with '/'
major_pattern: "(MAJOR)"
# Same as above except indicating a minor change, supports regular expressions wrapped with '/'
minor_pattern: "(MINOR)"
# A string to determine the format of the version output
version_format: "${major}.${minor}.${patch}-prerelease${increment}"
# Optional path to check for changes. If any changes are detected in the path the
# 'changed' output will true. Enter multiple paths separated by spaces.
change_path: "backend,frontend"
# Prevents pre-v1.0.0 version from automatically incrementing the major version.
# If enabled, when the major version is 0, major releases will be treated as minor and minor as patch. Note that the version_type output is unchanged.
enable_prerelease_mode: true
# - name: 🧪 Run tests
# run: npm run test:ci
# working-directory: backend
- name: version output
run: |
echo "Output Value: ${{ steps.version.outputs.major }}"
echo "Output Value: ${{ steps.version.outputs.minor }}"
echo "Output Value: ${{ steps.version.outputs.patch }}"
echo "Output Value: ${{ steps.version.outputs.version }}"
echo "Output Value: ${{ steps.version.outputs.version_type }}"
echo "Output Value: ${{ steps.version.outputs.increment }}"
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
- name: 🔧 Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: 🐋 Login to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Set up Depot CLI
uses: depot/setup-action@v1
- name: 📦 Build backend and export to Docker
uses: depot/build-push-action@v1
with:
project: 64mmf0n610
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: .
tags: |
infisical/infisical:latest
infisical/infisical:${{ steps.commit.outputs.short }}
platforms: linux/amd64,linux/arm64
file: Dockerfile.standalone-infisical
+4 -2
View File
@@ -4,7 +4,7 @@ on:
push: push:
# run only against tags # run only against tags
tags: tags:
- "v*" - "infisical-cli/v*.*.*"
permissions: permissions:
contents: write contents: write
@@ -41,13 +41,15 @@ jobs:
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v4 - uses: goreleaser/goreleaser-action@v4
with: with:
distribution: goreleaser distribution: goreleaser-pro
version: latest version: latest
args: release --clean args: release --clean
env: env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
POSTHOG_API_KEY_FOR_CLI: ${{ secrets.POSTHOG_API_KEY_FOR_CLI }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }} AUR_KEY: ${{ secrets.AUR_KEY }}
GORELEASER_KEY: ${{ secrets.GORELEASER_KEY }}
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli - run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith - name: Publish to CloudSmith
+16 -15
View File
@@ -11,10 +11,16 @@ before:
- ./cli/scripts/completions.sh - ./cli/scripts/completions.sh
- ./cli/scripts/manpages.sh - ./cli/scripts/manpages.sh
monorepo:
tag_prefix: infisical-cli/
dir: cli
builds: builds:
- id: darwin-build - id: darwin-build
binary: infisical binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} ldflags:
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags: flags:
- -trimpath - -trimpath
env: env:
@@ -32,7 +38,9 @@ builds:
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
binary: infisical binary: infisical
ldflags: -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }} ldflags:
- -X github.com/Infisical/infisical-merge/packages/util.CLI_VERSION={{ .Version }}
- -X github.com/Infisical/infisical-merge/packages/telemetry.POSTHOG_API_KEY_FOR_CLI={{ .Env.POSTHOG_API_KEY_FOR_CLI }}
flags: flags:
- -trimpath - -trimpath
goos: goos:
@@ -61,10 +69,10 @@ archives:
- goos: windows - goos: windows
format: zip format: zip
files: files:
- README* - ../README*
- LICENSE* - ../LICENSE*
- manpages/* - ../manpages/*
- completions/* - ../completions/*
release: release:
replace_existing_draft: true replace_existing_draft: true
@@ -74,14 +82,7 @@ checksum:
name_template: "checksums.txt" name_template: "checksums.txt"
snapshot: snapshot:
name_template: "{{ incpatch .Version }}-devel" name_template: "{{ .Version }}-devel"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
# publishers: # publishers:
# - name: fury.io # - name: fury.io
@@ -164,7 +165,7 @@ aurs:
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical" install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical" install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical"
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish" install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
# man pages # man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz" install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"
+2
View File
@@ -3,3 +3,5 @@
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npx lint-staged npx lint-staged
infisical scan git-changes --staged -v
+1
View File
@@ -0,0 +1 @@
.github/resources/docker-compose.be-test.yml:generic-api-key:16
+107
View File
@@ -0,0 +1,107 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
FROM node:16-alpine AS frontend-dependencies
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM node:16-alpine AS frontend-builder
WORKDIR /app
# Copy dependencies
COPY --from=frontend-dependencies /app/node_modules ./node_modules
# Copy all files
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID $INTERCOM_ID
# Build
RUN npm run build
# Production image
FROM node:16-alpine AS frontend-runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
ARG INTERCOM_ID
ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
BAKED_NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown nextjs:nodejs ./public/data
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM node:16-alpine AS backend-build
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
RUN npm run build
# Production stage
FROM node:16-alpine AS backend-runner
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY --from=backend-build /app .
# Production stage
FROM node:16-alpine AS production
WORKDIR /
# Install PM2
RUN npm install -g pm2
# Copy ecosystem.config.js
COPY ecosystem.config.js .
RUN apk add --no-cache nginx
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app/ /app/
EXPOSE 80
ENV HTTPS_ENABLED false
CMD ["pm2-runtime", "start", "ecosystem.config.js"]
+74 -297
View File
File diff suppressed because one or more lines are too long
+7 -3
View File
@@ -1,9 +1,13 @@
# Security Policy # Security Policy
## Supported Versions ## Supported versions
We always recommend using the latest version of Infisical to ensure you get all security updates. We always recommend using the latest version of Infisical to ensure you get all security updates.
## Reporting a Vulnerability ## Reporting vulnerabilities
Please report security vulnerabilities or concerns to team@infisical.com. Please do not file GitHub issues or post on our public forum for security vulnerabilities, as they are public!
Infisical takes security issues very seriously. If you have any concerns about Infisical or believe you have uncovered a vulnerability, please get in touch via the e-mail address security@infisical.com. In the message, try to provide a description of the issue and ideally a way of reproducing it. The security team will get back to you as soon as possible.
Note that this security address should be used only for undisclosed vulnerabilities. Please report any security problems to us before disclosing it publicly.
+3268 -3322
View File
File diff suppressed because it is too large Load Diff
+21 -14
View File
@@ -1,39 +1,45 @@
{ {
"dependencies": { "dependencies": {
"@aws-sdk/client-secrets-manager": "^3.281.0", "@aws-sdk/client-secrets-manager": "^3.319.0",
"@godaddy/terminus": "^4.11.2", "@godaddy/terminus": "^4.12.0",
"@octokit/rest": "^19.0.5", "@octokit/rest": "^19.0.5",
"@sentry/tracing": "^7.39.0", "@sentry/node": "^7.49.0",
"@sentry/node": "^7.40.0", "@sentry/tracing": "^7.48.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10", "@types/libsodium-wrappers": "^0.7.10",
"argon2": "^0.30.3",
"await-to-js": "^3.0.0", "await-to-js": "^3.0.0",
"aws-sdk": "^2.1324.0", "aws-sdk": "^2.1364.0",
"axios": "^1.1.3", "axios": "^1.3.5",
"axios-retry": "^3.4.0", "axios-retry": "^3.4.0",
"bcrypt": "^5.1.0", "bcrypt": "^5.1.0",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.4.0",
"builder-pattern": "^2.2.0", "builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
"cors": "^2.8.5", "cors": "^2.8.5",
"crypto-js": "^4.1.1", "crypto-js": "^4.1.1",
"dotenv": "^16.0.1", "dotenv": "^16.0.1",
"express": "^4.18.1", "express": "^4.18.1",
"express-async-errors": "^3.1.1",
"express-rate-limit": "^6.7.0", "express-rate-limit": "^6.7.0",
"express-validator": "^6.14.2", "express-validator": "^6.14.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"infisical-node": "^1.0.37", "infisical-node": "^1.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10", "libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"mongoose": "^6.10.2", "mongoose": "^6.10.5",
"nanoid": "^3.3.6",
"node-cache": "^5.1.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.5.4", "passport": "^0.6.0",
"passport-google-oauth20": "^2.0.0",
"posthog-node": "^2.6.0",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"request-ip": "^3.3.0", "rate-limit-mongo": "^2.3.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stripe": "^10.7.0", "stripe": "^10.7.0",
"swagger-autogen": "^2.22.0", "swagger-autogen": "^2.22.0",
@@ -52,12 +58,12 @@
"start": "node build/index.js", "start": "node build/index.js",
"dev": "nodemon", "dev": "nodemon",
"swagger-autogen": "node ./swagger/index.ts", "swagger-autogen": "node ./swagger/index.ts",
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix", "lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d", "pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles", "test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json", "test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down" "posttest": "docker compose -f test-resources/docker-compose.test.yml down"
}, },
@@ -80,11 +86,12 @@
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/jest": "^29.2.4", "@types/jest": "^29.5.0",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191", "@types/lodash": "^4.14.191",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/passport": "^1.0.12",
"@types/supertest": "^2.0.12", "@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1", "@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
+653 -7
View File
@@ -1532,7 +1532,15 @@
"/api/v1/invite-org/signup": { "/api/v1/invite-org/signup": {
"post": { "post": {
"description": "", "description": "",
"parameters": [], "parameters": [
{
"name": "host",
"in": "header",
"schema": {
"type": "string"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
@@ -2071,6 +2079,15 @@
"targetEnvironment": { "targetEnvironment": {
"example": "any" "example": "any"
}, },
"targetEnvironmentId": {
"example": "any"
},
"targetService": {
"example": "any"
},
"targetServiceId": {
"example": "any"
},
"owner": { "owner": {
"example": "any" "example": "any"
}, },
@@ -2297,6 +2314,13 @@
"schema": { "schema": {
"type": "string" "type": "string"
} }
},
{
"name": "teamId",
"in": "query",
"schema": {
"type": "string"
}
} }
], ],
"responses": { "responses": {
@@ -2309,6 +2333,107 @@
} }
} }
}, },
"/api/v1/integration-auth/{integrationAuthId}/teams": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/vercel/branches": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/railway/environments": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/railway/services": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/signup/complete-account/signup": { "/api/v2/signup/complete-account/signup": {
"post": { "post": {
"description": "", "description": "",
@@ -2870,9 +2995,6 @@
} }
} }
} }
},
"400": {
"description": "Bad Request"
} }
}, },
"security": [ "security": [
@@ -2882,6 +3004,26 @@
] ]
} }
}, },
"/api/v2/organizations/{organizationId}/service-accounts": {
"get": {
"description": "",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/workspace/{workspaceId}/environments": { "/api/v2/workspace/{workspaceId}/environments": {
"post": { "post": {
"description": "", "description": "",
@@ -4018,9 +4160,6 @@
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
},
"400": {
"description": "Bad Request"
} }
}, },
"requestBody": { "requestBody": {
@@ -4073,6 +4212,138 @@
} }
} }
], ],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/me": {
"get": {
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"delete": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/": {
"post": {
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/name": {
"patch": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": { "responses": {
"200": { "200": {
"description": "OK" "description": "OK"
@@ -4080,6 +4351,90 @@
"400": { "400": {
"description": "Bad Request" "description": "Bad Request"
} }
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environment": {
"example": "any"
},
"workspaceId": {
"example": "any"
},
"read": {
"example": "any"
},
"write": {
"example": "any"
},
"encryptedKey": {
"example": "any"
},
"nonce": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}": {
"delete": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "serviceAccountWorkspacePermissionId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/keys": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
} }
} }
}, },
@@ -4149,6 +4504,297 @@
} }
} }
}, },
"/api/v3/secrets/": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "environment",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/secrets/{secretName}": {
"post": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
},
"secretKeyCiphertext": {
"example": "any"
},
"secretKeyIV": {
"example": "any"
},
"secretKeyTag": {
"example": "any"
},
"secretValueCiphertext": {
"example": "any"
},
"secretValueIV": {
"example": "any"
},
"secretValueTag": {
"example": "any"
},
"secretCommentCiphertext": {
"example": "any"
},
"secretCommentIV": {
"example": "any"
},
"secretCommentTag": {
"example": "any"
}
}
}
}
}
}
},
"get": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "environment",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "type",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"patch": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
},
"secretValueCiphertext": {
"example": "any"
},
"secretValueIV": {
"example": "any"
},
"secretValueTag": {
"example": "any"
}
}
}
}
}
}
},
"delete": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets/blind-index-status": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets/names": {
"post": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretsToUpdate": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/status": { "/api/status": {
"get": { "get": {
"description": "", "description": "",
+93 -51
View File
@@ -1,51 +1,93 @@
import infisical from 'infisical-node'; import InfisicalClient from 'infisical-node';
export const getPort = () => infisical.get('PORT')! || 4000;
export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP'); export const client = new InfisicalClient({
export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!; token: process.env.INFISICAL_TOKEN!
export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10; });
export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d';
export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!; export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m'; export const getEncryptionKey = async () => {
export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m'; const secretValue = (await client.getSecret('ENCRYPTION_KEY')).secretValue;
export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d'; return secretValue === '' ? undefined : secretValue;
export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!; }
export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!; export const getRootEncryptionKey = async () => {
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m'; const secretValue = (await client.getSecret('ROOT_ENCRYPTION_KEY')).secretValue;
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!; return secretValue === '' ? undefined : secretValue;
export const getMongoURL = () => infisical.get('MONGO_URL')!; }
export const getNodeEnv = () => infisical.get('NODE_ENV')!; export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue === 'true'
export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true; export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getLokiHost = () => infisical.get('LOKI_HOST')!; export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!; export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!; export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!; export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!; export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d';
export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!; export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!; export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!; export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m';
export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!; export const getJwtProviderAuthSecret = async () => (await client.getSecret('JWT_PROVIDER_AUTH_SECRET')).secretValue;
export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!; export const getJwtProviderAuthLifetime = async () => (await client.getSecret('JWT_PROVIDER_AUTH_LIFETIME')).secretValue || '15m';
export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!; export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue;
export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!; export const getMongoURL = async () => (await client.getSecret('MONGO_URL')).secretValue;
export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!; export const getNodeEnv = async () => (await client.getSecret('NODE_ENV')).secretValue || 'production';
export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!; export const getVerboseErrorOutput = async () => (await client.getSecret('VERBOSE_ERROR_OUTPUT')).secretValue === 'true' && true;
export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com'; export const getLokiHost = async () => (await client.getSecret('LOKI_HOST')).secretValue;
export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; export const getClientIdAzure = async () => (await client.getSecret('CLIENT_ID_AZURE')).secretValue;
export const getSentryDSN = () => infisical.get('SENTRY_DSN')!; export const getClientIdHeroku = async () => (await client.getSecret('CLIENT_ID_HEROKU')).secretValue;
export const getSiteURL = () => infisical.get('SITE_URL')!; export const getClientIdVercel = async () => (await client.getSecret('CLIENT_ID_VERCEL')).secretValue;
export const getSmtpHost = () => infisical.get('SMTP_HOST')!; export const getClientIdNetlify = async () => (await client.getSecret('CLIENT_ID_NETLIFY')).secretValue;
export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false; export const getClientIdGitHub = async () => (await client.getSecret('CLIENT_ID_GITHUB')).secretValue;
export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587; export const getClientIdGitLab = async () => (await client.getSecret('CLIENT_ID_GITLAB')).secretValue;
export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!; export const getClientIdGoogle = async () => (await client.getSecret('CLIENT_ID_GOOGLE')).secretValue;
export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!; export const getClientSecretAzure = async () => (await client.getSecret('CLIENT_SECRET_AZURE')).secretValue;
export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!; export const getClientSecretHeroku = async () => (await client.getSecret('CLIENT_SECRET_HEROKU')).secretValue;
export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical'; export const getClientSecretVercel = async () => (await client.getSecret('CLIENT_SECRET_VERCEL')).secretValue;
export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!; export const getClientSecretNetlify = async () => (await client.getSecret('CLIENT_SECRET_NETLIFY')).secretValue;
export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!; export const getClientSecretGitHub = async () => (await client.getSecret('CLIENT_SECRET_GITHUB')).secretValue;
export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!; export const getClientSecretGitLab = async () => (await client.getSecret('CLIENT_SECRET_GITLAB')).secretValue;
export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!; export const getClientSecretGoogle = async () => (await client.getSecret('CLIENT_SECRET_GOOGLE')).secretValue;
export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!; export const getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue;
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!; export const getPostHogHost = async () => (await client.getSecret('POSTHOG_HOST')).secretValue || 'https://app.posthog.com';
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true; export const getPostHogProjectApiKey = async () => (await client.getSecret('POSTHOG_PROJECT_API_KEY')).secretValue || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!; export const getSentryDSN = async () => (await client.getSecret('SENTRY_DSN')).secretValue;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true export const getSiteURL = async () => (await client.getSecret('SITE_URL')).secretValue;
export const getSmtpHost = async () => (await client.getSecret('SMTP_HOST')).secretValue;
export const getSmtpSecure = async () => (await client.getSecret('SMTP_SECURE')).secretValue === 'true' || false;
export const getSmtpPort = async () => parseInt((await client.getSecret('SMTP_PORT')).secretValue) || 587;
export const getSmtpUsername = async () => (await client.getSecret('SMTP_USERNAME')).secretValue;
export const getSmtpPassword = async () => (await client.getSecret('SMTP_PASSWORD')).secretValue;
export const getSmtpFromAddress = async () => (await client.getSecret('SMTP_FROM_ADDRESS')).secretValue;
export const getSmtpFromName = async () => (await client.getSecret('SMTP_FROM_NAME')).secretValue || 'Infisical';
export const getLicenseKey = async () => {
const secretValue = (await client.getSecret('LICENSE_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getLicenseServerKey = async () => {
const secretValue = (await client.getSecret('LICENSE_SERVER_KEY')).secretValue;
return secretValue === '' ? undefined : secretValue;
}
export const getLicenseServerUrl = async () => (await client.getSecret('LICENSE_SERVER_URL')).secretValue || 'https://portal.infisical.com';
// TODO: deprecate from here
export const getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue;
export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue;
export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue;
export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue;
export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue;
export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue;
export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true;
export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue;
export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true
export const getHttpsEnabled = async () => {
if ((await getNodeEnv()) != "production") {
// no https for anything other than prod
return false
}
if ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") {
// default when no value present
return true
}
return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true
}
+111 -3
View File
@@ -1,10 +1,24 @@
import axios from 'axios'; import axios from 'axios';
import axiosRetry from 'axios-retry'; import axiosRetry from 'axios-retry';
import {
getLicenseServerKeyAuthToken,
setLicenseServerKeyAuthToken,
getLicenseKeyAuthToken,
setLicenseKeyAuthToken
} from './storage';
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl
} from './index';
const axiosInstance = axios.create(); // should have JWT to interact with the license server
export const licenseServerKeyRequest = axios.create();
export const licenseKeyRequest = axios.create();
export const standardRequest = axios.create();
// add retry functionality to the axios instance // add retry functionality to the axios instance
axiosRetry(axiosInstance, { axiosRetry(standardRequest, {
retries: 3, retries: 3,
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
retryCondition: (error) => { retryCondition: (error) => {
@@ -13,4 +27,98 @@ axiosRetry(axiosInstance, {
}, },
}); });
export default axiosInstance; export const refreshLicenseServerKeyToken = async () => {
const licenseServerKey = await getLicenseServerKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-server-login`, {},
{
headers: {
'X-API-KEY': licenseServerKey
}
}
);
setLicenseServerKeyAuthToken(token);
return token;
}
export const refreshLicenseKeyToken = async () => {
const licenseKey = await getLicenseKey();
const licenseServerUrl = await getLicenseServerUrl();
const { data: { token } } = await standardRequest.post(
`${licenseServerUrl}/api/auth/v1/license-login`, {},
{
headers: {
'X-API-KEY': licenseKey
}
}
);
setLicenseKeyAuthToken(token);
return token;
}
licenseServerKeyRequest.interceptors.request.use((config) => {
const token = getLicenseServerKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseServerKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseServerKeyToken();
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
return licenseServerKeyRequest(originalRequest);
}
return Promise.reject(err);
});
licenseKeyRequest.interceptors.request.use((config) => {
const token = getLicenseKeyAuthToken();
if (token && config.headers) {
// eslint-disable-next-line no-param-reassign
config.headers.Authorization = `Bearer ${token}`;
}
return config;
}, (err) => {
return Promise.reject(err);
});
licenseKeyRequest.interceptors.response.use((response) => {
return response
}, async function (err) {
const originalRequest = err.config;
if (err.response.status === 401 && !originalRequest._retry) {
originalRequest._retry = true;
// refresh
const token = await refreshLicenseKeyToken();
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
return licenseKeyRequest(originalRequest);
}
return Promise.reject(err);
});
+30
View File
@@ -0,0 +1,30 @@
const MemoryLicenseServerKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken
};
};
const MemoryLicenseKeyTokenStorage = () => {
let authToken: string;
return {
setToken: (token: string) => {
authToken = token;
},
getToken: () => authToken
};
};
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;
+197 -176
View File
@@ -1,29 +1,39 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import fs from 'fs';
import path from 'path';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models'; import {
User,
LoginSRPDetail,
TokenVersion
} from '../../models';
import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth'; import { createToken, issueAuthTokens, clearTokens } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user'; import { checkUserDevice } from '../../helpers/user';
import { import {
ACTION_LOGIN, ACTION_LOGIN,
ACTION_LOGOUT ACTION_LOGOUT,
AUTH_MODE_JWT
} from '../../variables'; } from '../../variables';
import { BadRequestError } from '../../utils/errors'; import {
BadRequestError,
UnauthorizedRequestError
} from '../../utils/errors';
import { EELogService } from '../../ee/services'; import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { getChannelFromUserAgent } from '../../utils/posthog';
import { import {
getNodeEnv,
getJwtRefreshSecret, getJwtRefreshSecret,
getJwtAuthLifetime, getJwtAuthLifetime,
getJwtAuthSecret getJwtAuthSecret,
getHttpsEnabled
} from '../../config'; } from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
userId: string; userId: string;
refreshVersion?: number;
} }
} }
@@ -34,47 +44,39 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
try { const {
const { email,
email, clientPublicKey
clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false }) }, { upsert: true, returnNewDocument: false })
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@@ -85,84 +87,80 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
try { const { email, clientProof } = req.body;
const { email, clientProof } = req.body; const user = await User.findOne({
const user = await User.findOne({ email
email }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!loginSRPDetailFromDB) { if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
} }
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// issue tokens // issue tokens
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.realIP,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: await getHttpsEnabled()
}); });
const loginAction = await EELogService.createAction({ const loginAction = await EELogService.createAction({
name: ACTION_LOGIN, name: ACTION_LOGIN,
userId: user._id userId: user._id
}); });
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
});
}
return res.status(400).send({ loginAction && await EELogService.createLog({
message: 'Failed to authenticate. Try again?' userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.realIP
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}); });
} }
);
} catch (err) { return res.status(400).send({
Sentry.setUser(null); message: 'Failed to authenticate. Try again?'
Sentry.captureException(err); });
return res.status(400).send({ }
message: 'Failed to authenticate. Try again?' );
});
}
}; };
/** /**
@@ -172,44 +170,59 @@ export const login2 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const logout = async (req: Request, res: Response) => { export const logout = async (req: Request, res: Response) => {
try { if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens({ await clearTokens(req.authData.tokenVersionId)
userId: req.user._id.toString()
});
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to logout'
});
} }
// clear httpOnly cookie
res.cookie('jid', '', {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.realIP
});
return res.status(200).send({ return res.status(200).send({
message: 'Successfully logged out.' message: 'Successfully logged out.'
}); });
}; };
export const getCommonPasswords = async (req: Request, res: Response) => {
const commonPasswords = fs.readFileSync(
path.resolve(__dirname, '../../data/' + 'common_passwords.txt'),
'utf8'
).split('\n');
return res.status(200).send(commonPasswords);
}
export const revokeAllSessions = async (req: Request, res: Response) => {
await TokenVersion.updateMany({
user: req.user._id
}, {
$inc: {
refreshVersion: 1,
accessVersion: 1
}
});
return res.status(200).send({
message: 'Successfully revoked all sessions.'
});
}
/** /**
* Return user is authenticated * Return user is authenticated
* @param req * @param req
@@ -223,47 +236,55 @@ export const checkAuth = async (req: Request, res: Response) => {
} }
/** /**
* Return new token by redeeming refresh token * Return new JWT access token by first validating the refresh token
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getNewToken = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => {
try { const refreshToken = req.cookies.jid;
const refreshToken = req.cookies.jid;
if (!refreshToken) { if (!refreshToken) {
throw new Error('Failed to find token in request cookies'); throw new Error('Failed to find refresh token in request cookies');
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, getJwtRefreshSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const token = createToken({
payload: {
userId: decodedToken.userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});
return res.status(200).send({
token
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Invalid request'
});
} }
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey +refreshVersion +accessVersion');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
if (!tokenVersion) throw UnauthorizedRequestError({
message: 'Failed to validate refresh token'
});
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion) throw BadRequestError({
message: 'Failed to validate refresh token'
});
const token = createToken({
payload: {
userId: decodedToken.userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
});
return res.status(200).send({
token
});
}; };
export const handleAuthProviderCallback = (req: Request, res: Response) => {
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
}
+49 -68
View File
@@ -1,5 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import { Types } from 'mongoose';
import { Bot, BotKey } from '../../models'; import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot'; import { createBot } from '../../helpers/bot';
@@ -16,33 +16,24 @@ interface BotKey {
* @returns * @returns
*/ */
export const getBotByWorkspaceId = async (req: Request, res: Response) => { export const getBotByWorkspaceId = async (req: Request, res: Response) => {
let bot; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
bot = await Bot.findOne({ let bot = await Bot.findOne({
workspace: workspaceId workspace: workspaceId
}); });
if (!bot) { if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId] // case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it // -> create a new bot and return it
bot = await createBot({ bot = await createBot({
name: 'Infisical Bot', name: 'Infisical Bot',
workspaceId workspaceId: new Types.ObjectId(workspaceId)
}); });
} }
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get bot for workspace'
});
}
return res.status(200).send({ return res.status(200).send({
bot bot
}); });
}; };
/** /**
@@ -52,54 +43,44 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const setBotActiveState = async (req: Request, res: Response) => { export const setBotActiveState = async (req: Request, res: Response) => {
let bot; const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
try {
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body; if (isActive) {
// bot state set to active -> share workspace key with bot
if (isActive) { if (!botKey?.encryptedKey || !botKey?.nonce) {
// bot state set to active -> share workspace key with bot return res.status(400).send({
if (!botKey?.encryptedKey || !botKey?.nonce) { message: 'Failed to set bot state to active - missing bot key'
return res.status(400).send({
message: 'Failed to set bot state to active - missing bot key'
});
}
await BotKey.findOneAndUpdate({
workspace: req.bot.workspace
}, {
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: req.user._id,
bot: req.bot._id,
workspace: req.bot.workspace
}, {
upsert: true,
new: true
});
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: req.bot._id
}); });
} }
bot = await Bot.findOneAndUpdate({ await BotKey.findOneAndUpdate({
_id: req.bot._id workspace: req.bot.workspace
}, { }, {
isActive encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: req.user._id,
bot: req.bot._id,
workspace: req.bot.workspace
}, { }, {
upsert: true,
new: true new: true
}); });
} else {
if (!bot) throw new Error('Failed to update bot active state'); // case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
} catch (err) { bot: req.bot._id
Sentry.setUser({ email: req.user.email }); });
Sentry.captureException(err); }
return res.status(400).send({
message: 'Failed to update bot active state' let bot = await Bot.findOneAndUpdate({
}); _id: req.bot._id
} }, {
isActive
}, {
new: true
});
if (!bot) throw new Error('Failed to update bot active state');
return res.status(200).send({ return res.status(200).send({
bot bot
@@ -1,45 +1,40 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { import {
IntegrationAuth, IntegrationAuth,
Bot Bot
} from '../../models'; } from '../../models';
import { INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables'; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, INTEGRATION_SET, getIntegrationOptions as getIntegrationOptionsFunc } from '../../variables';
import { IntegrationService } from '../../services'; import { IntegrationService } from '../../services';
import { import {
getApps, getApps,
getTeams, getTeams,
revokeAccess revokeAccess
} from '../../integrations'; } from '../../integrations';
import {
INTEGRATION_VERCEL_API_URL,
INTEGRATION_RAILWAY_API_URL
} from '../../variables';
import { standardRequest } from '../../config/request';
/*** /***
* Return integration authorization with id [integrationAuthId] * Return integration authorization with id [integrationAuthId]
*/ */
export const getIntegrationAuth = async (req: Request, res: Response) => { export const getIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth; const { integrationAuthId } = req.params;
try { const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
const { integrationAuthId } = req.params;
integrationAuth = await IntegrationAuth.findById(integrationAuthId); if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization'
if (!integrationAuth) return res.status(400).send({ });
message: 'Failed to find integration authorization'
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization'
});
}
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth
}); });
} }
export const getIntegrationOptions = async (req: Request, res: Response) => { export const getIntegrationOptions = async (req: Request, res: Response) => {
const INTEGRATION_OPTIONS = getIntegrationOptionsFunc(); const INTEGRATION_OPTIONS = await getIntegrationOptionsFunc();
return res.status(200).send({ return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS, integrationOptions: INTEGRATION_OPTIONS,
@@ -56,33 +51,25 @@ export const oAuthExchange = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
try { const { workspaceId, code, integration } = req.body;
const { workspaceId, code, integration } = req.body; if (!INTEGRATION_SET.has(integration))
if (!INTEGRATION_SET.has(integration)) throw new Error('Failed to validate integration');
throw new Error('Failed to validate integration');
const environments = req.membership.workspace?.environments || [];
const environments = req.membership.workspace?.environments || []; if(environments.length === 0){
if(environments.length === 0){ throw new Error("Failed to get environments")
throw new Error("Failed to get environments") }
}
const integrationAuth = await IntegrationService.handleOAuthExchange({
const integrationAuth = await IntegrationService.handleOAuthExchange({ workspaceId,
workspaceId, integration,
integration, code,
code, environment: environments[0].slug,
environment: environments[0].slug, });
});
return res.status(200).send({
return res.status(200).send({ integrationAuth
integrationAuth });
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get OAuth2 code-token exchange'
});
}
}; };
/** /**
@@ -99,53 +86,53 @@ export const saveIntegrationAccessToken = async (
// TODO: check if access token is valid for each integration // TODO: check if access token is valid for each integration
let integrationAuth; let integrationAuth;
try { const {
const { workspaceId,
workspaceId, accessId,
accessId, accessToken,
accessToken, url,
integration namespace,
}: { integration
workspaceId: string; }: {
accessId: string | null; workspaceId: string;
accessToken: string; accessId: string | null;
integration: string; accessToken: string;
} = req.body; url: string;
namespace: string;
integration: string;
} = req.body;
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
isActive: true isActive: true
}); });
if (!bot) throw new Error('Bot must be enabled to save integration access token'); if (!bot) throw new Error('Bot must be enabled to save integration access token');
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
integration integration
}, { }, {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
integration integration,
}, { url,
new: true, namespace,
upsert: true algorithm: ALGORITHM_AES_256_GCM,
}); keyEncoding: ENCODING_SCHEME_UTF8
}, {
// encrypt and save integration access details new: true,
integrationAuth = await IntegrationService.setIntegrationAuthAccess({ upsert: true
integrationAuthId: integrationAuth._id.toString(), });
accessId,
accessToken, // encrypt and save integration access details
accessExpiresAt: undefined integrationAuth = await IntegrationService.setIntegrationAuthAccess({
}); integrationAuthId: integrationAuth._id.toString(),
accessId,
if (!integrationAuth) throw new Error('Failed to save integration access token'); accessToken,
} catch (err) { accessExpiresAt: undefined
Sentry.setUser({ email: req.user.email }); });
Sentry.captureException(err);
return res.status(400).send({ if (!integrationAuth) throw new Error('Failed to save integration access token');
message: 'Failed to save access token for integration'
});
}
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth
@@ -159,22 +146,13 @@ export const saveIntegrationAccessToken = async (
* @returns * @returns
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps; const teamId = req.query.teamId as string;
try {
const teamId = req.query.teamId as string; const apps = await getApps({
integrationAuth: req.integrationAuth,
apps = await getApps({ accessToken: req.accessToken,
integrationAuth: req.integrationAuth, ...teamId && { teamId }
accessToken: req.accessToken, });
...teamId && { teamId }
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({ return res.status(200).send({
apps apps
@@ -188,22 +166,203 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getIntegrationAuthTeams = async (req: Request, res: Response) => { export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
let teams; const teams = await getTeams({
try { integrationAuth: req.integrationAuth,
teams = await getTeams({ accessToken: req.accessToken
integrationAuth: req.integrationAuth, });
accessToken: req.accessToken
return res.status(200).send({
teams
});
}
/**
* Return list of available Vercel (preview) branches for Vercel project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface VercelBranch {
ref: string;
lastCommit: string;
isProtected: boolean;
}
const params = new URLSearchParams({
projectId: appId,
...(req.integrationAuth.teamId ? {
teamId: req.integrationAuth.teamId
} : {})
});
let branches: string[] = [];
if (appId && appId !== '') {
const { data }: { data: VercelBranch[] } = await standardRequest.get(
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
{
params,
headers: {
Authorization: `Bearer ${req.accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
branches = data.map((b) => b.ref);
}
return res.status(200).send({
branches
});
}
/**
* Return list of Railway environments for Railway project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayEnvironment {
node: {
id: string;
name: string;
isEphemeral: boolean;
}
}
interface Environment {
environmentId: string;
name: string;
}
let environments: Environment[] = [];
if (appId && appId !== '') {
const query = `
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) {
edges {
node {
id
name
isEphemeral
}
}
}
}
`;
const variables = {
projectId: appId
}
const { data: { data: { environments: { edges } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email }); environments = edges.map((e: RailwayEnvironment) => {
Sentry.captureException(err); return ({
return res.status(400).send({ name: e.node.name,
message: "Failed to get integration authorization teams" environmentId: e.node.id
});
}); });
} }
return res.status(200).send({ return res.status(200).send({
teams environments
});
}
/**
* Return list of Railway services for Railway project with id
* [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayService {
node: {
id: string;
name: string;
}
}
interface Service {
name: string;
serviceId: string;
}
let services: Service[] = [];
const query = `
query project($id: String!) {
project(id: $id) {
createdAt
deletedAt
id
description
expiredAt
isPublic
isTempProject
isUpdatable
name
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
}
}
`;
if (appId && appId !== '') {
const variables = {
id: appId
}
const { data: { data: { project: { services: { edges } } } } } = await standardRequest.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
services = edges.map((e: RailwayService) => ({
name: e.node.name,
serviceId: e.node.id
}));
}
return res.status(200).send({
services
}); });
} }
@@ -214,19 +373,10 @@ export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegrationAuth = async (req: Request, res: Response) => { export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth; const integrationAuth = await revokeAccess({
try { integrationAuth: req.integrationAuth,
integrationAuth = await revokeAccess({ accessToken: req.accessToken,
integrationAuth: req.integrationAuth, });
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration authorization",
});
}
return res.status(200).send({ return res.status(200).send({
integrationAuth, integrationAuth,
@@ -1,6 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { import {
Integration Integration
} from '../../models'; } from '../../models';
@@ -14,54 +13,50 @@ import { eventPushSecrets } from '../../events';
* @returns * @returns
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment]
try { // initialize new integration after saving integration access token
const { const integration = await new Integration({
integrationAuthId, workspace: req.integrationAuth.workspace._id,
app, environment: sourceEnvironment,
appId, isActive,
isActive, app,
sourceEnvironment, appId,
targetEnvironment, targetEnvironment,
owner, targetEnvironmentId,
path, targetService,
region targetServiceId,
} = req.body; owner,
path,
// TODO: validate [sourceEnvironment] and [targetEnvironment] region,
integration: req.integrationAuth.integration,
// initialize new integration after saving integration access token integrationAuth: new Types.ObjectId(integrationAuthId)
integration = await new Integration({ }).save();
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment, if (integration) {
isActive, // trigger event - push secrets
app, EventService.handleEvent({
appId, event: eventPushSecrets({
targetEnvironment, workspaceId: integration.workspace,
owner, environment: sourceEnvironment
path, })
region, });
integration: req.integrationAuth.integration, }
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString()
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create integration'
});
}
return res.status(200).send({ return res.status(200).send({
integration, integration,
@@ -75,51 +70,43 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
let integration;
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
try { const {
const { environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
const integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment, environment,
isActive, isActive,
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner,
} = req.body; },
{
integration = await Integration.findOneAndUpdate( new: true,
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace.toString(),
}),
});
} }
} catch (err) { );
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); if (integration) {
return res.status(400).send({ // trigger event - push secrets
message: "Failed to update integration", EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
}),
}); });
} }
@@ -136,22 +123,13 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { export const deleteIntegration = async (req: Request, res: Response) => {
let integration; const { integrationId } = req.params;
try {
const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({ const integration = await Integration.findOneAndDelete({
_id: integrationId, _id: integrationId,
}); });
if (!integration) throw new Error("Failed to find integration"); if (!integration) throw new Error("Failed to find integration");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration",
});
}
return res.status(200).send({ return res.status(200).send({
integration, integration,
+27 -45
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../../models'; import { Key } from '../../models';
import { findMembership } from '../../helpers/membership'; import { findMembership } from '../../helpers/membership';
@@ -11,34 +10,26 @@ import { findMembership } from '../../helpers/membership';
* @returns * @returns
*/ */
export const uploadKey = async (req: Request, res: Response) => { export const uploadKey = async (req: Request, res: Response) => {
try { const { workspaceId } = req.params;
const { workspaceId } = req.params; const { key } = req.body;
const { key } = req.body;
// validate membership of receiver // validate membership of receiver
const receiverMembership = await findMembership({ const receiverMembership = await findMembership({
user: key.userId, user: key.userId,
workspace: workspaceId workspace: workspaceId
}); });
if (!receiverMembership) { if (!receiverMembership) {
throw new Error('Failed receiver membership validation for workspace'); throw new Error('Failed receiver membership validation for workspace');
} }
await new Key({ await new Key({
encryptedKey: key.encryptedKey, encryptedKey: key.encryptedKey,
nonce: key.nonce, nonce: key.nonce,
sender: req.user._id, sender: req.user._id,
receiver: key.userId, receiver: key.userId,
workspace: workspaceId workspace: workspaceId
}).save(); }).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload key to workspace'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully uploaded key to workspace' message: 'Successfully uploaded key to workspace'
@@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getLatestKey = async (req: Request, res: Response) => { export const getLatestKey = async (req: Request, res: Response) => {
let latestKey; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
// get latest key // get latest key
latestKey = await Key.find({ const latestKey = await Key.find({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.limit(1) .limit(1)
.populate('sender', '+publicKey'); .populate('sender', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get latest key'
});
}
const resObj: any = {}; const resObj: any = {};
@@ -79,4 +61,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
} }
return res.status(200).send(resObj); return res.status(200).send(resObj);
}; };
+112 -149
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Membership, MembershipOrg, User, Key } from '../../models'; import { Membership, MembershipOrg, User, Key } from '../../models';
import { import {
@@ -16,25 +15,16 @@ import { getSiteURL } from '../../config';
* @returns * @returns
*/ */
export const validateMembership = async (req: Request, res: Response) => { export const validateMembership = async (req: Request, res: Response) => {
try { const { workspaceId } = req.params;
const { workspaceId } = req.params; // validate membership
const membership = await findMembership({
user: req.user._id,
workspace: workspaceId
});
// validate membership if (!membership) {
const membership = await findMembership({ throw new Error('Failed to validate membership');
user: req.user._id, }
workspace: workspaceId
});
if (!membership) {
throw new Error('Failed to validate membership');
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed workspace connection check'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Workspace membership confirmed' message: 'Workspace membership confirmed'
@@ -48,48 +38,39 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteMembership = async (req: Request, res: Response) => { export const deleteMembership = async (req: Request, res: Response) => {
let deletedMembership; const { membershipId } = req.params;
try {
const { membershipId } = req.params;
// check if membership to delete exists // check if membership to delete exists
const membershipToDelete = await Membership.findOne({ const membershipToDelete = await Membership.findOne({
_id: membershipId _id: membershipId
}).populate('user'); }).populate('user');
if (!membershipToDelete) { if (!membershipToDelete) {
throw new Error( throw new Error(
"Failed to delete workspace membership that doesn't exist" "Failed to delete workspace membership that doesn't exist"
); );
} }
// check if user is a member and admin of the workspace // check if user is a member and admin of the workspace
// whose membership we wish to delete // whose membership we wish to delete
const membership = await Membership.findOne({ const membership = await Membership.findOne({
user: req.user._id, user: req.user._id,
workspace: membershipToDelete.workspace workspace: membershipToDelete.workspace
}); });
if (!membership) { if (!membership) {
throw new Error('Failed to validate workspace membership'); throw new Error('Failed to validate workspace membership');
} }
if (membership.role !== ADMIN) { if (membership.role !== ADMIN) {
// user is not an admin member of the workspace // user is not an admin member of the workspace
throw new Error('Insufficient role for deleting workspace membership'); throw new Error('Insufficient role for deleting workspace membership');
} }
// delete workspace membership // delete workspace membership
deletedMembership = await deleteMember({ const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString() membershipId: membershipToDelete._id.toString()
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete membership'
});
}
return res.status(200).send({ return res.status(200).send({
deletedMembership deletedMembership
@@ -103,49 +84,40 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeMembershipRole = async (req: Request, res: Response) => { export const changeMembershipRole = async (req: Request, res: Response) => {
let membershipToChangeRole; const { membershipId } = req.params;
try { const { role } = req.body;
const { membershipId } = req.params;
const { role } = req.body;
if (![ADMIN, MEMBER].includes(role)) { if (![ADMIN, MEMBER].includes(role)) {
throw new Error('Failed to validate role'); throw new Error('Failed to validate role');
} }
// validate target membership // validate target membership
membershipToChangeRole = await findMembership({ const membershipToChangeRole = await findMembership({
_id: membershipId _id: membershipId
}); });
if (!membershipToChangeRole) { if (!membershipToChangeRole) {
throw new Error('Failed to find membership to change role'); throw new Error('Failed to find membership to change role');
} }
// check if user is a member and admin of target membership's // check if user is a member and admin of target membership's
// workspace // workspace
const membership = await findMembership({ const membership = await findMembership({
user: req.user._id, user: req.user._id,
workspace: membershipToChangeRole.workspace workspace: membershipToChangeRole.workspace
}); });
if (!membership) { if (!membership) {
throw new Error('Failed to validate membership'); throw new Error('Failed to validate membership');
} }
if (membership.role !== ADMIN) { if (membership.role !== ADMIN) {
// user is not an admin member of the workspace // user is not an admin member of the workspace
throw new Error('Insufficient role for changing member roles'); throw new Error('Insufficient role for changing member roles');
} }
membershipToChangeRole.role = role; membershipToChangeRole.role = role;
await membershipToChangeRole.save(); await membershipToChangeRole.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change membership role'
});
}
return res.status(200).send({ return res.status(200).send({
membership: membershipToChangeRole membership: membershipToChangeRole
@@ -159,75 +131,66 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToWorkspace = async (req: Request, res: Response) => { export const inviteUserToWorkspace = async (req: Request, res: Response) => {
let invitee, latestKey; const { workspaceId } = req.params;
try { const { email }: { email: string } = req.body;
const { workspaceId } = req.params;
const { email }: { email: string } = req.body;
invitee = await User.findOne({ const invitee = await User.findOne({
email email
}).select('+publicKey'); }).select('+publicKey');
if (!invitee || !invitee?.publicKey) if (!invitee || !invitee?.publicKey)
throw new Error('Failed to validate invitee'); throw new Error('Failed to validate invitee');
// validate invitee's workspace membership - ensure member isn't // validate invitee's workspace membership - ensure member isn't
// already a member of the workspace // already a member of the workspace
const inviteeMembership = await Membership.findOne({ const inviteeMembership = await Membership.findOne({
user: invitee._id, user: invitee._id,
workspace: workspaceId workspace: workspaceId
}); });
if (inviteeMembership) if (inviteeMembership)
throw new Error('Failed to add existing member of workspace'); throw new Error('Failed to add existing member of workspace');
// validate invitee's organization membership - ensure that only // validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace // (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: invitee._id, user: invitee._id,
organization: req.membership.workspace.organization, organization: req.membership.workspace.organization,
status: ACCEPTED status: ACCEPTED
}); });
if (!membershipOrg) if (!membershipOrg)
throw new Error("Failed to validate invitee's organization membership"); throw new Error("Failed to validate invitee's organization membership");
// get latest key // get latest key
latestKey = await Key.findOne({ const latestKey = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.populate('sender', '+publicKey'); .populate('sender', '+publicKey');
// create new workspace membership // create new workspace membership
const m = await new Membership({ const m = await new Membership({
user: invitee._id, user: invitee._id,
workspace: workspaceId, workspace: workspaceId,
role: MEMBER role: MEMBER
}).save(); }).save();
await sendMail({ await sendMail({
template: 'workspaceInvitation.handlebars', template: 'workspaceInvitation.handlebars',
subjectLine: 'Infisical workspace invitation', subjectLine: 'Infisical workspace invitation',
recipients: [invitee.email], recipients: [invitee.email],
substitutions: { substitutions: {
inviterFirstName: req.user.firstName, inviterFirstName: req.user.firstName,
inviterEmail: req.user.email, inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name, workspaceName: req.membership.workspace.name,
callback_url: getSiteURL() + '/login' callback_url: (await getSiteURL()) + '/login'
} }
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to invite user to workspace'
});
}
return res.status(200).send({ return res.status(200).send({
invitee, invitee,
latestKey latestKey
}); });
}; };
@@ -1,13 +1,15 @@
import { Types } from 'mongoose';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { MembershipOrg, Organization, User } from '../../models'; import { MembershipOrg, Organization, User } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { EELicenseService } from '../../ee/services';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED, TOKEN_EMAIL_ORG_INVITATION } from '../../variables';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -16,52 +18,43 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
* @returns * @returns
*/ */
export const deleteMembershipOrg = async (req: Request, res: Response) => { export const deleteMembershipOrg = async (req: Request, res: Response) => {
let membershipOrgToDelete; const { membershipOrgId } = req.params;
try {
const { membershipOrgId } = req.params;
// check if organization membership to delete exists // check if organization membership to delete exists
membershipOrgToDelete = await MembershipOrg.findOne({ const membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId _id: membershipOrgId
}).populate('user'); }).populate('user');
if (!membershipOrgToDelete) { if (!membershipOrgToDelete) {
throw new Error( throw new Error(
"Failed to delete organization membership that doesn't exist" "Failed to delete organization membership that doesn't exist"
); );
} }
// check if user is a member and admin of the organization // check if user is a member and admin of the organization
// whose membership we wish to delete // whose membership we wish to delete
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: membershipOrgToDelete.organization organization: membershipOrgToDelete.organization
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); throw new Error('Failed to validate organization membership');
} }
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
// user is not an admin member of the organization // user is not an admin member of the organization
throw new Error('Insufficient role for deleting organization membership'); throw new Error('Insufficient role for deleting organization membership');
} }
// delete organization membership // delete organization membership
const deletedMembershipOrg = await deleteMemberFromOrg({ const deletedMembershipOrg = await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString() membershipOrgId: membershipOrgToDelete._id.toString()
}); });
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membershipOrg.organization.toString() organizationId: membershipOrg.organization.toString()
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return membershipOrgToDelete; return membershipOrgToDelete;
}; };
@@ -77,14 +70,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// [membershipOrgId] // [membershipOrgId]
let membershipToChangeRole; let membershipToChangeRole;
// try {
// } catch (err) {
// Sentry.setUser({ email: req.user.email });
// Sentry.captureException(err);
// return res.status(400).send({
// message: 'Failed to change organization membership role'
// });
// }
return res.status(200).send({ return res.status(200).send({
membershipOrg: membershipToChangeRole membershipOrg: membershipToChangeRole
@@ -100,103 +85,114 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg, completeInviteLink; let invitee, inviteeMembershipOrg, completeInviteLink;
try { const { organizationId, inviteeEmail } = req.body;
const { organizationId, inviteeEmail } = req.body; const host = req.headers.host;
const host = req.headers.host; const siteUrl = `${req.protocol}://${host}`;
const siteUrl = `${req.protocol}://${host}`;
// validate membership // validate membership
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: organizationId organization: organizationId
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); throw new Error('Failed to validate organization membership');
} }
const plan = await EELicenseService.getPlan(organizationId);
if (plan.memberLimit !== null) {
// case: limit imposed on number of members allowed
if (plan.membersUsed >= plan.memberLimit) {
// case: number of members used exceeds the number of members allowed
return res.status(400).send({
message: 'Failed to invite member due to member limit reached. Upgrade plan to invite more members.'
});
}
}
invitee = await User.findOne({ invitee = await User.findOne({
email: inviteeEmail email: inviteeEmail
}).select('+publicKey'); }).select('+publicKey');
if (invitee) { if (invitee) {
// case: invitee is an existing user // case: invitee is an existing user
inviteeMembershipOrg = await MembershipOrg.findOne({ inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id, user: invitee._id,
organization: organizationId organization: organizationId
}); });
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error( throw new Error(
'Failed to invite an existing member of the organization' 'Failed to invite an existing member of the organization'
); );
} }
if (!inviteeMembershipOrg) { if (!inviteeMembershipOrg) {
await new MembershipOrg({
user: invitee, await new MembershipOrg({
inviteEmail: inviteeEmail, user: invitee,
organization: organizationId, inviteEmail: inviteeEmail,
role: MEMBER, organization: organizationId,
status: invitee?.publicKey ? ACCEPTED : INVITED role: MEMBER,
}).save(); status: INVITED
} }).save();
} else { }
// check if invitee has been invited before } else {
inviteeMembershipOrg = await MembershipOrg.findOne({ // check if invitee has been invited before
inviteEmail: inviteeEmail, inviteeMembershipOrg = await MembershipOrg.findOne({
organization: organizationId inviteEmail: inviteeEmail,
}); organization: organizationId
});
if (!inviteeMembershipOrg) { if (!inviteeMembershipOrg) {
// case: invitee has never been invited before // case: invitee has never been invited before
// validate that email is not disposable
validateUserEmail(inviteeEmail);
await new MembershipOrg({ await new MembershipOrg({
inviteEmail: inviteeEmail, inviteEmail: inviteeEmail,
organization: organizationId, organization: organizationId,
role: MEMBER, role: MEMBER,
status: INVITED status: INVITED
}).save(); }).save();
} }
} }
const organization = await Organization.findOne({ _id: organizationId }); const organization = await Organization.findOne({ _id: organizationId });
if (organization) { if (organization) {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
organizationId: organization._id
});
await sendMail({ const token = await TokenService.createToken({
template: 'organizationInvitation.handlebars', type: TOKEN_EMAIL_ORG_INVITATION,
subjectLine: 'Infisical organization invitation', email: inviteeEmail,
recipients: [inviteeEmail], organizationId: organization._id
substitutions: { });
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
token,
callback_url: getSiteURL() + '/signupinvite'
}
});
if (!getSmtpConfigured()) { await sendMail({
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}` template: 'organizationInvitation.handlebars',
} subjectLine: 'Infisical organization invitation',
} recipients: [inviteeEmail],
substitutions: {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
organizationId: organization._id.toString(),
token,
callback_url: (await getSiteURL()) + '/signupinvite'
}
});
await updateSubscriptionOrgQuantity({ organizationId }); if (!(await getSmtpConfigured())) {
} catch (err) { completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
Sentry.setUser(null); }
Sentry.captureException(err); }
return res.status(400).send({
message: 'Failed to send organization invite' await updateSubscriptionOrgQuantity({ organizationId });
});
}
return res.status(200).send({ return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`, message: `Sent an invite link to ${req.body.inviteeEmail}`,
@@ -212,61 +208,62 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const verifyUserToOrganization = async (req: Request, res: Response) => { export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user, token; let user;
try { const {
const { email, code } = req.body; email,
organizationId,
code
} = req.body;
user = await User.findOne({ email }).select('+publicKey'); user = await User.findOne({ email }).select('+publicKey');
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED,
}); organization: new Types.ObjectId(organizationId)
});
if (!membershipOrg) if (!membershipOrg)
throw new Error('Failed to find any invitations for email'); throw new Error('Failed to find any invitations for email');
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION, type: TOKEN_EMAIL_ORG_INVITATION,
email, email,
organizationId: membershipOrg.organization, organizationId: membershipOrg.organization,
token: code token: code
}); });
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
// membership can be approved and redirected to login/dashboard // membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED; membershipOrg.status = ACCEPTED;
await membershipOrg.save(); await membershipOrg.save();
await updateSubscriptionOrgQuantity({
organizationId
});
return res.status(200).send({ return res.status(200).send({
message: 'Successfully verified email', message: 'Successfully verified email',
user, user,
}); });
} }
if (!user) { if (!user) {
// initialize user account // initialize user account
user = await new User({ user = await new User({
email email
}).save(); }).save();
} }
// generate temporary signup token // generate temporary signup token
token = createToken({ const token = createToken({
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: getJwtSignupSecret() secret: await getJwtSignupSecret()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email magic link verification for organization invitation'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully verified email', message: 'Successfully verified email',
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { import {
@@ -15,20 +14,12 @@ import _ from 'lodash';
import { getStripeSecretKey, getSiteURL } from '../../config'; import { getStripeSecretKey, getSiteURL } from '../../config';
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {
let organizations; const organizations = (
try { await MembershipOrg.find({
organizations = ( user: req.user._id,
await MembershipOrg.find({ status: ACCEPTED
user: req.user._id }).populate('organization')
}).populate('organization') ).map((m) => m.organization);
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organizations'
});
}
return res.status(200).send({ return res.status(200).send({
organizations organizations
@@ -43,33 +34,24 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createOrganization = async (req: Request, res: Response) => { export const createOrganization = async (req: Request, res: Response) => {
let organization; const { organizationName } = req.body;
try {
const { organizationName } = req.body;
if (organizationName.length < 1) { if (organizationName.length < 1) {
throw new Error('Organization names must be at least 1-character long'); throw new Error('Organization names must be at least 1-character long');
} }
// create organization and add user as member // create organization and add user as member
organization = await create({ const organization = await create({
email: req.user.email, email: req.user.email,
name: organizationName name: organizationName
}); });
await addMembershipsOrg({ await addMembershipsOrg({
userIds: [req.user._id.toString()], userIds: [req.user._id.toString()],
organizationId: organization._id.toString(), organizationId: organization._id.toString(),
roles: [OWNER], roles: [OWNER],
statuses: [ACCEPTED] statuses: [ACCEPTED]
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create organization'
});
}
return res.status(200).send({ return res.status(200).send({
organization organization
@@ -83,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => {
let organization; const organization = req.organization
try {
organization = req.membershipOrg.organization;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to find organization'
});
}
return res.status(200).send({ return res.status(200).send({
organization organization
}); });
@@ -106,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganizationMembers = async (req: Request, res: Response) => { export const getOrganizationMembers = async (req: Request, res: Response) => {
let users; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
users = await MembershipOrg.find({ const users = await MembershipOrg.find({
organization: organizationId organization: organizationId
}).populate('user', '+publicKey'); }).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization members'
});
}
return res.status(200).send({ return res.status(200).send({
users users
@@ -136,35 +99,26 @@ export const getOrganizationWorkspaces = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let workspaces; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
const workspacesSet = new Set( const workspacesSet = new Set(
( (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId organization: organizationId
}, },
'_id' '_id'
) )
).map((w) => w._id.toString()) ).map((w) => w._id.toString())
); );
workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id user: req.user._id
}).populate('workspace') }).populate('workspace')
) )
.filter((m) => workspacesSet.has(m.workspace._id.toString())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get my workspaces'
});
}
return res.status(200).send({ return res.status(200).send({
workspaces workspaces
@@ -178,29 +132,20 @@ export const getOrganizationWorkspaces = async (
* @returns * @returns
*/ */
export const changeOrganizationName = async (req: Request, res: Response) => { export const changeOrganizationName = async (req: Request, res: Response) => {
let organization; const { organizationId } = req.params;
try { const { name } = req.body;
const { organizationId } = req.params;
const { name } = req.body;
organization = await Organization.findOneAndUpdate( const organization = await Organization.findOneAndUpdate(
{ {
_id: organizationId _id: organizationId
}, },
{ {
name name
}, },
{ {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change organization name'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed organization name', message: 'Successfully changed organization name',
@@ -218,20 +163,11 @@ export const getOrganizationIncidentContacts = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let incidentContactsOrg; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
incidentContactsOrg = await IncidentContactOrg.find({ const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId organization: organizationId
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization incident contacts'
});
}
return res.status(200).send({ return res.status(200).send({
incidentContactsOrg incidentContactsOrg
@@ -248,23 +184,14 @@ export const addOrganizationIncidentContact = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let incidentContactOrg; const { organizationId } = req.params;
try { const { email } = req.body;
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId }, { email, organization: organizationId },
{ email, organization: organizationId }, { email, organization: organizationId },
{ upsert: true, new: true } { upsert: true, new: true }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to add incident contact for organization'
});
}
return res.status(200).send({ return res.status(200).send({
incidentContactOrg incidentContactOrg
@@ -281,22 +208,13 @@ export const deleteOrganizationIncidentContact = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let incidentContactOrg; const { organizationId } = req.params;
try { const { email } = req.body;
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email, email,
organization: organizationId organization: organizationId
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization incident contact'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully deleted organization incident contact', message: 'Successfully deleted organization incident contact',
@@ -316,41 +234,33 @@ export const createOrganizationPortalSession = async (
res: Response res: Response
) => { ) => {
let session; let session;
try { const stripe = new Stripe(await getStripeSecretKey(), {
const stripe = new Stripe(getStripeSecretKey(), { apiVersion: '2022-08-01'
apiVersion: '2022-08-01' });
});
// check if there is a payment method on file // check if there is a payment method on file
const paymentMethods = await stripe.paymentMethods.list({ const paymentMethods = await stripe.paymentMethods.list({
customer: req.membershipOrg.organization.customerId, customer: req.organization.customerId,
type: 'card' type: 'card'
}); });
if (paymentMethods.data.length < 1) {
// case: no payment method on file
session = await stripe.checkout.sessions.create({
customer: req.organization.customerId,
mode: 'setup',
payment_method_types: ['card'],
success_url: (await getSiteURL()) + '/dashboard',
cancel_url: (await getSiteURL()) + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.organization.customerId,
return_url: (await getSiteURL()) + '/dashboard'
});
}
if (paymentMethods.data.length < 1) { return res.status(200).send({ url: session.url });
// case: no payment method on file
session = await stripe.checkout.sessions.create({
customer: req.membershipOrg.organization.customerId,
mode: 'setup',
payment_method_types: ['card'],
success_url: getSiteURL() + '/dashboard',
cancel_url: getSiteURL() + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.membershipOrg.organization.customerId,
return_url: getSiteURL() + '/dashboard'
});
}
return res.status(200).send({ url: session.url });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to redirect to organization billing portal'
});
}
}; };
/** /**
@@ -363,22 +273,13 @@ export const getOrganizationSubscriptions = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let subscriptions; const stripe = new Stripe(await getStripeSecretKey(), {
try { apiVersion: '2022-08-01'
const stripe = new Stripe(getStripeSecretKey(), { });
apiVersion: '2022-08-01'
}); const subscriptions = await stripe.subscriptions.list({
customer: req.organization.customerId
subscriptions = await stripe.subscriptions.list({ });
customer: req.membershipOrg.organization.customerId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization subscriptions'
});
}
return res.status(200).send({ return res.status(200).send({
subscriptions subscriptions
@@ -424,4 +325,4 @@ export const getOrganizationMembersAndTheirWorkspaces = async (
}); });
return res.json(userToWorkspaceIds); return res.json(userToWorkspaceIds);
}; };
+240 -273
View File
@@ -1,15 +1,25 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
import { User, BackupPrivateKey, LoginSRPDetail } from '../../models'; import { User, BackupPrivateKey, LoginSRPDetail } from '../../models';
import { createToken } from '../../helpers/auth'; import {
import { sendMail } from '../../helpers/nodemailer'; createToken,
sendMail,
clearTokens
} from '../../helpers';
import { TokenService } from '../../services'; import { TokenService } from '../../services';
import { TOKEN_EMAIL_PASSWORD_RESET } from '../../variables'; import {
TOKEN_EMAIL_PASSWORD_RESET,
AUTH_MODE_JWT
} from '../../variables';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../config'; import {
getSiteURL,
getJwtSignupLifetime,
getJwtSignupSecret,
getHttpsEnabled
} from '../../config';
/** /**
* Password reset step 1: Send email verification link to email [email] * Password reset step 1: Send email verification link to email [email]
@@ -20,43 +30,35 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
*/ */
export const emailPasswordReset = async (req: Request, res: Response) => { export const emailPasswordReset = async (req: Request, res: Response) => {
let email: string; let email: string;
try { email = req.body.email;
email = req.body.email;
const user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(200).send({
error: 'Failed to send email verification for password reset' message:"If an account exists with this email, a password reset link has been sent"
}); });
} }
const token = await TokenService.createToken({ const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET, type: TOKEN_EMAIL_PASSWORD_RESET,
email email
}); });
await sendMail({ await sendMail({
template: 'passwordReset.handlebars', template: 'passwordReset.handlebars',
subjectLine: 'Infisical password reset', subjectLine: 'Infisical password reset',
recipients: [email], recipients: [email],
substitutions: { substitutions: {
email, email,
token, token,
callback_url: getSiteURL() + '/password-reset' callback_url: (await getSiteURL()) + '/password-reset'
} }
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send email for account recovery'
});
}
return res.status(200).send({ return res.status(200).send({
message: `Sent an email for account recovery to ${email}` message:"If an account exists with this email, a password reset link has been sent"
}); });
} }
@@ -67,40 +69,31 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const emailPasswordResetVerify = async (req: Request, res: Response) => { export const emailPasswordResetVerify = async (req: Request, res: Response) => {
let user, token; const { email, code } = req.body;
try {
const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or // case: user doesn't exist with email [email] or
// hasn't even completed their account // hasn't even completed their account
return res.status(403).send({ return res.status(403).send({
error: 'Failed email verification for password reset' error: 'Failed email verification for password reset'
}); });
} }
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET, type: TOKEN_EMAIL_PASSWORD_RESET,
email, email,
token: code token: code
}); });
// generate temporary password-reset token // generate temporary password-reset token
token = createToken({ const token = createToken({
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: getJwtSignupSecret() secret: await getJwtSignupSecret()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed email verification for password reset'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully verified email', message: 'Successfully verified email',
@@ -117,44 +110,38 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
*/ */
export const srp1 = async (req: Request, res: Response) => { export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol // return salt, serverPublicKey as part of first step of SRP protocol
try {
const { clientPublicKey } = req.body; const { clientPublicKey } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, { await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
email: req.user.email, email: req.user.email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false }) }, { upsert: true, returnNewDocument: false })
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
}
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to start change password process'
});
}
};
/** /**
* Change account SRP authentication information for user * Change account SRP authentication information for user
@@ -165,80 +152,85 @@ export const srp1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changePassword = async (req: Request, res: Response) => { export const changePassword = async (req: Request, res: Response) => {
try { const {
const { clientProof,
clientProof, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier
verifier } = req.body;
} = req.body;
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) { if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
} }
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// change password // change password
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true new: true
} }
); );
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User && req.authData.tokenVersionId) {
await clearTokens(req.authData.tokenVersionId)
}
return res.status(200).send({ // clear httpOnly cookie
message: 'Successfully changed password'
}); res.cookie('jid', '', {
} httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
});
return res.status(400).send({ return res.status(200).send({
error: 'Failed to change password. Try again?' message: 'Successfully changed password'
}); });
} }
);
} catch (err) { return res.status(400).send({
Sentry.setUser({ email: req.user.email }); error: 'Failed to change password. Try again?'
Sentry.captureException(err); });
return res.status(400).send({ }
error: 'Failed to change password. Try again?' );
});
}
}; };
/** /**
@@ -252,69 +244,61 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
// requires verifying [clientProof] as part of second step of SRP protocol // requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1 // as initiated in /srp1
try { const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = req.body;
req.body; const user = await User.findOne({
const user = await User.findOne({ email: req.user.email
email: req.user.email }).select('+salt +verifier');
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) { if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
} }
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey( server.setClientPublicKey(
loginSRPDetailFromDB.clientPublicKey loginSRPDetailFromDB.clientPublicKey
); );
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// create new or replace backup private key // create new or replace backup private key
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate( const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
{ user: req.user._id }, { user: req.user._id },
{ {
user: req.user._id, user: req.user._id,
encryptedPrivateKey, encryptedPrivateKey,
iv, iv,
tag, tag,
salt, salt,
verifier verifier
}, },
{ upsert: true, new: true } { upsert: true, new: true }
).select('+user, encryptedPrivateKey'); ).select('+user, encryptedPrivateKey');
// issue tokens // issue tokens
return res.status(200).send({ return res.status(200).send({
message: 'Successfully updated backup private key', message: 'Successfully updated backup private key',
backupPrivateKey backupPrivateKey
}); });
} }
return res.status(400).send({ return res.status(400).send({
message: 'Failed to update backup private key' message: 'Failed to update backup private key'
}); });
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
}; };
/** /**
@@ -324,20 +308,11 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getBackupPrivateKey = async (req: Request, res: Response) => { export const getBackupPrivateKey = async (req: Request, res: Response) => {
let backupPrivateKey; const backupPrivateKey = await BackupPrivateKey.findOne({
try { user: req.user._id
backupPrivateKey = await BackupPrivateKey.findOne({ }).select('+encryptedPrivateKey +iv +tag');
user: req.user._id
}).select('+encryptedPrivateKey +iv +tag');
if (!backupPrivateKey) throw new Error('Failed to find backup private key'); if (!backupPrivateKey) throw new Error('Failed to find backup private key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({ return res.status(200).send({
backupPrivateKey backupPrivateKey
@@ -345,44 +320,36 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
} }
export const resetPassword = async (req: Request, res: Response) => { export const resetPassword = async (req: Request, res: Response) => {
try { const {
const { protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier,
verifier, } = req.body;
} = req.body;
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully reset password' message: 'Successfully reset password'
}); });
} }
+123 -146
View File
@@ -1,5 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import { Types } from 'mongoose';
import { Key, Secret } from '../../models'; import { Key, Secret } from '../../models';
import { import {
v1PushSecrets as push, v1PushSecrets as push,
@@ -9,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
import { EventService } from '../../services'; import { EventService } from '../../services';
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -36,65 +36,56 @@ interface PushSecret {
*/ */
export const pushSecrets = async (req: Request, res: Response) => { export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
try { // validate environment
const postHogClient = getPostHogClient(); const workspaceEnvs = req.membership.workspace.environments;
let { secrets }: { secrets: PushSecret[] } = req.body; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
const { keys, environment, channel } = req.body; throw new Error('Failed to validate environment');
const { workspaceId } = req.params; }
// validate environment // sanitize secrets
const workspaceEnvs = req.membership.workspace.environments; secrets = secrets.filter(
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { (s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
throw new Error('Failed to validate environment'); );
}
// sanitize secrets await push({
secrets = secrets.filter( userId: req.user._id,
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== '' workspaceId,
); environment,
secrets
});
await push({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, keys
secrets });
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
await pushKeys({ // trigger event - push secrets
userId: req.user._id, EventService.handleEvent({
workspaceId, event: eventPushSecrets({
keys workspaceId: new Types.ObjectId(workspaceId),
}); environment
})
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully uploaded workspace secrets' message: 'Successfully uploaded workspace secrets'
@@ -111,55 +102,48 @@ export const pushSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment const postHogClient = await TelemetryService.getPostHogClient();
const workspaceEnvs = req.membership.workspace.environments; const environment: string = req.query.environment as string;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { const channel: string = req.query.channel as string;
throw new Error('Failed to validate environment'); const { workspaceId } = req.params;
}
secrets = await pull({ // validate environment
userId: req.user._id.toString(), const workspaceEnvs = req.membership.workspace.environments;
workspaceId, if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
environment, throw new Error('Failed to validate environment');
channel: channel ? channel : 'cli', }
ipAddress: req.ip
});
key = await Key.findOne({ secrets = await pull({
workspace: workspaceId, userId: req.user._id.toString(),
receiver: req.user._id workspaceId,
}) environment,
.sort({ createdAt: -1 }) channel: channel ? channel : 'cli',
.populate('sender', '+publicKey'); ipAddress: req.realIP
});
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) { key = await Key.findOne({
// capture secrets pushed event in production workspace: workspaceId,
postHogClient.capture({ receiver: req.user._id
distinctId: req.user.email, })
event: 'secrets pulled', .sort({ createdAt: -1 })
properties: { .populate('sender', '+publicKey');
numberOfSecrets: secrets.length,
environment, if (channel !== 'cli') {
workspaceId, secrets = reformatPullSecrets({ secrets });
channel: channel ? channel : 'cli' }
}
}); if (postHogClient) {
} // capture secrets pushed event in production
} catch (err) { postHogClient.capture({
Sentry.setUser({ email: req.user.email }); distinctId: req.user.email,
Sentry.captureException(err); event: 'secrets pulled',
return res.status(400).send({ properties: {
message: 'Failed to pull workspace secrets' numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
}); });
} }
@@ -180,54 +164,47 @@ export const pullSecrets = async (req: Request, res: Response) => {
export const pullSecretsServiceToken = async (req: Request, res: Response) => { export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets; let secrets;
let key; let key;
try {
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment const postHogClient = await TelemetryService.getPostHogClient();
const workspaceEnvs = req.membership.workspace.environments; const environment: string = req.query.environment as string;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { const channel: string = req.query.channel as string;
throw new Error('Failed to validate environment'); const { workspaceId } = req.params;
}
secrets = await pull({ // validate environment
userId: req.serviceToken.user._id.toString(), const workspaceEnvs = req.membership.workspace.environments;
workspaceId, if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
environment, throw new Error('Failed to validate environment');
channel: 'cli', }
ipAddress: req.ip
});
key = { secrets = await pull({
encryptedKey: req.serviceToken.encryptedKey, userId: req.serviceToken.user._id.toString(),
nonce: req.serviceToken.nonce, workspaceId,
sender: { environment,
publicKey: req.serviceToken.publicKey channel: 'cli',
}, ipAddress: req.realIP
receiver: req.serviceToken.user, });
workspace: req.serviceToken.workspace
};
if (postHogClient) { key = {
// capture secrets pulled event in production encryptedKey: req.serviceToken.encryptedKey,
postHogClient.capture({ nonce: req.serviceToken.nonce,
distinctId: req.serviceToken.user.email, sender: {
event: 'secrets pulled', publicKey: req.serviceToken.publicKey
properties: { },
numberOfSecrets: secrets.length, receiver: req.serviceToken.user,
environment, workspace: req.serviceToken.workspace
workspaceId, };
channel: channel ? channel : 'cli'
} if (postHogClient) {
}); // capture secrets pulled event in production
} postHogClient.capture({
} catch (err) { distinctId: req.serviceToken.user.email,
Sentry.setUser({ email: req.serviceToken.user.email }); event: 'secrets pulled',
Sentry.captureException(err); properties: {
return res.status(400).send({ numberOfSecrets: secrets.length,
message: 'Failed to pull workspace secrets' environment,
workspaceId,
channel: channel ? channel : 'cli'
}
}); });
} }
@@ -235,4 +212,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
secrets: reformatPullSecrets({ secrets }), secrets: reformatPullSecrets({ secrets }),
key key
}); });
}; };
@@ -0,0 +1,218 @@
import { Request, Response } from "express";
import { Secret } from "../../models";
import Folder from "../../models/folder";
import { BadRequestError } from "../../utils/errors";
import {
appendFolder,
deleteFolderById,
getAllFolderIds,
searchByFolderIdWithDir,
searchByFolderId,
validateFolderName,
generateFolderId,
getParentFromFolderId,
} from "../../services/FolderService";
import { ADMIN, MEMBER } from "../../variables";
import { validateMembership } from "../../helpers/membership";
import { FolderVersion } from "../../ee/models";
import { EESecretService } from "../../ee/services";
// TODO
// verify workspace id/environment
export const createFolder = async (req: Request, res: Response) => {
const { workspaceId, environment, folderName, parentFolderId } = req.body;
if (!validateFolderName(folderName)) {
throw BadRequestError({
message: "Folder name cannot contain spaces. Only underscore and dashes",
});
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
}).lean();
// space has no folders initialized
if (!folders) {
const id = generateFolderId();
const folder = new Folder({
workspace: workspaceId,
environment,
nodes: {
id: "root",
name: "root",
version: 1,
children: [{ id, name: folderName, children: [], version: 1 }],
},
});
await folder.save();
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: folder.nodes,
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
});
return res.json({ folder: { id, name: folderName } });
}
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
await Folder.findByIdAndUpdate(folders._id, folders);
const parentFolder = searchByFolderId(folders.nodes, parentFolderId);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolderId,
});
return res.json({ folder });
};
export const updateFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params;
const { name, workspaceId, environment } = req.body;
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
parentFolder.version += 1;
folder.name = name;
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
});
await folderVersion.save();
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolder.id,
});
return res.json({
message: "Successfully updated folder",
folder: { name: folder.name, id: folder.id },
});
};
export const deleteFolder = async (req: Request, res: Response) => {
const { folderId } = req.params;
const { workspaceId, environment } = req.body;
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { deletedNode: delFolder, parent: parentFolder } = delOp;
parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder);
await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: parentFolder,
});
await folderVersion.save();
if (delFolderIds.length) {
await Secret.deleteMany({
folder: { $in: delFolderIds.map(({ id }) => id) },
workspace: workspaceId,
environment,
});
}
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId: parentFolder.id,
});
res.send({ message: "successfully deleted folders", folders: delFolderIds });
};
// TODO: validate workspace
export const getFolders = async (req: Request, res: Response) => {
const { workspaceId, environment, parentFolderId } = req.query as {
workspaceId: string;
environment: string;
parentFolderId?: string;
};
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
res.send({ folders: [], dir: [] });
return;
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId,
acceptedRoles: [ADMIN, MEMBER],
});
if (!parentFolderId) {
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,
name,
}));
res.send({ folders: rootFolders });
return;
}
const folderBySearch = searchByFolderIdWithDir(folders.nodes, parentFolderId);
if (!folderBySearch) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { folder, dir } = folderBySearch;
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir,
});
};
@@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => {
workspaceId workspaceId
}, },
expiresIn: expiresIn, expiresIn: expiresIn,
secret: getJwtServiceSecret() secret: await getJwtServiceSecret()
}); });
} catch (err) { } catch (err) {
return res.status(400).send({ return res.status(400).send({
+51 -64
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../../models'; import { User } from '../../models';
import { import {
sendEmailVerification, sendEmailVerification,
@@ -8,6 +7,7 @@ import {
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config'; import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured } from '../../config';
import { validateUserEmail } from '../../validation';
/** /**
* Signup step 1: Initialize account for user under email [email] and send a verification code * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -18,35 +18,22 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
*/ */
export const beginEmailSignup = async (req: Request, res: Response) => { export const beginEmailSignup = async (req: Request, res: Response) => {
let email: string; let email: string;
try { email = req.body.email;
email = req.body.email;
// validate that email is not disposable
validateUserEmail(email);
if (getInviteOnlySignup()) { const user = await User.findOne({ email }).select('+publicKey');
// Only one user can create an account without being invited. The rest need to be invited in order to make an account if (user && user?.publicKey) {
const userCount = await User.countDocuments({}) // case: user has already completed account
if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
}
}
const user = await User.findOne({ email }).select('+publicKey'); return res.status(403).send({
if (user && user?.publicKey) { error: 'Failed to send email verification code for complete account'
// case: user has already completed account });
}
return res.status(403).send({ // send send verification email
error: 'Failed to send email verification code for complete account' await sendEmailVerification({ email });
});
}
// send send verification email
await sendEmailVerification({ email });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to send email verification code'
});
}
return res.status(200).send({ return res.status(200).send({
message: `Sent an email verification code to ${email}` message: `Sent an email verification code to ${email}`
@@ -62,47 +49,47 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
*/ */
export const verifyEmailSignup = async (req: Request, res: Response) => { export const verifyEmailSignup = async (req: Request, res: Response) => {
let user, token; let user, token;
try { const { email, code } = req.body;
const { email, code } = req.body;
// initialize user account // initialize user account
user = await User.findOne({ email }).select('+publicKey'); user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed email verification for complete user' error: 'Failed email verification for complete user'
}); });
} }
// verify email if (await getInviteOnlySignup()) {
if (getSmtpConfigured()) { // Only one user can create an account without being invited. The rest need to be invited in order to make an account
await checkEmailVerification({ const userCount = await User.countDocuments({})
email, if (userCount != 0) {
code throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
}); }
} }
if (!user) { // verify email
user = await new User({ if (await getSmtpConfigured()) {
email await checkEmailVerification({
}).save(); email,
} code
});
}
// generate temporary signup token if (!user) {
token = createToken({ user = await new User({
payload: { email
userId: user._id.toString() }).save();
}, }
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret() // generate temporary signup token
}); token = createToken({
} catch (err) { payload: {
Sentry.setUser(null); userId: user._id.toString()
Sentry.captureException(err); },
return res.status(400).send({ expiresIn: await getJwtSignupLifetime(),
error: 'Failed email verification' secret: await getJwtSignupSecret()
}); });
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfuly verified email', message: 'Successfuly verified email',
+10 -20
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
* @returns * @returns
*/ */
export const handleWebhook = async (req: Request, res: Response) => { export const handleWebhook = async (req: Request, res: Response) => {
let event; // check request for valid stripe signature
try { const stripe = new Stripe(await getStripeSecretKey(), {
// check request for valid stripe signature apiVersion: '2022-08-01'
const stripe = new Stripe(getStripeSecretKey(), { });
apiVersion: '2022-08-01'
});
const sig = req.headers['stripe-signature'] as string; const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent( const event = stripe.webhooks.constructEvent(
req.body, req.body,
sig, sig,
getStripeWebhookSecret() await getStripeWebhookSecret()
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) { switch (event.type) {
case '': case '':
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { UserAction } from '../../models'; import { UserAction } from '../../models';
/** /**
@@ -11,28 +10,19 @@ import { UserAction } from '../../models';
export const addUserAction = async (req: Request, res: Response) => { export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id] // add/record new action [action] for user with id [req.user._id]
let userAction; const { action } = req.body;
try {
const { action } = req.body;
userAction = await UserAction.findOneAndUpdate( const userAction = await UserAction.findOneAndUpdate(
{ {
user: req.user._id, user: req.user._id,
action action
}, },
{ user: req.user._id, action }, { user: req.user._id, action },
{ {
new: true, new: true,
upsert: true upsert: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to record user action'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully recorded user action', message: 'Successfully recorded user action',
@@ -48,21 +38,12 @@ export const addUserAction = async (req: Request, res: Response) => {
*/ */
export const getUserAction = async (req: Request, res: Response) => { export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id] // get user action [action] for user with id [req.user._id]
let userAction; const action: string = req.query.action as string;
try {
const action: string = req.query.action as string;
userAction = await UserAction.findOne({ const userAction = await UserAction.findOne({
user: req.user._id, user: req.user._id,
action action
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get user action'
});
}
return res.status(200).send({ return res.status(200).send({
userAction userAction
+93 -170
View File
@@ -1,5 +1,4 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { import {
Workspace, Workspace,
Membership, Membership,
@@ -14,6 +13,7 @@ import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork, deleteWorkspace as deleteWork,
} from "../../helpers/workspace"; } from "../../helpers/workspace";
import { EELicenseService } from '../../ee/services';
import { addMemberships } from "../../helpers/membership"; import { addMemberships } from "../../helpers/membership";
import { ADMIN } from "../../variables"; import { ADMIN } from "../../variables";
@@ -24,27 +24,18 @@ import { ADMIN } from "../../variables";
* @returns * @returns
*/ */
export const getWorkspacePublicKeys = async (req: Request, res: Response) => { export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
publicKeys = ( const publicKeys = (
await Membership.find({ await Membership.find({
workspace: workspaceId, workspace: workspaceId,
}).populate<{ user: IUser }>("user", "publicKey") }).populate<{ user: IUser }>("user", "publicKey")
).map((member) => { ).map((member) => {
return { return {
publicKey: member.user.publicKey, publicKey: member.user.publicKey,
userId: member.user._id, userId: member.user._id,
}; };
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace member public keys",
});
}
return res.status(200).send({ return res.status(200).send({
publicKeys, publicKeys,
@@ -58,20 +49,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
users = await Membership.find({ const users = await Membership.find({
workspace: workspaceId, workspace: workspaceId,
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace members",
});
}
return res.status(200).send({ return res.status(200).send({
users, users,
@@ -85,20 +67,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaces = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces; const workspaces = (
try { await Membership.find({
workspaces = ( user: req.user._id,
await Membership.find({ }).populate("workspace")
user: req.user._id, ).map((m) => m.workspace);
}).populate("workspace")
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspaces",
});
}
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces,
@@ -112,20 +85,11 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspace = async (req: Request, res: Response) => { export const getWorkspace = async (req: Request, res: Response) => {
let workspace; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
workspace = await Workspace.findOne({ const workspace = await Workspace.findOne({
_id: workspaceId, _id: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace",
});
}
return res.status(200).send({ return res.status(200).send({
workspace, workspace,
@@ -140,43 +104,46 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
let workspace; const { workspaceName, organizationId } = req.body;
try {
const { workspaceName, organizationId } = req.body;
// validate organization membership // validate organization membership
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: organizationId, organization: organizationId,
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error("Failed to validate organization membership"); throw new Error("Failed to validate organization membership");
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to create workspace",
});
} }
const plan = await EELicenseService.getPlan(organizationId);
if (plan.workspaceLimit !== null) {
// case: limit imposed on number of workspaces allowed
if (plan.workspacesUsed >= plan.workspaceLimit) {
// case: number of workspaces used exceeds the number of workspaces allowed
return res.status(400).send({
message: 'Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces.'
});
}
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
return res.status(200).send({ return res.status(200).send({
workspace, workspace,
}); });
@@ -189,20 +156,12 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteWorkspace = async (req: Request, res: Response) => { export const deleteWorkspace = async (req: Request, res: Response) => {
try { const { workspaceId } = req.params;
const { workspaceId } = req.params;
// delete workspace // delete workspace
await deleteWork({ await deleteWork({
id: workspaceId, id: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete workspace",
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted workspace", message: "Successfully deleted workspace",
@@ -216,29 +175,20 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeWorkspaceName = async (req: Request, res: Response) => { export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace; const { workspaceId } = req.params;
try { const { name } = req.body;
const { workspaceId } = req.params;
const { name } = req.body;
workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId, _id: workspaceId,
}, },
{ {
name, name,
}, },
{ {
new: true, new: true,
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to change workspace name",
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed workspace name", message: "Successfully changed workspace name",
@@ -253,20 +203,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrations = async (req: Request, res: Response) => { export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
integrations = await Integration.find({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integrations",
});
}
return res.status(200).send({ return res.status(200).send({
integrations, integrations,
@@ -283,20 +224,11 @@ export const getWorkspaceIntegrationAuthorizations = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let authorizations; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({ const authorizations = await IntegrationAuth.find({
workspace: workspaceId, workspace: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integration authorizations",
});
}
return res.status(200).send({ return res.status(200).send({
authorizations, authorizations,
@@ -313,21 +245,12 @@ export const getWorkspaceServiceTokens = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let serviceTokens; const { workspaceId } = req.params;
try { // ?? FIX.
const { workspaceId } = req.params; const serviceTokens = await ServiceToken.find({
// ?? FIX. user: req.user._id,
serviceTokens = await ServiceToken.find({ workspace: workspaceId,
user: req.user._id, });
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace service tokens",
});
}
return res.status(200).send({ return res.status(200).send({
serviceTokens, serviceTokens,
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
@@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config';
* @returns * @returns
*/ */
export const getAPIKeyData = async (req: Request, res: Response) => { export const getAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData; const apiKeyData = await APIKeyData.find({
try { user: req.user._id
apiKeyData = await APIKeyData.find({ });
user: req.user._id
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get API key data'
});
}
return res.status(200).send({ return res.status(200).send({
apiKeyData apiKeyData
@@ -38,38 +28,30 @@ export const getAPIKeyData = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createAPIKeyData = async (req: Request, res: Response) => { export const createAPIKeyData = async (req: Request, res: Response) => {
let apiKey, apiKeyData; const { name, expiresIn } = req.body;
try {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({
name,
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id);
if (!apiKeyData) throw new Error('Failed to find API key data');
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to API key data'
});
}
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
let apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash
}).save();
// return api key data without sensitive data
// FIX: fix this any
apiKeyData = await APIKeyData.findById(apiKeyData._id) as any
if (!apiKeyData) throw new Error('Failed to find API key data');
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
return res.status(200).send({ return res.status(200).send({
apiKey, apiKey,
apiKeyData apiKeyData
@@ -83,21 +65,10 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteAPIKeyData = async (req: Request, res: Response) => { export const deleteAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData; const { apiKeyDataId } = req.params;
try { const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
const { apiKeyDataId } = req.params;
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete API key data'
});
}
return res.status(200).send({ return res.status(200).send({
apiKeyData apiKeyData
}); });
} }
+175 -191
View File
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models'; import { User, LoginSRPDetail } from '../../models';
@@ -17,9 +16,9 @@ import {
} from '../../variables'; } from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import { import {
getNodeEnv,
getJwtMfaLifetime, getJwtMfaLifetime,
getJwtMfaSecret getJwtMfaSecret,
getHttpsEnabled
} from '../../config'; } from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
@@ -35,47 +34,40 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
try { const {
const { email,
email, clientPublicKey
clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false }); }, { upsert: true, returnNewDocument: false });
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@@ -86,149 +78,143 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
try { if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
const { email, clientProof } = req.body; if (!user) throw new Error('Failed to find user');
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user'); const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
if (!loginSRPDetail) { const server = new jsrp.server();
return BadRequestError(Error("Failed to find login details for SRP")) server.init(
} {
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
const server = new jsrp.server(); // compare server and client shared keys
server.init( if (server.checkClientProof(clientProof)) {
{ if (user.isMfaEnabled) {
salt: user.salt, // case: user has MFA enabled
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys // generate temporary MFA token
if (server.checkClientProof(clientProof)) { const token = createToken({
payload: {
if (user.isMfaEnabled) { userId: user._id.toString()
// case: user has MFA enabled },
expiresIn: await getJwtMfaLifetime(),
// generate temporary MFA token secret: await getJwtMfaSecret()
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: getJwtMfaLifetime(),
secret: getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens const code = await TokenService.createToken({
const tokens = await issueAuthTokens({ userId: user._id.toString() }); type: TOKEN_EMAIL_MFA,
email
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
}); });
// case: user does not have MFA enablgged // send MFA code [code] to [email]
// return (access) token in response await sendMail({
template: 'emailMfa.handlebars',
interface ResponseData { subjectLine: 'Infisical MFA code',
mfaEnabled: boolean; recipients: [email],
encryptionVersion: any; substitutions: {
protectedKey?: string; code
protectedKeyIV?: string; }
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
}); });
loginAction && await EELogService.createLog({ return res.status(200).send({
userId: user._id, mfaEnabled: true,
actions: [loginAction], token
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
}); });
return res.status(200).send(response);
} }
return res.status(400).send({ await checkUserDevice({
message: 'Failed to authenticate. Try again?' user,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// case: user does not have MFA enabled
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(response);
} }
);
} catch (err) { return res.status(400).send({
Sentry.setUser(null); message: 'Failed to authenticate. Try again?'
Sentry.captureException(err); });
return res.status(400).send({ }
message: 'Failed to authenticate. Try again?' );
});
}
}; };
/** /**
@@ -237,30 +223,22 @@ export const login2 = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const sendMfaToken = async (req: Request, res: Response) => { export const sendMfaToken = async (req: Request, res: Response) => {
try { const { email } = req.body;
const { email } = req.body;
const code = await TokenService.createToken({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
await sendMail({ await sendMail({
template: 'emailMfa.handlebars', template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code', subjectLine: 'Infisical MFA code',
recipients: [email], recipients: [email],
substitutions: { substitutions: {
code code
} }
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully sent new MFA code' message: 'Successfully sent new MFA code'
@@ -284,25 +262,31 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag'); }).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
await LoginSRPDetail.deleteOne({ userId: user.id })
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.realIP,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false secure: await getHttpsEnabled()
}); });
interface VerifyMfaTokenRes { interface VerifyMfaTokenRes {
@@ -353,7 +337,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
userId: user._id, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.realIP
}); });
return res.status(200).send(resObj); return res.status(200).send(resObj);
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { import {
Secret, Secret,
ServiceToken, ServiceToken,
@@ -9,9 +8,10 @@ import {
Membership, Membership,
} from '../../models'; } from '../../models';
import { SecretVersion } from '../../ee/models'; import { SecretVersion } from '../../ee/models';
import { EELicenseService } from '../../ee/services';
import { BadRequestError } from '../../utils/errors'; import { BadRequestError } from '../../utils/errors';
import _ from 'lodash'; import _ from 'lodash';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization'; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
/** /**
* Create new workspace environment named [environmentName] under workspace with id * Create new workspace environment named [environmentName] under workspace with id
@@ -25,30 +25,24 @@ export const createWorkspaceEnvironment = async (
) => { ) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body; const { environmentName, environmentSlug } = req.body;
try { const workspace = await Workspace.findById(workspaceId).exec();
const workspace = await Workspace.findById(workspaceId).exec(); if (
if ( !workspace ||
!workspace || workspace?.environments.find(
workspace?.environments.find( ({ name, slug }) => slug === environmentSlug || environmentName === name
({ name, slug }) => slug === environmentSlug || environmentName === name )
) ) {
) { throw new Error('Failed to create workspace environment');
throw new Error('Failed to create workspace environment');
}
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create new workspace environment',
});
} }
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({ return res.status(200).send({
message: 'Successfully created new environment', message: 'Successfully created new environment',
workspace: workspaceId, workspace: workspaceId,
@@ -72,75 +66,67 @@ export const renameWorkspaceEnvironment = async (
) => { ) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try { // user should pass both new slug and env name
// user should pass both new slug and env name if (!environmentSlug || !environmentName) {
if (!environmentSlug || !environmentName) { throw new Error('Invalid environment given.');
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace environment',
});
} }
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
return res.status(200).send({ return res.status(200).send({
message: 'Successfully update environment', message: 'Successfully update environment',
workspace: workspaceId, workspace: workspaceId,
@@ -163,57 +149,50 @@ export const deleteWorkspaceEnvironment = async (
) => { ) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentSlug } = req.body; const { environmentSlug } = req.body;
try { // atomic update the env to avoid conflict
// atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec();
const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) {
if (!workspace) { throw new Error('Failed to create workspace environment');
throw new Error('Failed to create workspace environment');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace environment',
});
} }
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
);
await EELicenseService.refreshPlan(workspace.organization.toString(), workspaceId);
return res.status(200).send({ return res.status(200).send({
message: 'Successfully deleted environment', message: 'Successfully deleted environment',
workspace: workspaceId, workspace: workspaceId,
@@ -244,8 +223,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
throw BadRequestError() throw BadRequestError()
} }
relatedWorkspace.environments.forEach(environment => { relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ }) const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE }) const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
if (isReadBlocked && isWriteBlocked) { if (isReadBlocked && isWriteBlocked) {
return return
} else { } else {
+2
View File
@@ -7,6 +7,7 @@ import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController'; import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretsController from './secretsController'; import * as secretsController from './secretsController';
import * as serviceAccountsController from './serviceAccountsController';
import * as environmentController from './environmentController'; import * as environmentController from './environmentController';
import * as tagController from './tagController'; import * as tagController from './tagController';
@@ -20,6 +21,7 @@ export {
apiKeyDataController, apiKeyDataController,
secretController, secretController,
secretsController, secretsController,
serviceAccountsController,
environmentController, environmentController,
tagController tagController
} }
@@ -1,9 +1,10 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import { Types } from 'mongoose';
import { import {
MembershipOrg, MembershipOrg,
Membership, Membership,
Workspace Workspace,
ServiceAccount
} from '../../models'; } from '../../models';
import { deleteMembershipOrg } from '../../helpers/membershipOrg'; import { deleteMembershipOrg } from '../../helpers/membershipOrg';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
@@ -47,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
} }
} }
*/ */
let memberships; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
memberships = await MembershipOrg.find({ const memberships = await MembershipOrg.find({
organization: organizationId organization: organizationId
}).populate('user', '+publicKey'); }).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization memberships'
});
}
return res.status(200).send({ return res.status(200).send({
memberships memberships
@@ -126,26 +118,17 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
let membership; const { membershipId } = req.params;
try { const { role } = req.body;
const { membershipId } = req.params;
const { role } = req.body; const membership = await MembershipOrg.findByIdAndUpdate(
membershipId,
membership = await MembershipOrg.findByIdAndUpdate( {
membershipId, role
{ }, {
role new: true
}, { }
new: true );
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update organization membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
@@ -195,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
let membership; const { membershipId } = req.params;
try {
const { membershipId } = req.params; // delete organization membership
const membership = await deleteMembershipOrg({
// delete organization membership membershipOrgId: membershipId
membership = await deleteMembershipOrg({ });
membershipOrgId: membershipId
});
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString() organizationId: membership.organization.toString()
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
@@ -260,37 +234,45 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
} }
} }
*/ */
let workspaces; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
const workspacesSet = new Set( const workspacesSet = new Set(
( (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId organization: organizationId
}, },
'_id' '_id'
) )
).map((w) => w._id.toString()) ).map((w) => w._id.toString())
); );
workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id user: req.user._id
}).populate('workspace') }).populate('workspace')
) )
.filter((m) => workspacesSet.has(m.workspace._id.toString())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email }); return res.status(200).send({
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization workspaces'
});
}
return res.status(200).send({
workspaces workspaces
}); });
} }
/**
* Return service accounts for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId)
});
return res.status(200).send({
serviceAccounts
});
}
+23 -14
View File
@@ -6,8 +6,10 @@ import { CreateSecretRequestBody, ModifySecretRequestBody, SanitizedSecretForCre
const { ValidationError } = mongoose.Error; const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors'; import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb'; import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables"; import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { getPostHogClient } from '../../services'; import { TelemetryService } from '../../services';
import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors';
/** /**
* Create secret for workspace with id [workspaceId] and environment [environment] * Create secret for workspace with id [workspaceId] and environment [environment]
@@ -15,7 +17,7 @@ import { getPostHogClient } from '../../services';
* @param res * @param res
*/ */
export const createSecret = async (req: Request, res: Response) => { export const createSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret; const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = { const sanitizedSecret: SanitizedSecretForCreate = {
@@ -34,7 +36,9 @@ export const createSecret = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type: secretToCreate.type, type: secretToCreate.type,
user: new Types.ObjectId(req.user._id) user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
} }
@@ -68,7 +72,7 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createSecrets = async (req: Request, res: Response) => { export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets; const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [] const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@@ -90,7 +94,9 @@ export const createSecrets = async (req: Request, res: Response) => {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type: rawSecret.type, type: rawSecret.type,
user: new Types.ObjectId(req.user._id) user: new Types.ObjectId(req.user._id),
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
} }
sanitizedSecretesToCreate.push(safeUpdateFields) sanitizedSecretesToCreate.push(safeUpdateFields)
@@ -130,7 +136,7 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecrets = async (req: Request, res: Response) => { export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds const secretIdsToDelete: string[] = req.body.secretIds
@@ -184,7 +190,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const deleteSecret = async (req: Request, res: Response) => { export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id) await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) { if (postHogClient) {
@@ -213,7 +219,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecrets = async (req: Request, res: Response) => { export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets; const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then()) const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@@ -281,7 +287,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateSecret = async (req: Request, res: Response) => { export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { workspaceId, environmentName } = req.params const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret; const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@@ -335,20 +341,23 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getSecrets = async (req: Request, res: Response) => { export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const { environment } = req.query; const { environment } = req.query;
const { workspaceId } = req.params; const { workspaceId } = req.params;
let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user let userId: Types.ObjectId | undefined = undefined // used for getting personal secrets for user
let userEmail: Types.ObjectId | undefined = undefined // used for posthog let userEmail: string | undefined = undefined // used for posthog
if (req.user) { if (req.user) {
userId = req.user._id; userId = req.user._id;
userEmail = req.user.email; userEmail = req.user.email;
} }
if (req.serviceTokenData) { if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id userId = req.serviceTokenData.user;
userEmail = req.serviceTokenData.user.email;
const user = await User.findById(req.serviceTokenData.user, 'email');
if (!user) throw AccountNotFoundError();
userEmail = user.email;
} }
const [err, secrets] = await to(Secret.find( const [err, secrets] = await to(Secret.find(
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,306 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
ServiceAccount,
ServiceAccountKey,
ServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission
} from '../../models';
import {
CreateServiceAccountDto
} from '../../interfaces/serviceAccounts/dto';
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
import { getSaltRounds } from '../../config';
/**
* Return service account tied to the request (service account) client
* @param req
* @param res
*/
export const getCurrentServiceAccount = async (req: Request, res: Response) => {
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
return res.status(200).send({
serviceAccount
});
}
/**
* Return service account with id [serviceAccountId]
* @param req
* @param res
*/
export const getServiceAccountById = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
return res.status(200).send({
serviceAccount
});
}
/**
* Create a new service account under organization with id [organizationId]
* that has access to workspaces [workspaces]
* @param req
* @param res
* @returns
*/
export const createServiceAccount = async (req: Request, res: Response) => {
const {
name,
organizationId,
publicKey,
expiresIn,
}: CreateServiceAccountDto = req.body;
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const secret = crypto.randomBytes(16).toString('base64');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
// create service account
const serviceAccount = await new ServiceAccount({
name,
organization: new Types.ObjectId(organizationId),
user: req.user,
publicKey,
lastUsed: new Date(),
expiresAt,
secretHash
}).save();
const serviceAccountObj = serviceAccount.toObject();
delete serviceAccountObj.secretHash;
// provision default org-level permission for service account
await new ServiceAccountOrganizationPermission({
serviceAccount: serviceAccount._id
}).save();
const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64');
return res.status(200).send({
serviceAccountAccessKey: `sa.${secretId}.${secret}`,
serviceAccount: serviceAccountObj
});
}
/**
* Change name of service account with id [serviceAccountId] to [name]
* @param req
* @param res
* @returns
*/
export const changeServiceAccountName = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const { name } = req.body;
const serviceAccount = await ServiceAccount.findOneAndUpdate(
{
_id: new Types.ObjectId(serviceAccountId)
},
{
name
},
{
new: true
}
);
return res.status(200).send({
serviceAccount
});
}
/**
* Add a service account key to service account with id [serviceAccountId]
* for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const addServiceAccountKey = async (req: Request, res: Response) => {
const {
workspaceId,
encryptedKey,
nonce
} = req.body;
const serviceAccountKey = await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: req.serviceAccount._d,
workspace: new Types.ObjectId(workspaceId)
}).save();
return serviceAccountKey;
}
/**
* Return workspace-level permission for service account with id [serviceAccountId]
* @param req
* @param res
*/
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: req.serviceAccount._id
}).populate('workspace');
return res.status(200).send({
serviceAccountWorkspacePermissions
});
}
/**
* Add a workspace permission to service account with id [serviceAccountId]
* @param req
* @param res
*/
export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const {
environment,
workspaceId,
read = false,
write = false,
encryptedKey,
nonce
} = req.body;
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
return res.status(400).send({
message: 'Failed to validate workspace environment'
});
}
const existingPermission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment
});
if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' });
const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment,
read,
write
}).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
});
if (!existingServiceAccountKey) {
await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
}).save();
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete workspace permission from service account with id [serviceAccountId]
* @param req
* @param res
*/
export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountWorkspacePermissionId } = req.params;
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findByIdAndDelete(serviceAccountWorkspacePermissionId);
if (serviceAccountWorkspacePermission) {
const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
const count = await ServiceAccountWorkspacePermission.countDocuments({
serviceAccount,
workspace
});
if (count === 0) {
await ServiceAccountKey.findOneAndDelete({
serviceAccount,
workspace
});
}
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const deleteServiceAccount = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId);
if (serviceAccount) {
await ServiceAccountKey.deleteMany({
serviceAccount: serviceAccount._id
});
await ServiceAccountOrganizationPermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
await ServiceAccountWorkspacePermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
}
return res.status(200).send({
serviceAccount
});
}
/**
* Return service account keys for service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const getServiceAccountKeys = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const serviceAccountKeys = await ServiceAccountKey.find({
serviceAccount: req.serviceAccount._id,
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {})
});
return res.status(200).send({
serviceAccountKeys
});
}
@@ -1,22 +1,27 @@
import * as Sentry from '@sentry/node'; import { Request, Response } from "express";
import { Request, Response } from 'express'; import crypto from "crypto";
import crypto from 'crypto'; import bcrypt from "bcrypt";
import bcrypt from 'bcrypt'; import { User, ServiceAccount, ServiceTokenData } from "../../models";
import { userHasWorkspaceAccess } from "../../ee/helpers/checkMembershipPermissions";
import { import {
ServiceTokenData PERMISSION_READ_SECRETS,
} from '../../models'; AUTH_MODE_JWT,
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions'; AUTH_MODE_SERVICE_ACCOUNT,
import { ABILITY_READ } from '../../variables/organization'; AUTH_MODE_SERVICE_TOKEN,
import { getSaltRounds } from '../../config'; } from "../../variables";
import { getSaltRounds } from "../../config";
import { BadRequestError } from "../../utils/errors";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
/** /**
* Return service token data associated with service token on request * Return service token data associated with service token on request
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getServiceTokenData = async (req: Request, res: Response) => { export const getServiceTokenData = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return Infisical Token data' #swagger.summary = 'Return Infisical Token data'
#swagger.description = 'Return Infisical Token data' #swagger.description = 'Return Infisical Token data'
@@ -29,115 +34,135 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
"application/json": { "application/json": {
"schema": { "schema": {
"type": "object", "type": "object",
"properties": { "properties": {
"serviceTokenData": { "serviceTokenData": {
"type": "object", "type": "object",
$ref: "#/components/schemas/ServiceTokenData", $ref: "#/components/schemas/ServiceTokenData",
"description": "Details of service token" "description": "Details of service token"
} }
} }
} }
} }
} }
} }
*/ */
return res.status(200).json(req.serviceTokenData); if (!(req.authData.authPayload instanceof ServiceTokenData))
} throw BadRequestError({
message: "Failed accepted client validation for service token data",
});
const serviceTokenData = await ServiceTokenData.findById(
req.authData.authPayload._id
)
.select("+encryptedKey +iv +tag")
.populate("user");
return res.status(200).json(serviceTokenData);
};
/** /**
* Create new service token data for workspace with id [workspaceId] and * Create new service token data for workspace with id [workspaceId] and
* environment [environment]. * environment [environment].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const createServiceTokenData = async (req: Request, res: Response) => { export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceToken, serviceTokenData; let serviceTokenData;
try { const {
const { name,
name, workspaceId,
workspaceId, environment,
environment, encryptedKey,
encryptedKey, iv,
iv, tag,
tag, expiresIn,
expiresIn, secretPath,
permissions permissions,
} = req.body; } = req.body;
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ) const folders = await Folder.findOne({
if (!hasAccess) { workspace: workspaceId,
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) environment,
} });
const secret = crypto.randomBytes(16).toString('hex'); if (folders) {
const secretHash = await bcrypt.hash(secret, getSaltRounds()); const folder = getFolderByPath(folders.nodes, secretPath);
if (folder == undefined) {
const expiresAt = new Date(); throw BadRequestError({ message: "Path for service token does not exist" })
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user: req.user._id,
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create service token data'
});
} }
}
return res.status(200).send({ const secret = crypto.randomBytes(16).toString("hex");
serviceToken, const secretHash = await bcrypt.hash(secret, await getSaltRounds());
serviceTokenData
}); let expiresAt;
} if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
let user, serviceAccount;
if (
req.authData.authMode === AUTH_MODE_JWT &&
req.authData.authPayload instanceof User
) {
user = req.authData.authPayload._id;
}
if (
req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
req.authData.authPayload instanceof ServiceAccount
) {
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
secretPath,
permissions,
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error("Failed to find service token data");
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
return res.status(200).send({
serviceToken,
serviceTokenData,
});
};
/** /**
* Delete service token data with id [serviceTokenDataId]. * Delete service token data with id [serviceTokenDataId].
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteServiceTokenData = async (req: Request, res: Response) => { export const deleteServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData; const { serviceTokenDataId } = req.params;
try {
const { serviceTokenDataId } = req.params;
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId); const serviceTokenData = await ServiceTokenData.findByIdAndDelete(
serviceTokenDataId
);
} catch (err) { return res.status(200).send({
Sentry.setUser({ email: req.user.email }); serviceTokenData,
Sentry.captureException(err); });
return res.status(400).send({ };
message: 'Failed to delete service token data'
});
}
return res.status(200).send({
serviceTokenData
});
}
function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
}
+203 -191
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models'; import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user'; import { completeAccount } from '../../helpers/user';
import { import {
@@ -7,8 +6,9 @@ import {
} from '../../helpers/signup'; } from '../../helpers/signup';
import { issueAuthTokens } from '../../helpers/auth'; import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables'; import { INVITED, ACCEPTED } from '../../variables';
import request from '../../config/request'; import { standardRequest } from '../../config/request';
import { getNodeEnv, getLoopsApiKey } from '../../config'; import { getLoopsApiKey, getHttpsEnabled } from '../../config';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
/** /**
* Complete setting up user by adding their personal and auth information as part of the * Complete setting up user by adding their personal and auth information as part of the
@@ -19,123 +19,130 @@ import { getNodeEnv, getLoopsApiKey } from '../../config';
*/ */
export const completeAccountSignup = async (req: Request, res: Response) => { export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken; let user, token, refreshToken;
try { const {
const { email,
email, firstName,
firstName, lastName,
lastName, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, publicKey,
publicKey, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier,
verifier, organizationName
organizationName }: {
}: { email: string;
email: string; firstName: string;
firstName: string; lastName: string;
lastName: string; protectedKey: string;
protectedKey: string; protectedKeyIV: string;
protectedKeyIV: string; protectedKeyTag: string;
protectedKeyTag: string; publicKey: string;
publicKey: string; encryptedPrivateKey: string;
encryptedPrivateKey: string; encryptedPrivateKeyIV: string;
encryptedPrivateKeyIV: string; encryptedPrivateKeyTag: string;
encryptedPrivateKeyTag: string; salt: string;
salt: string; verifier: string;
verifier: string; organizationName: string;
organizationName: string; } = req.body;
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
// complete setting up user's account if (!user || (user && user?.publicKey)) {
user = await completeAccount({ // case 1: user doesn't exist.
userId: user._id.toString(), // case 2: user has already completed account
firstName, return res.status(403).send({
lastName, error: 'Failed to complete account for complete user'
encryptionVersion: 2, });
protectedKey, }
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user) // complete setting up user's account
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
// initialize default organization and workspace if (!user)
await initializeDefaultOrg({ throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
organizationName,
user
});
// update organization membership statuses that are // initialize default organization and workspace
// invited to completed with user attached await initializeDefaultOrg({
await MembershipOrg.updateMany( organizationName,
{ user
inviteEmail: email, });
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens // update organization membership statuses that are
const tokens = await issueAuthTokens({ // invited to completed with user attached
userId: user._id.toString() const membershipsToUpdate = await MembershipOrg.find({
}); inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
token = tokens.token; // update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// sending a welcome email to new users // issue tokens
if (getLoopsApiKey()) { const tokens = await issueAuthTokens({
await request.post("https://app.loops.so/api/v1/events/send", { userId: user._id,
"email": email, ip: req.realIP,
"eventName": "Sign Up", userAgent: req.headers['user-agent'] ?? ''
"firstName": firstName, });
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + getLoopsApiKey()
},
});
}
// store (refresh) token in httpOnly cookie token = tokens.token;
res.cookie('jid', tokens.refreshToken, {
httpOnly: true, // sending a welcome email to new users
path: '/', if (await getLoopsApiKey()) {
sameSite: 'strict', await standardRequest.post("https://app.loops.so/api/v1/events/send", {
secure: getNodeEnv() === 'production' ? true : false "email": email,
}); "eventName": "Sign Up",
} catch (err) { "firstName": firstName,
Sentry.setUser(null); "lastName": lastName
Sentry.captureException(err); }, {
return res.status(400).send({ headers: {
message: 'Failed to complete account setup' "Accept": "application/json",
}); "Authorization": "Bearer " + (await getLoopsApiKey())
} },
});
}
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: 'Successfully set up account',
@@ -153,98 +160,103 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
*/ */
export const completeAccountInvite = async (req: Request, res: Response) => { export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken; let user, token, refreshToken;
try { const {
const { email,
email, firstName,
firstName, lastName,
lastName, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, publicKey,
publicKey, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier
verifier } = req.body;
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to complete account for complete user' error: 'Failed to complete account for complete user'
}); });
} }
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED
}); });
if (!membershipOrg) throw new Error('Failed to find invitations for email'); if (!membershipOrg) throw new Error('Failed to find invitations for email');
// complete setting up user's account // complete setting up user's account
user = await completeAccount({ user = await completeAccount({
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}); });
if (!user) if (!user)
throw new Error('Failed to complete account for non-existent user'); throw new Error('Failed to complete account for non-existent user');
// update organization membership statuses that are
// invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email,
status: INVITED
});
membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString()
});
});
// update organization membership statuses that are await MembershipOrg.updateMany(
// invited to completed with user attached {
await MembershipOrg.updateMany( inviteEmail: email,
{ status: INVITED
inviteEmail: email, },
status: INVITED {
}, user,
{ status: ACCEPTED
user, }
status: ACCEPTED );
}
);
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id,
}); ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token; token = tokens.token;
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: getNodeEnv() === 'production' ? true : false
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: 'Successfully set up account',
user, user,
token token
}); });
}; };
+1 -2
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Membership, Secret, Membership, Secret,
@@ -69,4 +68,4 @@ export const getWorkspaceTags = async (req: Request, res: Response) => {
return res.json({ return res.json({
workspaceTags workspaceTags
}) })
} }
+22 -50
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { import {
User, User,
MembershipOrg MembershipOrg
@@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => {
} }
} }
*/ */
let user; const user = await User
try { .findById(req.user._id)
user = await User .select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
.findById(req.user._id)
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get current user'
});
}
return res.status(200).send({ return res.status(200).send({
user user
@@ -64,29 +54,20 @@ export const getMe = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateMyMfaEnabled = async (req: Request, res: Response) => { export const updateMyMfaEnabled = async (req: Request, res: Response) => {
let user; const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
try { req.user.isMfaEnabled = isMfaEnabled;
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled; if (isMfaEnabled) {
// TODO: adapt this route/controller
if (isMfaEnabled) { // to work for different forms of MFA
// TODO: adapt this route/controller req.user.mfaMethods = ['email'];
// to work for different forms of MFA } else {
req.user.mfaMethods = ['email']; req.user.mfaMethods = [];
} else {
req.user.mfaMethods = [];
}
await req.user.save();
user = req.user;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update current user's MFA status"
});
} }
await req.user.save();
const user = req.user;
return res.status(200).send({ return res.status(200).send({
user user
@@ -126,22 +107,13 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
} }
} }
*/ */
let organizations; const organizations = (
try { await MembershipOrg.find({
organizations = ( user: req.user._id
await MembershipOrg.find({ }).populate('organization')
user: req.user._id ).map((m) => m.organization);
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get current user's organizations"
});
}
return res.status(200).send({ return res.status(200).send({
organizations organizations
}); });
} }
+139 -211
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Workspace, Workspace,
@@ -19,7 +18,7 @@ import {
reformatPullSecrets reformatPullSecrets
} from '../../helpers/secret'; } from '../../helpers/secret';
import { pushKeys } from '../../helpers/key'; import { pushKeys } from '../../helpers/key';
import { getPostHogClient, EventService } from '../../services'; import { TelemetryService, EventService } from '../../services';
import { eventPushSecrets } from '../../events'; import { eventPushSecrets } from '../../events';
interface V2PushSecret { interface V2PushSecret {
@@ -47,65 +46,57 @@ interface V2PushSecret {
*/ */
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body;
let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body;
const { keys, environment, channel } = req.body; const { workspaceId } = req.params;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
// sanitize secrets // sanitize secrets
secrets = secrets.filter( secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
); );
await push({ await push({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, environment,
secrets, secrets,
channel: channel ? channel : 'cli', channel: channel ? channel : 'cli',
ipAddress: req.ip ipAddress: req.realIP
}); });
await pushKeys({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
keys keys
}); });
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pushed', event: 'secrets pushed',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : 'cli'
} }
}); });
} }
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId workspaceId: new Types.ObjectId(workspaceId),
}) environment
}); })
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully uploaded workspace secrets' message: 'Successfully uploaded workspace secrets'
@@ -121,56 +112,48 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; let secrets;
try { const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient(); const environment: string = req.query.environment as string;
const environment: string = req.query.environment as string; const channel: string = req.query.channel as string;
const channel: string = req.query.channel as string; const { workspaceId } = req.params;
const { workspaceId } = req.params;
let userId; let userId;
if (req.user) { if (req.user) {
userId = req.user._id.toString(); userId = req.user._id.toString();
} else if (req.serviceTokenData) { } else if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id userId = req.serviceTokenData.user.toString();
} }
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
secrets = await pull({ secrets = await pull({
userId, userId,
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : 'cli', channel: channel ? channel : 'cli',
ipAddress: req.ip ipAddress: req.realIP
}); });
if (channel !== 'cli') { if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets }); secrets = reformatPullSecrets({ secrets });
} }
if (postHogClient) { if (postHogClient) {
// capture secrets pushed event in production // capture secrets pushed event in production
postHogClient.capture({ postHogClient.capture({
distinctId: req.user.email, distinctId: req.user.email,
event: 'secrets pulled', event: 'secrets pulled',
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : 'cli'
} }
}); });
} }
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
secrets secrets
@@ -207,22 +190,14 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
} }
*/ */
let key; let key;
try { const { workspaceId } = req.params;
const { workspaceId } = req.params;
key = await Key.findOne({ key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}).populate('sender', '+publicKey'); }).populate('sender', '+publicKey');
if (!key) throw new Error('Failed to find workspace key'); if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace key'
});
}
return res.status(200).json(key); return res.status(200).json(key);
} }
@@ -230,23 +205,13 @@ export const getWorkspaceServiceTokenData = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let serviceTokenData; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData
.find({ .find({
workspace: workspaceId workspace: workspaceId
}) })
.select('+encryptedKey +iv +tag'); .select('+encryptedKey +iv +tag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
});
}
return res.status(200).send({ return res.status(200).send({
serviceTokenData serviceTokenData
@@ -293,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
} }
} }
*/ */
let memberships; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
memberships = await Membership.find({ const memberships = await Membership.find({
workspace: workspaceId workspace: workspaceId
}).populate('user', '+publicKey'); }).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace memberships'
});
}
return res.status(200).send({ return res.status(200).send({
memberships memberships
@@ -373,29 +329,20 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
let membership; const {
try { membershipId
const { } = req.params;
membershipId const { role } = req.body;
} = req.params;
const { role } = req.body; const membership = await Membership.findByIdAndUpdate(
membershipId,
membership = await Membership.findByIdAndUpdate( {
membershipId, role
{ }, {
role new: true
}, { }
new: true );
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
}); });
@@ -444,27 +391,18 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
let membership; const {
try { membershipId
const { } = req.params;
membershipId
} = req.params; const membership = await Membership.findByIdAndDelete(membershipId);
membership = await Membership.findByIdAndDelete(membershipId); if (!membership) throw new Error('Failed to delete workspace membership');
if (!membership) throw new Error('Failed to delete workspace membership'); await Key.deleteMany({
receiver: membership.user,
await Key.deleteMany({ workspace: membership.workspace
receiver: membership.user, });
workspace: membership.workspace
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
@@ -478,33 +416,23 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns * @returns
*/ */
export const toggleAutoCapitalization = async (req: Request, res: Response) => { export const toggleAutoCapitalization = async (req: Request, res: Response) => {
let workspace; const { workspaceId } = req.params;
try { const { autoCapitalization } = req.body;
const { workspaceId } = req.params;
const { autoCapitalization } = req.body;
workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId _id: workspaceId
}, },
{ {
autoCapitalization autoCapitalization
}, },
{ {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change autoCapitalization setting'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed autoCapitalization setting', message: 'Successfully changed autoCapitalization setting',
workspace workspace
}); });
}; };
@@ -0,0 +1,264 @@
/* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models';
import { issueAuthTokens, createToken, validateProviderAuthToken } from '../../helpers/auth';
import { checkUserDevice } from '../../helpers/user';
import { sendMail } from '../../helpers/nodemailer';
import { TokenService } from '../../services';
import { EELogService } from '../../ee/services';
import { BadRequestError, InternalServerError } from '../../utils/errors';
import {
TOKEN_EMAIL_MFA,
ACTION_LOGIN
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getJwtMfaLifetime,
getJwtMfaSecret,
getHttpsEnabled,
} from '../../config';
import { AuthProvider } from '../../models/user';
declare module 'jsonwebtoken' {
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
userId: string;
email: string;
authProvider: AuthProvider;
isUserCompleted: boolean,
}
}
/**
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
* @param req
* @param res
* @returns
*/
export const login1 = async (req: Request, res: Response) => {
try {
const {
email,
providerAuthToken,
clientPublicKey
}: {
email: string;
clientPublicKey: string,
providerAuthToken?: string;
} = req.body;
const user = await User.findOne({
email,
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user');
if (user.authProvider) {
await validateProviderAuthToken({
email,
user,
providerAuthToken,
})
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier
},
async () => {
// generate server-side public key
const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({
email: email
}, {
email,
userId: user.id,
clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false });
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
};
/**
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
* private key
* @param req
* @param res
* @returns
*/
export const login2 = async (req: Request, res: Response) => {
try {
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
const { email, clientProof, providerAuthToken } = req.body;
const user = await User.findOne({
email,
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices');
if (!user) throw new Error('Failed to find user');
if (user.authProvider) {
await validateProviderAuthToken({
email,
user,
providerAuthToken,
})
}
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
const server = new jsrp.server();
server.init(
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys
if (server.checkClientProof(clientProof)) {
if (user.isMfaEnabled) {
// case: user has MFA enabled
// generate temporary MFA token
const token = createToken({
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [user.email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.realIP
});
return res.status(200).send(response);
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
});
}
};
+11
View File
@@ -0,0 +1,11 @@
import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController';
import * as authController from './authController';
import * as signupController from './signupController';
export {
authController,
secretsController,
signupController,
workspacesController,
}
@@ -0,0 +1,420 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { SecretService, EventService } from "../../services";
import { eventPushSecrets } from "../../events";
import { BotService } from "../../services";
import { repackageSecretToRaw } from "../../helpers/secrets";
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
/**
* Return secrets for workspace with id [workspaceId] and environment
* [environment] in plaintext
* @param req
* @param res
*/
export const getSecretsRaw = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
authData: req.authData,
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({
secret,
key
});
return rep;
})
});
};
/**
* Return secret with name [secretName] in plaintext
* @param req
* @param res
*/
export const getSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
secretPath,
authData: req.authData,
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret,
key
})
});
};
/**
* Create secret with name [secretName] in plaintext
* @param req
* @param res
*/
export const createSecretRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValue,
secretComment,
secretPath = "/"
} = req.body;
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretName,
key
});
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment,
key
});
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext: secretKeyEncrypted.ciphertext,
secretKeyIV: secretKeyEncrypted.iv,
secretKeyTag: secretKeyEncrypted.tag,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: repackageSecretToRaw({
secret: secretWithoutBlindIndex,
key
})
});
}
/**
* Update secret with name [secretName]
* @param req
* @param res
*/
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValue,
secretPath = "/",
} = req.body;
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue,
key
});
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag,
secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret,
key
})
});
};
/**
* Delete secret with name [secretName]
* @param req
* @param res
*/
export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretPath = "/"
} = req.body;
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId)
});
return res.status(200).send({
secret: repackageSecretToRaw({
secret,
key
})
});
};
/**
* Get secrets for workspace with id [workspaceId] and environment
* [environment]
* @param req
* @param res
*/
export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
authData: req.authData,
});
return res.status(200).send({
secrets,
});
};
/**
* Return secret with name [secretName]
* @param req
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secretPath = req.query.secretPath as string;
const type = req.query.type as "shared" | "personal" | undefined;
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
secretPath,
authData: req.authData,
});
return res.status(200).send({
secret,
});
};
/**
* Create secret with name [secretName]
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretPath = "/",
} = req.body;
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex,
});
};
/**
* Update secret with name [secretName]
* @param req
* @param res
*/
export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath = "/",
} = req.body;
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret,
});
};
/**
* Delete secret with name [secretName]
* @param req
* @param res
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretPath = "/"
} = req.body;
const { secret } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretPath,
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
}),
});
return res.status(200).send({
secret,
});
};
@@ -0,0 +1,194 @@
import jwt from 'jsonwebtoken';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user';
import {
initializeDefaultOrg
} from '../../helpers/signup';
import { issueAuthTokens, validateProviderAuthToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import { standardRequest } from '../../config/request';
import { getLoopsApiKey, getHttpsEnabled, getJwtSignupSecret } from '../../config';
import { BadRequestError } from '../../utils/errors';
import { TelemetryService } from '../../services';
/**
* Complete setting up user by adding their personal and auth information as part of the
* signup flow
* @param req
* @param res
* @returns
*/
export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken;
try {
const {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier,
organizationName,
providerAuthToken,
attributionSource,
}: {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
encryptedPrivateKeyTag: string;
salt: string;
verifier: string;
organizationName: string;
providerAuthToken?: string;
attributionSource?: string;
} = req.body;
user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist.
// case 2: user has already completed account
return res.status(403).send({
error: 'Failed to complete account for complete user'
});
}
if (providerAuthToken) {
await validateProviderAuthToken({
email,
providerAuthToken,
user,
});
} else {
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (AUTH_TOKEN_TYPE === null) {
throw BadRequestError({ message: `Missing Authorization Header in the request header.` });
}
if (AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') {
throw BadRequestError({ message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.` })
}
if (AUTH_TOKEN_VALUE === null) {
throw BadRequestError({
message: 'Missing Authorization Body in the request header',
})
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, await getJwtSignupSecret())
);
if (decodedToken.userId !== user.id) {
throw BadRequestError();
}
}
// complete setting up user's account
user = await completeAccount({
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
encryptedPrivateKeyTag,
salt,
verifier
});
if (!user)
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
// initialize default organization and workspace
await initializeDefaultOrg({
organizationName,
user
});
// update organization membership statuses that are
// invited to completed with user attached
await MembershipOrg.updateMany(
{
inviteEmail: email,
status: INVITED
},
{
user,
status: ACCEPTED
}
);
// issue tokens
const tokens = await issueAuthTokens({
userId: user._id,
ip: req.realIP,
userAgent: req.headers['user-agent'] ?? ''
});
token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey())
},
});
}
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'User Signed Up',
distinctId: email,
properties: {
email,
attributionSource
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
user,
token
});
};
@@ -0,0 +1,90 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import { Secret } from '../../models';
import { SecretService } from'../../services';
/**
* Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed
* @param req
* @param res
* @returns
*/
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex: {
$exists: false
}
});
return res.status(200).send(secretsWithoutBlindIndex === 0);
}
/**
* Get all secrets for workspace with id [workspaceId]
*/
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const secrets = await Secret.find({
workspace: new Types.ObjectId (workspaceId)
});
return res.status(200).send({
secrets
});
}
/**
* Update blind indices for secrets in workspace with id [workspaceId]
* @param req
* @param res
*/
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
interface SecretToUpdate {
secretName: string;
_id: string;
}
const { workspaceId } = req.params;
const {
secretsToUpdate
}: {
secretsToUpdate: SecretToUpdate[];
} = req.body;
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
// update secret blind indices
const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName,
salt
});
return ({
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
});
})
);
await Secret.bulkWrite(operations);
return res.status(200).send({
message: 'Successfully named workspace secrets'
});
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Action, SecretVersion } from '../../models'; import { Action, SecretVersion } from '../../models';
import { ActionNotFoundError } from '../../../utils/errors'; import { ActionNotFoundError } from '../../../utils/errors';
@@ -28,4 +27,4 @@ export const getAction = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
action action
}); });
} }
@@ -0,0 +1,28 @@
import { Request, Response } from 'express';
import { EELicenseService } from '../../services';
import { getLicenseServerUrl } from '../../../config';
import { licenseServerKeyRequest } from '../../../config/request';
/**
* Return available cloud product information.
* Note: Nicely formatted to easily construct a table from
* @param req
* @param res
* @returns
*/
export const getCloudProducts = async (req: Request, res: Response) => {
const billingCycle = req.query['billing-cycle'] as string;
if (EELicenseService.instanceType === 'cloud') {
const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
);
return res.status(200).send(data);
}
return res.status(200).send({
head: [],
rows: []
});
}
+5 -1
View File
@@ -1,15 +1,19 @@
import * as stripeController from './stripeController'; import * as stripeController from './stripeController';
import * as secretController from './secretController'; import * as secretController from './secretController';
import * as secretSnapshotController from './secretSnapshotController'; import * as secretSnapshotController from './secretSnapshotController';
import * as organizationsController from './organizationsController';
import * as workspaceController from './workspaceController'; import * as workspaceController from './workspaceController';
import * as actionController from './actionController'; import * as actionController from './actionController';
import * as membershipController from './membershipController'; import * as membershipController from './membershipController';
import * as cloudProductsController from './cloudProductsController';
export { export {
stripeController, stripeController,
secretController, secretController,
secretSnapshotController, secretSnapshotController,
organizationsController,
workspaceController, workspaceController,
actionController, actionController,
membershipController membershipController,
cloudProductsController
} }
@@ -2,7 +2,8 @@ import { Request, Response } from "express";
import { Membership, Workspace } from "../../../models"; import { Membership, Workspace } from "../../../models";
import { IMembershipPermission } from "../../../models/membership"; import { IMembershipPermission } from "../../../models/membership";
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
import { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization"; import { ADMIN, MEMBER } from "../../../variables/organization";
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../../variables';
import { Builder } from "builder-pattern" import { Builder } from "builder-pattern"
import _ from "lodash"; import _ from "lodash";
@@ -10,7 +11,7 @@ export const denyMembershipPermissions = async (req: Request, res: Response) =>
const { membershipId } = req.params; const { membershipId } = req.params;
const { permissions } = req.body; const { permissions } = req.body;
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => { const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) { if (!permission.ability || !permission.environmentSlug || ![PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS].includes(permission.ability)) {
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" }) throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
} }
@@ -0,0 +1,84 @@
import { Request, Response } from 'express';
import { getLicenseServerUrl } from '../../../config';
import { licenseServerKeyRequest } from '../../../config/request';
import { EELicenseService } from '../../services';
/**
* Return the organization's current plan and allowed feature set
*/
export const getOrganizationPlan = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const workspaceId = req.query.workspaceId as string;
const plan = await EELicenseService.getPlan(organizationId, workspaceId);
return res.status(200).send({
plan,
});
}
/**
* Update the organization plan to product with id [productId]
* @param req
* @param res
* @returns
*/
export const updateOrganizationPlan = async (req: Request, res: Response) => {
const {
productId
} = req.body;
const { data } = await licenseServerKeyRequest.patch(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/cloud-plan`,
{
productId
}
);
return res.status(200).send(data);
}
/**
* Return the organization's payment methods on file
*/
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
const { data: { pmtMethods } } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`
);
return res.status(200).send({
pmtMethods
});
}
/**
* Return a Stripe session URL to add payment method for organization
*/
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
const {
success_url,
cancel_url
} = req.body;
const { data: { url } } = await licenseServerKeyRequest.post(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods`,
{
success_url,
cancel_url
}
);
return res.status(200).send({
url
});
}
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
const { pmtMethodId } = req.params;
const { data } = await licenseServerKeyRequest.delete(
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${req.organization.customerId}/billing-details/payment-methods/${pmtMethodId}`,
);
return res.status(200).send(data);
}
+117 -121
View File
@@ -1,16 +1,15 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { Secret } from "../../../models";
import { Secret } from '../../../models'; import { SecretVersion } from "../../models";
import { SecretVersion } from '../../models'; import { EESecretService } from "../../services";
import { EESecretService } from '../../services';
/** /**
* Return secret versions for secret with id [secretId] * Return secret versions for secret with id [secretId]
* @param req * @param req
* @param res * @param res
*/ */
export const getSecretVersions = async (req: Request, res: Response) => { export const getSecretVersions = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return secret versions' #swagger.summary = 'Return secret versions'
#swagger.description = 'Return secret versions' #swagger.description = 'Return secret versions'
@@ -55,41 +54,34 @@ import { EESecretService } from '../../services';
} }
} }
*/ */
let secretVersions; const { secretId, workspaceId, environment, folderId } = req.params;
try {
const { secretId } = req.params;
const offset: number = parseInt(req.query.offset as string); const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
secretVersions = await SecretVersion.find({
secret: secretId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
} catch (err) { const secretVersions = await SecretVersion.find({
Sentry.setUser({ email: req.user.email }); secret: secretId,
Sentry.captureException(err); workspace: workspaceId,
return res.status(400).send({ environment,
message: 'Failed to get secret versions' folder: folderId,
}); })
} .sort({ createdAt: -1 })
.skip(offset)
return res.status(200).send({ .limit(limit);
secretVersions
}); return res.status(200).send({
} secretVersions,
});
};
/** /**
* Roll back secret with id [secretId] to version [version] * Roll back secret with id [secretId] to version [version]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const rollbackSecretVersion = async (req: Request, res: Response) => { export const rollbackSecretVersion = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Roll back secret to a version.' #swagger.summary = 'Roll back secret to a version.'
#swagger.description = 'Roll back secret to a version.' #swagger.description = 'Roll back secret to a version.'
@@ -137,88 +129,92 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
} }
} }
*/ */
let secret; const { secretId } = req.params;
try { const { version } = req.body;
const { secretId } = req.params;
const { version } = req.body;
// validate secret version
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version
});
if (!oldSecretVersion) throw new Error('Failed to find secret version');
const {
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
} = oldSecretVersion;
// update secret
secret = await Secret.findByIdAndUpdate(
secretId,
{
$inc: {
version: 1
},
workspace,
type,
user,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
},
{
new: true
}
);
if (!secret) throw new Error('Failed to find and update secret');
// add new secret version // validate secret version
await new SecretVersion({ const oldSecretVersion = await SecretVersion.findOne({
secret: secretId, secret: secretId,
version: secret.version, version,
workspace, }).select("+secretBlindIndex");
type,
user, if (!oldSecretVersion) throw new Error("Failed to find secret version");
environment,
isDeleted: false, const {
secretKeyCiphertext, workspace,
secretKeyIV, type,
secretKeyTag, user,
secretValueCiphertext, environment,
secretValueIV, secretBlindIndex,
secretValueTag secretKeyCiphertext,
}).save(); secretKeyIV,
secretKeyTag,
// take secret snapshot secretValueCiphertext,
await EESecretService.takeSecretSnapshot({ secretValueIV,
workspaceId: secret.workspace.toString() secretValueTag,
}); algorithm,
folder,
} catch (err) { keyEncoding,
Sentry.setUser({ email: req.user.email }); } = oldSecretVersion;
Sentry.captureException(err);
return res.status(400).send({ // update secret
message: 'Failed to roll back secret version' const secret = await Secret.findByIdAndUpdate(
}); secretId,
} {
$inc: {
return res.status(200).send({ version: 1,
secret },
}); workspace,
} type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folderId: folder,
algorithm,
keyEncoding,
},
{
new: true,
}
);
if (!secret) throw new Error("Failed to find and update secret");
// add new secret version
await new SecretVersion({
secret: secretId,
version: secret.version,
workspace,
type,
user,
environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folder,
algorithm,
keyEncoding,
}).save();
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace,
environment,
folderId: folder,
});
return res.status(200).send({
secret,
});
};
@@ -1,39 +1,45 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import {
import { SecretSnapshot } from '../../models'; ISecretVersion,
SecretSnapshot,
TFolderRootVersionSchema,
} from "../../models";
/** /**
* Return secret snapshot with id [secretSnapshotId] * Return secret snapshot with id [secretSnapshotId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getSecretSnapshot = async (req: Request, res: Response) => { export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot; const { secretSnapshotId } = req.params;
try {
const { secretSnapshotId } = req.params;
secretSnapshot = await SecretSnapshot const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
.findById(secretSnapshotId) .lean()
.populate({ .populate<{ secretVersions: ISecretVersion[] }>({
path: 'secretVersions', path: 'secretVersions',
populate: { populate: {
path: 'tags', path: 'tags',
model: 'Tag', model: 'Tag'
} }
}); })
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
} catch (err) {
Sentry.setUser({ email: req.user.email }); const folderId = secretSnapshot.folderId;
Sentry.captureException(err); // to show only the folder required secrets
return res.status(400).send({ secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
message: 'Failed to get secret snapshot' ({ folder }) => folder === folderId
}); );
}
secretSnapshot.folderVersion =
return res.status(200).send({ secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({
secretSnapshot id,
}); name,
} })) as any;
return res.status(200).send({
secretSnapshot,
});
};
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
* @returns * @returns
*/ */
export const handleWebhook = async (req: Request, res: Response) => { export const handleWebhook = async (req: Request, res: Response) => {
let event; const stripe = new Stripe(await getStripeSecretKey(), {
try { apiVersion: '2022-08-01'
const stripe = new Stripe(getStripeSecretKey(), { });
apiVersion: '2022-08-01'
});
// check request for valid stripe signature // check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string; const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent( const event = stripe.webhooks.constructEvent(
req.body, req.body,
sig, sig,
getStripeWebhookSecret() await getStripeWebhookSecret()
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) { switch (event.type) {
case '': case '':
@@ -1,25 +1,29 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import * as Sentry from '@sentry/node'; import { PipelineStage, Types } from "mongoose";
import { Types } from 'mongoose'; import { Secret } from "../../../models";
import { import {
Secret SecretSnapshot,
} from '../../../models'; Log,
import { SecretVersion,
SecretSnapshot, ISecretVersion,
Log, FolderVersion,
SecretVersion, TFolderRootVersionSchema,
ISecretVersion } from "../../models";
} from '../../models'; import { EESecretService } from "../../services";
import { EESecretService } from '../../services'; import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
import { getLatestSecretVersionIds } from '../../helpers/secretVersion'; import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService";
/** /**
* Return secret snapshots for workspace with id [workspaceId] * Return secret snapshots for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { export const getWorkspaceSecretSnapshots = async (
/* req: Request,
res: Response
) => {
/*
#swagger.summary = 'Return project secret snapshot ids' #swagger.summary = 'Return project secret snapshot ids'
#swagger.description = 'Return project secret snapshots ids' #swagger.description = 'Return project secret snapshots ids'
@@ -64,66 +68,60 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
} }
} }
*/ */
let secretSnapshots; const { workspaceId } = req.params;
try { const { environment, folderId } = req.query;
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string); const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId
})
.sort({ createdAt: -1 })
.skip(offset)
.limit(limit);
} catch (err) { const secretSnapshots = await SecretSnapshot.find({
Sentry.setUser({ email: req.user.email }); workspace: workspaceId,
Sentry.captureException(err); environment,
return res.status(400).send({ folderId: folderId || "root",
message: 'Failed to get secret snapshots' })
}); .sort({ createdAt: -1 })
} .skip(offset)
.limit(limit);
return res.status(200).send({
secretSnapshots return res.status(200).send({
}); secretSnapshots,
} });
};
/** /**
* Return count of secret snapshots for workspace with id [workspaceId] * Return count of secret snapshots for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
*/ */
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { export const getWorkspaceSecretSnapshotsCount = async (
let count; req: Request,
try { res: Response
const { workspaceId } = req.params; ) => {
count = await SecretSnapshot.countDocuments({ const { workspaceId } = req.params;
workspace: workspaceId const { environment, folderId } = req.query;
});
} catch (err) { const count = await SecretSnapshot.countDocuments({
Sentry.setUser({ email: req.user.email }); workspace: workspaceId,
Sentry.captureException(err); environment,
return res.status(400).send({ folderId: folderId || "root",
message: 'Failed to count number of secret snapshots' });
});
} return res.status(200).send({
count,
return res.status(200).send({ });
count };
});
}
/** /**
* Rollback secret snapshot with id [secretSnapshotId] to version [version] * Rollback secret snapshot with id [secretSnapshotId] to version [version]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => { export const rollbackWorkspaceSecretSnapshot = async (
/* req: Request,
res: Response
) => {
/*
#swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.' #swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.'
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.' #swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
@@ -173,160 +171,329 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
} }
} }
*/ */
let secrets;
try {
const { workspaceId } = req.params;
const { version } = req.body;
// validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
// TODO: fix any const { workspaceId } = req.params;
const oldSecretVersionsObj: any = secretSnapshot.secretVersions const { version, environment, folderId = "root" } = req.body;
.reduce((accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s
}), {});
const latestSecretVersionIds = await getLatestSecretVersionIds({
secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret)
});
// TODO: fix any
const latestSecretVersions: any = (await SecretVersion.find({
_id: {
$in: latestSecretVersionIds.map((s) => s.versionId)
}
}, 'secret version'))
.reduce((accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s
}), {});
// delete existing secrets
await Secret.deleteMany({
workspace: workspaceId
});
// add secrets // validate secret snapshot
secrets = await Secret.insertMany( const secretSnapshot = await SecretSnapshot.findOne({
secretSnapshot.secretVersions.map((sv) => { workspace: workspaceId,
const secretId = sv.secret; version,
const { environment,
workspace, folderId: folderId,
type, })
user, .populate<{ secretVersions: ISecretVersion[] }>({
environment, path: "secretVersions",
secretKeyCiphertext, select: "+secretBlindIndex",
secretKeyIV, })
secretKeyTag, .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
secretKeyHash,
secretValueCiphertext, if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
secretValueIV,
secretValueTag, const snapshotFolderTree = secretSnapshot.folderVersion;
secretValueHash, const latestFolderTree = await Folder.findOne({
createdAt workspace: workspaceId,
} = oldSecretVersionsObj[secretId.toString()]; environment,
});
return ({
_id: secretId, const latestFolderVersion = await FolderVersion.findOne({
version: latestSecretVersions[secretId.toString()].version + 1, environment,
workspace, workspace: workspaceId,
type, "nodes.id": folderId,
user, }).sort({ "nodes.version": -1 });
environment,
secretKeyCiphertext, const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
secretKeyIV, const secretIds: Types.ObjectId[] = [];
secretKeyTag, const folderIds: string[] = [folderId];
secretKeyHash,
secretValueCiphertext, secretSnapshot.secretVersions.forEach((snapSecVer) => {
secretValueIV, oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
secretValueTag, secretIds.push(snapSecVer.secret);
secretValueHash, });
secretCommentCiphertext: '',
secretCommentIV: '', // the parent node from current latest one
secretCommentTag: '', // this will be modified according to the snapshot and latest snapshots
createdAt const newFolderTree =
}); latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
})
); if (newFolderTree) {
newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
// add secret versions const queue = [newFolderTree];
await SecretVersion.insertMany( // a bfs algorithm in which we take the latest snapshots of all the folders in a level
secrets.map(({ while (queue.length) {
_id, const groupByFolderId: Record<string, TFolderSchema> = {};
version, // the original queue is popped out completely to get what ever in a level
workspace, // subqueue is filled with all the children thus next level folders
type, // subQueue will then be transfered to the oriinal queue
user, const subQueue: TFolderSchema[] = [];
environment, // get everything inside a level
secretKeyCiphertext, while (queue.length) {
secretKeyIV, const folder = queue.pop() as TFolderSchema;
secretKeyTag, folder.children.forEach((el) => {
secretKeyHash, folderIds.push(el.id); // push ids and data into queu
secretValueCiphertext, subQueue.push(el);
secretValueIV, // to modify the original tree very fast we keep a reference object
secretValueTag, // key with folder id and pointing to the various nodes
secretValueHash groupByFolderId[el.id] = el;
}) => ({ });
_id: new Types.ObjectId(), }
secret: _id, // get latest snapshots of all the folder
version, const matchWsFoldersPipeline = {
workspace, $match: {
type, workspace: new Types.ObjectId(workspaceId),
user, environment,
environment, folderId: {
isDeleted: false, $in: Object.keys(groupByFolderId),
secretKeyCiphertext, },
secretKeyIV, },
secretKeyTag, };
secretKeyHash, const sortByFolderIdAndVersion: PipelineStage = {
secretValueCiphertext, $sort: { folderId: 1, version: -1 },
secretValueIV, };
secretValueTag, const pickLatestVersionOfEachFolder = {
secretValueHash $group: {
})) _id: "$folderId",
); latestVersion: { $first: "$version" },
doc: {
// update secret versions of restored secrets as not deleted $first: "$$ROOT",
await SecretVersion.updateMany({ },
secret: { },
$in: secretSnapshot.secretVersions.map((sv) => sv.secret) };
} const populateSecVersion = {
}, { $lookup: {
isDeleted: false from: SecretVersion.collection.name,
}); localField: "doc.secretVersions",
foreignField: "_id",
// take secret snapshot as: "doc.secretVersions",
await EESecretService.takeSecretSnapshot({ },
workspaceId };
}); const populateFolderVersion = {
} catch (err) { $lookup: {
Sentry.setUser({ email: req.user.email }); from: FolderVersion.collection.name,
Sentry.captureException(err); localField: "doc.folderVersion",
return res.status(400).send({ foreignField: "_id",
message: 'Failed to roll back secret snapshot' as: "doc.folderVersion",
}); },
};
const unwindFolderVerField = {
$unwind: {
path: "$doc.folderVersion",
preserveNullAndEmptyArrays: true,
},
};
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
await SecretSnapshot.aggregate([
matchWsFoldersPipeline,
sortByFolderIdAndVersion,
pickLatestVersionOfEachFolder,
populateSecVersion,
populateFolderVersion,
unwindFolderVerField,
]);
// recursive snapshotting each level
latestSnapshotsByFolders.forEach((snap) => {
// mutate the folder tree to update the nodes to the latest version tree
// we are reconstructing the folder tree by latest snapshots here
if (groupByFolderId[snap.doc.folderId]) {
groupByFolderId[snap.doc.folderId].children =
snap.doc?.folderVersion?.nodes?.children || [];
}
// push all children of next level snapshots
if (snap.doc.folderVersion?.nodes?.children) {
queue.push(...snap.doc.folderVersion.nodes.children);
}
snap.doc.secretVersions.forEach((snapSecVer) => {
// record all the secrets
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
secretIds.push(snapSecVer.secret);
});
});
queue.push(...subQueue);
} }
}
return res.status(200).send({
secrets // TODO: fix any
}); const latestSecretVersionIds = await getLatestSecretVersionIds({
} secretIds,
});
// TODO: fix any
const latestSecretVersions: any = (
await SecretVersion.find(
{
_id: {
$in: latestSecretVersionIds.map((s) => s.versionId),
},
},
"secret version"
)
).reduce(
(accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s,
}),
{}
);
const secDelQuery: Record<string, unknown> = {
workspace: workspaceId,
environment,
// undefined means root thus collect all secrets
};
if (folderId !== "root" && folderIds.length)
secDelQuery.folder = { $in: folderIds };
// delete existing secrets
await Secret.deleteMany(secDelQuery);
await Folder.deleteOne({
workspace: workspaceId,
environment,
});
// add secrets
const secrets = await Secret.insertMany(
Object.keys(oldSecretVersionsObj).map((sv) => {
const {
secret: secretId,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
} = oldSecretVersionsObj[sv];
return {
_id: secretId,
version: latestSecretVersions[secretId.toString()].version + 1,
workspace,
type,
user,
environment,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext: "",
secretCommentIV: "",
secretCommentTag: "",
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
};
})
);
// add secret versions
const secretV = await SecretVersion.insertMany(
secrets.map(
({
_id,
version,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
})
)
);
if (newFolderTree && latestFolderTree) {
// save the updated folder tree to the present one
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
latestFolderTree._id = new Types.ObjectId();
latestFolderTree.isNew = true;
await latestFolderTree.save();
// create new folder version
const newFolderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: newFolderTree,
});
await newFolderVersion.save();
}
// update secret versions of restored secrets as not deleted
await SecretVersion.updateMany(
{
secret: {
$in: Object.keys(oldSecretVersionsObj).map(
(sv) => oldSecretVersionsObj[sv].secret
),
},
},
{
isDeleted: false,
}
);
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
});
return res.status(200).send({
secrets,
});
};
/** /**
* Return (audit) logs for workspace with id [workspaceId] * Return (audit) logs for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceLogs = async (req: Request, res: Response) => { export const getWorkspaceLogs = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return project (audit) logs' #swagger.summary = 'Return project (audit) logs'
#swagger.description = 'Return project (audit) logs' #swagger.description = 'Return project (audit) logs'
@@ -392,43 +559,32 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
} }
} }
*/ */
let logs const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string); const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
const sortBy: string = req.query.sortBy as string; const sortBy: string = req.query.sortBy as string;
const userId: string = req.query.userId as string; const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string; const actionNames: string = req.query.actionNames as string;
logs = await Log.find({
workspace: workspaceId,
...( userId ? { user: userId } : {}),
...(
actionNames
? {
actionNames: {
$in: actionNames.split(',')
}
} : {}
)
})
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
.skip(offset)
.limit(limit)
.populate('actions')
.populate('user');
} catch (err) { const logs = await Log.find({
Sentry.setUser({ email: req.user.email }); workspace: workspaceId,
Sentry.captureException(err); ...(userId ? { user: userId } : {}),
return res.status(400).send({ ...(actionNames
message: 'Failed to get workspace logs' ? {
}); actionNames: {
} $in: actionNames.split(","),
},
return res.status(200).send({ }
logs : {}),
}); })
} .sort({ createdAt: sortBy === "recent" ? -1 : 1 })
.skip(offset)
.limit(limit)
.populate("actions")
.populate("user serviceAccount serviceTokenData");
return res.status(200).send({
logs,
});
};
+97 -104
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Action } from '../models'; import { Action } from '../models';
import { import {
@@ -24,39 +23,37 @@ import {
const createActionUpdateSecret = async ({ const createActionUpdateSecret = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
let action; const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
try { secretIds,
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({ n: 2
secretIds, }))
n: 2 .map((s) => ({
})) oldSecretVersion: s.versions[0]._id,
.map((s) => ({ newSecretVersion: s.versions[1]._id
oldSecretVersion: s.versions[0]._id, }));
newSecretVersion: s.versions[1]._id
})); const action = await new Action({
name,
action = await new Action({ user: userId,
name, serviceAccount: serviceAccountId,
user: userId, serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions
} }
}).save(); }).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create update secret action');
}
return action; return action;
} }
@@ -72,68 +69,66 @@ const createActionUpdateSecret = async ({
const createActionSecret = async ({ const createActionSecret = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
let action; // case: action is adding, deleting, or reading secrets
try { // -> add new secret versions
// case: action is adding, deleting, or reading secrets const latestSecretVersions = (await getLatestSecretVersionIds({
// -> add new secret versions secretIds
const latestSecretVersions = (await getLatestSecretVersionIds({ }))
secretIds .map((s) => ({
})) newSecretVersion: s.versionId
.map((s) => ({ }));
newSecretVersion: s.versionId
})); const action = await new Action({
name,
action = await new Action({ user: userId,
name, serviceAccount: serviceAccountId,
user: userId, serviceTokenData: serviceTokenDataId,
workspace: workspaceId, workspace: workspaceId,
payload: { payload: {
secretVersions: latestSecretVersions secretVersions: latestSecretVersions
} }
}).save(); }).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action create/read/delete secret action');
}
return action; return action;
} }
/** /**
* Create an (audit) action for user with id [userId] * Create an (audit) action for client with id [userId],
* [serviceAccountId], or [serviceTokenDataId]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.name - name of action * @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action * @param {String} obj.userId - id of user associated with action
* @returns * @returns
*/ */
const createActionUser = ({ const createActionClient = ({
name, name,
userId userId,
serviceAccountId,
serviceTokenDataId
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
}) => { }) => {
let action; const action = new Action({
try { name,
action = new Action({ user: userId,
name, serviceAccount: serviceAccountId,
user: userId serviceTokenData: serviceTokenDataId
}).save(); }).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create user action');
}
return action; return action;
} }
@@ -149,49 +144,47 @@ const createActionUser = ({
const createActionHelper = async ({ const createActionHelper = async ({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds, secretIds,
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) => { }) => {
let action; let action;
try { switch (name) {
switch (name) { case ACTION_LOGIN:
case ACTION_LOGIN: case ACTION_LOGOUT:
case ACTION_LOGOUT: action = await createActionClient({
action = await createActionUser({ name,
name, userId
userId });
}); break;
break; case ACTION_ADD_SECRETS:
case ACTION_ADD_SECRETS: case ACTION_READ_SECRETS:
case ACTION_READ_SECRETS: case ACTION_DELETE_SECRETS:
case ACTION_DELETE_SECRETS: if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); action = await createActionSecret({
action = await createActionSecret({ name,
name, userId,
userId, workspaceId,
workspaceId, secretIds
secretIds });
}); break;
break; case ACTION_UPDATE_SECRETS:
case ACTION_UPDATE_SECRETS: if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); action = await createActionUpdateSecret({
action = await createActionUpdateSecret({ name,
name, userId,
userId, workspaceId,
workspaceId, secretIds
secretIds });
}); break;
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action');
} }
return action; return action;
@@ -199,4 +192,4 @@ const createActionHelper = async ({
export { export {
createActionHelper createActionHelper
}; };
@@ -1,8 +1,9 @@
import { Types } from 'mongoose';
import _ from "lodash"; import _ from "lodash";
import { Membership } from "../../models"; import { Membership } from "../../models";
import { ABILITY_READ, ABILITY_WRITE } from "../../variables/organization"; import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, environment: any, action: any) => { export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
@@ -18,15 +19,15 @@ export const userHasWorkspaceAccess = async (userId: any, workspaceId: any, envi
return true return true
} }
export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, environment: any) => { export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return false return false
} }
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
// case: you have write only if read is blocked and write is not // case: you have write only if read is blocked and write is not
if (isReadDisallowed && !isWriteDisallowed) { if (isReadDisallowed && !isWriteDisallowed) {
@@ -36,15 +37,15 @@ export const userHasWriteOnlyAbility = async (userId: any, workspaceId: any, env
return false return false
} }
export const userHasNoAbility = async (userId: any, workspaceId: any, environment: any) => { export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId }) const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
if (!membershipForWorkspace) { if (!membershipForWorkspace) {
return true return true
} }
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions; const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_WRITE }); const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: ABILITY_READ }); const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
if (isReadBlocked && isWriteDisallowed) { if (isReadBlocked && isWriteDisallowed) {
return true return true
+17 -18
View File
@@ -1,9 +1,9 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Log, Log,
IAction IAction
} from '../models'; } from '../models';
/** /**
* Create an (audit) log * Create an (audit) log
* @param {Object} obj * @param {Object} obj
@@ -16,36 +16,35 @@ import {
*/ */
const createLogHelper = async ({ const createLogHelper = async ({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
ipAddress ipAddress
}: { }: {
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
ipAddress: string; ipAddress: string;
}) => { }) => {
let log; const log = await new Log({
try { user: userId,
log = await new Log({ serviceAccount: serviceAccountId,
user: userId, serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined, workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name), actionNames: actions.map((a) => a.name),
actions, actions,
channel, channel,
ipAddress ipAddress
}).save(); }).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create log');
}
return log; return log;
} }
export { export {
createLogHelper createLogHelper
} }
+88 -135
View File
@@ -1,14 +1,11 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import { Secret, ISecret } from "../../models";
import { import {
Secret, SecretSnapshot,
ISecret SecretVersion,
} from '../../models'; ISecretVersion,
import { FolderVersion,
SecretSnapshot, } from "../models";
SecretVersion,
ISecretVersion
} from '../models';
/** /**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id * Save a secret snapshot that is a copy of the current state of secrets in workspace with id
@@ -19,56 +16,70 @@ import {
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot * @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/ */
const takeSecretSnapshotHelper = async ({ const takeSecretSnapshotHelper = async ({
workspaceId workspaceId,
environment,
folderId = "root",
}: { }: {
workspaceId: string; workspaceId: Types.ObjectId;
environment: string;
folderId?: string;
}) => { }) => {
// get all folder ids
const secretIds = (
await Secret.find(
{
workspace: workspaceId,
environment,
folder: folderId,
},
"_id"
).lean()
).map((s) => s._id);
let secretSnapshot; const latestSecretVersions = (
try { await SecretVersion.aggregate([
const secretIds = (await Secret.find({ {
workspace: workspaceId $match: {
}, '_id')).map((s) => s._id); environment,
workspace: new Types.ObjectId(workspaceId),
secret: {
$in: secretIds,
},
},
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // secret version id
},
},
{
$sort: { version: -1 },
},
]).exec()
).map((s) => s.versionId);
const latestFolderVersion = await FolderVersion.findOne({
environment,
workspace: workspaceId,
"nodes.id": folderId,
}).sort({ "nodes.version": -1 });
const latestSecretVersions = (await SecretVersion.aggregate([ const latestSecretSnapshot = await SecretSnapshot.findOne({
{ workspace: workspaceId,
$match: { }).sort({ version: -1 });
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // secret version id
}
},
{
$sort: { version: -1 }
}
])
.exec())
.map((s) => s.versionId);
const latestSecretSnapshot = await SecretSnapshot.findOne({ const secretSnapshot = await new SecretSnapshot({
workspace: workspaceId workspace: workspaceId,
}).sort({ version: -1 }); environment,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions,
folderId,
folderVersion: latestFolderVersion,
}).save();
secretSnapshot = await new SecretSnapshot({ return secretSnapshot;
workspace: workspaceId, };
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot');
}
return secretSnapshot;
}
/** /**
* Add secret versions [secretVersions] to the SecretVersion collection. * Add secret versions [secretVersions] to the SecretVersion collection.
@@ -77,93 +88,35 @@ const takeSecretSnapshotHelper = async ({
* @returns {SecretVersion[]} newSecretVersions - new secret versions * @returns {SecretVersion[]} newSecretVersions - new secret versions
*/ */
const addSecretVersionsHelper = async ({ const addSecretVersionsHelper = async ({
secretVersions secretVersions,
}: { }: {
secretVersions: ISecretVersion[] secretVersions: ISecretVersion[];
}) => { }) => {
let newSecretVersions; const newSecretVersions = await SecretVersion.insertMany(secretVersions);
try {
newSecretVersions = await SecretVersion.insertMany(secretVersions);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(`Failed to add secret versions [err=${err}]`);
}
return newSecretVersions; return newSecretVersions;
} };
const markDeletedSecretVersionsHelper = async ({ const markDeletedSecretVersionsHelper = async ({
secretIds secretIds,
}: { }: {
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
try { await SecretVersion.updateMany(
await SecretVersion.updateMany({ {
secret: { $in: secretIds } secret: { $in: secretIds },
}, { },
isDeleted: true {
}, { isDeleted: true,
new: true },
}); {
} catch (err) { new: true,
Sentry.setUser(null); }
Sentry.captureException(err); );
throw new Error('Failed to mark secret versions as deleted'); };
}
}
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
const initSecretVersioningHelper = async () => {
try {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: 'secretversions',
localField: '_id',
foreignField: 'secret',
as: 'versions',
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => ({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment
}))
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to ensure that secrets are versioned');
}
}
export { export {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper, addSecretVersionsHelper,
markDeletedSecretVersionsHelper, markDeletedSecretVersionsHelper,
initSecretVersioningHelper };
}
+62 -90
View File
@@ -1,110 +1,82 @@
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { Types } from 'mongoose'; import { SecretVersion } from "../models";
import { SecretVersion } from '../models';
/** /**
* Return latest secret versions for secrets with ids [secretIds] * Return latest secret versions for secrets with ids [secretIds]
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.secretIds = ids of secrets to get latest versions for * @param {Object} obj.secretIds = ids of secrets to get latest versions for
* @returns * @returns
*/ */
const getLatestSecretVersionIds = async ({ const getLatestSecretVersionIds = async ({
secretIds secretIds,
}: { }: {
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
const latestSecretVersionIds = await SecretVersion.aggregate([
interface LatestSecretVersionId { {
_id: Types.ObjectId; $match: {
version: number; secret: {
versionId: Types.ObjectId; $in: secretIds,
} },
},
let latestSecretVersionIds: LatestSecretVersionId[]; },
try { {
latestSecretVersionIds = (await SecretVersion.aggregate([ $group: {
{ _id: "$secret",
$match: { version: { $max: "$version" },
secret: { versionId: { $max: "$_id" }, // id of latest secret version
$in: secretIds },
} },
} {
}, $sort: { version: -1 },
{ },
$group: { ]).exec();
_id: '$secret',
version: { $max: '$version' }, return latestSecretVersionIds;
versionId: { $max: '$_id' } // id of latest secret version };
}
},
{
$sort: { version: -1 }
}
])
.exec());
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest secret versions');
}
return latestSecretVersionIds;
}
/** /**
* Return latest [n] secret versions for secrets with ids [secretIds] * Return latest [n] secret versions for secrets with ids [secretIds]
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.secretIds = ids of secrets to get latest versions for * @param {Object} obj.secretIds = ids of secrets to get latest versions for
* @param {Number} obj.n - number of latest secret versions to return for each secret * @param {Number} obj.n - number of latest secret versions to return for each secret
* @returns * @returns
*/ */
const getLatestNSecretSecretVersionIds = async ({ const getLatestNSecretSecretVersionIds = async ({
secretIds, secretIds,
n n,
}: { }: {
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
n: number; n: number;
}) => { }) => {
// TODO: optimize query
// TODO: optimize query const latestNSecretVersions = await SecretVersion.aggregate([
let latestNSecretVersions; {
try { $match: {
latestNSecretVersions = (await SecretVersion.aggregate([ secret: {
{ $in: secretIds,
$match: { },
secret: { },
$in: secretIds, },
}, {
}, $sort: { version: -1 },
}, },
{ {
$sort: { version: -1 }, $group: {
}, _id: "$secret",
{ versions: { $push: "$$ROOT" },
$group: { },
_id: "$secret", },
versions: { $push: "$$ROOT" }, {
}, $project: {
}, _id: 0,
{ secret: "$_id",
$project: { versions: { $slice: ["$versions", n] },
_id: 0, },
secret: "$_id", },
versions: { $slice: ["$versions", n] }, ]);
},
}
]));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest n secret versions');
}
return latestNSecretVersions;
}
export { return latestNSecretVersions;
getLatestSecretVersionIds, };
getLatestNSecretSecretVersionIds
} export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds };
@@ -15,32 +15,28 @@ import {
const requireSecretSnapshotAuth = ({ const requireSecretSnapshotAuth = ({
acceptedRoles, acceptedRoles,
}: { }: {
acceptedRoles: string[]; acceptedRoles: Array<'admin' | 'member'>;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { const { secretSnapshotId } = req.params;
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
if (!secretSnapshot) { return next(SecretSnapshotNotFoundError({
return next(SecretSnapshotNotFoundError({ message: 'Failed to find secret snapshot'
message: 'Failed to find secret snapshot' }));
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secretSnapshot.workspace.toString(),
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
} }
await validateMembership({
userId: req.user._id,
workspaceId: secretSnapshot.workspace,
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
} }
} }
+11 -2
View File
@@ -11,6 +11,8 @@ import {
export interface IAction { export interface IAction {
name: string; name: string;
user?: Types.ObjectId, user?: Types.ObjectId,
serviceAccount?: Types.ObjectId,
serviceTokenData?: Types.ObjectId,
workspace?: Types.ObjectId, workspace?: Types.ObjectId,
payload?: { payload?: {
secretVersions?: Types.ObjectId[] secretVersions?: Types.ObjectId[]
@@ -33,8 +35,15 @@ const actionSchema = new Schema<IAction>(
}, },
user: { user: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User', ref: 'User'
required: true },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
+60
View File
@@ -0,0 +1,60 @@
import { model, Schema, Types } from "mongoose";
export type TFolderRootVersionSchema = {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
nodes: TFolderVersionSchema;
};
export type TFolderVersionSchema = {
id: string;
name: string;
version: number;
children: TFolderVersionSchema[];
};
const folderVersionSchema = new Schema<TFolderVersionSchema>({
id: {
required: true,
type: String,
default: "root",
},
name: {
required: true,
type: String,
default: "root",
},
version: {
required: true,
type: Number,
default: 1,
},
});
folderVersionSchema.add({ children: [folderVersionSchema] });
const folderRootVersionSchema = new Schema<TFolderRootVersionSchema>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true,
},
environment: {
type: String,
required: true,
},
nodes: folderVersionSchema,
},
{
timestamps: true,
}
);
const FolderVersion = model<TFolderRootVersionSchema>(
"FolderVersion",
folderRootVersionSchema
);
export default FolderVersion;
+16 -13
View File
@@ -1,15 +1,18 @@
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot'; import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from './secretVersion'; import SecretVersion, { ISecretVersion } from "./secretVersion";
import Log, { ILog } from './log'; import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
import Action, { IAction } from './action'; import Log, { ILog } from "./log";
import Action, { IAction } from "./action";
export { export {
SecretSnapshot, SecretSnapshot,
ISecretSnapshot, ISecretSnapshot,
SecretVersion, SecretVersion,
ISecretVersion, ISecretVersion,
Log, FolderVersion,
ILog, TFolderRootVersionSchema,
Action, Log,
IAction ILog,
} Action,
IAction,
};
+10
View File
@@ -11,6 +11,8 @@ import {
export interface ILog { export interface ILog {
_id: Types.ObjectId; _id: Types.ObjectId;
user?: Types.ObjectId; user?: Types.ObjectId;
serviceAccount?: Types.ObjectId;
serviceTokenData?: Types.ObjectId;
workspace?: Types.ObjectId; workspace?: Types.ObjectId;
actionNames: string[]; actionNames: string[];
actions: Types.ObjectId[]; actions: Types.ObjectId[];
@@ -24,6 +26,14 @@ const logSchema = new Schema<ILog>(
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'User' ref: 'User'
}, },
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace' ref: 'Workspace'
+45 -24
View File
@@ -1,33 +1,54 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from "mongoose";
export interface ISecretSnapshot { export interface ISecretSnapshot {
workspace: Types.ObjectId; workspace: Types.ObjectId;
version: number; environment: string;
secretVersions: Types.ObjectId[]; folderId: string | "root";
version: number;
secretVersions: Types.ObjectId[];
folderVersion: Types.ObjectId;
} }
const secretSnapshotSchema = new Schema<ISecretSnapshot>( const secretSnapshotSchema = new Schema<ISecretSnapshot>(
{ {
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: "Workspace",
required: true required: true,
},
version: {
type: Number,
required: true
},
secretVersions: [{
type: Schema.Types.ObjectId,
ref: 'SecretVersion',
required: true
}]
}, },
{ environment: {
timestamps: true type: String,
} required: true,
},
folderId: {
type: String,
default: "root",
},
version: {
type: Number,
default: 1,
required: true,
},
secretVersions: [
{
type: Schema.Types.ObjectId,
ref: "SecretVersion",
required: true,
},
],
folderVersion: {
type: Schema.Types.ObjectId,
ref: "FolderVersion",
},
},
{
timestamps: true,
}
); );
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema); const SecretSnapshot = model<ISecretSnapshot>(
"SecretSnapshot",
secretSnapshotSchema
);
export default SecretSnapshot; export default SecretSnapshot;
+123 -88
View File
@@ -1,97 +1,132 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from "mongoose";
import { import {
SECRET_SHARED, SECRET_SHARED,
SECRET_PERSONAL, SECRET_PERSONAL,
} from '../../variables'; ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64,
} from "../../variables";
export interface ISecretVersion { export interface ISecretVersion {
secret: Types.ObjectId; _id: Types.ObjectId;
version: number; secret: Types.ObjectId;
workspace: Types.ObjectId; // new version: number;
type: string; // new workspace: Types.ObjectId; // new
user?: Types.ObjectId; // new type: string; // new
environment: string; // new user?: Types.ObjectId; // new
isDeleted: boolean; environment: string; // new
secretKeyCiphertext: string; isDeleted: boolean;
secretKeyIV: string; secretBlindIndex?: string;
secretKeyTag: string; secretKeyCiphertext: string;
secretValueCiphertext: string; secretKeyIV: string;
secretValueIV: string; secretKeyTag: string;
secretValueTag: string; secretValueCiphertext: string;
tags?: string[]; secretValueIV: string;
secretValueTag: string;
algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64";
createdAt: string;
folder?: string;
tags?: string[];
} }
const secretVersionSchema = new Schema<ISecretVersion>( const secretVersionSchema = new Schema<ISecretVersion>(
{ {
secret: { // could be deleted secret: {
type: Schema.Types.ObjectId, // could be deleted
ref: 'Secret', type: Schema.Types.ObjectId,
required: true ref: "Secret",
}, required: true,
version: { },
type: Number, version: {
default: 1, type: Number,
required: true default: 1,
}, required: true,
workspace: { },
type: Schema.Types.ObjectId, workspace: {
ref: 'Workspace', type: Schema.Types.ObjectId,
required: true ref: "Workspace",
}, required: true,
type: { },
type: String, type: {
enum: [SECRET_SHARED, SECRET_PERSONAL], type: String,
required: true enum: [SECRET_SHARED, SECRET_PERSONAL],
}, required: true,
user: { },
// user associated with the personal secret user: {
type: Schema.Types.ObjectId, // user associated with the personal secret
ref: 'User' type: Schema.Types.ObjectId,
}, ref: "User",
environment: { },
type: String, environment: {
required: true type: String,
}, required: true,
isDeleted: { // consider removing field },
type: Boolean, isDeleted: {
default: false, // consider removing field
required: true type: Boolean,
}, default: false,
secretKeyCiphertext: { required: true,
type: String, },
required: true secretBlindIndex: {
}, type: String,
secretKeyIV: { select: false,
type: String, // symmetric },
required: true secretKeyCiphertext: {
}, type: String,
secretKeyTag: { required: true,
type: String, // symmetric },
required: true secretKeyIV: {
}, type: String, // symmetric
secretValueCiphertext: { required: true,
type: String, },
required: true secretKeyTag: {
}, type: String, // symmetric
secretValueIV: { required: true,
type: String, // symmetric },
required: true secretValueCiphertext: {
}, type: String,
secretValueTag: { required: true,
type: String, // symmetric },
required: true secretValueIV: {
}, type: String, // symmetric
tags: { required: true,
ref: 'Tag', },
type: [Schema.Types.ObjectId], secretValueTag: {
default: [] type: String, // symmetric
}, required: true,
}, },
{ algorithm: {
timestamps: true // the encryption algorithm used
} type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
default: ALGORITHM_AES_256_GCM,
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
default: ENCODING_SCHEME_UTF8,
},
folder: {
type: String,
required: true,
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
},
{
timestamps: true,
}
); );
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema); const SecretVersion = model<ISecretVersion>(
"SecretVersion",
secretVersionSchema
);
export default SecretVersion; export default SecretVersion;
+20
View File
@@ -0,0 +1,20 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
validateRequest
} from '../../../middleware';
import { query } from 'express-validator';
import { cloudProductsController } from '../../controllers/v1';
router.get(
'/',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
query('billing-cycle').exists().isIn(['monthly', 'yearly']),
validateRequest,
cloudProductsController.getCloudProducts
);
export default router;
+5 -1
View File
@@ -1,11 +1,15 @@
import secret from './secret'; import secret from './secret';
import secretSnapshot from './secretSnapshot'; import secretSnapshot from './secretSnapshot';
import organizations from './organizations';
import workspace from './workspace'; import workspace from './workspace';
import action from './action'; import action from './action';
import cloudProducts from './cloudProducts';
export { export {
secret, secret,
secretSnapshot, secretSnapshot,
organizations,
workspace, workspace,
action action,
cloudProducts
} }
+88
View File
@@ -0,0 +1,88 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireOrganizationAuth,
validateRequest
} from '../../../middleware';
import { param, body, query } from 'express-validator';
import { organizationsController } from '../../controllers/v1';
import {
OWNER, ADMIN, MEMBER, ACCEPTED
} from '../../../variables';
router.get(
'/:organizationId/plan',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
query('workspaceId').optional().isString(),
validateRequest,
organizationsController.getOrganizationPlan
);
router.patch(
'/:organizationId/plan',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
body('productId').exists().isString(),
validateRequest,
organizationsController.updateOrganizationPlan
);
router.get(
'/:organizationId/billing-details/payment-methods',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
validateRequest,
organizationsController.getOrganizationPmtMethods
);
router.post(
'/:organizationId/billing-details/payment-methods',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
body('success_url').exists().isString(),
body('cancel_url').exists().isString(),
validateRequest,
organizationsController.addOrganizationPmtMethod
);
router.delete(
'/:organizationId/billing-details/payment-methods/:pmtMethodId',
requireAuth({
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireOrganizationAuth({
acceptedRoles: [OWNER, ADMIN, MEMBER],
acceptedStatuses: [ACCEPTED]
}),
param('organizationId').exists().trim(),
validateRequest,
organizationsController.deleteOrganizationPmtMethod
);
export default router;
+10 -3
View File
@@ -7,7 +7,12 @@ import {
} from '../../../middleware'; } from '../../../middleware';
import { query, param, body } from 'express-validator'; import { query, param, body } from 'express-validator';
import { secretController } from '../../controllers/v1'; import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER } from '../../../variables'; import {
ADMIN,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../../variables';
router.get( router.get(
'/:secretId/secret-versions', '/:secretId/secret-versions',
@@ -15,7 +20,8 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
param('secretId').exists().trim(), param('secretId').exists().trim(),
query('offset').exists().isInt(), query('offset').exists().isInt(),
@@ -30,7 +36,8 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ['jwt', 'apiKey']
}), }),
requireSecretAuth({ requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
}), }),
param('secretId').exists().trim(), param('secretId').exists().trim(),
body('version').exists().isInt(), body('version').exists().isInt(),
+1 -1
View File
@@ -7,7 +7,7 @@ import {
requireAuth, requireAuth,
validateRequest validateRequest
} from '../../../middleware'; } from '../../../middleware';
import { param, body } from 'express-validator'; import { param } from 'express-validator';
import { ADMIN, MEMBER } from '../../../variables'; import { ADMIN, MEMBER } from '../../../variables';
import { secretSnapshotController } from '../../controllers/v1'; import { secretSnapshotController } from '../../controllers/v1';
+67 -57
View File
@@ -1,72 +1,82 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest,
} from '../../../middleware'; } from "../../../middleware";
import { param, query, body } from 'express-validator'; import { param, query, body } from "express-validator";
import { ADMIN, MEMBER } from '../../../variables'; import { ADMIN, MEMBER } from "../../../variables";
import { workspaceController } from '../../controllers/v1'; import { workspaceController } from "../../controllers/v1";
router.get( router.get(
'/:workspaceId/secret-snapshots', "/:workspaceId/secret-snapshots",
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ["jwt", "apiKey"],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), locationWorkspaceId: "params",
param('workspaceId').exists().trim(), }),
query('offset').exists().isInt(), param("workspaceId").exists().trim(),
query('limit').exists().isInt(), query("environment").isString().exists().trim(),
validateRequest, query("folderId").default("root").isString().trim(),
workspaceController.getWorkspaceSecretSnapshots query("offset").exists().isInt(),
query("limit").exists().isInt(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshots
); );
router.get( router.get(
'/:workspaceId/secret-snapshots/count', "/:workspaceId/secret-snapshots/count",
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt'] acceptedAuthModes: ["jwt"],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), locationWorkspaceId: "params",
param('workspaceId').exists().trim(), }),
validateRequest, param("workspaceId").exists().trim(),
workspaceController.getWorkspaceSecretSnapshotsCount query("environment").isString().exists().trim(),
query("folderId").default("root").isString().trim(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshotsCount
); );
router.post( router.post(
'/:workspaceId/secret-snapshots/rollback', "/:workspaceId/secret-snapshots/rollback",
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ["jwt", "apiKey"],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), locationWorkspaceId: "params",
param('workspaceId').exists().trim(), }),
body('version').exists().isInt(), param("workspaceId").exists().trim(),
validateRequest, body("environment").isString().exists().trim(),
workspaceController.rollbackWorkspaceSecretSnapshot query("folderId").default("root").isString().exists().trim(),
body("version").exists().isInt(),
validateRequest,
workspaceController.rollbackWorkspaceSecretSnapshot
); );
router.get( router.get(
'/:workspaceId/logs', "/:workspaceId/logs",
requireAuth({ requireAuth({
acceptedAuthModes: ['jwt', 'apiKey'] acceptedAuthModes: ["jwt", "apiKey"],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), locationWorkspaceId: "params",
param('workspaceId').exists().trim(), }),
query('offset').exists().isInt(), param("workspaceId").exists().trim(),
query('limit').exists().isInt(), query("offset").exists().isInt(),
query('sortBy'), query("limit").exists().isInt(),
query('userId'), query("sortBy"),
query('actionNames'), query("userId"),
validateRequest, query("actionNames"),
workspaceController.getWorkspaceLogs validateRequest,
workspaceController.getWorkspaceLogs
); );
export default router; export default router;
+142 -4
View File
@@ -1,12 +1,150 @@
import * as Sentry from '@sentry/node';
import NodeCache from 'node-cache';
import {
getLicenseKey,
getLicenseServerKey,
getLicenseServerUrl
} from '../../config';
import {
licenseKeyRequest,
licenseServerKeyRequest,
refreshLicenseServerKeyToken,
refreshLicenseKeyToken
} from '../../config/request';
import { Organization } from '../../models';
import { OrganizationNotFoundError } from '../../utils/errors';
interface FeatureSet {
_id: string | null;
slug: 'starter' | 'team' | 'pro' | 'enterprise' | null;
tier: number;
workspaceLimit: number | null;
workspacesUsed: number;
memberLimit: number | null;
membersUsed: number;
environmentLimit: number | null;
environmentsUsed: number;
secretVersioning: boolean;
pitRecovery: boolean;
rbac: boolean;
customRateLimits: boolean;
customAlerts: boolean;
auditLogs: boolean;
envLimit?: number | null;
}
/** /**
* Class to handle Enterprise Edition license actions * Class to handle license/plan configurations:
* - Infisical Cloud: Fetch and cache customer plans in [localFeatureSet]
* - Self-hosted regular: Use default global feature set
* - Self-hosted enterprise: Fetch and update global feature set
*/ */
class EELicenseService { class EELicenseService {
private readonly _isLicenseValid: boolean; private readonly _isLicenseValid: boolean; // TODO: deprecate
public instanceType: 'self-hosted' | 'enterprise-self-hosted' | 'cloud' = 'self-hosted';
public globalFeatureSet: FeatureSet = {
_id: null,
slug: null,
tier: -1,
workspaceLimit: null,
workspacesUsed: 0,
memberLimit: null,
membersUsed: 0,
environmentLimit: null,
environmentsUsed: 0,
secretVersioning: true,
pitRecovery: true,
rbac: true,
customRateLimits: true,
customAlerts: true,
auditLogs: false,
envLimit: null
}
public localFeatureSet: NodeCache;
constructor(licenseKey: string) { constructor() {
this._isLicenseValid = true; this._isLicenseValid = true;
this.localFeatureSet = new NodeCache({
stdTTL: 300
});
}
public async getPlan(organizationId: string, workspaceId?: string): Promise<FeatureSet> {
try {
if (this.instanceType === 'cloud') {
const cachedPlan = this.localFeatureSet.get<FeatureSet>(`${organizationId}-${workspaceId ?? ''}`);
if (cachedPlan) {
return cachedPlan;
}
const organization = await Organization.findById(organizationId);
if (!organization) throw OrganizationNotFoundError();
let url = `${await getLicenseServerUrl()}/api/license-server/v1/customers/${organization.customerId}/cloud-plan`;
if (workspaceId) {
url += `?workspaceId=${workspaceId}`;
}
const { data: { currentPlan } } = await licenseServerKeyRequest.get(url);
// cache fetched plan for organization
this.localFeatureSet.set(`${organizationId}-${workspaceId ?? ''}`, currentPlan);
return currentPlan;
}
} catch (err) {
return this.globalFeatureSet;
}
return this.globalFeatureSet;
}
public async refreshPlan(organizationId: string, workspaceId?: string) {
if (this.instanceType === 'cloud') {
this.localFeatureSet.del(`${organizationId}-${workspaceId ?? ''}`);
await this.getPlan(organizationId, workspaceId);
}
}
public async initGlobalFeatureSet() {
const licenseServerKey = await getLicenseServerKey();
const licenseKey = await getLicenseKey();
try {
if (licenseServerKey) {
// license server key is present -> validate it
const token = await refreshLicenseServerKeyToken()
if (token) {
this.instanceType = 'cloud';
}
return;
}
if (licenseKey) {
// license key is present -> validate it
const token = await refreshLicenseKeyToken();
if (token) {
const { data: { currentPlan } } = await licenseKeyRequest.get(
`${await getLicenseServerUrl()}/api/license/v1/plan`
);
this.globalFeatureSet = currentPlan;
this.instanceType = 'enterprise-self-hosted';
}
}
} catch (err) {
// case: self-hosted free
Sentry.setUser(null);
Sentry.captureException(err);
}
} }
public get isLicenseValid(): boolean { public get isLicenseValid(): boolean {
@@ -14,4 +152,4 @@ class EELicenseService {
} }
} }
export default new EELicenseService('N/A'); export default new EELicenseService();
+14 -2
View File
@@ -26,12 +26,16 @@ class EELogService {
*/ */
static async createLog({ static async createLog({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
ipAddress ipAddress
}: { }: {
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
actions: IAction[]; actions: IAction[];
channel: string; channel: string;
@@ -40,6 +44,8 @@ class EELogService {
if (!EELicenseService.isLicenseValid) return null; if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({ return await createLogHelper({
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
actions, actions,
channel, channel,
@@ -59,17 +65,23 @@ class EELogService {
static async createAction({ static async createAction({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}: { }: {
name: string; name: string;
userId: Types.ObjectId; userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
workspaceId?: Types.ObjectId; workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) { }) {
return await createActionHelper({ return await createActionHelper({
name, name,
userId, userId,
serviceAccountId,
serviceTokenDataId,
workspaceId, workspaceId,
secretIds secretIds
}); });
+63 -66
View File
@@ -1,10 +1,9 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { ISecretVersion } from '../models'; import { ISecretVersion } from '../models';
import { import {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper, addSecretVersionsHelper,
markDeletedSecretVersionsHelper, markDeletedSecretVersionsHelper,
initSecretVersioningHelper
} from '../helpers/secret'; } from '../helpers/secret';
import EELicenseService from './EELicenseService'; import EELicenseService from './EELicenseService';
@@ -12,67 +11,65 @@ import EELicenseService from './EELicenseService';
* Class to handle Enterprise Edition secret actions * Class to handle Enterprise Edition secret actions
*/ */
class EESecretService { class EESecretService {
/**
/** * Save a secret snapshot that is a copy of the current state of secrets in workspace with id
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id * [workspaceId] under a new snapshot with incremented version under the
* [workspaceId] under a new snapshot with incremented version under the * SecretSnapshot collection.
* SecretSnapshot collection. * Requires a valid license key [licenseKey]
* Requires a valid license key [licenseKey] * @param {Object} obj
* @param {Object} obj * @param {String} obj.workspaceId
* @param {String} obj.workspaceId * @returns {SecretSnapshot} secretSnapshot - new secret snpashot
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot */
*/ static async takeSecretSnapshot({
static async takeSecretSnapshot({ workspaceId,
workspaceId environment,
}: { folderId,
workspaceId: string; }: {
}) { workspaceId: Types.ObjectId;
if (!EELicenseService.isLicenseValid) return; environment: string;
return await takeSecretSnapshotHelper({ workspaceId }); folderId?: string;
} }) {
if (!EELicenseService.isLicenseValid) return;
/** return await takeSecretSnapshotHelper({
* Add secret versions [secretVersions] to the SecretVersion collection. workspaceId,
* @param {Object} obj environment,
* @param {Object[]} obj.secretVersions folderId,
* @returns {SecretVersion[]} newSecretVersions - new secret versions });
*/ }
static async addSecretVersions({
secretVersions
}: {
secretVersions: ISecretVersion[];
}) {
if (!EELicenseService.isLicenseValid) return;
return await addSecretVersionsHelper({
secretVersions
});
}
/** /**
* Mark secret versions associated with secrets with ids [secretIds] * Add secret versions [secretVersions] to the SecretVersion collection.
* as deleted. * @param {Object} obj
* @param {Object} obj * @param {Object[]} obj.secretVersions
* @param {ObjectId[]} obj.secretIds - secret ids * @returns {SecretVersion[]} newSecretVersions - new secret versions
*/ */
static async markDeletedSecretVersions({ static async addSecretVersions({
secretIds secretVersions,
}: { }: {
secretIds: Types.ObjectId[]; secretVersions: ISecretVersion[];
}) { }) {
if (!EELicenseService.isLicenseValid) return; if (!EELicenseService.isLicenseValid) return;
await markDeletedSecretVersionsHelper({ return await addSecretVersionsHelper({
secretIds secretVersions,
}); });
} }
/** /**
* Initialize secret versioning by setting previously unversioned * Mark secret versions associated with secrets with ids [secretIds]
* secrets to version 1 and begin populating secret versions. * as deleted.
*/ * @param {Object} obj
static async initSecretVersioning() { * @param {ObjectId[]} obj.secretIds - secret ids
if (!EELicenseService.isLicenseValid) return; */
await initSecretVersioningHelper(); static async markDeletedSecretVersions({
} secretIds,
}: {
secretIds: Types.ObjectId[];
}) {
if (!EELicenseService.isLicenseValid) return;
await markDeletedSecretVersionsHelper({
secretIds,
});
}
} }
export default EESecretService; export default EESecretService;
+6 -2
View File
@@ -1,3 +1,4 @@
import { Types } from 'mongoose';
import { import {
EVENT_PUSH_SECRETS, EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS EVENT_PULL_SECRETS
@@ -22,13 +23,16 @@ interface PushSecret {
* @returns * @returns
*/ */
const eventPushSecrets = ({ const eventPushSecrets = ({
workspaceId workspaceId,
environment
}: { }: {
workspaceId: string; workspaceId: Types.ObjectId;
environment?: string;
}) => { }) => {
return ({ return ({
name: EVENT_PUSH_SECRETS, name: EVENT_PUSH_SECRETS,
workspaceId, workspaceId,
environment,
payload: { payload: {
} }
+232 -140
View File
@@ -1,15 +1,19 @@
import * as Sentry from '@sentry/node'; import { Types } from 'mongoose';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
import { import {
IUser, IUser,
User, User,
ServiceTokenData, ServiceTokenData,
APIKeyData ServiceAccount,
APIKeyData,
TokenVersion,
ITokenVersion
} from '../models'; } from '../models';
import { import {
AccountNotFoundError, AccountNotFoundError,
ServiceTokenDataNotFoundError, ServiceTokenDataNotFoundError,
ServiceAccountNotFoundError,
APIKeyDataNotFoundError, APIKeyDataNotFoundError,
UnauthorizedRequestError, UnauthorizedRequestError,
BadRequestError BadRequestError
@@ -17,27 +21,33 @@ import {
import { import {
getJwtAuthLifetime, getJwtAuthLifetime,
getJwtAuthSecret, getJwtAuthSecret,
getJwtProviderAuthSecret,
getJwtRefreshLifetime, getJwtRefreshLifetime,
getJwtRefreshSecret getJwtRefreshSecret
} from '../config'; } from '../config';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/** /**
* *
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.headers - HTTP request headers object * @param {Object} obj.headers - HTTP request headers object
*/ */
const validateAuthMode = ({ export const validateAuthMode = ({
headers, headers,
acceptedAuthModes acceptedAuthModes
}: { }: {
headers: { [key: string]: string | string[] | undefined }, headers: { [key: string]: string | string[] | undefined },
acceptedAuthModes: string[] acceptedAuthModes: string[]
}) => { }) => {
// TODO: refactor middleware
const apiKey = headers['x-api-key']; const apiKey = headers['x-api-key'];
const authHeader = headers['authorization']; const authHeader = headers['authorization'];
let authTokenType, authTokenValue; let authMode, authTokenValue;
if (apiKey === undefined && authHeader === undefined) { if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present // case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' }); throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
@@ -45,7 +55,7 @@ const validateAuthMode = ({
if (typeof apiKey === 'string') { if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key) // case: treat request authentication type as via X-API-KEY (i.e. API Key)
authTokenType = 'apiKey'; authMode = AUTH_MODE_API_KEY;
authTokenValue = apiKey; authTokenValue = apiKey;
} }
@@ -61,20 +71,24 @@ const validateAuthMode = ({
switch (tokenValue.split('.', 1)[0]) { switch (tokenValue.split('.', 1)[0]) {
case 'st': case 'st':
authTokenType = 'serviceToken'; authMode = AUTH_MODE_SERVICE_TOKEN;
break;
case 'sa':
authMode = AUTH_MODE_SERVICE_ACCOUNT;
break; break;
default: default:
authTokenType = 'jwt'; authMode = AUTH_MODE_JWT;
} }
authTokenValue = tokenValue; authTokenValue = tokenValue;
} }
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' }); if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' }); if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({ return ({
authTokenType, authMode,
authTokenValue authTokenValue
}); });
} }
@@ -85,32 +99,42 @@ const validateAuthMode = ({
* @param {String} obj.authTokenValue - JWT token value * @param {String} obj.authTokenValue - JWT token value
* @returns {User} user - user corresponding to JWT token * @returns {User} user - user corresponding to JWT token
*/ */
const getAuthUserPayload = async ({ export const getAuthUserPayload = async ({
authTokenValue authTokenValue
}: { }: {
authTokenValue: string; authTokenValue: string;
}) => { }) => {
let user; const decodedToken = <jwt.UserIDJwtPayload>(
try { jwt.verify(authTokenValue, await getJwtAuthSecret())
const decodedToken = <jwt.UserIDJwtPayload>( );
jwt.verify(authTokenValue, getJwtAuthSecret())
);
user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _id: new Types.ObjectId(decodedToken.userId)
}).select('+publicKey'); }).select('+publicKey +accessVersion');
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' }); if (!user) throw AccountNotFoundError({ message: 'Failed to find user' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' }); if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate user with partially set up account' });
} catch (err) { const tokenVersion = await TokenVersion.findOneAndUpdate({
throw UnauthorizedRequestError({ _id: new Types.ObjectId(decodedToken.tokenVersionId),
message: 'Failed to authenticate JWT token' user: user._id
}); }, {
} lastUsed: new Date()
});
if (!tokenVersion) throw UnauthorizedRequestError({
message: 'Failed to validate access token'
});
if (decodedToken.accessVersion !== tokenVersion.accessVersion) throw UnauthorizedRequestError({
message: 'Failed to validate access token'
});
return user; return ({
user,
tokenVersionId: tokenVersion._id
});
} }
/** /**
@@ -119,88 +143,124 @@ const getAuthUserPayload = async ({
* @param {String} obj.authTokenValue - service token value * @param {String} obj.authTokenValue - service token value
* @returns {ServiceTokenData} serviceTokenData - service token data * @returns {ServiceTokenData} serviceTokenData - service token data
*/ */
const getAuthSTDPayload = async ({ export const getAuthSTDPayload = async ({
authTokenValue authTokenValue
}: { }: {
authTokenValue: string; authTokenValue: string;
}) => { }) => {
let serviceTokenData; const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
// TODO: optimize double query let serviceTokenData = await ServiceTokenData
serviceTokenData = await ServiceTokenData .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
if (!serviceTokenData) { if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' }); throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) { } else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired // case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id); await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired service token'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} catch (err) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: 'Failed to authenticate service token' message: 'Failed to authenticate expired service token'
}); });
} }
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
})
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
return serviceTokenData; return serviceTokenData;
} }
/**
* Return service account access key payload
* @param {Object} obj
* @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount
*/
export const getAuthSAAKPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const serviceAccount = await ServiceAccount.findById(
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
).select('+secretHash');
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
if (!result) throw UnauthorizedRequestError({
message: 'Failed to authenticate service account access key'
});
return serviceAccount;
}
/** /**
* Return API key data payload corresponding to API key [authTokenValue] * Return API key data payload corresponding to API key [authTokenValue]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.authTokenValue - API key value * @param {String} obj.authTokenValue - API key value
* @returns {APIKeyData} apiKeyData - API key data * @returns {APIKeyData} apiKeyData - API key data
*/ */
const getAuthAPIKeyPayload = async ({ export const getAuthAPIKeyPayload = async ({
authTokenValue authTokenValue
}: { }: {
authTokenValue: string; authTokenValue: string;
}) => { }) => {
let user; const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const apiKeyData = await APIKeyData let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt') .findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey'); .populate<{ user: IUser }>('user', '+publicKey');
if (!apiKeyData) { if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' }); throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) { } else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired // case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id); await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
user = apiKeyData.user;
} catch (err) {
throw UnauthorizedRequestError({ throw UnauthorizedRequestError({
message: 'Failed to authenticate API key' message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
});
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
}
const user = await User.findById(apiKeyData.user).select('+publicKey');
if (!user) {
throw AccountNotFoundError({
message: 'Failed to find user'
}); });
} }
@@ -215,32 +275,58 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.token - issued JWT token * @return {String} obj.token - issued JWT token
* @return {String} obj.refreshToken - issued refresh token * @return {String} obj.refreshToken - issued refresh token
*/ */
const issueAuthTokens = async ({ userId }: { userId: string }) => { export const issueAuthTokens = async ({
let token: string; userId,
let refreshToken: string; ip,
try { userAgent
// issue tokens }: {
token = createToken({ userId: Types.ObjectId;
payload: { ip: string;
userId userAgent: string;
}, }) => {
expiresIn: getJwtAuthLifetime(), let tokenVersion: ITokenVersion | null;
secret: getJwtAuthSecret()
});
refreshToken = createToken({ // continue with (session) token version matching existing ip and user agent
payload: { tokenVersion = await TokenVersion.findOne({
userId user: userId,
}, ip,
expiresIn: getJwtRefreshLifetime(), userAgent
secret: getJwtRefreshSecret() });
});
} catch (err) { if (!tokenVersion) {
Sentry.setUser(null); // case: no existing ip and user agent exists
Sentry.captureException(err); // -> create new (session) token version for ip and user agent
throw new Error('Failed to issue tokens'); tokenVersion = await new TokenVersion({
user: userId,
refreshVersion: 0,
accessVersion: 0,
ip,
userAgent,
lastUsed: new Date()
}).save();
} }
// issue tokens
const token = createToken({
payload: {
userId,
tokenVersionId: tokenVersion._id.toString(),
accessVersion: tokenVersion.accessVersion
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
});
const refreshToken = createToken({
payload: {
userId,
tokenVersionId: tokenVersion._id.toString(),
refreshVersion: tokenVersion.refreshVersion
},
expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret()
});
return { return {
token, token,
refreshToken refreshToken
@@ -252,20 +338,17 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {Object} obj * @param {Object} obj
* @param {String} obj.userId - id of user whose tokens are cleared. * @param {String} obj.userId - id of user whose tokens are cleared.
*/ */
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => { export const clearTokens = async (tokenVersionId: Types.ObjectId): Promise<void> => {
try { // increment refreshVersion on user by 1
// increment refreshVersion on user by 1
User.findOneAndUpdate({ await TokenVersion.findOneAndUpdate({
_id: userId _id: tokenVersionId
}, { }, {
$inc: { $inc: {
refreshVersion: 1 refreshVersion: 1,
} accessVersion: 1
}); }
} catch (err) { });
Sentry.setUser(null);
Sentry.captureException(err);
}
}; };
/** /**
@@ -276,7 +359,7 @@ const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
* @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET] * @param {String} obj.secret - (JWT) secret such as [JWT_AUTH_SECRET]
* @param {String} obj.expiresIn - string describing time span such as '10h' or '7d' * @param {String} obj.expiresIn - string describing time span such as '10h' or '7d'
*/ */
const createToken = ({ export const createToken = ({
payload, payload,
expiresIn, expiresIn,
secret secret
@@ -285,23 +368,32 @@ const createToken = ({
expiresIn: string | number; expiresIn: string | number;
secret: string; secret: string;
}) => { }) => {
try { return jwt.sign(payload, secret, {
return jwt.sign(payload, secret, { expiresIn
expiresIn });
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create a token');
}
}; };
export { export const validateProviderAuthToken = async ({
validateAuthMode, email,
getAuthUserPayload, user,
getAuthSTDPayload, providerAuthToken,
getAuthAPIKeyPayload, }: {
createToken, email: string;
issueAuthTokens, user: IUser,
clearTokens providerAuthToken?: string;
}; }) => {
if (!providerAuthToken) {
throw new Error('Invalid authentication request.');
}
const decodedToken = <jwt.ProviderAuthJwtPayload>(
jwt.verify(providerAuthToken, await getJwtProviderAuthSecret())
);
if (
decodedToken.authProvider !== user.authProvider ||
decodedToken.email !== email
) {
throw new Error('Invalid authentication credentials.')
}
}
+203 -175
View File
@@ -1,57 +1,101 @@
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { import {
Bot, Bot,
BotKey, BotKey,
Secret, Secret,
ISecret, ISecret,
IUser IUser
} from '../models'; } from "../models";
import { import {
generateKeyPair, generateKeyPair,
encryptSymmetric, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric, decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric decryptAsymmetric
} from '../utils/crypto'; } from '../utils/crypto';
import { SECRET_SHARED } from '../variables'; import {
import { getEncryptionKey } from '../config'; SECRET_SHARED,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
} from "../variables";
import {
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { InternalServerError } from "../utils/errors";
/** /**
* Create an inactive bot with name [name] for workspace with id [workspaceId] * Create an inactive bot with name [name] for workspace with id [workspaceId]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.name - name of bot * @param {String} obj.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to * @param {String} obj.workspaceId - id of workspace that bot belongs to
*/ */
const createBot = async ({ export const createBot = async ({
name, name,
workspaceId, workspaceId,
}: { }: {
name: string; name: string;
workspaceId: string; workspaceId: Types.ObjectId;
}) => { }) => {
let bot; const encryptionKey = await getEncryptionKey();
try { const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({ const { publicKey, privateKey } = generateKeyPair();
plaintext: privateKey,
key: getEncryptionKey() if (rootEncryptionKey) {
}); const {
ciphertext,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
bot = await new Bot({ return await new Bot({
name, name,
workspace: workspaceId, workspace: workspaceId,
isActive: false, isActive: false,
publicKey, publicKey,
encryptedPrivateKey: ciphertext, encryptedPrivateKey: ciphertext,
iv, iv,
tag tag,
}).save(); algorithm: ALGORITHM_AES_256_GCM,
} catch (err) { keyEncoding: ENCODING_SCHEME_BASE64
Sentry.setUser(null); }).save();
Sentry.captureException(err);
throw new Error('Failed to create bot'); } else if (encryptionKey) {
} const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
return bot; key: await getEncryptionKey(),
});
return await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}).save();
}
throw InternalServerError({
message: 'Failed to create new bot due to missing encryption key'
});
};
/**
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
* @param {Types.ObjectId} workspaceId - id of workspace to check
*/
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
const botKey = await BotKey.exists({
workspace: workspaceId
});
return botKey ? false : true;
} }
/** /**
@@ -61,126 +105,126 @@ const createBot = async ({
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment * @param {String} obj.environment - environment
*/ */
const getSecretsHelper = async ({ export const getSecretsBotHelper = async ({
workspaceId, workspaceId,
environment environment,
}: { }: {
workspaceId: string; workspaceId: Types.ObjectId;
environment: string; environment: string;
}) => { }) => {
const content = {} as any; const content = {} as any;
try { const key = await getKey({ workspaceId: workspaceId });
const key = await getKey({ workspaceId }); const secrets = await Secret.find({
const secrets = await Secret.find({ workspace: workspaceId,
workspace: workspaceId, environment,
environment, type: SECRET_SHARED,
type: SECRET_SHARED });
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({ secrets.forEach((secret: ISecret) => {
ciphertext: secret.secretValueCiphertext, const secretKey = decryptSymmetric128BitHexKeyUTF8({
iv: secret.secretValueIV, ciphertext: secret.secretKeyCiphertext,
tag: secret.secretValueTag, iv: secret.secretKeyIV,
key tag: secret.secretKeyTag,
}); key,
});
content[secretKey] = secretValue; const secretValue = decryptSymmetric128BitHexKeyUTF8({
}); ciphertext: secret.secretValueCiphertext,
} catch (err) { iv: secret.secretValueIV,
Sentry.setUser(null); tag: secret.secretValueTag,
Sentry.captureException(err); key,
throw new Error('Failed to get secrets'); });
}
return content; content[secretKey] = secretValue;
} });
return content;
};
/** /**
* Return bot's copy of the workspace key for workspace * Return bot's copy of the workspace key for workspace
* with id [workspaceId] * with id [workspaceId]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key * @returns {String} key - decrypted workspace key
*/ */
const getKey = async ({ workspaceId }: { workspaceId: string }) => { export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => {
let key; const encryptionKey = await getEncryptionKey();
try { const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId const botKey = await BotKey.findOne({
}).populate<{ sender: IUser }>('sender', 'publicKey'); workspace: workspaceId,
})
if (!botKey) throw new Error('Failed to find bot key'); .populate<{ sender: IUser }>("sender", "publicKey");
const bot = await Bot.findOne({ if (!botKey) throw new Error("Failed to find bot key");
workspace: workspaceId
}).select('+encryptedPrivateKey +iv +tag'); const bot = await Bot.findOne({
workspace: workspaceId,
if (!bot) throw new Error('Failed to find bot'); }).select("+encryptedPrivateKey +iv +tag +algorithm +keyEncoding");
if (!bot.isActive) throw new Error('Bot is not active');
if (!bot) throw new Error("Failed to find bot");
const privateKeyBot = decryptSymmetric({ if (!bot.isActive) throw new Error("Bot is not active");
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv, if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
tag: bot.tag, // case: encoding scheme is base64
key: getEncryptionKey() const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag);
});
return decryptAsymmetric({
key = decryptAsymmetric({ ciphertext: botKey.encryptedKey,
ciphertext: botKey.encryptedKey, nonce: botKey.nonce,
nonce: botKey.nonce, publicKey: botKey.sender.publicKey as string,
publicKey: botKey.sender.publicKey as string, privateKey: privateKeyBot,
privateKey: privateKeyBot });
}); } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get workspace key');
}
return key; // case: encoding scheme is utf8
} const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
}
throw InternalServerError({
message: "Failed to obtain bot's copy of workspace key needed for bot operations"
});
};
/** /**
* Return symmetrically encrypted [plaintext] using the * Return symmetrically encrypted [plaintext] using the
* key for workspace with id [workspaceId] * key for workspace with id [workspaceId]
* @param {Object} obj1 * @param {Object} obj1
* @param {String} obj1.workspaceId - id of workspace * @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt * @param {String} obj1.plaintext - plaintext to encrypt
*/ */
const encryptSymmetricHelper = async ({ export const encryptSymmetricHelper = async ({
workspaceId, workspaceId,
plaintext plaintext,
}: { }: {
workspaceId: string; workspaceId: Types.ObjectId;
plaintext: string; plaintext: string;
}) => { }) => {
const key = await getKey({ workspaceId: workspaceId });
try { const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
const key = await getKey({ workspaceId }); plaintext,
const { ciphertext, iv, tag } = encryptSymmetric({ key,
plaintext, });
key
}); return {
ciphertext,
return ({ iv,
ciphertext, tag,
iv, };
tag };
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption with bot');
}
}
/** /**
* Return symmetrically decrypted [ciphertext] using the * Return symmetrically decrypted [ciphertext] using the
* key for workspace with id [workspaceId] * key for workspace with id [workspaceId]
@@ -190,40 +234,24 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.iv - iv * @param {String} obj.iv - iv
* @param {String} obj.tag - tag * @param {String} obj.tag - tag
*/ */
const decryptSymmetricHelper = async ({ export const decryptSymmetricHelper = async ({
workspaceId, workspaceId,
ciphertext,
iv,
tag,
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;
}) => {
const key = await getKey({ workspaceId: workspaceId });
const plaintext = decryptSymmetric128BitHexKeyUTF8({
ciphertext, ciphertext,
iv, iv,
tag tag,
}: { key,
workspaceId: string; });
ciphertext: string;
iv: string;
tag: string;
}) => {
let plaintext;
try {
const key = await getKey({ workspaceId });
const plaintext = decryptSymmetric({
ciphertext,
iv,
tag,
key
});
return plaintext;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption with bot');
}
return plaintext;
}
export { return plaintext;
createBot, };
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
}
+5 -12
View File
@@ -1,5 +1,4 @@
import mongoose from 'mongoose'; import mongoose from 'mongoose';
import { EESecretService } from '../ee/services';
import { getLogger } from '../utils/logger'; import { getLogger } from '../utils/logger';
/** /**
@@ -8,7 +7,7 @@ import { getLogger } from '../utils/logger';
* @param {String} obj.mongoURL - mongo connection string * @param {String} obj.mongoURL - mongo connection string
* @returns * @returns
*/ */
const initDatabaseHelper = async ({ export const initDatabaseHelper = async ({
mongoURL mongoURL
}: { }: {
mongoURL: string; mongoURL: string;
@@ -19,11 +18,10 @@ const initDatabaseHelper = async ({
// allow empty strings to pass the required validator // allow empty strings to pass the required validator
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string'); mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
getLogger("database").info("Database connection established"); (await getLogger("database")).info("Database connection established");
await EESecretService.initSecretVersioning();
} catch (err) { } catch (err) {
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`); (await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
} }
return mongoose.connection; return mongoose.connection;
@@ -32,7 +30,7 @@ const initDatabaseHelper = async ({
/** /**
* Close database conection * Close database conection
*/ */
const closeDatabaseHelper = async () => { export const closeDatabaseHelper = async () => {
return Promise.all([ return Promise.all([
new Promise((resolve) => { new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) { if (mongoose.connection && mongoose.connection.readyState == 1) {
@@ -43,9 +41,4 @@ const closeDatabaseHelper = async () => {
} }
}) })
]); ]);
}
export {
initDatabaseHelper,
closeDatabaseHelper
} }
+27 -38
View File
@@ -1,12 +1,13 @@
import { Bot, IBot } from '../models'; import { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import { Bot, IBot } from "../models";
import { EVENT_PUSH_SECRETS } from '../variables'; import { EVENT_PUSH_SECRETS } from "../variables";
import { IntegrationService } from '../services'; import { IntegrationService } from "../services";
interface Event { interface Event {
name: string; name: string;
workspaceId: string; workspaceId: Types.ObjectId;
payload: any; environment?: string;
payload: any;
} }
/** /**
@@ -17,35 +18,23 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of * @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event) * @param {Object} obj.event.payload - payload of event (depends on event)
*/ */
const handleEventHelper = async ({ export const handleEventHelper = async ({ event }: { event: Event }) => {
event const { workspaceId, environment } = event;
}: {
event: Event;
}) => {
const { workspaceId } = event;
// TODO: moduralize bot check into separate function
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
if (!bot) return;
try {
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
}
export { // TODO: moduralize bot check into separate function
handleEventHelper const bot = await Bot.findOne({
} workspace: workspaceId,
isActive: true,
});
if (!bot) return;
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId,
environment,
});
break;
}
};
+17
View File
@@ -0,0 +1,17 @@
export * from './auth';
export * from './bot';
export * from './database';
export * from './event';
export * from './integration';
export * from './key';
export * from './membership';
export * from './membershipOrg';
export * from './nodemailer';
export * from './organization';
export * from './rateLimiter';
export * from './secret';
export * from './secrets';
export * from './signup';
export * from './token';
export * from './user';
export * from './workspace';
+190 -237
View File
@@ -1,17 +1,20 @@
import * as Sentry from '@sentry/node'; import { Types } from 'mongoose';
import { import {
Bot, Bot,
Integration, Integration,
IntegrationAuth, IntegrationAuth
} from '../models'; } from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services'; import { BotService } from '../services';
import { import {
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables'; } from '../variables';
import { UnauthorizedRequestError } from '../utils/errors'; import {
import RequestError from '../utils/requestError'; UnauthorizedRequestError,
} from '../utils/errors';
interface Update { interface Update {
workspace: string; workspace: string;
@@ -32,7 +35,7 @@ interface Update {
* @param {String} obj.code - code * @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/ */
const handleOAuthExchangeHelper = async ({ export const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration, integration,
code, code,
@@ -43,66 +46,59 @@ const handleOAuthExchangeHelper = async ({
code: string; code: string;
environment: string; environment: string;
}) => { }) => {
let integrationAuth; const bot = await Bot.findOne({
try { workspace: workspaceId,
const bot = await Bot.findOne({ isActive: true
workspace: workspaceId, });
isActive: true
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code
});
const update: Update = {
workspace: workspaceId,
integration
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
}); });
}
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
if (res.accessToken) {
// exchange code for access and refresh tokens // case: access token returned from exchange
const res = await exchangeCode({ // set integration auth access token
integration, await setIntegrationAuthAccessHelper({
code integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
}); });
const update: Update = {
workspace: workspaceId,
integration
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
});
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
} }
return integrationAuth; return integrationAuth;
@@ -113,49 +109,47 @@ const handleOAuthExchangeHelper = async ({
* @param {Object} obj * @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace * @param {Object} obj.workspaceId - id of workspace
*/ */
const syncIntegrationsHelper = async ({ export const syncIntegrationsHelper = async ({
workspaceId workspaceId,
environment
}: { }: {
workspaceId: string; workspaceId: Types.ObjectId;
environment?: string;
}) => { }) => {
let integrations; const integrations = await Integration.find({
try { workspace: workspaceId,
integrations = await Integration.find({ ...(environment ? {
workspace: workspaceId, environment
isActive: true, } : {}),
app: { $ne: null } isActive: true,
app: { $ne: null }
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace,
environment: integration.environment
}); });
// for each workspace integration, sync/push secrets const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
// to that integration if (!integrationAuth) throw new Error('Failed to find integration auth');
for await (const integration of integrations) {
// get workspace, environment (shared) secrets // get integration auth access token
const secrets = await BotService.getSecrets({ // issue here? const access = await getIntegrationAuthAccessHelper({
workspaceId: integration.workspace.toString(), integrationAuthId: integration.integrationAuth
environment: integration.environment });
});
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); // sync secrets to integration
if (!integrationAuth) throw new Error('Failed to find integration auth'); await syncSecrets({
integration,
// get integration auth access token integrationAuth,
const access = await getIntegrationAuthAccessHelper({ secrets,
integrationAuthId: integration.integrationAuth.toString() accessId: access.accessId === undefined ? null : access.accessId,
}); accessToken: access.accessToken
});
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId,
accessToken: access.accessToken
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integrations');
} }
} }
@@ -167,31 +161,19 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token * @param {String} refreshToken - decrypted refresh token
*/ */
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let refreshToken; const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
try { .select('+refreshCiphertext +refreshIV +refreshTag');
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
refreshToken = await BotService.decryptSymmetric({ const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(), workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string, ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string, iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string tag: integrationAuth.refreshTag as string
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token');
}
return refreshToken; return refreshToken;
} }
@@ -204,53 +186,43 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token * @returns {String} accessToken - decrypted access token
*/ */
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId; let accessId;
let accessToken; let accessToken;
try { const integrationAuth = await IntegrationAuth
const integrationAuth = await IntegrationAuth .findById(integrationAuthId)
.findById(integrationAuthId) .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
accessToken = await BotService.decryptSymmetric({ accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(), workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string, ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string, iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string tag: integrationAuth.accessTag as string
}); });
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date // there is a access token expiration date
// and refresh token to exchange with the OAuth2 server // and refresh token to exchange with the OAuth2 server
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken
});
}
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) { if (integrationAuth.accessExpiresAt < new Date()) {
accessId = await BotService.decryptSymmetric({ // access token is expired
workspaceId: integrationAuth.workspace.toString(), const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
ciphertext: integrationAuth.accessIdCiphertext as string, accessToken = await exchangeRefresh({
iv: integrationAuth.accessIdIV as string, integrationAuth,
tag: integrationAuth.accessIdTag as string refreshToken
}); });
} }
}
} catch (err) {
Sentry.setUser(null); if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
Sentry.captureException(err); accessId = await BotService.decryptSymmetric({
if(err instanceof RequestError) workspaceId: integrationAuth.workspace,
throw err ciphertext: integrationAuth.accessIdCiphertext as string,
else iv: integrationAuth.accessIdIV as string,
throw new Error('Failed to get integration access token'); tag: integrationAuth.accessIdTag as string
});
} }
return ({ return ({
@@ -267,7 +239,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.refreshToken - refresh token * @param {String} obj.refreshToken - refresh token
*/ */
const setIntegrationAuthRefreshHelper = async ({ export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId, integrationAuthId,
refreshToken refreshToken
}: { }: {
@@ -275,32 +247,27 @@ const setIntegrationAuthRefreshHelper = async ({
refreshToken: string; refreshToken: string;
}) => { }) => {
let integrationAuth; let integrationAuth = await IntegrationAuth
try { .findById(integrationAuthId);
integrationAuth = await IntegrationAuth
.findById(integrationAuthId); if (!integrationAuth) throw new Error('Failed to find integration auth');
if (!integrationAuth) throw new Error('Failed to find integration auth'); const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
const obj = await BotService.encryptSymmetric({ plaintext: refreshToken
workspaceId: integrationAuth.workspace.toString(), });
plaintext: refreshToken
}); integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
integrationAuth = await IntegrationAuth.findOneAndUpdate({ }, {
_id: integrationAuthId refreshCiphertext: obj.ciphertext,
}, { refreshIV: obj.iv,
refreshCiphertext: obj.ciphertext, refreshTag: obj.tag,
refreshIV: obj.iv, algorithm: ALGORITHM_AES_256_GCM,
refreshTag: obj.tag keyEncoding: ENCODING_SCHEME_UTF8
}, { }, {
new: true new: true
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to set integration auth refresh token');
}
return integrationAuth; return integrationAuth;
} }
@@ -314,7 +281,7 @@ const setIntegrationAuthRefreshHelper = async ({
* @param {String} obj.accessToken - access token * @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token * @param {Date} obj.accessExpiresAt - expiration date of access token
*/ */
const setIntegrationAuthAccessHelper = async ({ export const setIntegrationAuthAccessHelper = async ({
integrationAuthId, integrationAuthId,
accessId, accessId,
accessToken, accessToken,
@@ -325,52 +292,38 @@ const setIntegrationAuthAccessHelper = async ({
accessToken: string; accessToken: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) => { }) => {
let integrationAuth; let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
try {
integrationAuth = await IntegrationAuth.findById(integrationAuthId); if (!integrationAuth) throw new Error('Failed to find integration auth');
if (!integrationAuth) throw new Error('Failed to find integration auth'); const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
const encryptedAccessTokenObj = await BotService.encryptSymmetric({ plaintext: accessToken
workspaceId: integrationAuth.workspace.toString(), });
plaintext: accessToken
}); let encryptedAccessIdObj;
if (accessId) {
let encryptedAccessIdObj; encryptedAccessIdObj = await BotService.encryptSymmetric({
if (accessId) { workspaceId: integrationAuth.workspace,
encryptedAccessIdObj = await BotService.encryptSymmetric({ plaintext: accessId
workspaceId: integrationAuth.workspace.toString(), });
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to save integration auth access token');
} }
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth; return integrationAuth;
}
export {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
} }
+24 -33
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Key, IKey } from '../models'; import { Key, IKey } from '../models';
interface Key { interface Key {
@@ -18,7 +17,7 @@ interface Key {
* @param {String} obj.keys.nonce - nonce for encryption * @param {String} obj.keys.nonce - nonce for encryption
* @param {String} obj.keys.userId - id of receiver user * @param {String} obj.keys.userId - id of receiver user
*/ */
const pushKeys = async ({ export const pushKeys = async ({
userId, userId,
workspaceId, workspaceId,
keys keys
@@ -27,36 +26,28 @@ const pushKeys = async ({
workspaceId: string; workspaceId: string;
keys: Key[]; keys: Key[];
}): Promise<void> => { }): Promise<void> => {
try { // filter out already-inserted keys
// filter out already-inserted keys const keysSet = new Set(
const keysSet = new Set( (
( await Key.find(
await Key.find( {
{ workspace: workspaceId
workspace: workspaceId },
}, 'receiver'
'receiver' )
) ).map((k: IKey) => k.receiver.toString())
).map((k: IKey) => k.receiver.toString()) );
);
keys = keys.filter((key) => !keysSet.has(key.userId)); keys = keys.filter((key) => !keysSet.has(key.userId));
// add new shared keys only // add new shared keys only
await Key.insertMany( await Key.insertMany(
keys.map((k) => ({ keys.map((k) => ({
encryptedKey: k.encryptedKey, encryptedKey: k.encryptedKey,
nonce: k.nonce, nonce: k.nonce,
sender: userId, sender: userId,
receiver: k.userId, receiver: k.userId,
workspace: workspaceId workspace: workspaceId
})) }))
); );
} catch (err) { };
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to push access keys');
}
};
export { pushKeys };
+65 -94
View File
@@ -1,5 +1,6 @@
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { Membership, Key } from '../models'; import { Membership, Key } from "../models";
import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
/** /**
* Validate that user with id [userId] is a member of workspace with id [workspaceId] * Validate that user with id [userId] is a member of workspace with id [workspaceId]
@@ -9,54 +10,45 @@ import { Membership, Key } from '../models';
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId]
*/ */
const validateMembership = async ({ export const validateMembership = async ({
userId, userId,
workspaceId, workspaceId,
acceptedRoles, acceptedRoles,
}: { }: {
userId: string; userId: Types.ObjectId | string;
workspaceId: string; workspaceId: Types.ObjectId | string;
acceptedRoles: string[]; acceptedRoles?: Array<"admin" | "member">;
}) => { }) => {
const membership = await Membership.findOne({
let membership; user: userId,
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors workspace: workspaceId,
try { }).populate("workspace");
membership = await Membership.findOne({
user: userId, if (!membership) {
workspace: workspaceId throw MembershipNotFoundError({
}).populate("workspace"); message: "Failed to find workspace membership",
});
if (!membership) throw new Error('Failed to find membership'); }
if (!acceptedRoles.includes(membership.role)) { if (acceptedRoles) {
throw new Error('Failed to validate membership role'); if (!acceptedRoles.includes(membership.role)) {
} throw BadRequestError({
} catch (err) { message: "Failed authorization for membership role",
Sentry.setUser(null); });
Sentry.captureException(err); }
throw new Error('Failed to validate membership'); }
}
return membership;
return membership; };
}
/** /**
* Return membership matching criteria specified in query [queryObj] * Return membership matching criteria specified in query [queryObj]
* @param {Object} queryObj - query object * @param {Object} queryObj - query object
* @return {Object} membership - membership * @return {Object} membership - membership
*/ */
const findMembership = async (queryObj: any) => { export const findMembership = async (queryObj: any) => {
let membership; const membership = await Membership.findOne(queryObj);
try { return membership;
membership = await Membership.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find membership');
}
return membership;
}; };
/** /**
@@ -67,40 +59,33 @@ const findMembership = async (queryObj: any) => {
* @param {String} obj.workspaceId - id of workspace. * @param {String} obj.workspaceId - id of workspace.
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
const addMemberships = async ({ export const addMemberships = async ({
userIds, userIds,
workspaceId, workspaceId,
roles roles
}: { }: {
userIds: string[]; userIds: string[];
workspaceId: string; workspaceId: string;
roles: string[]; roles: string[];
}): Promise<void> => { }): Promise<void> => {
try { const operations = userIds.map((userId, idx) => {
const operations = userIds.map((userId, idx) => { return {
return { updateOne: {
updateOne: { filter: {
filter: { user: userId,
user: userId, workspace: workspaceId,
workspace: workspaceId, role: roles[idx],
role: roles[idx] },
}, update: {
update: { user: userId,
user: userId, workspace: workspaceId,
workspace: workspaceId, role: roles[idx],
role: roles[idx] },
}, upsert: true,
upsert: true },
} };
}; });
}); await Membership.bulkWrite(operations as any);
await Membership.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to workspace');
}
}; };
/** /**
@@ -108,33 +93,19 @@ const addMemberships = async ({
* @param {Object} obj * @param {Object} obj
* @param {String} obj.membershipId - id of membership to delete * @param {String} obj.membershipId - id of membership to delete
*/ */
const deleteMembership = async ({ membershipId }: { membershipId: string }) => { export const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
let deletedMembership; const deletedMembership = await Membership.findOneAndDelete({
try { _id: membershipId
deletedMembership = await Membership.findOneAndDelete({ });
_id: membershipId
});
// delete keys associated with the membership // delete keys associated with the membership
if (deletedMembership?.user) { if (deletedMembership?.user) {
// case: membership had a registered user // case: membership had a registered user
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembership.user, receiver: deletedMembership.user,
workspace: deletedMembership.workspace workspace: deletedMembership.workspace,
}); });
} }
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete membership');
}
return deletedMembership; return deletedMembership;
}; };
export {
validateMembership,
addMemberships,
findMembership,
deleteMembership
};
+91 -103
View File
@@ -1,40 +1,56 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { MembershipOrg, Workspace, Membership, Key } from '../models'; import {
MembershipOrg,
Workspace,
Membership,
Key
} from '../models';
import {
MembershipOrgNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
/** /**
* Validate that user with id [userId] is a member of organization with id [organizationId] * Validate that user with id [userId] is a member of organization with id [organizationId]
* and has at least one of the roles in [acceptedRoles] * and has at least one of the roles in [acceptedRoles]
* * @param {Object} obj
* @param {Types.ObjectId} obj.userId
* @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles
*/ */
const validateMembership = async ({ export const validateMembershipOrg = async ({
userId, userId,
organizationId, organizationId,
acceptedRoles acceptedRoles,
acceptedStatuses
}: { }: {
userId: string; userId: Types.ObjectId;
organizationId: string; organizationId: Types.ObjectId;
acceptedRoles: string[]; acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses?: Array<'invited' | 'accepted'>;
}) => { }) => {
let membership; const membershipOrg = await MembershipOrg.findOne({
try { user: userId,
membership = await MembershipOrg.findOne({ organization: organizationId
user: new Types.ObjectId(userId), });
organization: new Types.ObjectId(organizationId)
}); if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
if (!membership) throw new Error('Failed to find organization membership');
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate organization membership role');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate organization membership');
} }
return membership; if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
}
}
if (acceptedStatuses) {
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
}
}
return membershipOrg;
} }
/** /**
@@ -43,16 +59,8 @@ const validateMembership = async ({
* @param {Object} queryObj - query object * @param {Object} queryObj - query object
* @return {Object} membershipOrg - membership * @return {Object} membershipOrg - membership
*/ */
const findMembershipOrg = (queryObj: any) => { export const findMembershipOrg = (queryObj: any) => {
let membershipOrg; const membershipOrg = MembershipOrg.findOne(queryObj);
try {
membershipOrg = MembershipOrg.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find organization membership');
}
return membershipOrg; return membershipOrg;
}; };
@@ -64,7 +72,7 @@ const findMembershipOrg = (queryObj: any) => {
* @param {String} obj.organizationId - id of organization. * @param {String} obj.organizationId - id of organization.
* @param {String[]} obj.roles - roles of users. * @param {String[]} obj.roles - roles of users.
*/ */
const addMembershipsOrg = async ({ export const addMembershipsOrg = async ({
userIds, userIds,
organizationId, organizationId,
roles, roles,
@@ -75,33 +83,27 @@ const addMembershipsOrg = async ({
roles: string[]; roles: string[];
statuses: string[]; statuses: string[];
}) => { }) => {
try { const operations = userIds.map((userId, idx) => {
const operations = userIds.map((userId, idx) => { return {
return { updateOne: {
updateOne: { filter: {
filter: { user: userId,
user: userId, organization: organizationId,
organization: organizationId, role: roles[idx],
role: roles[idx], status: statuses[idx]
status: statuses[idx] },
}, update: {
update: { user: userId,
user: userId, organization: organizationId,
organization: organizationId, role: roles[idx],
role: roles[idx], status: statuses[idx]
status: statuses[idx] },
}, upsert: true
upsert: true }
} };
}; });
});
await MembershipOrg.bulkWrite(operations as any); await MembershipOrg.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to organization');
}
}; };
/** /**
@@ -109,55 +111,41 @@ const addMembershipsOrg = async ({
* @param {Object} obj * @param {Object} obj
* @param {String} obj.membershipOrgId - id of organization membership to delete * @param {String} obj.membershipOrgId - id of organization membership to delete
*/ */
const deleteMembershipOrg = async ({ export const deleteMembershipOrg = async ({
membershipOrgId membershipOrgId
}: { }: {
membershipOrgId: string; membershipOrgId: string;
}) => { }) => {
let deletedMembershipOrg; const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
try { _id: membershipOrgId
deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ });
_id: membershipOrgId
});
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership'); if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
// delete keys associated with organization membership // delete keys associated with organization membership
if (deletedMembershipOrg?.user) { if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user // case: organization membership had a registered user
const workspaces = ( const workspaces = (
await Workspace.find({ await Workspace.find({
organization: deletedMembershipOrg.organization organization: deletedMembershipOrg.organization
}) })
).map((w) => w._id.toString()); ).map((w) => w._id.toString());
await Membership.deleteMany({ await Membership.deleteMany({
user: deletedMembershipOrg.user, user: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces $in: workspaces
} }
}); });
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembershipOrg.user, receiver: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces $in: workspaces
} }
}); });
} }
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete organization membership');
}
return deletedMembershipOrg; return deletedMembershipOrg;
}; };
export {
validateMembership,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg
};

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