mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 03:25:48 +00:00
Merge branch 'main' into snyk-fix-71fbba54e9eda81c90db390106760c64
This commit is contained in:
+7
-5
@@ -3,16 +3,18 @@
|
|||||||
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
|
||||||
|
|
||||||
|
# Required
|
||||||
|
DB_CONNECTION_URI=postgres://infisical:infisical@db:5432/infisical
|
||||||
|
|
||||||
# JWT
|
# JWT
|
||||||
# Required secrets to sign JWT tokens
|
# Required secrets to sign JWT tokens
|
||||||
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
# THIS IS A SAMPLE AUTH_SECRET KEY AND SHOULD NEVER BE USED FOR PRODUCTION
|
||||||
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
AUTH_SECRET=5lrMXKKWCVocS/uerPsl7V+TX/aaUaI7iDkgl3tSmLE=
|
||||||
|
|
||||||
# MongoDB
|
# Postgres creds
|
||||||
# Backend will connect to the MongoDB instance at connection string MONGO_URL which can either be a ref
|
POSTGRES_PASSWORD=infisical
|
||||||
# to the MongoDB container instance or Mongo Cloud
|
POSTGRES_USER=infisical
|
||||||
# Required
|
POSTGRES_DB=infisical
|
||||||
MONGO_URL=mongodb://root:example@mongo:27017/?authSource=admin
|
|
||||||
|
|
||||||
# Redis
|
# Redis
|
||||||
REDIS_URL=redis://redis:6379
|
REDIS_URL=redis://redis:6379
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
DB_CONNECTION_URI=
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Description 📣
|
# Description 📣
|
||||||
|
|
||||||
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. -->
|
<!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. Here's how we expect a pull request to be : https://infisical.com/docs/contributing/getting-started/pull-requests -->
|
||||||
|
|
||||||
## Type ✨
|
## Type ✨
|
||||||
|
|
||||||
@@ -19,4 +19,6 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/code-of-conduct). 📝
|
- [ ] I have read the [contributing guide](https://infisical.com/docs/contributing/getting-started/overview), agreed and acknowledged the [code of conduct](https://infisical.com/docs/contributing/getting-started/code-of-conduct). 📝
|
||||||
|
|
||||||
|
<!-- If you have any questions regarding contribution, here's the FAQ : https://infisical.com/docs/contributing/getting-started/faq -->
|
||||||
+42
-39
@@ -1,49 +1,52 @@
|
|||||||
backend:
|
## @section Common parameters
|
||||||
enabled: true
|
##
|
||||||
name: backend
|
|
||||||
podAnnotations: {}
|
## @param nameOverride Override release name
|
||||||
deploymentAnnotations:
|
##
|
||||||
secrets.infisical.com/auto-reload: "true"
|
nameOverride: ""
|
||||||
replicaCount: 2
|
## @param fullnameOverride Override release fullname
|
||||||
|
##
|
||||||
|
fullnameOverride: ""
|
||||||
|
|
||||||
|
## @section Infisical backend parameters
|
||||||
|
## Documentation : https://infisical.com/docs/self-hosting/deployments/kubernetes
|
||||||
|
##
|
||||||
|
|
||||||
|
infisical:
|
||||||
|
## @param backend.enabled Enable backend
|
||||||
|
##
|
||||||
|
enabled: false
|
||||||
|
## @param backend.name Backend name
|
||||||
|
##
|
||||||
|
name: infisical
|
||||||
|
replicaCount: 3
|
||||||
image:
|
image:
|
||||||
repository: infisical/staging_infisical
|
repository: infisical/staging_infisical
|
||||||
tag: "latest"
|
tag: "latest"
|
||||||
pullPolicy: Always
|
pullPolicy: Always
|
||||||
kubeSecretRef: managed-backend-secret
|
|
||||||
service:
|
|
||||||
annotations: {}
|
|
||||||
type: ClusterIP
|
|
||||||
nodePort: ""
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: 300Mi
|
|
||||||
|
|
||||||
backendEnvironmentVariables: null
|
deploymentAnnotations:
|
||||||
|
secrets.infisical.com/auto-reload: "true"
|
||||||
|
|
||||||
## Mongo DB persistence
|
kubeSecretRef: "infisical-gamma-secrets"
|
||||||
mongodb:
|
|
||||||
enabled: false
|
|
||||||
persistence:
|
|
||||||
enabled: false
|
|
||||||
|
|
||||||
## By default the backend will be connected to a Mongo instance within the cluster
|
|
||||||
## However, it is recommended to add a managed document DB connection string for production-use (DBaaS)
|
|
||||||
## Learn about connection string type here https://www.mongodb.com/docs/manual/reference/connection-string/
|
|
||||||
## e.g. "mongodb://<user>:<pass>@<host>:<port>/<database-name>"
|
|
||||||
mongodbConnection:
|
|
||||||
externalMongoDBConnectionString: ""
|
|
||||||
|
|
||||||
ingress:
|
ingress:
|
||||||
|
## @param ingress.enabled Enable ingress
|
||||||
|
##
|
||||||
enabled: true
|
enabled: true
|
||||||
# annotations:
|
## @param ingress.ingressClassName Ingress class name
|
||||||
# kubernetes.io/ingress.class: "nginx"
|
##
|
||||||
# cert-manager.io/issuer: letsencrypt-nginx
|
ingressClassName: nginx
|
||||||
hostName: gamma.infisical.com ## <- Replace with your own domain
|
## @param ingress.nginx.enabled Ingress controller
|
||||||
|
##
|
||||||
|
# nginx:
|
||||||
|
# enabled: true
|
||||||
|
## @param ingress.annotations Ingress annotations
|
||||||
|
##
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
||||||
|
hostName: "gamma.infisical.com"
|
||||||
tls:
|
tls:
|
||||||
[]
|
- secretName: letsencrypt-prod
|
||||||
# - secretName: letsencrypt-nginx
|
hosts:
|
||||||
# hosts:
|
- gamma.infisical.com
|
||||||
# - infisical.local
|
|
||||||
|
|
||||||
mailhog:
|
|
||||||
enabled: false
|
|
||||||
|
|||||||
+34
-13
@@ -35,15 +35,15 @@ jobs:
|
|||||||
context: .
|
context: .
|
||||||
file: Dockerfile.standalone-infisical
|
file: Dockerfile.standalone-infisical
|
||||||
tags: infisical/infisical:test
|
tags: infisical/infisical:test
|
||||||
- name: ⏻ Spawn backend container and dependencies
|
# - name: ⏻ Spawn backend container and dependencies
|
||||||
run: |
|
# run: |
|
||||||
docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
# docker compose -f .github/resources/docker-compose.be-test.yml up --wait --quiet-pull
|
||||||
- name: 🧪 Test backend image
|
# - name: 🧪 Test backend image
|
||||||
run: |
|
# run: |
|
||||||
./.github/resources/healthcheck.sh infisical-backend-test
|
# ./.github/resources/healthcheck.sh infisical-backend-test
|
||||||
- name: ⏻ Shut down backend container and dependencies
|
# - name: ⏻ Shut down backend container and dependencies
|
||||||
run: |
|
# run: |
|
||||||
docker compose -f .github/resources/docker-compose.be-test.yml down
|
# docker compose -f .github/resources/docker-compose.be-test.yml down
|
||||||
- name: 🏗️ Build backend and push
|
- name: 🏗️ Build backend and push
|
||||||
uses: depot/build-push-action@v1
|
uses: depot/build-push-action@v1
|
||||||
with:
|
with:
|
||||||
@@ -59,11 +59,32 @@ jobs:
|
|||||||
build-args: |
|
build-args: |
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
|
postgres-migration:
|
||||||
|
name: Run latest migration files
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [infisical-image]
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
- name: Setup Node.js environment
|
||||||
|
uses: actions/setup-node@v2
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
- name: Change directory to backend and install dependencies
|
||||||
|
env:
|
||||||
|
DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||||
|
run: |
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run migration:latest
|
||||||
|
# - name: Run postgres DB migration files
|
||||||
|
# env:
|
||||||
|
# DB_CONNECTION_URI: ${{ secrets.DB_CONNECTION_URI }}
|
||||||
|
# run: npm run migration:latest
|
||||||
gamma-deployment:
|
gamma-deployment:
|
||||||
name: Deploy to gamma
|
name: Deploy to gamma
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [infisical-image]
|
needs: [postgres-migration]
|
||||||
steps:
|
steps:
|
||||||
- name: ☁️ Checkout source
|
- name: ☁️ Checkout source
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -82,7 +103,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
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 infisical-gamma-postgres
|
||||||
- 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
|
||||||
@@ -90,7 +111,7 @@ jobs:
|
|||||||
- 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 --wait --install
|
helm upgrade infisical infisical-helm-charts/infisical-standalone --values values.yaml --wait --install
|
||||||
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
if [[ $(helm status infisical) == *"FAILED"* ]]; then
|
||||||
echo "Helm upgrade failed"
|
echo "Helm upgrade failed"
|
||||||
exit 1
|
exit 1
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
name: "Check API For Breaking Changes"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
paths:
|
||||||
|
- "backend/src/server/routes/**"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-be-api-changes:
|
||||||
|
name: Check API Changes
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
# - name: Setup Node 20
|
||||||
|
# uses: actions/setup-node@v3
|
||||||
|
# with:
|
||||||
|
# node-version: "20"
|
||||||
|
# uncomment this when testing locally using nektos/act
|
||||||
|
- uses: KengoTODA/actions-setup-docker-compose@v1
|
||||||
|
if: ${{ env.ACT }}
|
||||||
|
name: Install `docker-compose` for local simulations
|
||||||
|
with:
|
||||||
|
version: "2.14.2"
|
||||||
|
- name: 📦Build the latest image
|
||||||
|
run: docker build --tag infisical-api .
|
||||||
|
working-directory: backend
|
||||||
|
- name: Start postgres and redis
|
||||||
|
run: touch .env && docker-compose -f docker-compose.dev.yml up -d db redis
|
||||||
|
- name: Start the server
|
||||||
|
run: |
|
||||||
|
echo "SECRET_SCANNING_GIT_APP_ID=793712" >> .env
|
||||||
|
echo "SECRET_SCANNING_PRIVATE_KEY=some-random" >> .env
|
||||||
|
echo "SECRET_SCANNING_WEBHOOK_SECRET=some-random" >> .env
|
||||||
|
docker run --name infisical-api -d -p 4000:4000 -e DB_CONNECTION_URI=$DB_CONNECTION_URI -e REDIS_URL=$REDIS_URL -e JWT_AUTH_SECRET=$JWT_AUTH_SECRET --env-file .env --entrypoint '/bin/sh' infisical-api -c "npm run migration:latest && ls && node dist/main.mjs"
|
||||||
|
env:
|
||||||
|
REDIS_URL: redis://172.17.0.1:6379
|
||||||
|
DB_CONNECTION_URI: postgres://infisical:[email protected]:5432/infisical?sslmode=disable
|
||||||
|
JWT_AUTH_SECRET: something-random
|
||||||
|
- uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.21.5'
|
||||||
|
- name: Wait for container to be stable and check logs
|
||||||
|
run: |
|
||||||
|
SECONDS=0
|
||||||
|
HEALTHY=0
|
||||||
|
while [ $SECONDS -lt 60 ]; do
|
||||||
|
if docker ps | grep infisical-api | grep -q healthy; then
|
||||||
|
echo "Container is healthy."
|
||||||
|
HEALTHY=1
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
echo "Waiting for container to be healthy... ($SECONDS seconds elapsed)"
|
||||||
|
|
||||||
|
docker logs infisical-api
|
||||||
|
|
||||||
|
sleep 2
|
||||||
|
SECONDS=$((SECONDS+2))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ $HEALTHY -ne 1 ]; then
|
||||||
|
echo "Container did not become healthy in time"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Install openapi-diff
|
||||||
|
run: go install github.com/tufin/oasdiff@latest
|
||||||
|
- name: Running OpenAPI Spec diff action
|
||||||
|
run: oasdiff breaking https://app.infisical.com/api/docs/json http://localhost:4000/api/docs/json --fail-on ERR
|
||||||
|
- name: cleanup
|
||||||
|
run: |
|
||||||
|
docker-compose -f "docker-compose.dev.yml" down
|
||||||
|
docker stop infisical-api
|
||||||
|
docker remove infisical-api
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
name: "Check Backend Pull Request"
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize]
|
|
||||||
paths:
|
|
||||||
- "backend/**"
|
|
||||||
- "!backend/README.md"
|
|
||||||
- "!backend/.*"
|
|
||||||
- "backend/.eslintrc.js"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
check-be-pr:
|
|
||||||
name: Check
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 15
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: ☁️ Checkout source
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
- name: 🔧 Setup Node 16
|
|
||||||
uses: actions/setup-node@v3
|
|
||||||
with:
|
|
||||||
node-version: "16"
|
|
||||||
cache: "npm"
|
|
||||||
cache-dependency-path: backend/package-lock.json
|
|
||||||
- name: 📦 Install dependencies
|
|
||||||
run: npm ci --only-production
|
|
||||||
working-directory: backend
|
|
||||||
# - name: 🧪 Run tests
|
|
||||||
# run: npm run test:ci
|
|
||||||
# working-directory: backend
|
|
||||||
# - name: 📁 Upload test results
|
|
||||||
# uses: actions/upload-artifact@v3
|
|
||||||
# if: always()
|
|
||||||
# with:
|
|
||||||
# name: be-test-results
|
|
||||||
# path: |
|
|
||||||
# ./backend/reports
|
|
||||||
# ./backend/coverage
|
|
||||||
- name: 🏗️ Run build
|
|
||||||
run: npm run build
|
|
||||||
working-directory: backend
|
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
name: "Check Backend PR types and lint"
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize]
|
||||||
|
paths:
|
||||||
|
- "backend/**"
|
||||||
|
- "!backend/README.md"
|
||||||
|
- "!backend/.*"
|
||||||
|
- "backend/.eslintrc.js"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
check-be-pr:
|
||||||
|
name: Check TS and Lint
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: ☁️ Checkout source
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: 🔧 Setup Node 20
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "20"
|
||||||
|
cache: "npm"
|
||||||
|
cache-dependency-path: backend/package-lock.json
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm install
|
||||||
|
working-directory: backend
|
||||||
|
- name: Run type check
|
||||||
|
run: npm run type:check
|
||||||
|
working-directory: backend
|
||||||
|
- name: Run lint check
|
||||||
|
run: npm run lint
|
||||||
|
working-directory: backend
|
||||||
+19
-19
@@ -1,4 +1,4 @@
|
|||||||
name: Release standalone postgres version
|
name: Release standalone docker image
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
@@ -30,28 +30,28 @@ jobs:
|
|||||||
- 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
|
||||||
|
- name: 🔧 Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v2
|
||||||
- name: 🐋 Login to Docker Hub
|
- name: 🐋 Login to Docker Hub
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v2
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
- name: Set up QEMU
|
- name: Set up Depot CLI
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: depot/setup-action@v1
|
||||||
- name: Set up Docker Buildx
|
- name: 📦 Build backend and export to Docker
|
||||||
uses: docker/setup-buildx-action@v3
|
uses: depot/build-push-action@v1
|
||||||
- name: Login to Docker Hub
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
with:
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
project: 64mmf0n610
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
|
||||||
- name: Build and push
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64
|
|
||||||
file: Dockerfile.standalone-infisical
|
|
||||||
push: true
|
push: true
|
||||||
|
context: .
|
||||||
tags: |
|
tags: |
|
||||||
akhilmhdh/destruction:latest
|
infisical/infisical:latest-postgres
|
||||||
akhilmhdh/destruction:${{ steps.commit.outputs.short }}
|
infisical/infisical:${{ steps.commit.outputs.short }}
|
||||||
akhilmhdh/destruction:${{ steps.extract_version.outputs.version }}
|
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
file: Dockerfile.standalone-infisical
|
||||||
|
build-args: |
|
||||||
|
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
||||||
|
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
name: Release standalone docker image
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "infisical/v*.*.*"
|
|
||||||
- "!infisical/v*.*.*-postgres"
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
infisical-standalone:
|
|
||||||
name: Build infisical standalone image
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Extract version from tag
|
|
||||||
id: extract_version
|
|
||||||
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
|
|
||||||
- name: ☁️ Checkout source
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: 📦 Install dependencies to test all dependencies
|
|
||||||
run: npm ci --only-production
|
|
||||||
working-directory: backend
|
|
||||||
- uses: paulhatch/[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 }}
|
|
||||||
infisical/infisical:${{ steps.extract_version.outputs.version }}
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
file: Dockerfile.standalone-infisical
|
|
||||||
build-args: |
|
|
||||||
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
|
|
||||||
INFISICAL_PLATFORM_VERSION=${{ steps.extract_version.outputs.version }}
|
|
||||||
+1
-1
@@ -6,7 +6,7 @@ node_modules
|
|||||||
.env.gamma
|
.env.gamma
|
||||||
.env.prod
|
.env.prod
|
||||||
.env.infisical
|
.env.infisical
|
||||||
|
.env.migration
|
||||||
*~
|
*~
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
|
|||||||
@@ -104,7 +104,6 @@ ENV NEXT_PUBLIC_INTERCOM_ID=$INTERCOM_ID \
|
|||||||
WORKDIR /
|
WORKDIR /
|
||||||
|
|
||||||
COPY --from=backend-runner /app /backend
|
COPY --from=backend-runner /app /backend
|
||||||
COPY --from=backend-runner /app/dist/services/smtp/templates /backend/dist/templates
|
|
||||||
|
|
||||||
COPY --from=frontend-runner /app ./backend/frontend-build
|
COPY --from=frontend-runner /app ./backend/frontend-build
|
||||||
|
|
||||||
|
|||||||
@@ -5,16 +5,10 @@ push:
|
|||||||
docker-compose -f docker-compose.yml push
|
docker-compose -f docker-compose.yml push
|
||||||
|
|
||||||
up-dev:
|
up-dev:
|
||||||
docker-compose -f docker-compose.dev.yml up --build
|
docker compose -f docker-compose.dev.yml up --build
|
||||||
|
|
||||||
up-pg-dev:
|
|
||||||
docker compose -f docker-compose.pg.yml up --build
|
|
||||||
|
|
||||||
i-dev:
|
|
||||||
infisical run -- docker-compose -f docker-compose.dev.yml up --build
|
|
||||||
|
|
||||||
up-prod:
|
up-prod:
|
||||||
docker-compose -f docker-compose.yml up --build
|
docker-compose -f docker-compose.prod.yml up --build
|
||||||
|
|
||||||
down:
|
down:
|
||||||
docker-compose down
|
docker-compose down
|
||||||
|
|||||||
@@ -33,7 +33,7 @@
|
|||||||
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
<img src="https://img.shields.io/github/commit-activity/m/infisical/infisical" alt="git commit activity" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://cloudsmith.io/~infisical/repos/">
|
<a href="https://cloudsmith.io/~infisical/repos/">
|
||||||
<img src="https://img.shields.io/badge/Downloads-2.58M-orange" alt="Cloudsmith downloads" />
|
<img src="https://img.shields.io/badge/Downloads-6.95M-orange" alt="Cloudsmith downloads" />
|
||||||
</a>
|
</a>
|
||||||
<a href="https://infisical.com/slack">
|
<a href="https://infisical.com/slack">
|
||||||
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
<img src="https://img.shields.io/badge/chat-on%20Slack-blueviolet" alt="Slack community channel" />
|
||||||
@@ -53,17 +53,19 @@ We're on a mission to make secret management more accessible to everyone, not ju
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.)
|
- **[User-friendly dashboard](https://infisical.com/docs/documentation/platform/project)** to manage secrets across projects and environments (e.g. development, production, etc.).
|
||||||
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand
|
- **[Client SDKs](https://infisical.com/docs/sdks/overview)** to fetch secrets for your apps and infrastructure on demand.
|
||||||
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development
|
- **[Infisical CLI](https://infisical.com/docs/cli/overview)** to fetch and inject secrets into any framework in local development and CI/CD.
|
||||||
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like GitHub, Vercel, Netlify, and more
|
- **[Infisical API](https://infisical.com/docs/api-reference/overview/introduction)** to perform CRUD operation on secrets, users, projects, and any other resource in Infisical.
|
||||||
- [**Automatic Kubernetes deployment secret reloads**](https://infisical.com/docs/documentation/getting-started/kubernetes)
|
- **[Native integrations](https://infisical.com/docs/integrations/overview)** with platforms like [GitHub](https://infisical.com/docs/integrations/cicd/githubactions), [Vercel](https://infisical.com/docs/integrations/cloud/vercel), [AWS](https://infisical.com/docs/integrations/cloud/aws-secret-manager), and tools like [Terraform](https://infisical.com/docs/integrations/frameworks/terraform), [Ansible](https://infisical.com/docs/integrations/platforms/ansible), and more.
|
||||||
- **[Complete control over your data](https://infisical.com/docs/self-hosting/overview)** - host it yourself on any infrastructure
|
- **[Infisical Kubernetes operator](https://infisical.com/docs/documentation/getting-started/kubernetes)** to managed secrets in k8s, automatically reload deployments, and more.
|
||||||
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery]()** to version every secret and project state
|
- **[Infisical Agent](https://infisical.com/docs/infisical-agent/overview)** to inject secrets into your applications without modifying any code logic.
|
||||||
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project
|
- **[Self-hosting and on-prem](https://infisical.com/docs/self-hosting/overview)** to get complete control over your data.
|
||||||
- **Role-based Access Controls** per environment
|
- **[Secret versioning](https://infisical.com/docs/documentation/platform/secret-versioning)** and **[Point-in-Time Recovery](https://infisical.com/docs/documentation/platform/pit-recovery)** to version every secret and project state.
|
||||||
- [**Simple on-premise deployments** to AWS, Digital Ocean, and more](https://infisical.com/docs/self-hosting/overview)
|
- **[Audit logs](https://infisical.com/docs/documentation/platform/audit-logs)** to record every action taken in a project.
|
||||||
- [**Secret Scanning and Leak Prevention**](https://infisical.com/docs/cli/scanning-overview)
|
- **[Role-based Access Controls](https://infisical.com/docs/documentation/platform/role-based-access-controls)** to create permission sets on any resource in Infisica and assign those to user or machine identities.
|
||||||
|
- **[Simple on-premise deployments](https://infisical.com/docs/self-hosting/overview)** to AWS, Digital Ocean, and more.
|
||||||
|
- **[Secret Scanning and Leak Prevention](https://infisical.com/docs/cli/scanning-overview)** to prevent secrets from leaking to git.
|
||||||
|
|
||||||
And much more.
|
And much more.
|
||||||
|
|
||||||
@@ -82,13 +84,13 @@ To set up and run Infisical locally, make sure you have Git and Docker installed
|
|||||||
Linux/macOS:
|
Linux/macOS:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.yml up
|
git clone https://github.com/Infisical/infisical && cd "$(basename $_ .git)" && cp .env.example .env && docker-compose -f docker-compose.prod.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
Windows Command Prompt:
|
Windows Command Prompt:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.yml up
|
git clone https://github.com/Infisical/infisical && cd infisical && copy .env.example .env && docker-compose -f docker-compose.prod.yml up
|
||||||
```
|
```
|
||||||
|
|
||||||
Create an account at `http://localhost:80`
|
Create an account at `http://localhost:80`
|
||||||
@@ -115,9 +117,9 @@ Lean about Infisical's code scanning feature [here](https://infisical.com/docs/c
|
|||||||
|
|
||||||
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
|
This repo available under the [MIT expat license](https://github.com/Infisical/infisical/blob/main/LICENSE), with the exception of the `ee` directory which will contain premium enterprise features requiring a Infisical license.
|
||||||
|
|
||||||
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://cal.com/vmatsiiako/infisical-demo):
|
If you are interested in managed Infisical Cloud of self-hosted Enterprise Offering, take a look at [our website](https://infisical.com/) or [book a meeting with us](https://infisical.cal.com/vlad/infisical-demo):
|
||||||
|
|
||||||
<a href="https://cal.com/vmatsiiako/infisical-demo"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
<a href="[https://infisical.cal.com/vlad/infisical-demo](https://infisical.cal.com/vlad/infisical-demo)"><img alt="Schedule a meeting" src="https://cal.com/book-with-cal-dark.svg" /></a>
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
node_modules
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
Dockerfile
|
|
||||||
.dockerignore
|
|
||||||
docker-compose.*
|
|
||||||
.DS_Store
|
|
||||||
*.swp
|
|
||||||
*~
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
node_modules
|
|
||||||
built
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
{
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint",
|
|
||||||
"unused-imports"
|
|
||||||
],
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:@typescript-eslint/eslint-recommended",
|
|
||||||
"plugin:@typescript-eslint/recommended"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"no-empty-function": "off",
|
|
||||||
"@typescript-eslint/no-empty-function": "off",
|
|
||||||
"no-console": 2,
|
|
||||||
"quotes": [
|
|
||||||
"error",
|
|
||||||
"double",
|
|
||||||
{
|
|
||||||
"avoidEscape": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"comma-dangle": [
|
|
||||||
"error",
|
|
||||||
"only-multiline"
|
|
||||||
],
|
|
||||||
"@typescript-eslint/no-unused-vars": "off",
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
|
||||||
"@typescript-eslint/no-extra-semi": "off", // added to be able to push
|
|
||||||
"unused-imports/no-unused-vars": [
|
|
||||||
"warn",
|
|
||||||
{
|
|
||||||
"vars": "all",
|
|
||||||
"varsIgnorePattern": "^_",
|
|
||||||
"args": "after-used",
|
|
||||||
"argsIgnorePattern": "^_"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"sort-imports": 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"singleQuote": false,
|
|
||||||
"printWidth": 100,
|
|
||||||
"trailingComma": "none",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Build stage
|
|
||||||
FROM node:16-alpine AS build
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --only-production
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
# Production stage
|
|
||||||
FROM node:16-alpine
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ENV npm_config_cache /home/node/.npm
|
|
||||||
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm ci --only-production && npm cache clean --force
|
|
||||||
|
|
||||||
COPY --from=build /app .
|
|
||||||
|
|
||||||
RUN apk add --no-cache bash curl && curl -1sLf \
|
|
||||||
'https://dl.cloudsmith.io/public/infisical/infisical-cli/setup.alpine.sh' | bash \
|
|
||||||
&& apk add infisical=0.8.1 && apk add --no-cache git
|
|
||||||
|
|
||||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
|
||||||
CMD node healthcheck.js
|
|
||||||
|
|
||||||
EXPOSE 4000
|
|
||||||
|
|
||||||
CMD ["node", "build/index.js"]
|
|
||||||
Vendored
-46
@@ -1,46 +0,0 @@
|
|||||||
export {};
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
namespace NodeJS {
|
|
||||||
interface ProcessEnv {
|
|
||||||
PORT: string;
|
|
||||||
ENCRYPTION_KEY: string;
|
|
||||||
SALT_ROUNDS: string;
|
|
||||||
JWT_AUTH_LIFETIME: string;
|
|
||||||
JWT_AUTH_SECRET: string;
|
|
||||||
JWT_REFRESH_LIFETIME: string;
|
|
||||||
JWT_REFRESH_SECRET: string;
|
|
||||||
JWT_SERVICE_SECRET: string;
|
|
||||||
JWT_SIGNUP_LIFETIME: string;
|
|
||||||
JWT_SIGNUP_SECRET: string;
|
|
||||||
MONGO_URL: string;
|
|
||||||
NODE_ENV: "development" | "staging" | "testing" | "production";
|
|
||||||
VERBOSE_ERROR_OUTPUT: string;
|
|
||||||
LOKI_HOST: string;
|
|
||||||
CLIENT_ID_HEROKU: string;
|
|
||||||
CLIENT_ID_VERCEL: string;
|
|
||||||
CLIENT_ID_NETLIFY: string;
|
|
||||||
CLIENT_ID_GITHUB: string;
|
|
||||||
CLIENT_ID_GITLAB: string;
|
|
||||||
CLIENT_SECRET_HEROKU: string;
|
|
||||||
CLIENT_SECRET_VERCEL: string;
|
|
||||||
CLIENT_SECRET_NETLIFY: string;
|
|
||||||
CLIENT_SECRET_GITHUB: string;
|
|
||||||
CLIENT_SECRET_GITLAB: string;
|
|
||||||
CLIENT_SLUG_VERCEL: string;
|
|
||||||
POSTHOG_HOST: string;
|
|
||||||
POSTHOG_PROJECT_API_KEY: string;
|
|
||||||
SENTRY_DSN: string;
|
|
||||||
SITE_URL: string;
|
|
||||||
SMTP_HOST: string;
|
|
||||||
SMTP_SECURE: string;
|
|
||||||
SMTP_PORT: string;
|
|
||||||
SMTP_USERNAME: string;
|
|
||||||
SMTP_PASSWORD: string;
|
|
||||||
SMTP_FROM_ADDRESS: string;
|
|
||||||
SMTP_FROM_NAME: string;
|
|
||||||
TELEMETRY_ENABLED: string;
|
|
||||||
LICENSE_KEY: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const http = require('http');
|
|
||||||
const PORT = process.env.PORT || 4000;
|
|
||||||
const options = {
|
|
||||||
host: 'localhost',
|
|
||||||
port: PORT,
|
|
||||||
timeout: 2000,
|
|
||||||
path: '/healthcheck'
|
|
||||||
};
|
|
||||||
|
|
||||||
const healthCheck = http.request(options, (res) => {
|
|
||||||
console.log(`HEALTHCHECK STATUS: ${res.statusCode}`);
|
|
||||||
if (res.statusCode == 200) {
|
|
||||||
process.exit(0);
|
|
||||||
} else {
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
healthCheck.on('error', function (err) {
|
|
||||||
console.error(`HEALTH CHECK ERROR: ${err}`);
|
|
||||||
process.exit(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
healthCheck.end();
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 493 KiB |
@@ -1,9 +0,0 @@
|
|||||||
export default {
|
|
||||||
preset: "ts-jest",
|
|
||||||
testEnvironment: "node",
|
|
||||||
collectCoverageFrom: ["src/*.{js,ts}", "!**/node_modules/**"],
|
|
||||||
modulePaths: ["<rootDir>/src"],
|
|
||||||
testMatch: ["<rootDir>/tests/**/*.test.ts"],
|
|
||||||
setupFiles: ["<rootDir>/test-resources/env-vars.js"],
|
|
||||||
setupFilesAfterEnv: ["<rootDir>/tests/setupTests.ts"],
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"watch": ["src"],
|
|
||||||
"ext": ".ts,.js",
|
|
||||||
"ignore": [],
|
|
||||||
"exec": "ts-node ./src/index.ts"
|
|
||||||
}
|
|
||||||
Generated
-32861
File diff suppressed because it is too large
Load Diff
@@ -1,148 +0,0 @@
|
|||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"@aws-sdk/client-secrets-manager": "^3.319.0",
|
|
||||||
"@casl/ability": "^6.5.0",
|
|
||||||
"@casl/mongoose": "^7.2.1",
|
|
||||||
"@godaddy/terminus": "^4.12.0",
|
|
||||||
"@node-saml/passport-saml": "^4.0.4",
|
|
||||||
"@octokit/rest": "^19.0.5",
|
|
||||||
"@sentry/node": "^7.77.0",
|
|
||||||
"@sentry/tracing": "^7.48.0",
|
|
||||||
"@serdnam/pino-cloudwatch-transport": "^1.0.4",
|
|
||||||
"@types/crypto-js": "^4.1.1",
|
|
||||||
"@types/libsodium-wrappers": "^0.7.10",
|
|
||||||
"@ucast/mongo2js": "^1.3.4",
|
|
||||||
"ajv": "^8.12.0",
|
|
||||||
"argon2": "^0.30.3",
|
|
||||||
"aws-sdk": "^2.1364.0",
|
|
||||||
"axios": "^1.6.0",
|
|
||||||
"axios-retry": "^3.4.0",
|
|
||||||
"bcrypt": "^5.1.0",
|
|
||||||
"bigint-conversion": "^2.4.0",
|
|
||||||
"cookie-parser": "^1.4.6",
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"dotenv": "^16.0.1",
|
|
||||||
"express": "^4.18.1",
|
|
||||||
"express-async-errors": "^3.1.1",
|
|
||||||
"express-rate-limit": "^6.7.0",
|
|
||||||
"express-validator": "^6.14.2",
|
|
||||||
"handlebars": "^4.7.7",
|
|
||||||
"helmet": "^5.1.1",
|
|
||||||
"infisical-node": "^1.2.1",
|
|
||||||
"ioredis": "^5.3.2",
|
|
||||||
"jmespath": "^0.16.0",
|
|
||||||
"js-yaml": "^4.1.0",
|
|
||||||
"jsonwebtoken": "^9.0.0",
|
|
||||||
"jsrp": "^0.2.4",
|
|
||||||
"libsodium-wrappers": "^0.7.10",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"mongoose": "^7.4.1",
|
|
||||||
"mysql2": "^3.6.2",
|
|
||||||
"nanoid": "^3.3.6",
|
|
||||||
"node-cache": "^5.1.2",
|
|
||||||
"nodemailer": "^6.8.0",
|
|
||||||
"ora": "^5.4.1",
|
|
||||||
"passport": "^0.6.0",
|
|
||||||
"passport-github": "^1.1.0",
|
|
||||||
"passport-gitlab2": "^5.0.0",
|
|
||||||
"passport-google-oauth20": "^2.0.0",
|
|
||||||
"pg": "^8.11.3",
|
|
||||||
"pino": "^8.16.1",
|
|
||||||
"pino-http": "^8.5.1",
|
|
||||||
"posthog-node": "^2.6.0",
|
|
||||||
"probot": "^12.3.3",
|
|
||||||
"query-string": "^7.1.3",
|
|
||||||
"rate-limit-mongo": "^2.3.2",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"swagger-ui-express": "^4.6.2",
|
|
||||||
"tweetnacl": "^1.0.3",
|
|
||||||
"tweetnacl-util": "^0.15.1",
|
|
||||||
"typescript": "^4.9.3",
|
|
||||||
"utility-types": "^3.10.0",
|
|
||||||
"zod": "^3.22.3"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"rate-limit-mongo": {
|
|
||||||
"mongodb": "5.8.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"name": "infisical-api",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"start": "node build/index.js",
|
|
||||||
"dev": "nodemon index.js",
|
|
||||||
"swagger-autogen": "node ./swagger/index.ts",
|
|
||||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build && cp -R ./src/data ./build",
|
|
||||||
"lint": "eslint . --ext .ts",
|
|
||||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
|
||||||
"lint-staged": "lint-staged",
|
|
||||||
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
|
|
||||||
"test": "cross-env NODE_ENV=test jest --verbose --testTimeout=10000 --detectOpenHandles; npm run posttest",
|
|
||||||
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
|
|
||||||
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/Infisical/infisical-api.git"
|
|
||||||
},
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"bugs": {
|
|
||||||
"url": "https://github.com/Infisical/infisical-api/issues"
|
|
||||||
},
|
|
||||||
"homepage": "https://github.com/Infisical/infisical-api#readme",
|
|
||||||
"description": "",
|
|
||||||
"devDependencies": {
|
|
||||||
"@jest/globals": "^29.3.1",
|
|
||||||
"@posthog/plugin-scaffold": "^1.3.4",
|
|
||||||
"@swc/core": "^1.3.99",
|
|
||||||
"@swc/helpers": "^0.5.3",
|
|
||||||
"@types/bcrypt": "^5.0.0",
|
|
||||||
"@types/bcryptjs": "^2.4.2",
|
|
||||||
"@types/bull": "^4.10.0",
|
|
||||||
"@types/cookie-parser": "^1.4.3",
|
|
||||||
"@types/cors": "^2.8.12",
|
|
||||||
"@types/express": "^4.17.14",
|
|
||||||
"@types/jest": "^29.5.0",
|
|
||||||
"@types/jmespath": "^0.15.1",
|
|
||||||
"@types/jsonwebtoken": "^8.5.9",
|
|
||||||
"@types/lodash": "^4.14.191",
|
|
||||||
"@types/node": "^18.11.3",
|
|
||||||
"@types/nodemailer": "^6.4.6",
|
|
||||||
"@types/passport": "^1.0.12",
|
|
||||||
"@types/pg": "^8.10.7",
|
|
||||||
"@types/picomatch": "^2.3.0",
|
|
||||||
"@types/pino": "^7.0.5",
|
|
||||||
"@types/supertest": "^2.0.12",
|
|
||||||
"@types/swagger-jsdoc": "^6.0.1",
|
|
||||||
"@types/swagger-ui-express": "^4.1.3",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.54.0",
|
|
||||||
"@typescript-eslint/parser": "^5.40.1",
|
|
||||||
"cross-env": "^7.0.3",
|
|
||||||
"eslint": "^8.26.0",
|
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
|
||||||
"install": "^0.13.0",
|
|
||||||
"jest": "^29.3.1",
|
|
||||||
"jest-junit": "^15.0.0",
|
|
||||||
"nodemon": "^2.0.19",
|
|
||||||
"npm": "^8.19.3",
|
|
||||||
"pino-pretty": "^10.2.3",
|
|
||||||
"regenerator-runtime": "^0.14.0",
|
|
||||||
"smee-client": "^1.2.3",
|
|
||||||
"supertest": "^6.3.3",
|
|
||||||
"swagger-autogen": "^2.23.5",
|
|
||||||
"ts-jest": "^29.0.3",
|
|
||||||
"ts-node": "^10.9.1"
|
|
||||||
},
|
|
||||||
"jest-junit": {
|
|
||||||
"outputDirectory": "reports",
|
|
||||||
"outputName": "jest-junit.xml",
|
|
||||||
"ancestorSeparator": " › ",
|
|
||||||
"uniqueOutputName": "false",
|
|
||||||
"suiteNameTemplate": "{filepath}",
|
|
||||||
"classNameTemplate": "{classname}",
|
|
||||||
"titleTemplate": "{title}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +0,0 @@
|
|||||||
import ora from "ora";
|
|
||||||
import nodemailer from "nodemailer";
|
|
||||||
import { getSmtpHost, getSmtpPort } from "./config";
|
|
||||||
import { logger } from "./utils/logging";
|
|
||||||
import mongoose from "mongoose";
|
|
||||||
import { redisClient } from "./services/RedisService";
|
|
||||||
|
|
||||||
type BootstrapOpt = {
|
|
||||||
transporter: nodemailer.Transporter;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const bootstrap = async ({ transporter }: BootstrapOpt) => {
|
|
||||||
const spinner = ora().start();
|
|
||||||
spinner.info("Checking configurations...");
|
|
||||||
spinner.info("Testing smtp connection");
|
|
||||||
|
|
||||||
await transporter
|
|
||||||
.verify()
|
|
||||||
.then(async () => {
|
|
||||||
spinner.succeed("SMTP successfully connected");
|
|
||||||
})
|
|
||||||
.catch(async (err) => {
|
|
||||||
spinner.fail(`SMTP - Failed to connect to ${await getSmtpHost()}:${await getSmtpPort()}`);
|
|
||||||
logger.error(err);
|
|
||||||
});
|
|
||||||
|
|
||||||
spinner.info("Testing mongodb connection");
|
|
||||||
if (mongoose.connection.readyState !== mongoose.ConnectionStates.connected) {
|
|
||||||
spinner.fail("Mongo DB - Failed to connect");
|
|
||||||
} else {
|
|
||||||
spinner.succeed("Mongodb successfully connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
spinner.info("Testing redis connection");
|
|
||||||
const redisPing = await redisClient?.ping();
|
|
||||||
if (!redisPing) {
|
|
||||||
spinner.fail("Redis - Failed to connect");
|
|
||||||
} else {
|
|
||||||
spinner.succeed("Redis successfully connected");
|
|
||||||
}
|
|
||||||
|
|
||||||
spinner.stop();
|
|
||||||
};
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
import { GITLAB_URL } from "../variables";
|
|
||||||
|
|
||||||
import InfisicalClient from "infisical-node";
|
|
||||||
|
|
||||||
export const client = new InfisicalClient({
|
|
||||||
token: process.env.INFISICAL_TOKEN!
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getIsMigrationMode = async () =>
|
|
||||||
(await client.getSecret("MIGRATION_MODE")).secretValue === "true";
|
|
||||||
|
|
||||||
export const getPort = async () => (await client.getSecret("PORT")).secretValue || 4000;
|
|
||||||
export const getEncryptionKey = async () => {
|
|
||||||
const secretValue = (await client.getSecret("ENCRYPTION_KEY")).secretValue;
|
|
||||||
return secretValue === "" ? undefined : secretValue;
|
|
||||||
};
|
|
||||||
export const getRootEncryptionKey = async () => {
|
|
||||||
const secretValue = (await client.getSecret("ROOT_ENCRYPTION_KEY")).secretValue;
|
|
||||||
return secretValue === "" ? undefined : secretValue;
|
|
||||||
};
|
|
||||||
export const getInviteOnlySignup = async () =>
|
|
||||||
(await client.getSecret("INVITE_ONLY_SIGNUP")).secretValue === "true";
|
|
||||||
export const getSaltRounds = async () =>
|
|
||||||
parseInt((await client.getSecret("SALT_ROUNDS")).secretValue) || 10;
|
|
||||||
export const getAuthSecret = async () =>
|
|
||||||
(await client.getSecret("JWT_AUTH_SECRET")).secretValue ??
|
|
||||||
(await client.getSecret("AUTH_SECRET")).secretValue;
|
|
||||||
export const getJwtAuthLifetime = async () =>
|
|
||||||
(await client.getSecret("JWT_AUTH_LIFETIME")).secretValue || "10d";
|
|
||||||
export const getJwtMfaLifetime = async () =>
|
|
||||||
(await client.getSecret("JWT_MFA_LIFETIME")).secretValue || "5m";
|
|
||||||
export const getJwtRefreshLifetime = async () =>
|
|
||||||
(await client.getSecret("JWT_REFRESH_LIFETIME")).secretValue || "90d";
|
|
||||||
export const getJwtServiceSecret = async () =>
|
|
||||||
(await client.getSecret("JWT_SERVICE_SECRET")).secretValue; // TODO: deprecate (related to ST V1)
|
|
||||||
export const getJwtSignupLifetime = async () =>
|
|
||||||
(await client.getSecret("JWT_SIGNUP_LIFETIME")).secretValue || "15m";
|
|
||||||
export const getJwtProviderAuthLifetime = async () =>
|
|
||||||
(await client.getSecret("JWT_PROVIDER_AUTH_LIFETIME")).secretValue || "15m";
|
|
||||||
export const getMongoURL = async () => (await client.getSecret("MONGO_URL")).secretValue;
|
|
||||||
export const getNodeEnv = async () =>
|
|
||||||
(await client.getSecret("NODE_ENV")).secretValue || "production";
|
|
||||||
export const getVerboseErrorOutput = async () =>
|
|
||||||
(await client.getSecret("VERBOSE_ERROR_OUTPUT")).secretValue === "true" && true;
|
|
||||||
export const getLokiHost = async () => (await client.getSecret("LOKI_HOST")).secretValue;
|
|
||||||
export const getClientIdAzure = async () => (await client.getSecret("CLIENT_ID_AZURE")).secretValue;
|
|
||||||
export const getClientIdHeroku = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_HEROKU")).secretValue;
|
|
||||||
export const getClientIdVercel = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_VERCEL")).secretValue;
|
|
||||||
export const getClientIdNetlify = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_NETLIFY")).secretValue;
|
|
||||||
export const getClientIdGitHub = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_GITHUB")).secretValue;
|
|
||||||
export const getClientIdGitLab = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_GITLAB")).secretValue;
|
|
||||||
export const getClientIdBitBucket = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_BITBUCKET")).secretValue;
|
|
||||||
export const getClientIdGCPSecretManager = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_GCP_SECRET_MANAGER")).secretValue;
|
|
||||||
export const getClientSecretAzure = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_AZURE")).secretValue;
|
|
||||||
export const getClientSecretHeroku = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_HEROKU")).secretValue;
|
|
||||||
export const getClientSecretVercel = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_VERCEL")).secretValue;
|
|
||||||
export const getClientSecretNetlify = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_NETLIFY")).secretValue;
|
|
||||||
export const getClientSecretGitHub = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_GITHUB")).secretValue;
|
|
||||||
export const getClientSecretGitLab = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_GITLAB")).secretValue;
|
|
||||||
export const getClientSecretBitBucket = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_BITBUCKET")).secretValue;
|
|
||||||
export const getClientSecretGCPSecretManager = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_GCP_SECRET_MANAGER")).secretValue;
|
|
||||||
export const getClientSlugVercel = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SLUG_VERCEL")).secretValue;
|
|
||||||
|
|
||||||
export const getClientIdGoogleLogin = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_GOOGLE_LOGIN")).secretValue;
|
|
||||||
export const getClientSecretGoogleLogin = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_GOOGLE_LOGIN")).secretValue;
|
|
||||||
export const getClientIdGitHubLogin = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_GITHUB_LOGIN")).secretValue;
|
|
||||||
export const getClientSecretGitHubLogin = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_GITHUB_LOGIN")).secretValue;
|
|
||||||
export const getClientIdGitLabLogin = async () =>
|
|
||||||
(await client.getSecret("CLIENT_ID_GITLAB_LOGIN")).secretValue;
|
|
||||||
export const getClientSecretGitLabLogin = async () =>
|
|
||||||
(await client.getSecret("CLIENT_SECRET_GITLAB_LOGIN")).secretValue;
|
|
||||||
export const getUrlGitLabLogin = async () =>
|
|
||||||
(await client.getSecret("URL_GITLAB_LOGIN")).secretValue || GITLAB_URL;
|
|
||||||
|
|
||||||
export const getAwsCloudWatchLog = async () => {
|
|
||||||
const logGroupName =
|
|
||||||
(await client.getSecret("AWS_CLOUDWATCH_LOG_GROUP_NAME")).secretValue || "infisical-log-stream";
|
|
||||||
const region = (await client.getSecret("AWS_CLOUDWATCH_LOG_REGION")).secretValue;
|
|
||||||
const accessKeyId = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_ID")).secretValue;
|
|
||||||
const accessKeySecret = (await client.getSecret("AWS_CLOUDWATCH_LOG_ACCESS_KEY_SECRET"))
|
|
||||||
.secretValue;
|
|
||||||
const interval = parseInt(
|
|
||||||
(await client.getSecret("AWS_CLOUDWATCH_LOG_INTERVAL")).secretValue || 1000,
|
|
||||||
10
|
|
||||||
);
|
|
||||||
if (!region || !accessKeyId || !accessKeySecret) return;
|
|
||||||
return { logGroupName, region, accessKeySecret, accessKeyId, interval };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getPostHogHost = async () =>
|
|
||||||
(await client.getSecret("POSTHOG_HOST")).secretValue || "https://app.posthog.com";
|
|
||||||
export const getPostHogProjectApiKey = async () =>
|
|
||||||
(await client.getSecret("POSTHOG_PROJECT_API_KEY")).secretValue ||
|
|
||||||
"phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE";
|
|
||||||
export const getSentryDSN = async () => (await client.getSecret("SENTRY_DSN")).secretValue;
|
|
||||||
export const getSiteURL = async () => (await client.getSecret("SITE_URL")).secretValue;
|
|
||||||
export const getSmtpHost = async () => (await client.getSecret("SMTP_HOST")).secretValue;
|
|
||||||
export const getSmtpSecure = async () =>
|
|
||||||
(await client.getSecret("SMTP_SECURE")).secretValue === "true" || false;
|
|
||||||
export const getSmtpPort = async () =>
|
|
||||||
parseInt((await client.getSecret("SMTP_PORT")).secretValue) || 587;
|
|
||||||
export const getSmtpUsername = async () => (await client.getSecret("SMTP_USERNAME")).secretValue;
|
|
||||||
export const getSmtpPassword = async () => (await client.getSecret("SMTP_PASSWORD")).secretValue;
|
|
||||||
export const getSmtpFromAddress = async () =>
|
|
||||||
(await client.getSecret("SMTP_FROM_ADDRESS")).secretValue;
|
|
||||||
export const getSmtpFromName = async () =>
|
|
||||||
(await client.getSecret("SMTP_FROM_NAME")).secretValue || "Infisical";
|
|
||||||
|
|
||||||
export const getSecretScanningWebhookProxy = async () =>
|
|
||||||
(await client.getSecret("SECRET_SCANNING_WEBHOOK_PROXY")).secretValue;
|
|
||||||
export const getSecretScanningWebhookSecret = async () =>
|
|
||||||
(await client.getSecret("SECRET_SCANNING_WEBHOOK_SECRET")).secretValue;
|
|
||||||
export const getSecretScanningGitAppId = async () =>
|
|
||||||
(await client.getSecret("SECRET_SCANNING_GIT_APP_ID")).secretValue;
|
|
||||||
export const getSecretScanningPrivateKey = async () =>
|
|
||||||
(await client.getSecret("SECRET_SCANNING_PRIVATE_KEY")).secretValue;
|
|
||||||
|
|
||||||
export const getRedisUrl = async () => (await client.getSecret("REDIS_URL")).secretValue;
|
|
||||||
export const getIsInfisicalCloud = async () =>
|
|
||||||
(await client.getSecret("INFISICAL_CLOUD")).secretValue === "true";
|
|
||||||
|
|
||||||
export const getLicenseKey = async () => {
|
|
||||||
const secretValue = (await client.getSecret("LICENSE_KEY")).secretValue;
|
|
||||||
return secretValue === "" ? undefined : secretValue;
|
|
||||||
};
|
|
||||||
export const getLicenseServerKey = async () => {
|
|
||||||
const secretValue = (await client.getSecret("LICENSE_SERVER_KEY")).secretValue;
|
|
||||||
return secretValue === "" ? undefined : secretValue;
|
|
||||||
};
|
|
||||||
export const getLicenseServerUrl = async () =>
|
|
||||||
(await client.getSecret("LICENSE_SERVER_URL")).secretValue || "https://portal.infisical.com";
|
|
||||||
|
|
||||||
export const getTelemetryEnabled = async () =>
|
|
||||||
(await client.getSecret("TELEMETRY_ENABLED")).secretValue !== "false" && true;
|
|
||||||
export const getLoopsApiKey = async () => (await client.getSecret("LOOPS_API_KEY")).secretValue;
|
|
||||||
export const getSmtpConfigured = async () =>
|
|
||||||
(await client.getSecret("SMTP_HOST")).secretValue == "" ||
|
|
||||||
(await client.getSecret("SMTP_HOST")).secretValue == undefined
|
|
||||||
? false
|
|
||||||
: true;
|
|
||||||
export const getHttpsEnabled = async () => {
|
|
||||||
if ((await getNodeEnv()) != "production") {
|
|
||||||
// no https for anything other than prod
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
(await client.getSecret("HTTPS_ENABLED")).secretValue == undefined ||
|
|
||||||
(await client.getSecret("HTTPS_ENABLED")).secretValue == ""
|
|
||||||
) {
|
|
||||||
// default when no value present
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (await client.getSecret("HTTPS_ENABLED")).secretValue === "true" && true;
|
|
||||||
};
|
|
||||||
@@ -1,124 +0,0 @@
|
|||||||
import axios from "axios";
|
|
||||||
import axiosRetry from "axios-retry";
|
|
||||||
import {
|
|
||||||
getLicenseKeyAuthToken,
|
|
||||||
getLicenseServerKeyAuthToken,
|
|
||||||
setLicenseKeyAuthToken,
|
|
||||||
setLicenseServerKeyAuthToken,
|
|
||||||
} from "./storage";
|
|
||||||
import {
|
|
||||||
getLicenseKey,
|
|
||||||
getLicenseServerKey,
|
|
||||||
getLicenseServerUrl,
|
|
||||||
} from "./index";
|
|
||||||
|
|
||||||
// should have JWT to interact with the license server
|
|
||||||
export const licenseServerKeyRequest = axios.create();
|
|
||||||
export const licenseKeyRequest = axios.create();
|
|
||||||
export const standardRequest = axios.create();
|
|
||||||
|
|
||||||
// add retry functionality to the axios instance
|
|
||||||
axiosRetry(standardRequest, {
|
|
||||||
retries: 3,
|
|
||||||
retryDelay: axiosRetry.exponentialDelay, // exponential back-off delay between retries
|
|
||||||
retryCondition: (error) => {
|
|
||||||
// only retry if the error is a network error or a 5xx server error
|
|
||||||
return axiosRetry.isNetworkError(error) || axiosRetry.isRetryableError(error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
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);
|
|
||||||
});
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import { IServerConfig, ServerConfig } from "../models/serverConfig";
|
|
||||||
|
|
||||||
let serverConfig: IServerConfig;
|
|
||||||
|
|
||||||
export const serverConfigInit = async () => {
|
|
||||||
const cfg = await ServerConfig.findOne({}).lean();
|
|
||||||
if (!cfg) {
|
|
||||||
const cfg = new ServerConfig();
|
|
||||||
await cfg.save();
|
|
||||||
serverConfig = cfg.toObject();
|
|
||||||
} else {
|
|
||||||
serverConfig = cfg;
|
|
||||||
}
|
|
||||||
return serverConfig;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getServerConfig = () => serverConfig;
|
|
||||||
|
|
||||||
export const updateServerConfig = async (data: Partial<IServerConfig>) => {
|
|
||||||
const cfg = await ServerConfig.findByIdAndUpdate(serverConfig._id, data, { new: true });
|
|
||||||
if (!cfg) throw new Error("Failed to update server config");
|
|
||||||
serverConfig = cfg.toObject();
|
|
||||||
return serverConfig;
|
|
||||||
};
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
const MemoryLicenseServerKeyTokenStorage = () => {
|
|
||||||
let authToken: string;
|
|
||||||
|
|
||||||
return {
|
|
||||||
setToken: (token: string) => {
|
|
||||||
authToken = token;
|
|
||||||
},
|
|
||||||
getToken: () => authToken,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const MemoryLicenseKeyTokenStorage = () => {
|
|
||||||
let authToken: string;
|
|
||||||
|
|
||||||
return {
|
|
||||||
setToken: (token: string) => {
|
|
||||||
authToken = token;
|
|
||||||
},
|
|
||||||
getToken: () => authToken,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const licenseServerTokenStorage = MemoryLicenseServerKeyTokenStorage();
|
|
||||||
const licenseTokenStorage = MemoryLicenseKeyTokenStorage();
|
|
||||||
|
|
||||||
export const getLicenseServerKeyAuthToken = licenseServerTokenStorage.getToken;
|
|
||||||
export const setLicenseServerKeyAuthToken = licenseServerTokenStorage.setToken;
|
|
||||||
|
|
||||||
export const getLicenseKeyAuthToken = licenseTokenStorage.getToken;
|
|
||||||
export const setLicenseKeyAuthToken = licenseTokenStorage.setToken;
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { getHttpsEnabled, getIsMigrationMode } from "../../config";
|
|
||||||
import { getServerConfig, updateServerConfig as setServerConfig } from "../../config/serverConfig";
|
|
||||||
import { initializeDefaultOrg, issueAuthTokens } from "../../helpers";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import { User } from "../../models";
|
|
||||||
import { TelemetryService } from "../../services";
|
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
|
||||||
import * as reqValidator from "../../validation/admin";
|
|
||||||
|
|
||||||
export const getServerConfigInfo = async (_req: Request, res: Response) => {
|
|
||||||
const config = getServerConfig();
|
|
||||||
const isMigrationModeOn = await getIsMigrationMode();
|
|
||||||
return res.send({ config: { ...config, isMigrationModeOn } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateServerConfig = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { allowSignUp }
|
|
||||||
} = await validateRequest(reqValidator.UpdateServerConfigV1, req);
|
|
||||||
const config = await setServerConfig({ allowSignUp });
|
|
||||||
return res.send({ config });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const adminSignUp = async (req: Request, res: Response) => {
|
|
||||||
const cfg = getServerConfig();
|
|
||||||
if (cfg.initialized) throw UnauthorizedRequestError({ message: "Admin has been created" });
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
email,
|
|
||||||
publicKey,
|
|
||||||
salt,
|
|
||||||
lastName,
|
|
||||||
verifier,
|
|
||||||
firstName,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.SignupV1, req);
|
|
||||||
let user = await User.findOne({ email });
|
|
||||||
if (user) throw BadRequestError({ message: "User already exist" });
|
|
||||||
user = new User({
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
encryptionVersion: 2,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
superAdmin: true
|
|
||||||
});
|
|
||||||
await user.save();
|
|
||||||
await initializeDefaultOrg({ organizationName: "Admin Org", user });
|
|
||||||
|
|
||||||
await setServerConfig({ initialized: true });
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
const tokens = await issueAuthTokens({
|
|
||||||
userId: user._id,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = tokens.token;
|
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "admin initialization",
|
|
||||||
properties: {
|
|
||||||
email: user.email,
|
|
||||||
lastName,
|
|
||||||
firstName
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie("jid", tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: await getHttpsEnabled()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully set up admin account",
|
|
||||||
user,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import * as bigintConversion from "bigint-conversion";
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const jsrp = require("jsrp");
|
|
||||||
import {
|
|
||||||
LoginSRPDetail,
|
|
||||||
TokenVersion,
|
|
||||||
User
|
|
||||||
} from "../../models";
|
|
||||||
import { clearTokens, createToken, issueAuthTokens } from "../../helpers/auth";
|
|
||||||
import { checkUserDevice } from "../../helpers/user";
|
|
||||||
import { AuthTokenType } from "../../variables";
|
|
||||||
import {
|
|
||||||
BadRequestError,
|
|
||||||
UnauthorizedRequestError
|
|
||||||
} from "../../utils/errors";
|
|
||||||
import {
|
|
||||||
getAuthSecret,
|
|
||||||
getHttpsEnabled,
|
|
||||||
getJwtAuthLifetime,
|
|
||||||
} from "../../config";
|
|
||||||
import { ActorType } from "../../ee/models";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/auth";
|
|
||||||
|
|
||||||
declare module "jsonwebtoken" {
|
|
||||||
export interface AuthnJwtPayload extends jwt.JwtPayload {
|
|
||||||
authTokenType: AuthTokenType;
|
|
||||||
}
|
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
|
||||||
userId: string;
|
|
||||||
refreshVersion?: number;
|
|
||||||
}
|
|
||||||
export interface IdentityAccessTokenJwtPayload extends jwt.JwtPayload {
|
|
||||||
_id: string;
|
|
||||||
clientSecretId: string;
|
|
||||||
identityAccessTokenId: string;
|
|
||||||
authTokenType: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { email, clientPublicKey }
|
|
||||||
} = await validateRequest(reqValidator.Login1V1, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+salt +verifier");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
// generate server-side public key
|
|
||||||
const serverPublicKey = server.getPublicKey();
|
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace(
|
|
||||||
{ email: email },
|
|
||||||
{
|
|
||||||
email: email,
|
|
||||||
clientPublicKey: clientPublicKey,
|
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
|
||||||
},
|
|
||||||
{ upsert: true, returnNewDocument: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serverPublicKey,
|
|
||||||
salt: user.salt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
|
||||||
* private key
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { email, clientProof }
|
|
||||||
} = await validateRequest(reqValidator.Login2V1, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+salt +verifier +publicKey +encryptedPrivateKey +iv +tag");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email });
|
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
|
||||||
return BadRequestError(
|
|
||||||
Error(
|
|
||||||
"It looks like some details from the first login are not found. Please try login one again"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
|
||||||
if (server.checkClientProof(clientProof)) {
|
|
||||||
// issue tokens
|
|
||||||
|
|
||||||
await checkUserDevice({
|
|
||||||
user,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
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()
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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({
|
|
||||||
message: "Failed to authenticate. Try again?"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log out user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const logout = async (req: Request, res: Response) => {
|
|
||||||
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
|
|
||||||
await clearTokens(req.authData.tokenVersionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear httpOnly cookie
|
|
||||||
res.cookie("jid", "", {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: (await getHttpsEnabled()) as boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully logged out."
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const revokeAllSessions = async (req: Request, res: Response) => {
|
|
||||||
await TokenVersion.updateMany(
|
|
||||||
{
|
|
||||||
user: req.user._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$inc: {
|
|
||||||
refreshVersion: 1,
|
|
||||||
accessVersion: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully revoked all sessions."
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return user is authenticated
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const checkAuth = async (req: Request, res: Response) => {
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Authenticated"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return new JWT access token by first validating the refresh token
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getNewToken = async (req: Request, res: Response) => {
|
|
||||||
|
|
||||||
const refreshToken = req.cookies.jid;
|
|
||||||
|
|
||||||
if (!refreshToken)
|
|
||||||
throw BadRequestError({
|
|
||||||
message: "Failed to find refresh token in request cookies"
|
|
||||||
});
|
|
||||||
|
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>jwt.verify(refreshToken, await getAuthSecret());
|
|
||||||
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.REFRESH_TOKEN) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
_id: decodedToken.userId
|
|
||||||
}).select("+publicKey +refreshVersion +accessVersion");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to authenticate unfound user");
|
|
||||||
if (!user?.publicKey) throw new Error("Failed to authenticate not fully set up account");
|
|
||||||
|
|
||||||
const tokenVersion = await TokenVersion.findById(decodedToken.tokenVersionId);
|
|
||||||
|
|
||||||
if (!tokenVersion)
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "Failed to validate refresh token"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (decodedToken.refreshVersion !== tokenVersion.refreshVersion)
|
|
||||||
throw BadRequestError({
|
|
||||||
message: "Failed to validate refresh token"
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.ACCESS_TOKEN,
|
|
||||||
userId: decodedToken.userId,
|
|
||||||
tokenVersionId: tokenVersion._id.toString(),
|
|
||||||
accessVersion: tokenVersion.refreshVersion
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtAuthLifetime(),
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
token
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleAuthProviderCallback = (req: Request, res: Response) => {
|
|
||||||
res.redirect(`/login/provider/success?token=${encodeURIComponent(req.providerAuthToken)}`);
|
|
||||||
};
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { Bot, BotKey } from "../../models";
|
|
||||||
import { createBot } from "../../helpers/bot";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/bot";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
|
|
||||||
interface BotKey {
|
|
||||||
encryptedKey: string;
|
|
||||||
nonce: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
|
||||||
* then create and return a new bot.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetBotByWorkspaceIdV1, req);
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
let bot = await Bot.findOne({
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bot) {
|
|
||||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
|
||||||
// -> create a new bot and return it
|
|
||||||
bot = await createBot({
|
|
||||||
name: "Infisical Bot",
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
bot
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return bot with id [req.bot._id] with active state set to [isActive].
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { botKey, isActive },
|
|
||||||
params: { botId }
|
|
||||||
} = await validateRequest(reqValidator.SetBotActiveStateV1, req);
|
|
||||||
|
|
||||||
const bot = await Bot.findById(botId);
|
|
||||||
if (!bot) {
|
|
||||||
throw BadRequestError({ message: "Bot not found" });
|
|
||||||
}
|
|
||||||
const userId = req.user._id;
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: bot.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isActive) {
|
|
||||||
// bot state set to active -> share workspace key with bot
|
|
||||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to set bot state to active - missing bot key"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await BotKey.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
workspace: bot.workspace
|
|
||||||
},
|
|
||||||
{
|
|
||||||
encryptedKey: botKey.encryptedKey,
|
|
||||||
nonce: botKey.nonce,
|
|
||||||
sender: userId,
|
|
||||||
bot: bot._id,
|
|
||||||
workspace: bot.workspace
|
|
||||||
},
|
|
||||||
{
|
|
||||||
upsert: true,
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
// case: bot state set to inactive -> delete bot's workspace key
|
|
||||||
await BotKey.deleteOne({
|
|
||||||
bot: bot._id
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedBot = await Bot.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: bot._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isActive
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!updatedBot) throw new Error("Failed to update bot active state");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
bot
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import * as authController from "./authController";
|
|
||||||
import * as universalAuthController from "./universalAuthController";
|
|
||||||
import * as botController from "./botController";
|
|
||||||
import * as integrationAuthController from "./integrationAuthController";
|
|
||||||
import * as integrationController from "./integrationController";
|
|
||||||
import * as keyController from "./keyController";
|
|
||||||
import * as membershipController from "./membershipController";
|
|
||||||
import * as membershipOrgController from "./membershipOrgController";
|
|
||||||
import * as organizationController from "./organizationController";
|
|
||||||
import * as passwordController from "./passwordController";
|
|
||||||
import * as secretController from "./secretController";
|
|
||||||
import * as serviceTokenController from "./serviceTokenController";
|
|
||||||
import * as signupController from "./signupController";
|
|
||||||
import * as userActionController from "./userActionController";
|
|
||||||
import * as userController from "./userController";
|
|
||||||
import * as workspaceController from "./workspaceController";
|
|
||||||
import * as secretScanningController from "./secretScanningController";
|
|
||||||
import * as webhookController from "./webhookController";
|
|
||||||
import * as secretImpsController from "./secretImpsController";
|
|
||||||
import * as adminController from "./adminController";
|
|
||||||
|
|
||||||
export {
|
|
||||||
authController,
|
|
||||||
universalAuthController,
|
|
||||||
botController,
|
|
||||||
integrationAuthController,
|
|
||||||
integrationController,
|
|
||||||
keyController,
|
|
||||||
membershipController,
|
|
||||||
membershipOrgController,
|
|
||||||
organizationController,
|
|
||||||
passwordController,
|
|
||||||
secretController,
|
|
||||||
serviceTokenController,
|
|
||||||
signupController,
|
|
||||||
userActionController,
|
|
||||||
userController,
|
|
||||||
workspaceController,
|
|
||||||
secretScanningController,
|
|
||||||
webhookController,
|
|
||||||
secretImpsController,
|
|
||||||
adminController
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,322 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { Folder, IWorkspace, Integration, IntegrationAuth } from "../../models";
|
|
||||||
import { EventService } from "../../services";
|
|
||||||
import { eventStartIntegration } from "../../events";
|
|
||||||
import { getFolderByPath } from "../../services/FolderService";
|
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
import { EEAuditLogService } from "../../ee/services";
|
|
||||||
import { EventType } from "../../ee/models";
|
|
||||||
import { syncSecretsToActiveIntegrationsQueue } from "../../queues/integrations/syncSecretsToThirdPartyServices";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/integration";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create/initialize an (empty) integration for integration authorization
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createIntegration = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
isActive,
|
|
||||||
sourceEnvironment,
|
|
||||||
secretPath,
|
|
||||||
app,
|
|
||||||
path,
|
|
||||||
appId,
|
|
||||||
owner,
|
|
||||||
region,
|
|
||||||
scope,
|
|
||||||
targetService,
|
|
||||||
targetServiceId,
|
|
||||||
integrationAuthId,
|
|
||||||
targetEnvironment,
|
|
||||||
targetEnvironmentId,
|
|
||||||
metadata
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.CreateIntegrationV1, req);
|
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findById(integrationAuthId)
|
|
||||||
.populate<{ workspace: IWorkspace }>("workspace")
|
|
||||||
.select(
|
|
||||||
"+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!integrationAuth) throw BadRequestError({ message: "Integration auth not found" });
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: integrationAuth.workspace._id
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: integrationAuth.workspace._id,
|
|
||||||
environment: sourceEnvironment
|
|
||||||
});
|
|
||||||
|
|
||||||
if (folders) {
|
|
||||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
|
||||||
if (!folder) {
|
|
||||||
throw BadRequestError({
|
|
||||||
message: "Folder path doesn't exist"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: validate [sourceEnvironment] and [targetEnvironment]
|
|
||||||
|
|
||||||
// initialize new integration after saving integration access token
|
|
||||||
const integration = await new Integration({
|
|
||||||
workspace: integrationAuth.workspace._id,
|
|
||||||
environment: sourceEnvironment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
targetEnvironmentId,
|
|
||||||
targetService,
|
|
||||||
targetServiceId,
|
|
||||||
owner,
|
|
||||||
path,
|
|
||||||
region,
|
|
||||||
scope,
|
|
||||||
secretPath,
|
|
||||||
integration: integrationAuth.integration,
|
|
||||||
integrationAuth: new Types.ObjectId(integrationAuthId),
|
|
||||||
metadata
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
if (integration) {
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventStartIntegration({
|
|
||||||
workspaceId: integration.workspace,
|
|
||||||
environment: sourceEnvironment
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_INTEGRATION,
|
|
||||||
metadata: {
|
|
||||||
integrationId: integration._id.toString(),
|
|
||||||
integration: integration.integration,
|
|
||||||
environment: integration.environment,
|
|
||||||
secretPath,
|
|
||||||
url: integration.url,
|
|
||||||
app: integration.app,
|
|
||||||
appId: integration.appId,
|
|
||||||
targetEnvironment: integration.targetEnvironment,
|
|
||||||
targetEnvironmentId: integration.targetEnvironmentId,
|
|
||||||
targetService: integration.targetService,
|
|
||||||
targetServiceId: integration.targetServiceId,
|
|
||||||
path: integration.path,
|
|
||||||
region: integration.region
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: integration.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integration
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change environment or name of integration with id [integrationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateIntegration = async (req: Request, res: Response) => {
|
|
||||||
// TODO: add integration-specific validation to ensure that each
|
|
||||||
// integration has the correct fields populated in [Integration]
|
|
||||||
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
environment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
owner, // github-specific integration param
|
|
||||||
secretPath
|
|
||||||
},
|
|
||||||
params: { integrationId }
|
|
||||||
} = await validateRequest(reqValidator.UpdateIntegrationV1, req);
|
|
||||||
|
|
||||||
const integration = await Integration.findById(integrationId);
|
|
||||||
if (!integration) throw BadRequestError({ message: "Integration not found" });
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: integration.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: integration.workspace,
|
|
||||||
environment
|
|
||||||
});
|
|
||||||
|
|
||||||
if (folders) {
|
|
||||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
|
||||||
if (!folder) {
|
|
||||||
throw BadRequestError({
|
|
||||||
message: "Path for service token does not exist"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const updatedIntegration = await Integration.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: integration._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environment,
|
|
||||||
isActive,
|
|
||||||
app,
|
|
||||||
appId,
|
|
||||||
targetEnvironment,
|
|
||||||
owner,
|
|
||||||
secretPath
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (updatedIntegration) {
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventStartIntegration({
|
|
||||||
workspaceId: updatedIntegration.workspace,
|
|
||||||
environment
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integration: updatedIntegration
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete integration with id [integrationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteIntegration = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { integrationId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteIntegrationV1, req);
|
|
||||||
|
|
||||||
const integration = await Integration.findById(integrationId);
|
|
||||||
if (!integration) throw BadRequestError({ message: "Integration not found" });
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: integration.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
const deletedIntegration = await Integration.findOneAndDelete({
|
|
||||||
_id: integrationId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!deletedIntegration) throw new Error("Failed to find integration");
|
|
||||||
|
|
||||||
const numOtherIntegrationsUsingSameAuth = await Integration.countDocuments({
|
|
||||||
integrationAuth: deletedIntegration.integrationAuth,
|
|
||||||
_id: {
|
|
||||||
$nin: [deletedIntegration._id]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (numOtherIntegrationsUsingSameAuth === 0) {
|
|
||||||
// no other integrations are using the same integration auth
|
|
||||||
// -> delete integration auth associated with the integration being deleted
|
|
||||||
await IntegrationAuth.deleteOne({
|
|
||||||
_id: deletedIntegration.integrationAuth
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.DELETE_INTEGRATION,
|
|
||||||
metadata: {
|
|
||||||
integrationId: integration._id.toString(),
|
|
||||||
integration: integration.integration,
|
|
||||||
environment: integration.environment,
|
|
||||||
secretPath: integration.secretPath,
|
|
||||||
url: integration.url,
|
|
||||||
app: integration.app,
|
|
||||||
appId: integration.appId,
|
|
||||||
targetEnvironment: integration.targetEnvironment,
|
|
||||||
targetEnvironmentId: integration.targetEnvironmentId,
|
|
||||||
targetService: integration.targetService,
|
|
||||||
targetServiceId: integration.targetServiceId,
|
|
||||||
path: integration.path,
|
|
||||||
region: integration.region
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: integration.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integration
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Will trigger sync for all integrations within the given env and workspace id
|
|
||||||
export const manualSync = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { workspaceId, environment }
|
|
||||||
} = await validateRequest(reqValidator.ManualSyncV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
syncSecretsToActiveIntegrationsQueue({
|
|
||||||
workspaceId,
|
|
||||||
environment
|
|
||||||
});
|
|
||||||
|
|
||||||
res.status(200).send();
|
|
||||||
};
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { Key } from "../../models";
|
|
||||||
import { findMembership } from "../../helpers/membership";
|
|
||||||
import { EventType } from "../../ee/models";
|
|
||||||
import { EEAuditLogService } from "../../ee/services";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/key";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
|
|
||||||
* id [key.userId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const uploadKey = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { key }
|
|
||||||
} = await validateRequest(reqValidator.UploadKeyV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
// validate membership of receiver
|
|
||||||
const receiverMembership = await findMembership({
|
|
||||||
user: key.userId,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!receiverMembership) {
|
|
||||||
throw new Error("Failed receiver membership validation for workspace");
|
|
||||||
}
|
|
||||||
|
|
||||||
await new Key({
|
|
||||||
encryptedKey: key.encryptedKey,
|
|
||||||
nonce: key.nonce,
|
|
||||||
sender: req.user._id,
|
|
||||||
receiver: key.userId,
|
|
||||||
workspace: workspaceId
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully uploaded key to workspace"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return latest (encrypted) copy of workspace key for user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getLatestKey = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetLatestKeyV1, req);
|
|
||||||
|
|
||||||
// get latest key
|
|
||||||
const latestKey = await Key.find({
|
|
||||||
workspace: workspaceId,
|
|
||||||
receiver: req.user._id
|
|
||||||
})
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.limit(1)
|
|
||||||
.populate("sender", "+publicKey");
|
|
||||||
|
|
||||||
const resObj: any = {};
|
|
||||||
|
|
||||||
if (latestKey.length > 0) {
|
|
||||||
resObj["latestKey"] = latestKey[0];
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.GET_WORKSPACE_KEY,
|
|
||||||
metadata: {
|
|
||||||
keyId: latestKey[0]._id.toString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
|
||||||
};
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { IUser, Key, Membership, MembershipOrg, User, Workspace } from "../../models";
|
|
||||||
import { EventType, Role } from "../../ee/models";
|
|
||||||
import { deleteMembership as deleteMember, findMembership } from "../../helpers/membership";
|
|
||||||
import { sendMail } from "../../helpers/nodemailer";
|
|
||||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
|
|
||||||
import { getSiteURL } from "../../config";
|
|
||||||
import { EEAuditLogService, EELicenseService } from "../../ee/services";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/membership";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
import { InviteUserToWorkspaceV1 } from "../../validation/workspace";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check that user is a member of workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const validateMembership = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.ValidateMembershipV1, req);
|
|
||||||
|
|
||||||
// validate membership
|
|
||||||
const membership = await findMembership({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error("Failed to validate membership");
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Workspace membership confirmed"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete membership with id [membershipId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteMembership = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { membershipId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteMembershipV1, req);
|
|
||||||
|
|
||||||
// check if membership to delete exists
|
|
||||||
const membershipToDelete = await Membership.findOne({
|
|
||||||
_id: membershipId
|
|
||||||
}).populate<{ user: IUser }>("user");
|
|
||||||
|
|
||||||
if (!membershipToDelete) {
|
|
||||||
throw new Error("Failed to delete workspace membership that doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: membershipToDelete.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete workspace membership
|
|
||||||
const deletedMembership = await deleteMember({
|
|
||||||
membershipId: membershipToDelete._id.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.REMOVE_WORKSPACE_MEMBER,
|
|
||||||
metadata: {
|
|
||||||
userId: membershipToDelete.user._id.toString(),
|
|
||||||
email: membershipToDelete.user.email
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: membershipToDelete.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
deletedMembership
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change and return workspace membership role
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const changeMembershipRole = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { role },
|
|
||||||
params: { membershipId }
|
|
||||||
} = await validateRequest(reqValidator.ChangeMembershipRoleV1, req);
|
|
||||||
|
|
||||||
// validate target membership
|
|
||||||
const membershipToChangeRole = await Membership.findById(membershipId).populate<{ user: IUser }>(
|
|
||||||
"user"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!membershipToChangeRole) {
|
|
||||||
throw new Error("Failed to find membership to change role");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: membershipToChangeRole.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
|
||||||
if (isCustomRole) {
|
|
||||||
const wsRole = await Role.findOne({
|
|
||||||
slug: role,
|
|
||||||
isOrgRole: false,
|
|
||||||
workspace: membershipToChangeRole.workspace
|
|
||||||
});
|
|
||||||
if (!wsRole) throw BadRequestError({ message: "Role not found" });
|
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(wsRole.organization);
|
|
||||||
|
|
||||||
if (!plan.rbac) return res.status(400).send({
|
|
||||||
message: "Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
|
||||||
});
|
|
||||||
|
|
||||||
const membership = await Membership.findByIdAndUpdate(membershipId, {
|
|
||||||
role: CUSTOM,
|
|
||||||
customRole: wsRole
|
|
||||||
});
|
|
||||||
return res.status(200).send({
|
|
||||||
membership
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const membership = await Membership.findByIdAndUpdate(
|
|
||||||
membershipId,
|
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
role
|
|
||||||
},
|
|
||||||
$unset: {
|
|
||||||
customRole: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
|
||||||
metadata: {
|
|
||||||
userId: membershipToChangeRole.user._id.toString(),
|
|
||||||
email: membershipToChangeRole.user.email,
|
|
||||||
oldRole: membershipToChangeRole.role,
|
|
||||||
newRole: role
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: membershipToChangeRole.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
membership
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add user with email [email] to workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const inviteUserToWorkspace = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { email }
|
|
||||||
} = await validateRequest(InviteUserToWorkspaceV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const invitee = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+publicKey");
|
|
||||||
|
|
||||||
if (!invitee || !invitee?.publicKey) throw new Error("Failed to validate invitee");
|
|
||||||
|
|
||||||
// validate invitee's workspace membership - ensure member isn't
|
|
||||||
// already a member of the workspace
|
|
||||||
const inviteeMembership = await Membership.findOne({
|
|
||||||
user: invitee._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
}).populate<{ user: IUser }>("user");
|
|
||||||
|
|
||||||
if (inviteeMembership) throw new Error("Failed to add existing member of workspace");
|
|
||||||
|
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
|
||||||
if (!workspace) throw new Error("Failed to find workspace");
|
|
||||||
// validate invitee's organization membership - ensure that only
|
|
||||||
// (accepted) organization members can be added to the workspace
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
|
||||||
user: invitee._id,
|
|
||||||
organization: workspace.organization,
|
|
||||||
status: ACCEPTED
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membershipOrg) throw new Error("Failed to validate invitee's organization membership");
|
|
||||||
|
|
||||||
// get latest key
|
|
||||||
const latestKey = await Key.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
receiver: req.user._id
|
|
||||||
})
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.populate("sender", "+publicKey");
|
|
||||||
|
|
||||||
// create new workspace membership
|
|
||||||
await new Membership({
|
|
||||||
user: invitee._id,
|
|
||||||
workspace: workspaceId,
|
|
||||||
role: MEMBER
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
await sendMail({
|
|
||||||
template: "workspaceInvitation.handlebars",
|
|
||||||
subjectLine: "Infisical workspace invitation",
|
|
||||||
recipients: [invitee.email],
|
|
||||||
substitutions: {
|
|
||||||
inviterFirstName: req.user.firstName,
|
|
||||||
inviterEmail: req.user.email,
|
|
||||||
workspaceName: workspace.name,
|
|
||||||
callback_url: (await getSiteURL()) + "/login"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.ADD_WORKSPACE_MEMBER,
|
|
||||||
metadata: {
|
|
||||||
userId: invitee._id.toString(),
|
|
||||||
email: invitee.email
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
invitee,
|
|
||||||
latestKey
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,292 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { MembershipOrg, Organization, User } from "../../models";
|
|
||||||
import { SSOConfig } from "../../ee/models";
|
|
||||||
import { deleteMembershipOrg as deleteMemberFromOrg } from "../../helpers/membershipOrg";
|
|
||||||
import { createToken } from "../../helpers/auth";
|
|
||||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
|
||||||
import { sendMail } from "../../helpers/nodemailer";
|
|
||||||
import { TokenService } from "../../services";
|
|
||||||
import { EELicenseService } from "../../ee/services";
|
|
||||||
import { ACCEPTED, AuthTokenType, INVITED, MEMBER, TOKEN_EMAIL_ORG_INVITATION } from "../../variables";
|
|
||||||
import * as reqValidator from "../../validation/membershipOrg";
|
|
||||||
import {
|
|
||||||
getAuthSecret,
|
|
||||||
getJwtSignupLifetime,
|
|
||||||
getSiteURL,
|
|
||||||
getSmtpConfigured
|
|
||||||
} from "../../config";
|
|
||||||
import { validateUserEmail } from "../../validation";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
getAuthDataOrgPermissions
|
|
||||||
} from "../../ee/services/RoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete organization membership with id [membershipOrgId] from organization
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteMembershipOrg = async (req: Request, _res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { membershipOrgId }
|
|
||||||
} = await validateRequest(reqValidator.DelOrgMembershipv1, req);
|
|
||||||
|
|
||||||
// check if organization membership to delete exists
|
|
||||||
const membershipOrgToDelete = await MembershipOrg.findOne({
|
|
||||||
_id: membershipOrgId
|
|
||||||
}).populate("user");
|
|
||||||
|
|
||||||
if (!membershipOrgToDelete) {
|
|
||||||
throw new Error("Failed to delete organization membership that doesn't exist");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: membershipOrgToDelete.organization
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Delete,
|
|
||||||
OrgPermissionSubjects.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete organization membership
|
|
||||||
await deleteMemberFromOrg({
|
|
||||||
membershipOrgId: membershipOrgToDelete._id.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
|
||||||
organizationId: membershipOrgToDelete.organization.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return membershipOrgToDelete;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change and return organization membership role
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const changeMembershipOrgRole = async (req: Request, res: Response) => {
|
|
||||||
// change role for (target) organization membership with id
|
|
||||||
// [membershipOrgId]
|
|
||||||
|
|
||||||
let membershipToChangeRole;
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
membershipOrg: membershipToChangeRole
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Organization invitation step 1: Send email invitation to user with email [email]
|
|
||||||
* for organization with id [organizationId] containing magic link
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const inviteUserToOrganization = async (req: Request, res: Response) => {
|
|
||||||
let inviteeMembershipOrg, completeInviteLink;
|
|
||||||
const {
|
|
||||||
body: { inviteeEmail, organizationId }
|
|
||||||
} = await validateRequest(reqValidator.InviteUserToOrgv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const host = req.headers.host;
|
|
||||||
const siteUrl = `${req.protocol}://${host}`;
|
|
||||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
const ssoConfig = await SSOConfig.findOne({
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (ssoConfig && ssoConfig.isActive) {
|
|
||||||
// case: SAML SSO is enabled for the organization
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to invite member due to SAML SSO configured for organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const invitee = await User.findOne({
|
|
||||||
email: inviteeEmail
|
|
||||||
}).select("+publicKey");
|
|
||||||
|
|
||||||
if (invitee) {
|
|
||||||
// case: invitee is an existing user
|
|
||||||
|
|
||||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
|
||||||
user: invitee._id,
|
|
||||||
organization: organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
|
|
||||||
throw new Error("Failed to invite an existing member of the organization");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!inviteeMembershipOrg) {
|
|
||||||
await new MembershipOrg({
|
|
||||||
user: invitee,
|
|
||||||
inviteEmail: inviteeEmail,
|
|
||||||
organization: organizationId,
|
|
||||||
role: MEMBER,
|
|
||||||
status: INVITED
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check if invitee has been invited before
|
|
||||||
inviteeMembershipOrg = await MembershipOrg.findOne({
|
|
||||||
inviteEmail: inviteeEmail,
|
|
||||||
organization: organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!inviteeMembershipOrg) {
|
|
||||||
// case: invitee has never been invited before
|
|
||||||
|
|
||||||
// validate that email is not disposable
|
|
||||||
validateUserEmail(inviteeEmail);
|
|
||||||
|
|
||||||
await new MembershipOrg({
|
|
||||||
inviteEmail: inviteeEmail,
|
|
||||||
organization: organizationId,
|
|
||||||
role: MEMBER,
|
|
||||||
status: INVITED
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const organization = await Organization.findOne({ _id: organizationId });
|
|
||||||
|
|
||||||
if (organization) {
|
|
||||||
const token = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
|
||||||
email: inviteeEmail,
|
|
||||||
organizationId: organization._id
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendMail({
|
|
||||||
template: "organizationInvitation.handlebars",
|
|
||||||
subjectLine: "Infisical organization invitation",
|
|
||||||
recipients: [inviteeEmail],
|
|
||||||
substitutions: {
|
|
||||||
inviterFirstName: req.user.firstName,
|
|
||||||
inviterEmail: req.user.email,
|
|
||||||
organizationName: organization.name,
|
|
||||||
email: inviteeEmail,
|
|
||||||
organizationId: organization._id.toString(),
|
|
||||||
token,
|
|
||||||
callback_url: (await getSiteURL()) + "/signupinvite"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!(await getSmtpConfigured())) {
|
|
||||||
completeInviteLink = `${
|
|
||||||
siteUrl + "/signupinvite"
|
|
||||||
}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({ organizationId });
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: `Sent an invite link to ${req.body.inviteeEmail}`,
|
|
||||||
completeInviteLink
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Organization invitation step 2: Verify that code [code] was sent to email [email] as part of
|
|
||||||
* magic link and issue a temporary signup token for user to complete setting up their account
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const verifyUserToOrganization = async (req: Request, res: Response) => {
|
|
||||||
let user;
|
|
||||||
|
|
||||||
const {
|
|
||||||
body: { organizationId, email, code }
|
|
||||||
} = await validateRequest(reqValidator.VerifyUserToOrgv1, req);
|
|
||||||
|
|
||||||
user = await User.findOne({ email }).select("+publicKey");
|
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED,
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membershipOrg) throw new Error("Failed to find any invitations for email");
|
|
||||||
|
|
||||||
await TokenService.validateToken({
|
|
||||||
type: TOKEN_EMAIL_ORG_INVITATION,
|
|
||||||
email,
|
|
||||||
organizationId: membershipOrg.organization,
|
|
||||||
token: code
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user && user?.publicKey) {
|
|
||||||
// case: user has already completed account
|
|
||||||
// membership can be approved and redirected to login/dashboard
|
|
||||||
membershipOrg.status = ACCEPTED;
|
|
||||||
await membershipOrg.save();
|
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
|
||||||
organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully verified email",
|
|
||||||
user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
// initialize user account
|
|
||||||
user = await new User({
|
|
||||||
email
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate temporary signup token
|
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
|
||||||
userId: user._id.toString()
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtSignupLifetime(),
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully verified email",
|
|
||||||
user,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,387 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
|
||||||
IncidentContactOrg,
|
|
||||||
Membership,
|
|
||||||
MembershipOrg,
|
|
||||||
Organization,
|
|
||||||
Workspace
|
|
||||||
} from "../../models";
|
|
||||||
import { getLicenseServerUrl, getSiteURL } from "../../config";
|
|
||||||
import { licenseServerKeyRequest } from "../../config/request";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/organization";
|
|
||||||
import { ACCEPTED } from "../../variables";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
getAuthDataOrgPermissions
|
|
||||||
} from "../../ee/services/RoleService";
|
|
||||||
import { OrganizationNotFoundError } from "../../utils/errors";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
export const getOrganizations = async (req: Request, res: Response) => {
|
|
||||||
const organizations = (
|
|
||||||
await MembershipOrg.find({
|
|
||||||
user: req.user._id,
|
|
||||||
status: ACCEPTED
|
|
||||||
}).populate("organization")
|
|
||||||
).map((m) => m.organization);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
organizations
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganization = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgv1, req);
|
|
||||||
|
|
||||||
// ensure user has membership
|
|
||||||
await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
})
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
organization
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return organization memberships for organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationMembers = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const users = await MembershipOrg.find({
|
|
||||||
organization: organizationId
|
|
||||||
}).populate("user", "+publicKey");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
users
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return workspaces that user is part of in organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgWorkspacesv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
})
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Workspace
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspacesSet = new Set(
|
|
||||||
(
|
|
||||||
await Workspace.find(
|
|
||||||
{
|
|
||||||
organization: organizationId
|
|
||||||
},
|
|
||||||
"_id"
|
|
||||||
)
|
|
||||||
).map((w) => w._id.toString())
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaces = (
|
|
||||||
await Membership.find({
|
|
||||||
user: req.user._id
|
|
||||||
}).populate("workspace")
|
|
||||||
)
|
|
||||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
|
||||||
.map((m) => m.workspace);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
workspaces
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change name of organization with id [organizationId] to [name]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const changeOrganizationName = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId },
|
|
||||||
body: { name }
|
|
||||||
} = await validateRequest(reqValidator.ChangeOrgNamev1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.Settings
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: organizationId
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully changed organization name",
|
|
||||||
organization
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return incident contacts of organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationIncidentContacts = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgIncidentContactv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.IncidentAccount
|
|
||||||
);
|
|
||||||
|
|
||||||
const incidentContactsOrg = await IncidentContactOrg.find({
|
|
||||||
organization: organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
incidentContactsOrg
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add and return new incident contact with email [email] for organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const addOrganizationIncidentContact = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId },
|
|
||||||
body: { email }
|
|
||||||
} = await validateRequest(reqValidator.CreateOrgIncideContact, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.IncidentAccount
|
|
||||||
);
|
|
||||||
|
|
||||||
const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
|
|
||||||
{ email, organization: organizationId },
|
|
||||||
{ email, organization: organizationId },
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
incidentContactOrg
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete incident contact with email [email] for organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteOrganizationIncidentContact = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId },
|
|
||||||
body: { email }
|
|
||||||
} = await validateRequest(reqValidator.DelOrgIncideContact, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Delete,
|
|
||||||
OrgPermissionSubjects.IncidentAccount
|
|
||||||
);
|
|
||||||
|
|
||||||
const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
|
|
||||||
email,
|
|
||||||
organization: organizationId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully deleted organization incident contact",
|
|
||||||
incidentContactOrg
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect user to billing portal or add card page depending on
|
|
||||||
* if there is a card on file
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createOrganizationPortalSession = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { pmtMethods }
|
|
||||||
} = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/payment-methods`
|
|
||||||
);
|
|
||||||
|
|
||||||
if (pmtMethods.length < 1) {
|
|
||||||
// case: organization has no payment method on file
|
|
||||||
// -> redirect to add payment method portal
|
|
||||||
const {
|
|
||||||
data: { url }
|
|
||||||
} = await licenseServerKeyRequest.post(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/payment-methods`,
|
|
||||||
{
|
|
||||||
success_url: (await getSiteURL()) + "/dashboard",
|
|
||||||
cancel_url: (await getSiteURL()) + "/dashboard"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return res.status(200).send({ url });
|
|
||||||
} else {
|
|
||||||
// case: organization has payment method on file
|
|
||||||
// -> redirect to billing portal
|
|
||||||
const {
|
|
||||||
data: { url }
|
|
||||||
} = await licenseServerKeyRequest.post(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/billing-portal`,
|
|
||||||
{
|
|
||||||
return_url: (await getSiteURL()) + "/dashboard"
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return res.status(200).send({ url });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Given a org id, return the projects each member of the org belongs to
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationMembersAndTheirWorkspaces = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgMembersv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Member
|
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Workspace
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspacesSet = (
|
|
||||||
await Workspace.find(
|
|
||||||
{
|
|
||||||
organization: organizationId
|
|
||||||
},
|
|
||||||
"_id"
|
|
||||||
)
|
|
||||||
).map((w) => w._id.toString());
|
|
||||||
|
|
||||||
const memberships = await Membership.find({
|
|
||||||
workspace: { $in: workspacesSet }
|
|
||||||
}).populate("workspace");
|
|
||||||
const userToWorkspaceIds: any = {};
|
|
||||||
|
|
||||||
memberships.forEach((membership) => {
|
|
||||||
const user = membership.user.toString();
|
|
||||||
if (userToWorkspaceIds[user]) {
|
|
||||||
userToWorkspaceIds[user].push(membership.workspace);
|
|
||||||
} else {
|
|
||||||
userToWorkspaceIds[user] = [membership.workspace];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json(userToWorkspaceIds);
|
|
||||||
};
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
const jsrp = require("jsrp");
|
|
||||||
import * as bigintConversion from "bigint-conversion";
|
|
||||||
import { BackupPrivateKey, LoginSRPDetail, User } from "../../models";
|
|
||||||
import { clearTokens, createToken, sendMail } from "../../helpers";
|
|
||||||
import { TokenService } from "../../services";
|
|
||||||
import { AuthTokenType, TOKEN_EMAIL_PASSWORD_RESET } from "../../variables";
|
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
import {
|
|
||||||
getAuthSecret,
|
|
||||||
getHttpsEnabled,
|
|
||||||
getJwtSignupLifetime,
|
|
||||||
getSiteURL
|
|
||||||
} from "../../config";
|
|
||||||
import { ActorType } from "../../ee/models";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/auth";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Password reset step 1: Send email verification link to email [email]
|
|
||||||
* for account recovery.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const emailPasswordReset = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { email }
|
|
||||||
} = await validateRequest(reqValidator.EmailPasswordResetV1, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select("+publicKey");
|
|
||||||
if (!user || !user?.publicKey) {
|
|
||||||
// case: user has already completed account
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "If an account exists with this email, a password reset link has been sent"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
|
||||||
email
|
|
||||||
});
|
|
||||||
|
|
||||||
await sendMail({
|
|
||||||
template: "passwordReset.handlebars",
|
|
||||||
subjectLine: "Infisical password reset",
|
|
||||||
recipients: [email],
|
|
||||||
substitutions: {
|
|
||||||
email,
|
|
||||||
token,
|
|
||||||
callback_url: (await getSiteURL()) + "/password-reset"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "If an account exists with this email, a password reset link has been sent"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Password reset step 2: Verify email verification link sent to email [email]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const emailPasswordResetVerify = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { email, code }
|
|
||||||
} = await validateRequest(reqValidator.EmailPasswordResetVerifyV1, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select("+publicKey");
|
|
||||||
if (!user || !user?.publicKey) {
|
|
||||||
// case: user doesn't exist with email [email] or
|
|
||||||
// hasn't even completed their account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: "Failed email verification for password reset"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await TokenService.validateToken({
|
|
||||||
type: TOKEN_EMAIL_PASSWORD_RESET,
|
|
||||||
email,
|
|
||||||
token: code
|
|
||||||
});
|
|
||||||
|
|
||||||
// generate temporary password-reset token
|
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
|
||||||
userId: user._id.toString()
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtSignupLifetime(),
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully verified email",
|
|
||||||
user,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const srp1 = async (req: Request, res: Response) => {
|
|
||||||
// return salt, serverPublicKey as part of first step of SRP protocol
|
|
||||||
const {
|
|
||||||
body: { clientPublicKey }
|
|
||||||
} = await validateRequest(reqValidator.Srp1V1, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email: req.user.email
|
|
||||||
}).select("+salt +verifier");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
// generate server-side public key
|
|
||||||
const serverPublicKey = server.getPublicKey();
|
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace(
|
|
||||||
{ email: req.user.email },
|
|
||||||
{
|
|
||||||
email: req.user.email,
|
|
||||||
clientPublicKey: clientPublicKey,
|
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
|
||||||
},
|
|
||||||
{ upsert: true, returnNewDocument: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serverPublicKey,
|
|
||||||
salt: user.salt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change account SRP authentication information for user
|
|
||||||
* Requires verifying [clientProof] as part of step 2 of SRP protocol
|
|
||||||
* as initiated in POST /srp1
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const changePassword = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
clientProof,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.ChangePasswordV1, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email: req.user.email
|
|
||||||
}).select("+salt +verifier");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
|
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
|
||||||
return BadRequestError(
|
|
||||||
Error(
|
|
||||||
"It looks like some details from the first login are not found. Please try login one again"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
|
||||||
if (server.checkClientProof(clientProof)) {
|
|
||||||
// change password
|
|
||||||
|
|
||||||
await User.findByIdAndUpdate(
|
|
||||||
req.user._id.toString(),
|
|
||||||
{
|
|
||||||
encryptionVersion: 2,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (req.authData.actor.type === ActorType.USER && req.authData.tokenVersionId) {
|
|
||||||
await clearTokens(req.authData.tokenVersionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// clear httpOnly cookie
|
|
||||||
|
|
||||||
res.cookie("jid", "", {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: (await getHttpsEnabled()) as boolean
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully changed password"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).send({
|
|
||||||
error: "Failed to change password. Try again?"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create or change backup private key for user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createBackupPrivateKey = async (req: Request, res: Response) => {
|
|
||||||
// create/change backup private key
|
|
||||||
// requires verifying [clientProof] as part of second step of SRP protocol
|
|
||||||
// as initiated in /srp1
|
|
||||||
const {
|
|
||||||
body: { clientProof, encryptedPrivateKey, salt, verifier, iv, tag }
|
|
||||||
} = await validateRequest(reqValidator.CreateBackupPrivateKeyV1, req);
|
|
||||||
const user = await User.findOne({
|
|
||||||
email: req.user.email
|
|
||||||
}).select("+salt +verifier");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email });
|
|
||||||
|
|
||||||
if (!loginSRPDetailFromDB) {
|
|
||||||
return BadRequestError(
|
|
||||||
Error(
|
|
||||||
"It looks like some details from the first login are not found. Please try login one again"
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetailFromDB.serverBInt
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
|
||||||
if (server.checkClientProof(clientProof)) {
|
|
||||||
// create new or replace backup private key
|
|
||||||
|
|
||||||
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
|
|
||||||
{ user: req.user._id },
|
|
||||||
{
|
|
||||||
user: req.user._id,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
).select("+user, encryptedPrivateKey");
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully updated backup private key",
|
|
||||||
backupPrivateKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to update backup private key"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return backup private key for user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getBackupPrivateKey = async (req: Request, res: Response) => {
|
|
||||||
const backupPrivateKey = await BackupPrivateKey.findOne({
|
|
||||||
user: req.user._id
|
|
||||||
}).select("+encryptedPrivateKey +iv +tag");
|
|
||||||
|
|
||||||
if (!backupPrivateKey) throw new Error("Failed to find backup private key");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
backupPrivateKey
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const resetPassword = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
encryptedPrivateKey,
|
|
||||||
protectedKeyTag,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.ResetPasswordV1, req);
|
|
||||||
|
|
||||||
await User.findByIdAndUpdate(
|
|
||||||
req.user._id.toString(),
|
|
||||||
{
|
|
||||||
encryptionVersion: 2,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
iv: encryptedPrivateKeyIV,
|
|
||||||
tag: encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully reset password"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { Key } from "../../models";
|
|
||||||
import {
|
|
||||||
pullSecrets as pull,
|
|
||||||
v1PushSecrets as push,
|
|
||||||
reformatPullSecrets
|
|
||||||
} from "../../helpers/secret";
|
|
||||||
import { pushKeys } from "../../helpers/key";
|
|
||||||
import { eventPushSecrets } from "../../events";
|
|
||||||
import { EventService } from "../../services";
|
|
||||||
import { TelemetryService } from "../../services";
|
|
||||||
|
|
||||||
interface PushSecret {
|
|
||||||
ciphertextKey: string;
|
|
||||||
ivKey: string;
|
|
||||||
tagKey: string;
|
|
||||||
hashKey: string;
|
|
||||||
ciphertextValue: string;
|
|
||||||
ivValue: string;
|
|
||||||
tagValue: string;
|
|
||||||
hashValue: string;
|
|
||||||
ciphertextComment: string;
|
|
||||||
ivComment: string;
|
|
||||||
tagComment: string;
|
|
||||||
hashComment: string;
|
|
||||||
type: "shared" | "personal";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
|
||||||
* for environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const pushSecrets = async (req: Request, res: Response) => {
|
|
||||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
let { secrets }: { secrets: PushSecret[] } = req.body;
|
|
||||||
const { keys, environment, channel } = req.body;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
|
||||||
throw new Error("Failed to validate environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitize secrets
|
|
||||||
secrets = secrets.filter((s: PushSecret) => s.ciphertextKey !== "" && s.ciphertextValue !== "");
|
|
||||||
|
|
||||||
await push({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secrets
|
|
||||||
});
|
|
||||||
|
|
||||||
await pushKeys({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId,
|
|
||||||
keys
|
|
||||||
});
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets pushed",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: channel ? channel : "cli"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
secretPath: "/"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully uploaded workspace secrets"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
|
||||||
* for environment [environment] and (encrypted) workspace key
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const pullSecrets = async (req: Request, res: Response) => {
|
|
||||||
let secrets;
|
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const environment: string = req.query.environment as string;
|
|
||||||
const channel: string = req.query.channel as string;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
|
||||||
throw new Error("Failed to validate environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
secrets = await pull({
|
|
||||||
userId: req.user._id.toString(),
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
channel: channel ? channel : "cli",
|
|
||||||
ipAddress: req.realIP
|
|
||||||
});
|
|
||||||
|
|
||||||
const key = await Key.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
receiver: req.user._id
|
|
||||||
})
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.populate("sender", "+publicKey");
|
|
||||||
|
|
||||||
if (channel !== "cli") {
|
|
||||||
secrets = reformatPullSecrets({ secrets });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
// capture secrets pushed event in production
|
|
||||||
postHogClient.capture({
|
|
||||||
distinctId: req.user.email,
|
|
||||||
event: "secrets pulled",
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: channel ? channel : "cli"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secrets,
|
|
||||||
key
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
|
||||||
* for environment [environment] and (encrypted) workspace key
|
|
||||||
* via service token
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const environment: string = req.query.environment as string;
|
|
||||||
const channel: string = req.query.channel as string;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
|
||||||
throw new Error("Failed to validate environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
const secrets = await pull({
|
|
||||||
userId: req.serviceToken.user._id.toString(),
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
channel: "cli",
|
|
||||||
ipAddress: req.realIP
|
|
||||||
});
|
|
||||||
|
|
||||||
const key = {
|
|
||||||
encryptedKey: req.serviceToken.encryptedKey,
|
|
||||||
nonce: req.serviceToken.nonce,
|
|
||||||
sender: {
|
|
||||||
publicKey: req.serviceToken.publicKey
|
|
||||||
},
|
|
||||||
receiver: req.serviceToken.user,
|
|
||||||
workspace: req.serviceToken.workspace
|
|
||||||
};
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
// capture secrets pulled event in production
|
|
||||||
postHogClient.capture({
|
|
||||||
distinctId: req.serviceToken.user.email,
|
|
||||||
event: "secrets pulled",
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: channel ? channel : "cli"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secrets: reformatPullSecrets({ secrets }),
|
|
||||||
key
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,734 +0,0 @@
|
|||||||
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { isValidScope } from "../../helpers";
|
|
||||||
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
|
|
||||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
|
||||||
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
|
|
||||||
import {
|
|
||||||
BadRequestError,
|
|
||||||
ResourceNotFoundError,
|
|
||||||
UnauthorizedRequestError
|
|
||||||
} from "../../utils/errors";
|
|
||||||
import { EEAuditLogService } from "../../ee/services";
|
|
||||||
import { EventType } from "../../ee/models";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/secretImports";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
|
||||||
|
|
||||||
export const createSecretImp = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Create secret import'
|
|
||||||
#swagger.description = 'Create secret import'
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of workspace where to create secret import",
|
|
||||||
"example": "someWorkspaceId"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment where to create secret import",
|
|
||||||
"example": "dev"
|
|
||||||
},
|
|
||||||
"directory": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path where to create secret import like / or /foo/bar. Default is /",
|
|
||||||
"example": "/foo/bar"
|
|
||||||
},
|
|
||||||
"secretImport": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"environment": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment to import from",
|
|
||||||
"example": "development"
|
|
||||||
},
|
|
||||||
"secretPath": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path where to import from like / or /foo/bar.",
|
|
||||||
"example": "/user/oauth"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["workspaceId", "environment", "directory", "secretImport"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "successfully created secret import"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Confirmation of secret import creation"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: "Bad Request. For example, 'Secret import already exist'"
|
|
||||||
}
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
|
||||||
}
|
|
||||||
#swagger.responses[404] = {
|
|
||||||
description: "Resource Not Found. For example, 'Failed to find folder'"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const {
|
|
||||||
body: { workspaceId, environment, directory, secretImport }
|
|
||||||
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
// root check
|
|
||||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
|
||||||
);
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment: secretImport.environment, secretPath: secretImport.secretPath })
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
if (!folders && directory !== "/")
|
|
||||||
throw ResourceNotFoundError({ message: "Failed to find folder" });
|
|
||||||
|
|
||||||
let folderId = "root";
|
|
||||||
if (folders) {
|
|
||||||
const folder = getFolderByPath(folders.nodes, directory);
|
|
||||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
|
||||||
folderId = folder.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importSecDoc = await SecretImport.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
folderId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!importSecDoc) {
|
|
||||||
const doc = new SecretImport({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
folderId,
|
|
||||||
imports: [{ environment: secretImport.environment, secretPath: secretImport.secretPath }]
|
|
||||||
});
|
|
||||||
|
|
||||||
await doc.save();
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_SECRET_IMPORT,
|
|
||||||
metadata: {
|
|
||||||
secretImportId: doc._id.toString(),
|
|
||||||
folderId: doc.folderId.toString(),
|
|
||||||
importFromEnvironment: secretImport.environment,
|
|
||||||
importFromSecretPath: secretImport.secretPath,
|
|
||||||
importToEnvironment: environment,
|
|
||||||
importToSecretPath: directory
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: doc.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return res.status(200).json({ message: "successfully created secret import" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const doesImportExist = importSecDoc.imports.find(
|
|
||||||
(el) => el.environment === secretImport.environment && el.secretPath === secretImport.secretPath
|
|
||||||
);
|
|
||||||
if (doesImportExist) {
|
|
||||||
throw BadRequestError({ message: "Secret import already exist" });
|
|
||||||
}
|
|
||||||
|
|
||||||
importSecDoc.imports.push({
|
|
||||||
environment: secretImport.environment,
|
|
||||||
secretPath: secretImport.secretPath
|
|
||||||
});
|
|
||||||
await importSecDoc.save();
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_SECRET_IMPORT,
|
|
||||||
metadata: {
|
|
||||||
secretImportId: importSecDoc._id.toString(),
|
|
||||||
folderId: importSecDoc.folderId.toString(),
|
|
||||||
importFromEnvironment: secretImport.environment,
|
|
||||||
importFromSecretPath: secretImport.secretPath,
|
|
||||||
importToEnvironment: environment,
|
|
||||||
importToSecretPath: directory
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: importSecDoc.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return res.status(200).json({ message: "successfully created secret import" });
|
|
||||||
};
|
|
||||||
|
|
||||||
// to keep the ordering, you must pass all the imports in here not the only updated one
|
|
||||||
// this is because the order decide which import gets overriden
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update secret import
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateSecretImport = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Update secret import'
|
|
||||||
#swagger.description = 'Update secret import'
|
|
||||||
|
|
||||||
#swagger.parameters['id'] = {
|
|
||||||
in: 'path',
|
|
||||||
description: 'ID of secret import to update',
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
example: 'import12345'
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"secretImports": {
|
|
||||||
"type": "array",
|
|
||||||
"description": "List of secret imports to update to",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"environment": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment to import from",
|
|
||||||
"example": "dev"
|
|
||||||
},
|
|
||||||
"secretPath": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path where to import secrets from like / or /foo/bar",
|
|
||||||
"example": "/foo/bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["environment", "secretPath"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["secretImports"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
description: 'Successfully updated the secret import',
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "successfully updated secret import"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: 'Bad Request - Import not found',
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[403] = {
|
|
||||||
description: 'Forbidden access due to insufficient permissions',
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: 'Unauthorized access due to invalid token or scope',
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
body: { secretImports },
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.UpdateSecretImportV1, req);
|
|
||||||
|
|
||||||
const importSecDoc = await SecretImport.findById(id);
|
|
||||||
if (!importSecDoc) {
|
|
||||||
throw BadRequestError({ message: "Import not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for service token validity
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: importSecDoc.workspace,
|
|
||||||
environment: importSecDoc.environment
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
let secretPath = "/";
|
|
||||||
if (folders) {
|
|
||||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
|
||||||
secretPath = folderPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
// token permission check
|
|
||||||
const isValidScopeAccess = isValidScope(
|
|
||||||
req.authData.authPayload,
|
|
||||||
importSecDoc.environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// non token entry check
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: importSecDoc.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: importSecDoc.environment,
|
|
||||||
secretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
secretImports.forEach(({ environment, secretPath }) => {
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
|
||||||
);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const orderBefore = importSecDoc.imports;
|
|
||||||
importSecDoc.imports = secretImports;
|
|
||||||
|
|
||||||
await importSecDoc.save();
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.UPDATE_SECRET_IMPORT,
|
|
||||||
metadata: {
|
|
||||||
importToEnvironment: importSecDoc.environment,
|
|
||||||
importToSecretPath: secretPath,
|
|
||||||
secretImportId: importSecDoc._id.toString(),
|
|
||||||
folderId: importSecDoc.folderId.toString(),
|
|
||||||
orderBefore,
|
|
||||||
orderAfter: secretImports
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: importSecDoc.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return res.status(200).json({ message: "successfully updated secret import" });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete secret import
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteSecretImport = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Delete secret import'
|
|
||||||
#swagger.description = 'Delete secret import'
|
|
||||||
|
|
||||||
#swagger.parameters['id'] = {
|
|
||||||
in: 'path',
|
|
||||||
description: 'ID of parent secret import document from which to delete secret import',
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
example: '12345abcde'
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"secretImportEnv": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment of import to delete",
|
|
||||||
"example": "someWorkspaceId"
|
|
||||||
},
|
|
||||||
"secretImportPath": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path like / or /foo/bar of import to delete",
|
|
||||||
"example": "production"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["id", "secretImportEnv", "secretImportPath"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "successfully delete secret import"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Confirmation of secret import deletion"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { id },
|
|
||||||
body: { secretImportEnv, secretImportPath }
|
|
||||||
} = await validateRequest(reqValidator.DeleteSecretImportV1, req);
|
|
||||||
|
|
||||||
const importSecDoc = await SecretImport.findById(id);
|
|
||||||
if (!importSecDoc) {
|
|
||||||
throw BadRequestError({ message: "Import not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// check for service token validity
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: importSecDoc.workspace,
|
|
||||||
environment: importSecDoc.environment
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
let secretPath = "/";
|
|
||||||
if (folders) {
|
|
||||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
|
||||||
secretPath = folderPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
const isValidScopeAccess = isValidScope(
|
|
||||||
req.authData.authPayload,
|
|
||||||
importSecDoc.environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: importSecDoc.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: importSecDoc.environment,
|
|
||||||
secretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
importSecDoc.imports = importSecDoc.imports.filter(
|
|
||||||
({ environment, secretPath }) =>
|
|
||||||
!(environment === secretImportEnv && secretPath === secretImportPath)
|
|
||||||
);
|
|
||||||
await importSecDoc.save();
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.DELETE_SECRET_IMPORT,
|
|
||||||
metadata: {
|
|
||||||
secretImportId: importSecDoc._id.toString(),
|
|
||||||
folderId: importSecDoc.folderId.toString(),
|
|
||||||
importFromEnvironment: secretImportEnv,
|
|
||||||
importFromSecretPath: secretImportPath,
|
|
||||||
importToEnvironment: importSecDoc.environment,
|
|
||||||
importToSecretPath: secretPath
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: importSecDoc.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).json({ message: "successfully delete secret import" });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get secret imports
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getSecretImports = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Get secret imports'
|
|
||||||
#swagger.description = 'Get secret imports'
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
in: 'query',
|
|
||||||
description: 'ID of workspace where to get secret imports from',
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
example: 'workspace12345'
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['environment'] = {
|
|
||||||
in: 'query',
|
|
||||||
description: 'Slug of environment where to get secret imports from',
|
|
||||||
required: true,
|
|
||||||
type: 'string',
|
|
||||||
example: 'production'
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['directory'] = {
|
|
||||||
in: 'query',
|
|
||||||
description: 'Path where to get secret imports from like / or /foo/bar. Default is /',
|
|
||||||
required: false,
|
|
||||||
type: 'string',
|
|
||||||
example: 'folder12345'
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
description: 'Successfully retrieved secret import',
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"secretImport": {
|
|
||||||
$ref: '#/definitions/SecretImport'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[403] = {
|
|
||||||
description: 'Forbidden access due to insufficient permissions',
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: 'Unauthorized access due to invalid token or scope',
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
query: { workspaceId, environment, directory }
|
|
||||||
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath: directory
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment
|
|
||||||
}).lean();
|
|
||||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
|
||||||
|
|
||||||
let folderId = "root";
|
|
||||||
if (folders) {
|
|
||||||
const folder = getFolderByPath(folders.nodes, directory);
|
|
||||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
|
||||||
folderId = folder.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importSecDoc = await SecretImport.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
folderId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!importSecDoc) {
|
|
||||||
return res.status(200).json({ secretImport: {} });
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json({ secretImport: importSecDoc });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all secret imports
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceId, environment, directory }
|
|
||||||
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
// check for service token validity
|
|
||||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment,
|
|
||||||
secretPath: directory
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment
|
|
||||||
}).lean();
|
|
||||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
|
||||||
|
|
||||||
let folderId = "root";
|
|
||||||
if (folders) {
|
|
||||||
const folder = getFolderByPath(folders.nodes, directory);
|
|
||||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
|
||||||
folderId = folder.id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importSecDoc = await SecretImport.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
folderId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!importSecDoc) {
|
|
||||||
return res.status(200).json({ secrets: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
let secretPath = "/";
|
|
||||||
if (folders) {
|
|
||||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
|
||||||
secretPath = folderPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
let permissionCheckFn: (env: string, secPath: string) => boolean; // used to pass as callback function to import secret
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
// check for service token validity
|
|
||||||
const isValidScopeAccess = isValidScope(
|
|
||||||
req.authData.authPayload,
|
|
||||||
importSecDoc.environment,
|
|
||||||
secretPath
|
|
||||||
);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
permissionCheckFn = (env: string, secPath: string) =>
|
|
||||||
isValidScope(req.authData.authPayload as IServiceTokenData, env, secPath);
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: importSecDoc.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: importSecDoc.environment,
|
|
||||||
secretPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
permissionCheckFn = (env: string, secPath: string) =>
|
|
||||||
permission.can(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, {
|
|
||||||
environment: env,
|
|
||||||
secretPath: secPath
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.GET_SECRET_IMPORTS,
|
|
||||||
metadata: {
|
|
||||||
environment,
|
|
||||||
secretImportId: importSecDoc._id.toString(),
|
|
||||||
folderId,
|
|
||||||
numberOfImports: importSecDoc.imports.length
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: importSecDoc.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const secrets = await getAllImportedSecrets(
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
folderId,
|
|
||||||
permissionCheckFn
|
|
||||||
);
|
|
||||||
return res.status(200).json({ secrets });
|
|
||||||
};
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import {
|
|
||||||
GitAppInstallationSession,
|
|
||||||
GitAppOrganizationInstallation,
|
|
||||||
GitRisks
|
|
||||||
} from "../../ee/models";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { OrganizationNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
|
|
||||||
import { scanGithubFullRepoForSecretLeaks } from "../../queues/secret-scanning/githubScanFullRepository";
|
|
||||||
import { getSecretScanningGitAppId, getSecretScanningPrivateKey } from "../../config";
|
|
||||||
import {
|
|
||||||
STATUS_RESOLVED_FALSE_POSITIVE,
|
|
||||||
STATUS_RESOLVED_NOT_REVOKED,
|
|
||||||
STATUS_RESOLVED_REVOKED
|
|
||||||
} from "../../ee/models/gitRisks";
|
|
||||||
import { ProbotOctokit } from "probot";
|
|
||||||
import { Organization } from "../../models";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/secretScanning";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
getAuthDataOrgPermissions
|
|
||||||
} from "../../ee/services/RoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
export const createInstallationSession = async (req: Request, res: Response) => {
|
|
||||||
const sessionId = crypto.randomBytes(16).toString("hex");
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.CreateInstalLSessionv1, req);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.SecretScanning
|
|
||||||
);
|
|
||||||
|
|
||||||
await GitAppInstallationSession.findByIdAndUpdate(
|
|
||||||
organization,
|
|
||||||
{
|
|
||||||
organization: organization.id,
|
|
||||||
sessionId: sessionId,
|
|
||||||
user: new Types.ObjectId(req.user._id)
|
|
||||||
},
|
|
||||||
{ upsert: true }
|
|
||||||
).lean();
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
sessionId: sessionId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const linkInstallationToOrganization = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { sessionId, installationId }
|
|
||||||
} = await validateRequest(reqValidator.LinkInstallationToOrgv1, req);
|
|
||||||
|
|
||||||
const installationSession = await GitAppInstallationSession.findOneAndDelete({
|
|
||||||
sessionId: sessionId
|
|
||||||
});
|
|
||||||
if (!installationSession) {
|
|
||||||
throw UnauthorizedRequestError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: installationSession.organization
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.SecretScanning
|
|
||||||
);
|
|
||||||
|
|
||||||
const installationLink = await GitAppOrganizationInstallation.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
organizationId: installationSession.organization
|
|
||||||
},
|
|
||||||
{
|
|
||||||
installationId: installationId,
|
|
||||||
organizationId: installationSession.organization,
|
|
||||||
user: installationSession.user
|
|
||||||
},
|
|
||||||
{
|
|
||||||
upsert: true
|
|
||||||
}
|
|
||||||
).lean();
|
|
||||||
|
|
||||||
const octokit = new ProbotOctokit({
|
|
||||||
auth: {
|
|
||||||
appId: await getSecretScanningGitAppId(),
|
|
||||||
privateKey: await getSecretScanningPrivateKey(),
|
|
||||||
installationId: installationId.toString()
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { repositories }
|
|
||||||
} = await octokit.apps.listReposAccessibleToInstallation();
|
|
||||||
for (const repository of repositories) {
|
|
||||||
scanGithubFullRepoForSecretLeaks({
|
|
||||||
organizationId: installationSession.organization.toString(),
|
|
||||||
installationId,
|
|
||||||
repository: { id: repository.id, fullName: repository.full_name }
|
|
||||||
});
|
|
||||||
}
|
|
||||||
res.json(installationLink);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCurrentOrganizationInstallationStatus = async (req: Request, res: Response) => {
|
|
||||||
const { organizationId } = req.params;
|
|
||||||
try {
|
|
||||||
const appInstallation = await GitAppOrganizationInstallation.findOne({
|
|
||||||
organizationId: organizationId
|
|
||||||
}).lean();
|
|
||||||
if (!appInstallation) {
|
|
||||||
res.json({
|
|
||||||
appInstallationComplete: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.json({
|
|
||||||
appInstallationComplete: true
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
res.json({
|
|
||||||
appInstallationComplete: false
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRisksForOrganization = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgRisksv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.SecretScanning
|
|
||||||
);
|
|
||||||
|
|
||||||
const risks = await GitRisks.find({ organization: organizationId })
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.lean();
|
|
||||||
res.json({
|
|
||||||
risks: risks
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateRisksStatus = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId, riskId },
|
|
||||||
body: { status }
|
|
||||||
} = await validateRequest(reqValidator.UpdateRiskStatusv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.SecretScanning
|
|
||||||
);
|
|
||||||
|
|
||||||
const isRiskResolved =
|
|
||||||
status == STATUS_RESOLVED_FALSE_POSITIVE ||
|
|
||||||
status == STATUS_RESOLVED_REVOKED ||
|
|
||||||
status == STATUS_RESOLVED_NOT_REVOKED
|
|
||||||
? true
|
|
||||||
: false;
|
|
||||||
const risk = await GitRisks.findByIdAndUpdate(riskId, {
|
|
||||||
status: status,
|
|
||||||
isResolved: isRiskResolved
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
res.json(risk);
|
|
||||||
};
|
|
||||||
@@ -1,680 +0,0 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { EventType, FolderVersion } from "../../ee/models";
|
|
||||||
import { EEAuditLogService, EESecretService } from "../../ee/services";
|
|
||||||
import { isValidScope } from "../../helpers/secrets";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import { Secret, ServiceTokenData } from "../../models";
|
|
||||||
import { Folder } from "../../models/folder";
|
|
||||||
import {
|
|
||||||
appendFolder,
|
|
||||||
getAllFolderIds,
|
|
||||||
getFolderByPath,
|
|
||||||
getFolderWithPathFromId,
|
|
||||||
validateFolderName
|
|
||||||
} from "../../services/FolderService";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
|
||||||
import * as reqValidator from "../../validation/folders";
|
|
||||||
|
|
||||||
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
|
|
||||||
|
|
||||||
// verify workspace id/environment
|
|
||||||
export const createFolder = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Create folder'
|
|
||||||
#swagger.description = 'Create folder'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of the workspace where to create folder",
|
|
||||||
"example": "someWorkspaceId"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment where to create folder",
|
|
||||||
"example": "production"
|
|
||||||
},
|
|
||||||
"folderName": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of folder to create",
|
|
||||||
"example": "my_folder"
|
|
||||||
},
|
|
||||||
"directory": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path where to create folder like / or /foo/bar. Default is /",
|
|
||||||
"example": "/foo/bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["workspaceId", "environment", "folderName"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"folder": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of folder",
|
|
||||||
"example": "someFolderId"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of folder",
|
|
||||||
"example": "my_folder"
|
|
||||||
},
|
|
||||||
"version": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "Version of folder",
|
|
||||||
"example": 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Details of created folder"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: "Bad Request. For example, 'Folder name cannot contain spaces. Only underscore and dashes'"
|
|
||||||
}
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
body: { workspaceId, environment, folderName, directory }
|
|
||||||
} = await validateRequest(reqValidator.CreateFolderV1, req);
|
|
||||||
|
|
||||||
if (!validateFolderName(folderName)) {
|
|
||||||
throw BadRequestError({
|
|
||||||
message: "Folder name cannot contain spaces. Only underscore and dashes"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
// token check
|
|
||||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// user check
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
// space has no folders initialized
|
|
||||||
if (!folders) {
|
|
||||||
const folder = new Folder({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
nodes: {
|
|
||||||
id: "root",
|
|
||||||
name: "root",
|
|
||||||
version: 1,
|
|
||||||
children: []
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
|
|
||||||
await folder.save();
|
|
||||||
const folderVersion = new FolderVersion({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
nodes: parent
|
|
||||||
});
|
|
||||||
await folderVersion.save();
|
|
||||||
await EESecretService.takeSecretSnapshot({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
|
||||||
environment
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_FOLDER,
|
|
||||||
metadata: {
|
|
||||||
environment,
|
|
||||||
folderId: child.id,
|
|
||||||
folderName,
|
|
||||||
folderPath: directory
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json({ folder: { id: child.id, name: folderName } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
|
|
||||||
|
|
||||||
if (!hasCreated) return res.json({ folder: child });
|
|
||||||
|
|
||||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
|
||||||
|
|
||||||
const folderVersion = new FolderVersion({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
nodes: parent
|
|
||||||
});
|
|
||||||
await folderVersion.save();
|
|
||||||
|
|
||||||
await EESecretService.takeSecretSnapshot({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
folderId: child.id
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_FOLDER,
|
|
||||||
metadata: {
|
|
||||||
environment,
|
|
||||||
folderId: child.id,
|
|
||||||
folderName,
|
|
||||||
folderPath: directory
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json({ folder: child });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update folder with id [folderId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateFolderById = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Update folder'
|
|
||||||
#swagger.description = 'Update folder'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['folderName'] = {
|
|
||||||
"description": "Name of folder to update",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of workspace where to update folder",
|
|
||||||
"example": "someWorkspaceId"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment where to update folder",
|
|
||||||
"example": "production"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of folder to update to",
|
|
||||||
"example": "updated_folder_name"
|
|
||||||
},
|
|
||||||
"directory": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path where to update folder like / or /foo/bar. Default is /",
|
|
||||||
"example": "/foo/bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["workspaceId", "environment", "name"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Success message",
|
|
||||||
"example": "Successfully updated folder"
|
|
||||||
},
|
|
||||||
"folder": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of updated folder",
|
|
||||||
"example": "updated_folder_name"
|
|
||||||
},
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of created folder",
|
|
||||||
"example": "abc123"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Details of the updated folder"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: "Bad Request. Reasons can include 'The folder doesn't exist' or 'Folder name cannot contain spaces. Only underscore and dashes'"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
body: { workspaceId, environment, name, directory },
|
|
||||||
params: { folderName }
|
|
||||||
} = await validateRequest(reqValidator.UpdateFolderV1, req);
|
|
||||||
|
|
||||||
if (!validateFolderName(name)) {
|
|
||||||
throw BadRequestError({
|
|
||||||
message: "Folder name cannot contain spaces. Only underscore and dashes"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
|
||||||
if (!folders) {
|
|
||||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
|
||||||
if (!parentFolder) {
|
|
||||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const folder = parentFolder.children.find(({ name }) => name === folderName);
|
|
||||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
|
||||||
|
|
||||||
const oldFolderName = folder.name;
|
|
||||||
parentFolder.version += 1;
|
|
||||||
folder.name = name;
|
|
||||||
|
|
||||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
|
||||||
const folderVersion = new FolderVersion({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
nodes: parentFolder
|
|
||||||
});
|
|
||||||
await folderVersion.save();
|
|
||||||
|
|
||||||
await EESecretService.takeSecretSnapshot({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
folderId: parentFolder.id
|
|
||||||
});
|
|
||||||
|
|
||||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.UPDATE_FOLDER,
|
|
||||||
metadata: {
|
|
||||||
environment,
|
|
||||||
folderId: folder.id,
|
|
||||||
oldFolderName,
|
|
||||||
newFolderName: name,
|
|
||||||
folderPath
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
message: "Successfully updated folder",
|
|
||||||
folder: { name: folder.name, id: folder.id }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete folder with id [folderId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteFolder = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Delete folder'
|
|
||||||
#swagger.description = 'Delete folder'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['folderName'] = {
|
|
||||||
"description": "Name of folder to delete",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workspaceId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of the workspace where to delete folder",
|
|
||||||
"example": "someWorkspaceId"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment where to delete folder",
|
|
||||||
"example": "production"
|
|
||||||
},
|
|
||||||
"directory": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Path where to delete folder like / or /foo/bar. Default is /",
|
|
||||||
"example": "/foo/bar"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["workspaceId", "environment"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Success message",
|
|
||||||
"example": "successfully deleted folders"
|
|
||||||
},
|
|
||||||
"folders": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of deleted folder",
|
|
||||||
"example": "abc123"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of deleted folder",
|
|
||||||
"example": "someFolderName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "List of IDs and names of deleted folders"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: "Bad Request. Reasons can include 'The folder doesn't exist'"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { folderName },
|
|
||||||
body: { environment, workspaceId, directory }
|
|
||||||
} = await validateRequest(reqValidator.DeleteFolderV1, req);
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check that user is a member of the workspace
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
|
||||||
if (!folders) throw ERR_FOLDER_NOT_FOUND;
|
|
||||||
|
|
||||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
|
||||||
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
|
|
||||||
|
|
||||||
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
|
|
||||||
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
|
|
||||||
|
|
||||||
const deletedFolder = parentFolder.children.splice(index, 1)[0];
|
|
||||||
|
|
||||||
parentFolder.version += 1;
|
|
||||||
const delFolderIds = getAllFolderIds(deletedFolder);
|
|
||||||
|
|
||||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
|
||||||
const folderVersion = new FolderVersion({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
nodes: parentFolder
|
|
||||||
});
|
|
||||||
await folderVersion.save();
|
|
||||||
if (delFolderIds.length) {
|
|
||||||
await Secret.deleteMany({
|
|
||||||
folder: { $in: delFolderIds.map(({ id }) => id) },
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await EESecretService.takeSecretSnapshot({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
folderId: parentFolder.id
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.DELETE_FOLDER,
|
|
||||||
metadata: {
|
|
||||||
environment,
|
|
||||||
folderId: deletedFolder.id,
|
|
||||||
folderName: deletedFolder.name,
|
|
||||||
folderPath: directory
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get folders for workspace with id [workspaceId] and environment [environment]
|
|
||||||
* considering directory/path [directory]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getFolders = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Get folders'
|
|
||||||
#swagger.description = 'Get folders'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of the workspace where to get folders from",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['environment'] = {
|
|
||||||
"description": "Slug of environment where to get folders from",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['directory'] = {
|
|
||||||
"description": "Path where to get fodlers from like / or /foo/bar. Default is /",
|
|
||||||
"required": false,
|
|
||||||
"type": "string",
|
|
||||||
"in": "query"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"folders": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "someFolderId"
|
|
||||||
},
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"example": "someFolderName"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "List of folders"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[400] = {
|
|
||||||
description: "Bad Request. For instance, 'The folder doesn't exist'"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[401] = {
|
|
||||||
description: "Unauthorized request. For example, 'Folder Permission Denied'"
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
query: { workspaceId, environment, directory }
|
|
||||||
} = await validateRequest(reqValidator.GetFoldersV1, req);
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
|
||||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
|
||||||
if (!isValidScopeAccess) {
|
|
||||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// check that user is a member of the workspace
|
|
||||||
await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
|
||||||
if (!folders) {
|
|
||||||
return res.send({ folders: [], dir: [] });
|
|
||||||
}
|
|
||||||
|
|
||||||
const folder = getFolderByPath(folders.nodes, directory);
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { ServiceToken } from "../../models";
|
|
||||||
import { createToken } from "../../helpers/auth";
|
|
||||||
import { getJwtServiceSecret } from "../../config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return service token on request
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getServiceToken = async (req: Request, res: Response) => {
|
|
||||||
return res.status(200).send({
|
|
||||||
serviceToken: req.serviceToken,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and return a new service token
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createServiceToken = async (req: Request, res: Response) => {
|
|
||||||
let token;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
name,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
expiresIn,
|
|
||||||
publicKey,
|
|
||||||
encryptedKey,
|
|
||||||
nonce,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// validate environment
|
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
|
||||||
throw new Error("Failed to validate environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
// compute access token expiration date
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
|
|
||||||
const serviceToken = await new ServiceToken({
|
|
||||||
name,
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
expiresAt,
|
|
||||||
publicKey,
|
|
||||||
encryptedKey,
|
|
||||||
nonce,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
token = createToken({
|
|
||||||
payload: {
|
|
||||||
serviceTokenId: serviceToken._id.toString(),
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
expiresIn: expiresIn,
|
|
||||||
secret: await getJwtServiceSecret(),
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to create service token",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { AuthMethod, User } from "../../models";
|
|
||||||
import { checkEmailVerification, sendEmailVerification } from "../../helpers/signup";
|
|
||||||
import { createToken } from "../../helpers/auth";
|
|
||||||
import {
|
|
||||||
getAuthSecret,
|
|
||||||
getJwtSignupLifetime,
|
|
||||||
getSmtpConfigured
|
|
||||||
} from "../../config";
|
|
||||||
import { validateUserEmail } from "../../validation";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/auth";
|
|
||||||
import { AuthTokenType } from "../../variables";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signup step 1: Initialize account for user under email [email] and send a verification code
|
|
||||||
* to that email
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const beginEmailSignup = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { email }
|
|
||||||
} = await validateRequest(reqValidator.BeginEmailSignUpV1, req);
|
|
||||||
|
|
||||||
// validate that email is not disposable
|
|
||||||
validateUserEmail(email);
|
|
||||||
|
|
||||||
const user = await User.findOne({ email }).select("+publicKey");
|
|
||||||
if (user && user?.publicKey) {
|
|
||||||
// case: user has already completed account
|
|
||||||
|
|
||||||
return res.status(403).send({
|
|
||||||
error: "Failed to send email verification code for complete account"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// send send verification email
|
|
||||||
await sendEmailVerification({ email });
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: `Sent an email verification code to ${email}`
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Signup step 2: Verify that code [code] was sent to email [email] and issue
|
|
||||||
* a temporary signup token for user to complete setting up their account
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const verifyEmailSignup = async (req: Request, res: Response) => {
|
|
||||||
let user;
|
|
||||||
const {
|
|
||||||
body: { email, code }
|
|
||||||
} = await validateRequest(reqValidator.VerifyEmailSignUpV1, req);
|
|
||||||
|
|
||||||
// initialize user account
|
|
||||||
user = await User.findOne({ email }).select("+publicKey");
|
|
||||||
if (user && user?.publicKey) {
|
|
||||||
// case: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: "Failed email verification for complete user"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// verify email
|
|
||||||
if (await getSmtpConfigured()) {
|
|
||||||
await checkEmailVerification({
|
|
||||||
email,
|
|
||||||
code
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
user = await new User({
|
|
||||||
email,
|
|
||||||
authMethods: [AuthMethod.EMAIL]
|
|
||||||
}).save();
|
|
||||||
}
|
|
||||||
|
|
||||||
// generate temporary signup token
|
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.SIGNUP_TOKEN,
|
|
||||||
userId: user._id.toString()
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtSignupLifetime(),
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfuly verified email",
|
|
||||||
user,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,56 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import { UserAction } from "../../models";
|
|
||||||
import * as reqValidator from "../../validation/action";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add user action [action]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const addUserAction = async (req: Request, res: Response) => {
|
|
||||||
// add/record new action [action] for user with id [req.user._id]
|
|
||||||
const {
|
|
||||||
body: { action }
|
|
||||||
} = await validateRequest(reqValidator.AddUserActionV1, req);
|
|
||||||
|
|
||||||
const userAction = await UserAction.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
user: req.user._id,
|
|
||||||
action
|
|
||||||
},
|
|
||||||
{ user: req.user._id, action },
|
|
||||||
{
|
|
||||||
new: true,
|
|
||||||
upsert: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully recorded user action",
|
|
||||||
userAction
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return user action [action] for user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getUserAction = async (req: Request, res: Response) => {
|
|
||||||
// get user action [action] for user with id [req.user._id]
|
|
||||||
const {
|
|
||||||
query: { action }
|
|
||||||
} = await validateRequest(reqValidator.GetUserActionV1, req);
|
|
||||||
|
|
||||||
const userAction = await UserAction.findOne({
|
|
||||||
user: req.user._id,
|
|
||||||
action
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
userAction
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return user on request
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getUser = async (req: Request, res: Response) => {
|
|
||||||
return res.status(200).send({
|
|
||||||
user: req.user,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
|
||||||
import { Webhook } from "../../models";
|
|
||||||
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
|
|
||||||
import { BadRequestError, ResourceNotFoundError } from "../../utils/errors";
|
|
||||||
import { EEAuditLogService } from "../../ee/services";
|
|
||||||
import { EventType } from "../../ee/models";
|
|
||||||
import {
|
|
||||||
ALGORITHM_AES_256_GCM,
|
|
||||||
ENCODING_SCHEME_BASE64,
|
|
||||||
ENCODING_SCHEME_UTF8
|
|
||||||
} from "../../variables";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/webhooks";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { encryptSymmetric128BitHexKeyUTF8 } from "../../utils/crypto";
|
|
||||||
|
|
||||||
export const createWebhook = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath }
|
|
||||||
} = await validateRequest(reqValidator.CreateWebhookV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.Webhooks
|
|
||||||
);
|
|
||||||
|
|
||||||
const webhook = new Webhook({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
url: webhookUrl
|
|
||||||
});
|
|
||||||
|
|
||||||
if (webhookSecretKey) {
|
|
||||||
const encryptionKey = await getEncryptionKey();
|
|
||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
|
||||||
|
|
||||||
if (rootEncryptionKey) {
|
|
||||||
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
|
|
||||||
webhook.iv = iv;
|
|
||||||
webhook.tag = tag;
|
|
||||||
webhook.encryptedSecretKey = ciphertext;
|
|
||||||
webhook.algorithm = ALGORITHM_AES_256_GCM;
|
|
||||||
webhook.keyEncoding = ENCODING_SCHEME_BASE64;
|
|
||||||
} else if (encryptionKey) {
|
|
||||||
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
|
|
||||||
plaintext: webhookSecretKey,
|
|
||||||
key: encryptionKey
|
|
||||||
});
|
|
||||||
webhook.iv = iv;
|
|
||||||
webhook.tag = tag;
|
|
||||||
webhook.encryptedSecretKey = ciphertext;
|
|
||||||
webhook.algorithm = ALGORITHM_AES_256_GCM;
|
|
||||||
webhook.keyEncoding = ENCODING_SCHEME_UTF8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await webhook.save();
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_WEBHOOK,
|
|
||||||
metadata: {
|
|
||||||
webhookId: webhook._id.toString(),
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
webhookUrl,
|
|
||||||
isDisabled: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
webhook,
|
|
||||||
message: "successfully created webhook"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateWebhook = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { isDisabled },
|
|
||||||
params: { webhookId }
|
|
||||||
} = await validateRequest(reqValidator.UpdateWebhookV1, req);
|
|
||||||
|
|
||||||
const webhook = await Webhook.findById(webhookId);
|
|
||||||
if (!webhook) {
|
|
||||||
throw BadRequestError({ message: "Webhook not found!!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: webhook.workspace
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Webhooks
|
|
||||||
);
|
|
||||||
|
|
||||||
if (typeof isDisabled !== undefined) {
|
|
||||||
webhook.isDisabled = isDisabled;
|
|
||||||
}
|
|
||||||
await webhook.save();
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.UPDATE_WEBHOOK_STATUS,
|
|
||||||
metadata: {
|
|
||||||
webhookId: webhook._id.toString(),
|
|
||||||
environment: webhook.environment,
|
|
||||||
secretPath: webhook.secretPath,
|
|
||||||
webhookUrl: webhook.url,
|
|
||||||
isDisabled
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: webhook.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
webhook,
|
|
||||||
message: "successfully updated webhook"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteWebhook = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { webhookId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteWebhookV1, req);
|
|
||||||
let webhook = await Webhook.findById(webhookId);
|
|
||||||
|
|
||||||
if (!webhook) {
|
|
||||||
throw ResourceNotFoundError({ message: "Webhook not found!!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: webhook.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Webhooks
|
|
||||||
);
|
|
||||||
|
|
||||||
webhook = await Webhook.findByIdAndDelete(webhookId);
|
|
||||||
|
|
||||||
if (!webhook) {
|
|
||||||
throw ResourceNotFoundError({ message: "Webhook not found!!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.DELETE_WEBHOOK,
|
|
||||||
metadata: {
|
|
||||||
webhookId: webhook._id.toString(),
|
|
||||||
environment: webhook.environment,
|
|
||||||
secretPath: webhook.secretPath,
|
|
||||||
webhookUrl: webhook.url,
|
|
||||||
isDisabled: webhook.isDisabled
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: webhook.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "successfully removed webhook"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const testWebhook = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { webhookId }
|
|
||||||
} = await validateRequest(reqValidator.TestWebhookV1, req);
|
|
||||||
|
|
||||||
const webhook = await Webhook.findById(webhookId);
|
|
||||||
if (!webhook) {
|
|
||||||
throw BadRequestError({ message: "Webhook not found!!" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: webhook.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Webhooks
|
|
||||||
);
|
|
||||||
|
|
||||||
try {
|
|
||||||
await triggerWebhookRequest(
|
|
||||||
webhook,
|
|
||||||
getWebhookPayload(
|
|
||||||
"test",
|
|
||||||
webhook.workspace.toString(),
|
|
||||||
webhook.environment,
|
|
||||||
webhook.secretPath
|
|
||||||
)
|
|
||||||
);
|
|
||||||
await Webhook.findByIdAndUpdate(webhookId, {
|
|
||||||
lastStatus: "success",
|
|
||||||
lastRunErrorMessage: null
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
await Webhook.findByIdAndUpdate(webhookId, {
|
|
||||||
lastStatus: "failed",
|
|
||||||
lastRunErrorMessage: (err as Error).message
|
|
||||||
});
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to receive response",
|
|
||||||
error: (err as Error).message
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully received response"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const listWebhooks = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { environment, workspaceId, secretPath }
|
|
||||||
} = await validateRequest(reqValidator.ListWebhooksV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Webhooks
|
|
||||||
);
|
|
||||||
|
|
||||||
const optionalFilters: Record<string, string> = {};
|
|
||||||
if (environment) optionalFilters.environment = environment as string;
|
|
||||||
if (secretPath) optionalFilters.secretPath = secretPath as string;
|
|
||||||
|
|
||||||
const webhooks = await Webhook.find({
|
|
||||||
workspace: new Types.ObjectId(workspaceId as string),
|
|
||||||
...optionalFilters
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
webhooks
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,359 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import {
|
|
||||||
IUser,
|
|
||||||
Integration,
|
|
||||||
IntegrationAuth,
|
|
||||||
Membership,
|
|
||||||
Organization,
|
|
||||||
ServiceToken,
|
|
||||||
Workspace
|
|
||||||
} from "../../models";
|
|
||||||
import { createWorkspace as create, deleteWorkspace as deleteWork } from "../../helpers/workspace";
|
|
||||||
import { EELicenseService } from "../../ee/services";
|
|
||||||
import { addMemberships } from "../../helpers/membership";
|
|
||||||
import { ADMIN } from "../../variables";
|
|
||||||
import { OrganizationNotFoundError } from "../../utils/errors";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
getAuthDataOrgPermissions
|
|
||||||
} from "../../ee/services/RoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return public keys of members of workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspacePublicKeysV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const publicKeys = (
|
|
||||||
await Membership.find({
|
|
||||||
workspace: workspaceId
|
|
||||||
}).populate<{ user: IUser }>("user", "publicKey")
|
|
||||||
).map((member) => {
|
|
||||||
return {
|
|
||||||
publicKey: member.user.publicKey,
|
|
||||||
userId: member.user._id
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
publicKeys
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return memberships for workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const users = await Membership.find({
|
|
||||||
workspace: workspaceId
|
|
||||||
}).populate("user", "+publicKey");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
users
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return workspaces that user is part of
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaces = async (req: Request, res: Response) => {
|
|
||||||
const workspaces = (
|
|
||||||
await Membership.find({
|
|
||||||
user: req.user._id
|
|
||||||
}).populate("workspace")
|
|
||||||
).map((m) => m.workspace);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
workspaces
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspace = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceV1, req);
|
|
||||||
|
|
||||||
const workspace = await Workspace.findOne({
|
|
||||||
_id: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
workspace
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new workspace named [workspaceName] under organization with id
|
|
||||||
* [organizationId] and add user as admin
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createWorkspace = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { organizationId, workspaceName }
|
|
||||||
} = await validateRequest(reqValidator.CreateWorkspaceV1, req);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.Workspace
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
if (plan.workspaceLimit !== null) {
|
|
||||||
// case: limit imposed on number of workspaces allowed
|
|
||||||
if (plan.workspacesUsed >= plan.workspaceLimit) {
|
|
||||||
// case: number of workspaces used exceeds the number of workspaces allowed
|
|
||||||
return res.status(400).send({
|
|
||||||
message:
|
|
||||||
"Failed to create workspace due to plan limit reached. Upgrade plan to add more workspaces."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (workspaceName.length < 1) {
|
|
||||||
throw new Error("Workspace names must be at least 1-character long");
|
|
||||||
}
|
|
||||||
|
|
||||||
// create workspace and add user as member
|
|
||||||
const workspace = await create({
|
|
||||||
name: workspaceName,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
await addMemberships({
|
|
||||||
userIds: [req.user._id],
|
|
||||||
workspaceId: workspace._id.toString(),
|
|
||||||
roles: [ADMIN]
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
workspace
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteWorkspace = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteWorkspaceV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Workspace
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete workspace
|
|
||||||
const workspace = await deleteWork({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
workspace
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change name of workspace with id [workspaceId] to [name]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const changeWorkspaceName = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { name }
|
|
||||||
} = await validateRequest(reqValidator.ChangeWorkspaceNameV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Workspace
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspace = await Workspace.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: workspaceId
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully changed workspace name",
|
|
||||||
workspace
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return integrations for workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceIntegrationsV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
const integrations = await Integration.find({
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
integrations
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return (integration) authorizations for workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaceIntegrationAuthorizations = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceIntegrationAuthorizationsV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Integrations
|
|
||||||
);
|
|
||||||
|
|
||||||
const authorizations = await IntegrationAuth.find({
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
authorizations
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return service service tokens for workspace [workspaceId] belonging to user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaceServiceTokens = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceServiceTokensV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.ServiceTokens
|
|
||||||
);
|
|
||||||
|
|
||||||
// ?? FIX.
|
|
||||||
const serviceTokens = await ServiceToken.find({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serviceTokens
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,315 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import * as bigintConversion from "bigint-conversion";
|
|
||||||
const jsrp = require("jsrp");
|
|
||||||
import { LoginSRPDetail, User } from "../../models";
|
|
||||||
import { createToken, issueAuthTokens } from "../../helpers/auth";
|
|
||||||
import { checkUserDevice } from "../../helpers/user";
|
|
||||||
import { sendMail } from "../../helpers/nodemailer";
|
|
||||||
import { TokenService } from "../../services";
|
|
||||||
import { BadRequestError, InternalServerError } from "../../utils/errors";
|
|
||||||
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
|
|
||||||
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/auth";
|
|
||||||
|
|
||||||
declare module "jsonwebtoken" {
|
|
||||||
export interface UserIDJwtPayload extends jwt.JwtPayload {
|
|
||||||
userId: string;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
|
||||||
const { email, clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+salt +verifier");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
// generate server-side public key
|
|
||||||
const serverPublicKey = server.getPublicKey();
|
|
||||||
|
|
||||||
await LoginSRPDetail.findOneAndReplace(
|
|
||||||
{ email: email },
|
|
||||||
{
|
|
||||||
email: email,
|
|
||||||
clientPublicKey: clientPublicKey,
|
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
|
||||||
},
|
|
||||||
{ upsert: true, returnNewDocument: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serverPublicKey,
|
|
||||||
salt: user.salt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
|
||||||
* private key
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
|
||||||
if (!req.headers["user-agent"])
|
|
||||||
throw InternalServerError({ message: "User-Agent header is required" });
|
|
||||||
|
|
||||||
const { email, clientProof } = 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");
|
|
||||||
|
|
||||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
|
|
||||||
|
|
||||||
if (!loginSRPDetail) {
|
|
||||||
return BadRequestError(Error("Failed to find login details for SRP"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetail.serverBInt
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
|
||||||
if (server.checkClientProof(clientProof)) {
|
|
||||||
if (user.isMfaEnabled) {
|
|
||||||
// case: user has MFA enabled
|
|
||||||
|
|
||||||
// generate temporary MFA token
|
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
|
||||||
userId: user._id.toString()
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtMfaLifetime(),
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_MFA,
|
|
||||||
email
|
|
||||||
});
|
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
|
||||||
await sendMail({
|
|
||||||
template: "emailMfa.handlebars",
|
|
||||||
subjectLine: "Infisical MFA code",
|
|
||||||
recipients: [email],
|
|
||||||
substitutions: {
|
|
||||||
code
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
mfaEnabled: true,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkUserDevice({
|
|
||||||
user,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
const tokens = await issueAuthTokens({
|
|
||||||
userId: user._id,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie("jid", tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: await getHttpsEnabled()
|
|
||||||
});
|
|
||||||
|
|
||||||
// case: user does not have MFA enabled
|
|
||||||
// return (access) token in response
|
|
||||||
|
|
||||||
interface ResponseData {
|
|
||||||
mfaEnabled: boolean;
|
|
||||||
encryptionVersion: any;
|
|
||||||
protectedKey?: string;
|
|
||||||
protectedKeyIV?: string;
|
|
||||||
protectedKeyTag?: string;
|
|
||||||
token: string;
|
|
||||||
publicKey?: string;
|
|
||||||
encryptedPrivateKey?: string;
|
|
||||||
iv?: string;
|
|
||||||
tag?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response: ResponseData = {
|
|
||||||
mfaEnabled: false,
|
|
||||||
encryptionVersion: user.encryptionVersion,
|
|
||||||
token: tokens.token,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
|
||||||
iv: user.iv,
|
|
||||||
tag: user.tag
|
|
||||||
};
|
|
||||||
|
|
||||||
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
|
|
||||||
response.protectedKey = user.protectedKey;
|
|
||||||
response.protectedKeyIV = user.protectedKeyIV;
|
|
||||||
response.protectedKeyTag = user.protectedKeyTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to authenticate. Try again?"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Send MFA token to email [email]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const sendMfaToken = async (req: Request, res: Response) => {
|
|
||||||
const code = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_MFA,
|
|
||||||
email: req.user.email
|
|
||||||
});
|
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
|
||||||
await sendMail({
|
|
||||||
template: "emailMfa.handlebars",
|
|
||||||
subjectLine: "Infisical MFA code",
|
|
||||||
recipients: [req.user.email],
|
|
||||||
substitutions: {
|
|
||||||
code
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully sent new MFA code"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Verify MFA token [mfaToken] and issue JWT and refresh tokens if the
|
|
||||||
* MFA token [mfaToken] is valid
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const verifyMfaToken = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { mfaToken }
|
|
||||||
} = await validateRequest(reqValidator.VerifyMfaTokenV2, req);
|
|
||||||
|
|
||||||
await TokenService.validateToken({
|
|
||||||
type: TOKEN_EMAIL_MFA,
|
|
||||||
email: req.user.email,
|
|
||||||
token: mfaToken
|
|
||||||
});
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email: req.user.email
|
|
||||||
}).select(
|
|
||||||
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
await LoginSRPDetail.deleteOne({ userId: user.id });
|
|
||||||
|
|
||||||
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()
|
|
||||||
});
|
|
||||||
|
|
||||||
interface VerifyMfaTokenRes {
|
|
||||||
encryptionVersion: number;
|
|
||||||
protectedKey?: string;
|
|
||||||
protectedKeyIV?: string;
|
|
||||||
protectedKeyTag?: string;
|
|
||||||
token: string;
|
|
||||||
publicKey: string;
|
|
||||||
encryptedPrivateKey: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface VerifyMfaTokenRes {
|
|
||||||
encryptionVersion: number;
|
|
||||||
protectedKey?: string;
|
|
||||||
protectedKeyIV?: string;
|
|
||||||
protectedKeyTag?: string;
|
|
||||||
token: string;
|
|
||||||
publicKey: string;
|
|
||||||
encryptedPrivateKey: string;
|
|
||||||
iv: string;
|
|
||||||
tag: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const resObj: VerifyMfaTokenRes = {
|
|
||||||
encryptionVersion: user.encryptionVersion,
|
|
||||||
token: tokens.token,
|
|
||||||
publicKey: user.publicKey as string,
|
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey as string,
|
|
||||||
iv: user.iv as string,
|
|
||||||
tag: user.tag as string
|
|
||||||
};
|
|
||||||
|
|
||||||
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
|
|
||||||
resObj.protectedKey = user.protectedKey;
|
|
||||||
resObj.protectedKeyIV = user.protectedKeyIV;
|
|
||||||
resObj.protectedKeyTag = user.protectedKeyTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(resObj);
|
|
||||||
};
|
|
||||||
@@ -1,604 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
|
||||||
Folder,
|
|
||||||
Integration,
|
|
||||||
Membership,
|
|
||||||
Secret,
|
|
||||||
ServiceToken,
|
|
||||||
ServiceTokenData,
|
|
||||||
Workspace
|
|
||||||
} from "../../models";
|
|
||||||
import { EventType, SecretVersion } from "../../ee/models";
|
|
||||||
import { EEAuditLogService, EELicenseService } from "../../ee/services";
|
|
||||||
import { BadRequestError, WorkspaceNotFoundError } from "../../utils/errors";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/environments";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { SecretImport } from "../../models";
|
|
||||||
import { Webhook } from "../../models";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new workspace environment named [environmentName]
|
|
||||||
* with slug [environmentSlug] under workspace with id
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createWorkspaceEnvironment = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Create environment'
|
|
||||||
#swagger.description = 'Create environment'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of workspace where to create environment",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"environmentName": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of the environment to create",
|
|
||||||
"example": "development"
|
|
||||||
},
|
|
||||||
"environmentSlug": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment to create",
|
|
||||||
"example": "dev-environment"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["environmentName", "environmentSlug"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Sucess message",
|
|
||||||
"example": "Successfully created environment"
|
|
||||||
},
|
|
||||||
"workspace": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of workspace where environment was created",
|
|
||||||
"example": "abc123"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of created environment",
|
|
||||||
"example": "Staging"
|
|
||||||
},
|
|
||||||
"slug": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of created environment",
|
|
||||||
"example": "staging"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Details of the created environment"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { environmentName, environmentSlug }
|
|
||||||
} = await validateRequest(reqValidator.CreateWorkspaceEnvironmentV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.Environments
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
|
||||||
|
|
||||||
if (!workspace) throw WorkspaceNotFoundError();
|
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(workspace.organization);
|
|
||||||
|
|
||||||
if (plan.environmentLimit !== null) {
|
|
||||||
// case: limit imposed on number of environments allowed
|
|
||||||
if (workspace.environments.length >= plan.environmentLimit) {
|
|
||||||
// case: number of environments used exceeds the number of environments allowed
|
|
||||||
|
|
||||||
return res.status(400).send({
|
|
||||||
message:
|
|
||||||
"Failed to create environment due to environment limit reached. Upgrade plan to create more environments."
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!workspace ||
|
|
||||||
workspace?.environments.find(
|
|
||||||
({ name, slug }) => slug === environmentSlug || environmentName === name
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw new Error("Failed to create workspace environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
workspace?.environments.push({
|
|
||||||
name: environmentName,
|
|
||||||
slug: environmentSlug.toLowerCase()
|
|
||||||
});
|
|
||||||
await workspace.save();
|
|
||||||
|
|
||||||
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_ENVIRONMENT,
|
|
||||||
metadata: {
|
|
||||||
name: environmentName,
|
|
||||||
slug: environmentSlug
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: workspace._id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully created new environment",
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: {
|
|
||||||
name: environmentName,
|
|
||||||
slug: environmentSlug
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Swaps the ordering of two environments in the database. This is purely for aesthetic purposes.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const reorderWorkspaceEnvironments = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { environmentName, environmentSlug, otherEnvironmentSlug, otherEnvironmentName }
|
|
||||||
} = await validateRequest(reqValidator.ReorderWorkspaceEnvironmentsV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Environments
|
|
||||||
);
|
|
||||||
|
|
||||||
// atomic update the env to avoid conflict
|
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
|
||||||
if (!workspace) {
|
|
||||||
throw BadRequestError({ message: "Couldn't load workspace" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const environmentIndex = workspace.environments.findIndex(
|
|
||||||
(env) => env.name === environmentName && env.slug === environmentSlug
|
|
||||||
);
|
|
||||||
const otherEnvironmentIndex = workspace.environments.findIndex(
|
|
||||||
(env) => env.name === otherEnvironmentName && env.slug === otherEnvironmentSlug
|
|
||||||
);
|
|
||||||
|
|
||||||
if (environmentIndex === -1 || otherEnvironmentIndex === -1) {
|
|
||||||
throw BadRequestError({ message: "environment or otherEnvironment couldn't be found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// swap the order of the environments
|
|
||||||
[workspace.environments[environmentIndex], workspace.environments[otherEnvironmentIndex]] = [
|
|
||||||
workspace.environments[otherEnvironmentIndex],
|
|
||||||
workspace.environments[environmentIndex]
|
|
||||||
];
|
|
||||||
|
|
||||||
await workspace.save();
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully reordered environments",
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Rename workspace environment with new name and slug of a workspace with [workspaceId]
|
|
||||||
* Old slug [oldEnvironmentSlug] must be provided
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const renameWorkspaceEnvironment = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Update environment'
|
|
||||||
#swagger.description = 'Update environment'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of workspace where to update environment",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"environmentName": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of environment to update to",
|
|
||||||
"example": "Staging-Renamed"
|
|
||||||
},
|
|
||||||
"environmentSlug": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment to update to",
|
|
||||||
"example": "staging-renamed"
|
|
||||||
},
|
|
||||||
"oldEnvironmentSlug": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Current slug of environment",
|
|
||||||
"example": "staging-old"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["environmentName", "environmentSlug", "oldEnvironmentSlug"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Success message",
|
|
||||||
"example": "Successfully update environment"
|
|
||||||
},
|
|
||||||
"workspace": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of workspace where environment was updated",
|
|
||||||
"example": "abc123"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of updated environment",
|
|
||||||
"example": "Staging-Renamed"
|
|
||||||
},
|
|
||||||
"slug": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of updated environment",
|
|
||||||
"example": "staging-renamed"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Details of the renamed environment"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { environmentName, environmentSlug, oldEnvironmentSlug }
|
|
||||||
} = await validateRequest(reqValidator.UpdateWorkspaceEnvironmentV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Environments
|
|
||||||
);
|
|
||||||
|
|
||||||
// user should pass both new slug and env name
|
|
||||||
if (!environmentSlug || !environmentName) {
|
|
||||||
throw new Error("Invalid environment given.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// atomic update the env to avoid conflict
|
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
|
||||||
if (!workspace) {
|
|
||||||
throw new Error("Failed to create workspace environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
const isEnvExist = workspace.environments.some(
|
|
||||||
({ name, slug }) =>
|
|
||||||
slug !== oldEnvironmentSlug && (name === environmentName || slug === environmentSlug)
|
|
||||||
);
|
|
||||||
if (isEnvExist) {
|
|
||||||
throw new Error("Invalid environment given");
|
|
||||||
}
|
|
||||||
|
|
||||||
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === oldEnvironmentSlug);
|
|
||||||
if (envIndex === -1) {
|
|
||||||
throw new Error("Invalid environment given");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldEnvironment = workspace.environments[envIndex];
|
|
||||||
|
|
||||||
workspace.environments[envIndex].name = environmentName;
|
|
||||||
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
|
|
||||||
|
|
||||||
await workspace.save();
|
|
||||||
await Secret.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await SecretVersion.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await ServiceToken.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await ServiceTokenData.updateMany(
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
"scopes.environment": oldEnvironmentSlug
|
|
||||||
},
|
|
||||||
{ $set: { "scopes.$[element].environment": environmentSlug } },
|
|
||||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
|
|
||||||
);
|
|
||||||
await Integration.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
|
|
||||||
await Folder.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
|
|
||||||
await SecretImport.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
await SecretImport.updateMany(
|
|
||||||
{ workspace: workspaceId, "imports.environment": oldEnvironmentSlug },
|
|
||||||
{ $set: { "imports.$[element].environment": environmentSlug } },
|
|
||||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] },
|
|
||||||
);
|
|
||||||
|
|
||||||
await Webhook.updateMany(
|
|
||||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
|
||||||
{ environment: environmentSlug }
|
|
||||||
);
|
|
||||||
|
|
||||||
await Membership.updateMany(
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
"deniedPermissions.environmentSlug": oldEnvironmentSlug
|
|
||||||
},
|
|
||||||
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
|
|
||||||
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
|
|
||||||
);
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.UPDATE_ENVIRONMENT,
|
|
||||||
metadata: {
|
|
||||||
oldName: oldEnvironment.name,
|
|
||||||
newName: environmentName,
|
|
||||||
oldSlug: oldEnvironment.slug,
|
|
||||||
newSlug: environmentSlug.toLowerCase()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: workspace._id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully update environment",
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: {
|
|
||||||
name: environmentName,
|
|
||||||
slug: environmentSlug
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete workspace environment by [environmentSlug] of workspace [workspaceId] and do the clean up
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteWorkspaceEnvironment = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Delete environment'
|
|
||||||
#swagger.description = 'Delete environment'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of workspace where to delete environment",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"environmentSlug": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of environment to delete",
|
|
||||||
"example": "dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["environmentSlug"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"message": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Success message",
|
|
||||||
"example": "Successfully deleted environment"
|
|
||||||
},
|
|
||||||
"workspace": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of workspace where environment was deleted",
|
|
||||||
"example": "abc123"
|
|
||||||
},
|
|
||||||
"environment": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Slug of deleted environment",
|
|
||||||
"example": "dev"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Response after deleting an environment from a workspace"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { environmentSlug }
|
|
||||||
} = await validateRequest(reqValidator.DeleteWorkspaceEnvironmentV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Environments
|
|
||||||
);
|
|
||||||
|
|
||||||
// atomic update the env to avoid conflict
|
|
||||||
const workspace = await Workspace.findById(workspaceId).exec();
|
|
||||||
if (!workspace) {
|
|
||||||
throw new Error("Failed to create workspace environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
const envIndex = workspace?.environments.findIndex(({ slug }) => slug === environmentSlug);
|
|
||||||
if (envIndex === -1) {
|
|
||||||
throw new Error("Invalid environment given");
|
|
||||||
}
|
|
||||||
|
|
||||||
const oldEnvironment = workspace.environments[envIndex];
|
|
||||||
|
|
||||||
workspace.environments.splice(envIndex, 1);
|
|
||||||
await workspace.save();
|
|
||||||
|
|
||||||
// clean up
|
|
||||||
await Secret.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug
|
|
||||||
});
|
|
||||||
await SecretVersion.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug
|
|
||||||
});
|
|
||||||
|
|
||||||
// await ServiceToken.deleteMany({
|
|
||||||
// workspace: workspaceId,
|
|
||||||
// environment: environmentSlug,
|
|
||||||
// });
|
|
||||||
|
|
||||||
const result = await ServiceTokenData.updateMany(
|
|
||||||
{ workspace: workspaceId },
|
|
||||||
{ $pull: { scopes: { environment: environmentSlug } } }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (result.modifiedCount > 0) {
|
|
||||||
await ServiceTokenData.deleteMany({ workspace: workspaceId, scopes: { $size: 0 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
await Integration.deleteMany({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug
|
|
||||||
});
|
|
||||||
await Membership.updateMany(
|
|
||||||
{ workspace: workspaceId },
|
|
||||||
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
|
|
||||||
);
|
|
||||||
|
|
||||||
await EELicenseService.refreshPlan(workspace.organization, new Types.ObjectId(workspaceId));
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.DELETE_ENVIRONMENT,
|
|
||||||
metadata: {
|
|
||||||
name: oldEnvironment.name,
|
|
||||||
slug: oldEnvironment.slug
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: workspace._id
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully deleted environment",
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environmentSlug
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import * as authController from "./authController";
|
|
||||||
import * as signupController from "./signupController";
|
|
||||||
import * as usersController from "./usersController";
|
|
||||||
import * as organizationsController from "./organizationsController";
|
|
||||||
import * as workspaceController from "./workspaceController";
|
|
||||||
import * as serviceTokenDataController from "./serviceTokenDataController";
|
|
||||||
import * as secretController from "./secretController";
|
|
||||||
import * as secretsController from "./secretsController";
|
|
||||||
import * as environmentController from "./environmentController";
|
|
||||||
import * as tagController from "./tagController";
|
|
||||||
import * as membershipController from "./membershipController";
|
|
||||||
|
|
||||||
export {
|
|
||||||
authController,
|
|
||||||
signupController,
|
|
||||||
usersController,
|
|
||||||
organizationsController,
|
|
||||||
workspaceController,
|
|
||||||
serviceTokenDataController,
|
|
||||||
secretController,
|
|
||||||
secretsController,
|
|
||||||
environmentController,
|
|
||||||
tagController,
|
|
||||||
membershipController
|
|
||||||
};
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
|
|
||||||
import { getSiteURL } from "../../config";
|
|
||||||
import { EventType } from "../../ee/models";
|
|
||||||
import { EEAuditLogService } from "../../ee/services";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { sendMail } from "../../helpers";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import { IUser, Key, Membership, MembershipOrg, Workspace } from "../../models";
|
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
import * as reqValidator from "../../validation/membership";
|
|
||||||
import { ACCEPTED, MEMBER } from "../../variables";
|
|
||||||
|
|
||||||
export const addUserToWorkspace = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { members }
|
|
||||||
} = await validateRequest(reqValidator.AddUserToWorkspaceV2, req);
|
|
||||||
// check workspace
|
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
|
||||||
if (!workspace) throw new Error("Failed to find workspace");
|
|
||||||
|
|
||||||
// check permission
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
// validate members are part of the organization
|
|
||||||
const orgMembers = await MembershipOrg.find({
|
|
||||||
status: ACCEPTED,
|
|
||||||
_id: { $in: members.map(({ orgMembershipId }) => orgMembershipId) },
|
|
||||||
organization: workspace.organization
|
|
||||||
})
|
|
||||||
.populate<{ user: IUser }>("user")
|
|
||||||
.select({ _id: 1, user: 1 })
|
|
||||||
.lean();
|
|
||||||
if (orgMembers.length !== members.length)
|
|
||||||
throw BadRequestError({ message: "Org member not found" });
|
|
||||||
|
|
||||||
const existingMember = await Membership.find({
|
|
||||||
workspace: workspaceId,
|
|
||||||
user: { $in: orgMembers.map(({ user }) => user) }
|
|
||||||
});
|
|
||||||
if (existingMember?.length)
|
|
||||||
throw BadRequestError({ message: "Some users are already part of workspace" });
|
|
||||||
|
|
||||||
await Membership.insertMany(
|
|
||||||
orgMembers.map(({ user }) => ({ user: user._id, workspace: workspaceId, role: MEMBER }))
|
|
||||||
);
|
|
||||||
|
|
||||||
const encKeyGroupedByOrgMemberId = members.reduce<Record<string, (typeof members)[number]>>(
|
|
||||||
(prev, curr) => ({ ...prev, [curr.orgMembershipId]: curr }),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
await Key.insertMany(
|
|
||||||
orgMembers.map(({ user, _id: id }) => ({
|
|
||||||
encryptedKey: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedKey,
|
|
||||||
nonce: encKeyGroupedByOrgMemberId[id.toString()].workspaceEncryptedNonce,
|
|
||||||
sender: req.user._id,
|
|
||||||
receiver: user._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
|
|
||||||
await sendMail({
|
|
||||||
template: "workspaceInvitation.handlebars",
|
|
||||||
subjectLine: "Infisical workspace invitation",
|
|
||||||
recipients: orgMembers.map(({ user }) => user.email),
|
|
||||||
substitutions: {
|
|
||||||
inviterFirstName: req.user.firstName,
|
|
||||||
inviterEmail: req.user.email,
|
|
||||||
workspaceName: workspace.name,
|
|
||||||
callback_url: (await getSiteURL()) + "/login"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER,
|
|
||||||
metadata: orgMembers.map(({ user }) => ({
|
|
||||||
userId: user._id.toString(),
|
|
||||||
email: user.email
|
|
||||||
}))
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
success: true,
|
|
||||||
data: orgMembers
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,505 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
|
||||||
IWorkspace,
|
|
||||||
Identity,
|
|
||||||
IdentityMembership,
|
|
||||||
IdentityMembershipOrg,
|
|
||||||
Membership,
|
|
||||||
MembershipOrg,
|
|
||||||
User,
|
|
||||||
Workspace
|
|
||||||
} from "../../models";
|
|
||||||
import { Role } from "../../ee/models";
|
|
||||||
import { deleteMembershipOrg } from "../../helpers/membershipOrg";
|
|
||||||
import {
|
|
||||||
createOrganization as create,
|
|
||||||
deleteOrganization,
|
|
||||||
updateSubscriptionOrgQuantity
|
|
||||||
} from "../../helpers/organization";
|
|
||||||
import { addMembershipsOrg } from "../../helpers/membershipOrg";
|
|
||||||
import { BadRequestError, ResourceNotFoundError, UnauthorizedRequestError } from "../../utils/errors";
|
|
||||||
import { ACCEPTED, ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../variables";
|
|
||||||
import * as reqValidator from "../../validation/organization";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
getAuthDataOrgPermissions
|
|
||||||
} from "../../ee/services/RoleService";
|
|
||||||
import { EELicenseService } from "../../ee/services";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return memberships for organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const getOrganizationMemberships = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return organization user memberships'
|
|
||||||
#swagger.description = 'Return organization user memberships'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['organizationId'] = {
|
|
||||||
"description": "ID of organization",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"memberships": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/MembershipOrg"
|
|
||||||
},
|
|
||||||
"description": "Memberships of organization"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgMembersv2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const memberships = await MembershipOrg.find({
|
|
||||||
organization: organizationId
|
|
||||||
}).populate("user", "+publicKey");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
memberships
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update role of membership with id [membershipId] to role [role]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const updateOrganizationMembership = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Update organization user membership'
|
|
||||||
#swagger.description = 'Update organization user membership'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['organizationId'] = {
|
|
||||||
"description": "ID of organization",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['membershipId'] = {
|
|
||||||
"description": "ID of organization membership to update",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"role": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Role of organization membership - either owner, admin, or member",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"membership": {
|
|
||||||
$ref: "#/components/schemas/MembershipOrg",
|
|
||||||
"description": "Updated organization membership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { organizationId, membershipId },
|
|
||||||
body: { role }
|
|
||||||
} = await validateRequest(reqValidator.UpdateOrgMemberv2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
|
||||||
if (isCustomRole) {
|
|
||||||
const orgRole = await Role.findOne({
|
|
||||||
slug: role,
|
|
||||||
isOrgRole: true,
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!orgRole) throw BadRequestError({ message: "Role not found" });
|
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
if (!plan.rbac) return res.status(400).send({
|
|
||||||
message:
|
|
||||||
"Failed to assign custom role due to RBAC restriction. Upgrade plan to assign custom role to member."
|
|
||||||
});
|
|
||||||
|
|
||||||
const membership = await MembershipOrg.findByIdAndUpdate(membershipId, {
|
|
||||||
role: CUSTOM,
|
|
||||||
customRole: orgRole
|
|
||||||
});
|
|
||||||
return res.status(200).send({
|
|
||||||
membership
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const membership = await MembershipOrg.findByIdAndUpdate(
|
|
||||||
membershipId,
|
|
||||||
{
|
|
||||||
$set: {
|
|
||||||
role
|
|
||||||
},
|
|
||||||
$unset: {
|
|
||||||
customRole: 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
membership
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete organization membership with id [membershipId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteOrganizationMembership = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Delete organization user membership'
|
|
||||||
#swagger.description = 'Delete organization user membership'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['organizationId'] = {
|
|
||||||
"description": "ID of organization",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['membershipId'] = {
|
|
||||||
"description": "ID of organization membership to delete",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"membership": {
|
|
||||||
$ref: "#/components/schemas/MembershipOrg",
|
|
||||||
"description": "Deleted organization membership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { organizationId, membershipId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteOrgMemberv2, req);
|
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
|
||||||
_id: new Types.ObjectId(membershipId),
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membershipOrg) throw ResourceNotFoundError();
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: membershipOrg.organization
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Delete,
|
|
||||||
OrgPermissionSubjects.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
// delete organization membership
|
|
||||||
const membership = await deleteMembershipOrg({
|
|
||||||
membershipOrgId: membershipId
|
|
||||||
});
|
|
||||||
|
|
||||||
await updateSubscriptionOrgQuantity({
|
|
||||||
organizationId: membership.organization.toString()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
membership
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return workspaces for organization with id [organizationId] that user has
|
|
||||||
* access to
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const getOrganizationWorkspaces = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return projects in organization that user is part of'
|
|
||||||
#swagger.description = 'Return projects in organization that user is part of'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['organizationId'] = {
|
|
||||||
"description": "ID of organization",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"workspaces": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/Project"
|
|
||||||
},
|
|
||||||
"description": "Projects of organization"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgWorkspacesv2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Workspace
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspacesSet = new Set(
|
|
||||||
(
|
|
||||||
await Workspace.find(
|
|
||||||
{
|
|
||||||
organization: organizationId
|
|
||||||
},
|
|
||||||
"_id"
|
|
||||||
)
|
|
||||||
).map((w) => w._id.toString())
|
|
||||||
);
|
|
||||||
|
|
||||||
let workspaces: IWorkspace[] = [];
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof Identity) {
|
|
||||||
workspaces = (
|
|
||||||
await IdentityMembership.find({
|
|
||||||
identity: req.authData.authPayload._id
|
|
||||||
}).populate<{ workspace: IWorkspace }>("workspace")
|
|
||||||
)
|
|
||||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
|
||||||
.map((m) => m.workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof User) {
|
|
||||||
workspaces = (
|
|
||||||
await Membership.find({
|
|
||||||
user: req.authData.authPayload._id
|
|
||||||
}).populate<{ workspace: IWorkspace }>("workspace")
|
|
||||||
)
|
|
||||||
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
|
|
||||||
.map((m) => m.workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
workspaces
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new organization named [organizationName]
|
|
||||||
* and add user as owner
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createOrganization = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { name }
|
|
||||||
} = await validateRequest(reqValidator.CreateOrgv2, req);
|
|
||||||
|
|
||||||
// create organization and add user as member
|
|
||||||
const organization = await create({
|
|
||||||
email: req.user.email,
|
|
||||||
name
|
|
||||||
});
|
|
||||||
|
|
||||||
await addMembershipsOrg({
|
|
||||||
userIds: [req.user._id.toString()],
|
|
||||||
organizationId: organization._id.toString(),
|
|
||||||
roles: [ADMIN],
|
|
||||||
statuses: [ACCEPTED]
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
organization
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const deleteOrganizationById = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteOrgv2, req);
|
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
|
||||||
user: req.user._id,
|
|
||||||
organization: new Types.ObjectId(organizationId),
|
|
||||||
role: ADMIN
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membershipOrg) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
const organization = await deleteOrganization({
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
organization
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return list of identity memberships for organization with id [organizationId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationIdentityMemberships = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return organization identity memberships'
|
|
||||||
#swagger.description = 'Return organization identity memberships'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['organizationId'] = {
|
|
||||||
"description": "ID of organization",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"identityMemberships": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/IdentityMembershipOrg"
|
|
||||||
},
|
|
||||||
"description": "Identity memberships of organization"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgIdentityMembershipsV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const identityMemberships = await IdentityMembershipOrg.find({
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
}).populate("identity customRole");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identityMemberships
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,419 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import mongoose, { Types } from "mongoose";
|
|
||||||
import {
|
|
||||||
CreateSecretRequestBody,
|
|
||||||
ModifySecretRequestBody,
|
|
||||||
SanitizedSecretForCreate,
|
|
||||||
SanitizedSecretModify
|
|
||||||
} from "../../types/secret";
|
|
||||||
const { ValidationError } = mongoose.Error;
|
|
||||||
import {
|
|
||||||
ValidationError as RouteValidationError,
|
|
||||||
UnauthorizedRequestError
|
|
||||||
} from "../../utils/errors";
|
|
||||||
import {
|
|
||||||
ALGORITHM_AES_256_GCM,
|
|
||||||
ENCODING_SCHEME_UTF8,
|
|
||||||
SECRET_PERSONAL,
|
|
||||||
SECRET_SHARED
|
|
||||||
} from "../../variables";
|
|
||||||
import { TelemetryService } from "../../services";
|
|
||||||
import { Secret, User } from "../../models";
|
|
||||||
import { AccountNotFoundError } from "../../utils/errors";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create secret for workspace with id [workspaceId] and environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const createSecret = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const secretToCreate: CreateSecretRequestBody = req.body.secret;
|
|
||||||
const { workspaceId, environment } = req.params;
|
|
||||||
const sanitizedSecret: SanitizedSecretForCreate = {
|
|
||||||
secretKeyCiphertext: secretToCreate.secretKeyCiphertext,
|
|
||||||
secretKeyIV: secretToCreate.secretKeyIV,
|
|
||||||
secretKeyTag: secretToCreate.secretKeyTag,
|
|
||||||
secretKeyHash: secretToCreate.secretKeyHash,
|
|
||||||
secretValueCiphertext: secretToCreate.secretValueCiphertext,
|
|
||||||
secretValueIV: secretToCreate.secretValueIV,
|
|
||||||
secretValueTag: secretToCreate.secretValueTag,
|
|
||||||
secretValueHash: secretToCreate.secretValueHash,
|
|
||||||
secretCommentCiphertext: secretToCreate.secretCommentCiphertext,
|
|
||||||
secretCommentIV: secretToCreate.secretCommentIV,
|
|
||||||
secretCommentTag: secretToCreate.secretCommentTag,
|
|
||||||
secretCommentHash: secretToCreate.secretCommentHash,
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
type: secretToCreate.type,
|
|
||||||
user: new Types.ObjectId(req.user._id),
|
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
|
||||||
};
|
|
||||||
|
|
||||||
const secret = await new Secret(sanitizedSecret).save();
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets added",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: 1,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
|
||||||
userAgent: req.headers?.["user-agent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
secret
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create many secrets for workspace with id [workspaceId] and environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const createSecrets = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
|
|
||||||
const { workspaceId, environment } = req.params;
|
|
||||||
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = [];
|
|
||||||
|
|
||||||
secretsToCreate.forEach((rawSecret) => {
|
|
||||||
const safeUpdateFields: SanitizedSecretForCreate = {
|
|
||||||
secretKeyCiphertext: rawSecret.secretKeyCiphertext,
|
|
||||||
secretKeyIV: rawSecret.secretKeyIV,
|
|
||||||
secretKeyTag: rawSecret.secretKeyTag,
|
|
||||||
secretKeyHash: rawSecret.secretKeyHash,
|
|
||||||
secretValueCiphertext: rawSecret.secretValueCiphertext,
|
|
||||||
secretValueIV: rawSecret.secretValueIV,
|
|
||||||
secretValueTag: rawSecret.secretValueTag,
|
|
||||||
secretValueHash: rawSecret.secretValueHash,
|
|
||||||
secretCommentCiphertext: rawSecret.secretCommentCiphertext,
|
|
||||||
secretCommentIV: rawSecret.secretCommentIV,
|
|
||||||
secretCommentTag: rawSecret.secretCommentTag,
|
|
||||||
secretCommentHash: rawSecret.secretCommentHash,
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
type: rawSecret.type,
|
|
||||||
user: new Types.ObjectId(req.user._id),
|
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
|
||||||
};
|
|
||||||
|
|
||||||
sanitizedSecretesToCreate.push(safeUpdateFields);
|
|
||||||
});
|
|
||||||
|
|
||||||
const secrets = await Secret.insertMany(sanitizedSecretesToCreate);
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets added",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: (secretsToCreate ?? []).length,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
|
||||||
userAgent: req.headers?.["user-agent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
secrets
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete secrets in workspace with id [workspaceId] and environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const deleteSecrets = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const { workspaceId, environmentName } = req.params;
|
|
||||||
const secretIdsToDelete: string[] = req.body.secretIds;
|
|
||||||
|
|
||||||
const secretIdsUserCanDelete = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
|
||||||
|
|
||||||
const secretsUserCanDeleteSet: Set<string> = new Set(
|
|
||||||
secretIdsUserCanDelete.map((objectId) => objectId._id.toString())
|
|
||||||
);
|
|
||||||
|
|
||||||
// Filter out IDs that user can delete and then map them to delete operations
|
|
||||||
const deleteOperationsToPerform = secretIdsToDelete
|
|
||||||
.filter(secretIdToDelete => {
|
|
||||||
if (!secretsUserCanDeleteSet.has(secretIdToDelete)) {
|
|
||||||
throw RouteValidationError({
|
|
||||||
message: "You cannot delete secrets that you do not have access to"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
})
|
|
||||||
.map(secretIdToDelete => ({
|
|
||||||
deleteOne: { filter: { _id: new Types.ObjectId(secretIdToDelete) } }
|
|
||||||
}));
|
|
||||||
|
|
||||||
const numSecretsDeleted = deleteOperationsToPerform.length;
|
|
||||||
|
|
||||||
await Secret.bulkWrite(deleteOperationsToPerform);
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets deleted",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: numSecretsDeleted,
|
|
||||||
environment: environmentName,
|
|
||||||
workspaceId,
|
|
||||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
|
||||||
userAgent: req.headers?.["user-agent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete secret with id [secretId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const deleteSecret = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
await Secret.findByIdAndDelete(req._secret._id);
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets deleted",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: 1,
|
|
||||||
workspaceId: req._secret.workspace.toString(),
|
|
||||||
environment: req._secret.environment,
|
|
||||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
|
||||||
userAgent: req.headers?.["user-agent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
secret: req._secret
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update secrets for workspace with id [workspaceId] and environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateSecrets = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const { workspaceId, environmentName } = req.params;
|
|
||||||
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
|
|
||||||
const secretIdsUserCanModify = await Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
|
||||||
|
|
||||||
const secretsUserCanModifySet: Set<string> = new Set(
|
|
||||||
secretIdsUserCanModify.map((objectId) => objectId._id.toString())
|
|
||||||
);
|
|
||||||
const updateOperationsToPerform: any = [];
|
|
||||||
|
|
||||||
secretsModificationsRequested.forEach((userModifiedSecret) => {
|
|
||||||
if (secretsUserCanModifySet.has(userModifiedSecret._id.toString())) {
|
|
||||||
const sanitizedSecret: SanitizedSecretModify = {
|
|
||||||
secretKeyCiphertext: userModifiedSecret.secretKeyCiphertext,
|
|
||||||
secretKeyIV: userModifiedSecret.secretKeyIV,
|
|
||||||
secretKeyTag: userModifiedSecret.secretKeyTag,
|
|
||||||
secretKeyHash: userModifiedSecret.secretKeyHash,
|
|
||||||
secretValueCiphertext: userModifiedSecret.secretValueCiphertext,
|
|
||||||
secretValueIV: userModifiedSecret.secretValueIV,
|
|
||||||
secretValueTag: userModifiedSecret.secretValueTag,
|
|
||||||
secretValueHash: userModifiedSecret.secretValueHash,
|
|
||||||
secretCommentCiphertext: userModifiedSecret.secretCommentCiphertext,
|
|
||||||
secretCommentIV: userModifiedSecret.secretCommentIV,
|
|
||||||
secretCommentTag: userModifiedSecret.secretCommentTag,
|
|
||||||
secretCommentHash: userModifiedSecret.secretCommentHash
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateOperation = {
|
|
||||||
updateOne: {
|
|
||||||
filter: { _id: userModifiedSecret._id, workspace: workspaceId },
|
|
||||||
update: { $inc: { version: 1 }, $set: sanitizedSecret }
|
|
||||||
}
|
|
||||||
};
|
|
||||||
updateOperationsToPerform.push(updateOperation);
|
|
||||||
} else {
|
|
||||||
throw UnauthorizedRequestError({
|
|
||||||
message: "You do not have permission to modify one or more of the requested secrets"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
await Secret.bulkWrite(updateOperationsToPerform);
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets modified",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: (secretsModificationsRequested ?? []).length,
|
|
||||||
environment: environmentName,
|
|
||||||
workspaceId,
|
|
||||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
|
||||||
userAgent: req.headers?.["user-agent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send();
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update a secret within workspace with id [workspaceId] and environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateSecret = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const { workspaceId, environmentName } = req.params;
|
|
||||||
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
|
|
||||||
|
|
||||||
await Secret.findOne({ workspace: workspaceId, environment: environmentName }, { _id: 1 });
|
|
||||||
|
|
||||||
const sanitizedSecret: SanitizedSecretModify = {
|
|
||||||
secretKeyCiphertext: secretModificationsRequested.secretKeyCiphertext,
|
|
||||||
secretKeyIV: secretModificationsRequested.secretKeyIV,
|
|
||||||
secretKeyTag: secretModificationsRequested.secretKeyTag,
|
|
||||||
secretKeyHash: secretModificationsRequested.secretKeyHash,
|
|
||||||
secretValueCiphertext: secretModificationsRequested.secretValueCiphertext,
|
|
||||||
secretValueIV: secretModificationsRequested.secretValueIV,
|
|
||||||
secretValueTag: secretModificationsRequested.secretValueTag,
|
|
||||||
secretValueHash: secretModificationsRequested.secretValueHash,
|
|
||||||
secretCommentCiphertext: secretModificationsRequested.secretCommentCiphertext,
|
|
||||||
secretCommentIV: secretModificationsRequested.secretCommentIV,
|
|
||||||
secretCommentTag: secretModificationsRequested.secretCommentTag,
|
|
||||||
secretCommentHash: secretModificationsRequested.secretCommentHash
|
|
||||||
};
|
|
||||||
|
|
||||||
const singleModificationUpdate = await Secret.updateOne(
|
|
||||||
{ _id: secretModificationsRequested._id, workspace: workspaceId },
|
|
||||||
{ $inc: { version: 1 }, $set: sanitizedSecret }
|
|
||||||
)
|
|
||||||
.catch((error) => {
|
|
||||||
if (error instanceof ValidationError) {
|
|
||||||
throw RouteValidationError({
|
|
||||||
message: "Unable to apply modifications, please try again",
|
|
||||||
stack: error.stack
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw error;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets modified",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: 1,
|
|
||||||
environment: environmentName,
|
|
||||||
workspaceId,
|
|
||||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
|
||||||
userAgent: req.headers?.["user-agent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(singleModificationUpdate);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return secrets for workspace with id [workspaceId], environment [environment] and user
|
|
||||||
* with id [req.user._id]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getSecrets = async (req: Request, res: Response) => {
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const { environment } = req.query;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
let userId: Types.ObjectId | undefined = undefined; // used for getting personal secrets for user
|
|
||||||
let userEmail: string | undefined = undefined; // used for posthog
|
|
||||||
if (req.user) {
|
|
||||||
userId = req.user._id;
|
|
||||||
userEmail = req.user.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (req.serviceTokenData) {
|
|
||||||
userId = req.serviceTokenData.user;
|
|
||||||
|
|
||||||
const user = await User.findById(req.serviceTokenData.user, "email");
|
|
||||||
if (!user) throw AccountNotFoundError();
|
|
||||||
userEmail = user.email;
|
|
||||||
}
|
|
||||||
|
|
||||||
const secrets = await Secret.find({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
$or: [{ user: userId }, { user: { $exists: false } }],
|
|
||||||
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
throw RouteValidationError({
|
|
||||||
message: "Failed to get secrets, please try again",
|
|
||||||
stack: err.stack
|
|
||||||
});
|
|
||||||
})
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets pulled",
|
|
||||||
distinctId: userEmail,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: (secrets ?? []).length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") ? "web" : "cli",
|
|
||||||
userAgent: req.headers?.["user-agent"]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.json(secrets);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return secret with id [secretId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getSecret = async (req: Request, res: Response) => {
|
|
||||||
// if (postHogClient) {
|
|
||||||
// postHogClient.capture({
|
|
||||||
// event: 'secrets pulled',
|
|
||||||
// distinctId: req.user.email,
|
|
||||||
// properties: {
|
|
||||||
// numberOfSecrets: 1,
|
|
||||||
// workspaceId: req._secret.workspace.toString(),
|
|
||||||
// environment: req._secret.environment,
|
|
||||||
// channel: req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli',
|
|
||||||
// userAgent: req.headers?.['user-agent']
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secret: req._secret
|
|
||||||
});
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,201 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import { ServiceTokenData } from "../../models";
|
|
||||||
import { getSaltRounds } from "../../config";
|
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
import { ActorType, EventType } from "../../ee/models";
|
|
||||||
import { EEAuditLogService } from "../../ee/services";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/serviceTokenData";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return service token data associated with service token on request
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getServiceTokenData = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return Infisical Token data'
|
|
||||||
#swagger.description = 'Return Infisical Token data'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"serviceTokenData": {
|
|
||||||
"type": "object",
|
|
||||||
$ref: "#/components/schemas/ServiceTokenData",
|
|
||||||
"description": "Details of service token"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
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")
|
|
||||||
.lean();
|
|
||||||
|
|
||||||
return res.status(200).json(serviceTokenData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new service token data for workspace with id [workspaceId] and
|
|
||||||
* environment [environment].
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createServiceTokenData = async (req: Request, res: Response) => {
|
|
||||||
let serviceTokenData;
|
|
||||||
|
|
||||||
const {
|
|
||||||
body: { workspaceId, permissions, tag, encryptedKey, scopes, name, expiresIn, iv }
|
|
||||||
} = await validateRequest(reqValidator.CreateServiceTokenV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.ServiceTokens
|
|
||||||
);
|
|
||||||
|
|
||||||
scopes.forEach(({ environment, secretPath }) => {
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: secretPath })
|
|
||||||
);
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString("hex");
|
|
||||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
|
||||||
|
|
||||||
let expiresAt;
|
|
||||||
if (expiresIn) {
|
|
||||||
expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
}
|
|
||||||
|
|
||||||
let user;
|
|
||||||
|
|
||||||
if (req.authData.actor.type === ActorType.USER) {
|
|
||||||
user = req.authData.authPayload._id;
|
|
||||||
}
|
|
||||||
|
|
||||||
serviceTokenData = await new ServiceTokenData({
|
|
||||||
name,
|
|
||||||
workspace: workspaceId,
|
|
||||||
user,
|
|
||||||
scopes,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
expiresAt,
|
|
||||||
secretHash,
|
|
||||||
encryptedKey,
|
|
||||||
iv,
|
|
||||||
tag,
|
|
||||||
permissions
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
// return service token data without sensitive data
|
|
||||||
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
|
|
||||||
|
|
||||||
if (!serviceTokenData) throw new Error("Failed to find service token data");
|
|
||||||
|
|
||||||
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_SERVICE_TOKEN,
|
|
||||||
metadata: {
|
|
||||||
name,
|
|
||||||
scopes
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serviceToken,
|
|
||||||
serviceTokenData
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete service token data with id [serviceTokenDataId].
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteServiceTokenData = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { serviceTokenDataId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteServiceTokenV2, req);
|
|
||||||
|
|
||||||
let serviceTokenData = await ServiceTokenData.findById(serviceTokenDataId);
|
|
||||||
if (!serviceTokenData) throw BadRequestError({ message: "Service token not found" });
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: serviceTokenData.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.ServiceTokens
|
|
||||||
);
|
|
||||||
|
|
||||||
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
|
|
||||||
|
|
||||||
if (!serviceTokenData)
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Failed to delete service token"
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.DELETE_SERVICE_TOKEN,
|
|
||||||
metadata: {
|
|
||||||
name: serviceTokenData.name,
|
|
||||||
scopes: serviceTokenData?.scopes
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: serviceTokenData.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serviceTokenData
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,262 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { MembershipOrg, User } from "../../models";
|
|
||||||
import { completeAccount } from "../../helpers/user";
|
|
||||||
import {
|
|
||||||
initializeDefaultOrg,
|
|
||||||
} from "../../helpers/signup";
|
|
||||||
import { issueAuthTokens } from "../../helpers/auth";
|
|
||||||
import { ACCEPTED, INVITED } from "../../variables";
|
|
||||||
import { standardRequest } from "../../config/request";
|
|
||||||
import { getHttpsEnabled, getLoopsApiKey } from "../../config";
|
|
||||||
import { updateSubscriptionOrgQuantity } from "../../helpers/organization";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
const {
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
organizationName,
|
|
||||||
}: {
|
|
||||||
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;
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
|
||||||
user = await User.findOne({ email });
|
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
|
||||||
// case 1: user doesn't exist.
|
|
||||||
// case 2: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: "Failed to complete account for complete user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
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
|
|
||||||
// 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"] ?? "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const 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(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully set up account",
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
|
||||||
* invite flow
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const completeAccountInvite = async (req: Request, res: Response) => {
|
|
||||||
let user;
|
|
||||||
const {
|
|
||||||
email,
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier,
|
|
||||||
} = req.body;
|
|
||||||
|
|
||||||
// get user
|
|
||||||
user = await User.findOne({ email });
|
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
|
||||||
// case 1: user doesn't exist.
|
|
||||||
// case 2: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: "Failed to complete account for complete user",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const membershipOrg = await MembershipOrg.findOne({
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membershipOrg) throw new Error("Failed to find invitations for email");
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
|
|
||||||
// update organization membership statuses that are
|
|
||||||
// invited to completed with user attached
|
|
||||||
const membershipsToUpdate = await MembershipOrg.find({
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED,
|
|
||||||
});
|
|
||||||
|
|
||||||
membershipsToUpdate.forEach(async (membership) => {
|
|
||||||
await updateSubscriptionOrgQuantity({
|
|
||||||
organizationId: membership.organization.toString(),
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await MembershipOrg.updateMany(
|
|
||||||
{
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user,
|
|
||||||
status: ACCEPTED,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
const tokens = await issueAuthTokens({
|
|
||||||
userId: user._id,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? "",
|
|
||||||
});
|
|
||||||
|
|
||||||
const token = tokens.token;
|
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie("jid", tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: await getHttpsEnabled(),
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully set up account",
|
|
||||||
user,
|
|
||||||
token,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { Secret, Tag } from "../../models";
|
|
||||||
import { BadRequestError } from "../../utils/errors";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import * as reqValidator from "../../validation/tags";
|
|
||||||
|
|
||||||
export const createWorkspaceTag = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { name, slug },
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.CreateWorkspaceTagsV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.Tags
|
|
||||||
);
|
|
||||||
|
|
||||||
const tagToCreate = {
|
|
||||||
name,
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
slug,
|
|
||||||
user: new Types.ObjectId(req.user._id)
|
|
||||||
};
|
|
||||||
|
|
||||||
const createdTag = await new Tag(tagToCreate).save();
|
|
||||||
|
|
||||||
res.json(createdTag);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteWorkspaceTag = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { tagId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteWorkspaceTagsV2, req);
|
|
||||||
|
|
||||||
const tagFromDB = await Tag.findById(tagId);
|
|
||||||
if (!tagFromDB) {
|
|
||||||
throw BadRequestError();
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: tagFromDB.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Tags
|
|
||||||
);
|
|
||||||
|
|
||||||
const result = await Tag.findByIdAndDelete(tagId);
|
|
||||||
|
|
||||||
// remove the tag from secrets
|
|
||||||
await Secret.updateMany({ tags: { $in: [tagId] } }, { $pull: { tags: tagId } });
|
|
||||||
|
|
||||||
res.json(result);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWorkspaceTags = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceTagsV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Tags
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspaceTags = await Tag.find({
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.json({
|
|
||||||
workspaceTags
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,314 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import crypto from "crypto";
|
|
||||||
import bcrypt from "bcrypt";
|
|
||||||
import { APIKeyData, AuthMethod, MembershipOrg, TokenVersion, User } from "../../models";
|
|
||||||
import { getSaltRounds } from "../../config";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import { deleteUser } from "../../helpers/user";
|
|
||||||
import * as reqValidator from "../../validation";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the current user's MFA-enabled status [isMfaEnabled].
|
|
||||||
* Note: Infisical currently only supports email-based 2FA only; this will expand to
|
|
||||||
* include SMS and authenticator app modes of authentication in the future.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateMyMfaEnabled = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { isMfaEnabled }
|
|
||||||
} = await validateRequest(reqValidator.UpdateMyMfaEnabledV2, req);
|
|
||||||
|
|
||||||
req.user.isMfaEnabled = isMfaEnabled;
|
|
||||||
|
|
||||||
if (isMfaEnabled) {
|
|
||||||
// TODO: adapt this route/controller
|
|
||||||
// to work for different forms of MFA
|
|
||||||
req.user.mfaMethods = ["email"];
|
|
||||||
} else {
|
|
||||||
req.user.mfaMethods = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
await req.user.save();
|
|
||||||
|
|
||||||
const user = req.user;
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
user
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update name of the current user to [firstName, lastName].
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateName = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { lastName, firstName }
|
|
||||||
} = await validateRequest(reqValidator.UpdateNameV2, req);
|
|
||||||
|
|
||||||
const user = await User.findByIdAndUpdate(
|
|
||||||
req.user._id.toString(),
|
|
||||||
{
|
|
||||||
firstName,
|
|
||||||
lastName: lastName ?? ""
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
user
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update auth method of the current user to [authMethods]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateAuthMethods = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { authMethods }
|
|
||||||
} = await validateRequest(reqValidator.UpdateAuthMethodsV2, req);
|
|
||||||
|
|
||||||
const hasSamlEnabled = req.user.authMethods.some((authMethod: AuthMethod) =>
|
|
||||||
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (hasSamlEnabled) {
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to update user authentication method because SAML SSO is enforced"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await User.findByIdAndUpdate(
|
|
||||||
req.user._id.toString(),
|
|
||||||
{
|
|
||||||
authMethods
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
user
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return organizations that the current user is part of.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const getMyOrganizations = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return organizations that current user is part of'
|
|
||||||
#swagger.description = 'Return organizations that current user is part of'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"organizations": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/Organization"
|
|
||||||
},
|
|
||||||
"description": "Organizations that user is part of"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const organizations = (
|
|
||||||
await MembershipOrg.find({
|
|
||||||
user: req.user._id
|
|
||||||
}).populate("organization")
|
|
||||||
).map((m) => m.organization);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
organizations
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return API keys belonging to current user.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
|
||||||
const apiKeyData = await APIKeyData.find({
|
|
||||||
user: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send(apiKeyData);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create new API key for current user.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createAPIKey = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { name, expiresIn }
|
|
||||||
} = await validateRequest(reqValidator.CreateApiKeyV2, req);
|
|
||||||
|
|
||||||
const secret = crypto.randomBytes(16).toString("hex");
|
|
||||||
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
|
|
||||||
|
|
||||||
const expiresAt = new Date();
|
|
||||||
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
|
|
||||||
|
|
||||||
let apiKeyData = await new APIKeyData({
|
|
||||||
name,
|
|
||||||
lastUsed: new Date(),
|
|
||||||
expiresAt,
|
|
||||||
user: req.user._id,
|
|
||||||
secretHash
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
// return api key data without sensitive data
|
|
||||||
apiKeyData = (await APIKeyData.findById(apiKeyData._id)) as any;
|
|
||||||
|
|
||||||
if (!apiKeyData) throw new Error("Failed to find API key data");
|
|
||||||
|
|
||||||
const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKey,
|
|
||||||
apiKeyData
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete API key with id [apiKeyDataId] belonging to current user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const deleteAPIKey = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { apiKeyDataId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteApiKeyV2, req);
|
|
||||||
|
|
||||||
const apiKeyData = await APIKeyData.findOneAndDelete({
|
|
||||||
_id: new Types.ObjectId(apiKeyDataId),
|
|
||||||
user: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return active sessions (TokenVersion) belonging to user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getMySessions = async (req: Request, res: Response) => {
|
|
||||||
const tokenVersions = await TokenVersion.find({
|
|
||||||
user: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send(tokenVersions);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Revoke all active sessions belong to user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteMySessions = async (req: Request, res: Response) => {
|
|
||||||
await TokenVersion.updateMany(
|
|
||||||
{
|
|
||||||
user: req.user._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$inc: {
|
|
||||||
refreshVersion: 1,
|
|
||||||
accessVersion: 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully revoked all sessions"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the current user.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getMe = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = "Retrieve the current user on the request"
|
|
||||||
#swagger.description = "Retrieve the current user on the request"
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"user": {
|
|
||||||
"type": "object",
|
|
||||||
$ref: "#/components/schemas/CurrentUser",
|
|
||||||
"description": "Current user on request"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const user = await User.findById(req.user._id).select(
|
|
||||||
"+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag"
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
user
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the current user.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const deleteMe = async (req: Request, res: Response) => {
|
|
||||||
const user = await deleteUser({
|
|
||||||
userId: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,883 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
|
||||||
IIdentity,
|
|
||||||
IdentityMembership,
|
|
||||||
IdentityMembershipOrg,
|
|
||||||
Key,
|
|
||||||
Membership,
|
|
||||||
ServiceTokenData,
|
|
||||||
Workspace
|
|
||||||
} from "../../models";
|
|
||||||
import { IRole, Role } from "../../ee/models";
|
|
||||||
import {
|
|
||||||
pullSecrets as pull,
|
|
||||||
v2PushSecrets as push,
|
|
||||||
reformatPullSecrets
|
|
||||||
} from "../../helpers/secret";
|
|
||||||
import { pushKeys } from "../../helpers/key";
|
|
||||||
import { EventService, TelemetryService } from "../../services";
|
|
||||||
import { eventPushSecrets } from "../../events";
|
|
||||||
import { EEAuditLogService } from "../../ee/services";
|
|
||||||
import { EventType } from "../../ee/models";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions,
|
|
||||||
getWorkspaceRolePermissions,
|
|
||||||
isAtLeastAsPrivilegedWorkspace
|
|
||||||
} from "../../ee/services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { BadRequestError, ForbiddenRequestError, ResourceNotFoundError } from "../../utils/errors";
|
|
||||||
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS, VIEWER } from "../../variables";
|
|
||||||
|
|
||||||
interface V2PushSecret {
|
|
||||||
type: string; // personal or shared
|
|
||||||
secretKeyCiphertext: string;
|
|
||||||
secretKeyIV: string;
|
|
||||||
secretKeyTag: string;
|
|
||||||
secretKeyHash: string;
|
|
||||||
secretValueCiphertext: string;
|
|
||||||
secretValueIV: string;
|
|
||||||
secretValueTag: string;
|
|
||||||
secretValueHash: string;
|
|
||||||
secretCommentCiphertext?: string;
|
|
||||||
secretCommentIV?: string;
|
|
||||||
secretCommentTag?: string;
|
|
||||||
secretCommentHash?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload (encrypted) secrets to workspace with id [workspaceId]
|
|
||||||
* for environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
|
|
||||||
// upload (encrypted) secrets to workspace with id [workspaceId]
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
let { secrets }: { secrets: V2PushSecret[] } = req.body;
|
|
||||||
const { keys, environment, channel } = req.body;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
// validate environment
|
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
|
||||||
throw new Error("Failed to validate environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
// sanitize secrets
|
|
||||||
secrets = secrets.filter(
|
|
||||||
(s: V2PushSecret) => s.secretKeyCiphertext !== "" && s.secretValueCiphertext !== ""
|
|
||||||
);
|
|
||||||
|
|
||||||
await push({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
secrets,
|
|
||||||
channel: channel ? channel : "cli",
|
|
||||||
ipAddress: req.realIP
|
|
||||||
});
|
|
||||||
|
|
||||||
await pushKeys({
|
|
||||||
userId: req.user._id,
|
|
||||||
workspaceId,
|
|
||||||
keys
|
|
||||||
});
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "secrets pushed",
|
|
||||||
distinctId: req.user.email,
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: channel ? channel : "cli"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// trigger event - push secrets
|
|
||||||
EventService.handleEvent({
|
|
||||||
event: eventPushSecrets({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
secretPath: "/"
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully uploaded workspace secrets"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return (encrypted) secrets for workspace with id [workspaceId]
|
|
||||||
* for environment [environment]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const pullSecrets = async (req: Request, res: Response) => {
|
|
||||||
let secrets;
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
const environment: string = req.query.environment as string;
|
|
||||||
const channel: string = req.query.channel as string;
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
let userId;
|
|
||||||
if (req.user) {
|
|
||||||
userId = req.user._id.toString();
|
|
||||||
} else if (req.serviceTokenData) {
|
|
||||||
userId = req.serviceTokenData.user.toString();
|
|
||||||
}
|
|
||||||
// validate environment
|
|
||||||
const workspaceEnvs = req.membership.workspace.environments;
|
|
||||||
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
|
|
||||||
throw new Error("Failed to validate environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
secrets = await pull({
|
|
||||||
userId,
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
channel: channel ? channel : "cli",
|
|
||||||
ipAddress: req.realIP
|
|
||||||
});
|
|
||||||
|
|
||||||
if (channel !== "cli") {
|
|
||||||
secrets = reformatPullSecrets({ secrets });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (postHogClient) {
|
|
||||||
// capture secrets pushed event in production
|
|
||||||
postHogClient.capture({
|
|
||||||
distinctId: req.user.email,
|
|
||||||
event: "secrets pulled",
|
|
||||||
properties: {
|
|
||||||
numberOfSecrets: secrets.length,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
channel: channel ? channel : "cli"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secrets
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWorkspaceKey = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return encrypted project key'
|
|
||||||
#swagger.description = 'Return encrypted project key'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/ProjectKey"
|
|
||||||
},
|
|
||||||
"description": "Encrypted project key for the given project"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceKeyV2, req);
|
|
||||||
|
|
||||||
const key = await Key.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
receiver: req.user._id
|
|
||||||
}).populate("sender", "+publicKey");
|
|
||||||
|
|
||||||
if (!key) throw new Error(`getWorkspaceKey: Failed to find workspace key [workspaceId=${workspaceId}] [receiver=${req.user._id}]`);
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.GET_WORKSPACE_KEY,
|
|
||||||
metadata: {
|
|
||||||
keyId: key._id.toString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).json(key);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
|
|
||||||
const { workspaceId } = req.params;
|
|
||||||
|
|
||||||
const serviceTokenData = await ServiceTokenData.find({
|
|
||||||
workspace: workspaceId
|
|
||||||
}).select("+encryptedKey +iv +tag");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serviceTokenData
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return memberships for workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return project user memberships'
|
|
||||||
#swagger.description = 'Return project user memberships'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"memberships": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/Membership"
|
|
||||||
},
|
|
||||||
"description": "Memberships of project"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceMembershipsV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const memberships = await Membership.find({
|
|
||||||
workspace: workspaceId
|
|
||||||
}).populate("user", "+publicKey");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
memberships
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update role of membership with id [membershipId] to role [role]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateWorkspaceMembership = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Update project user membership'
|
|
||||||
#swagger.description = 'Update project user membership'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['membershipId'] = {
|
|
||||||
"description": "ID of project membership to update",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"role": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Role to update to for project membership",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"membership": {
|
|
||||||
$ref: "#/components/schemas/Membership",
|
|
||||||
"description": "Updated membership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId, membershipId },
|
|
||||||
body: { role }
|
|
||||||
} = await validateRequest(reqValidator.UpdateWorkspaceMembershipsV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const membership = await Membership.findByIdAndUpdate(
|
|
||||||
membershipId,
|
|
||||||
{
|
|
||||||
role
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
membership
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete workspace membership with id [membershipId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Delete project user membership'
|
|
||||||
#swagger.description = 'Delete project user membership'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": [],
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['membershipId'] = {
|
|
||||||
"description": "ID of project membership to delete",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"membership": {
|
|
||||||
$ref: "#/components/schemas/Membership",
|
|
||||||
"description": "Deleted membership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId, membershipId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteWorkspaceMembershipsV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Member
|
|
||||||
);
|
|
||||||
|
|
||||||
const membership = await Membership.findByIdAndDelete(membershipId);
|
|
||||||
|
|
||||||
if (!membership) throw new Error("Failed to delete workspace membership");
|
|
||||||
|
|
||||||
await Key.deleteMany({
|
|
||||||
receiver: membership.user,
|
|
||||||
workspace: membership.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
membership
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Change autoCapitilzation Rule of workspace
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const toggleAutoCapitalization = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { autoCapitalization }
|
|
||||||
} = await validateRequest(reqValidator.ToggleAutoCapitalizationV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Settings
|
|
||||||
);
|
|
||||||
|
|
||||||
const workspace = await Workspace.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: workspaceId
|
|
||||||
},
|
|
||||||
{
|
|
||||||
autoCapitalization
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully changed autoCapitalization setting",
|
|
||||||
workspace
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add identity with id [identityId] to workspace
|
|
||||||
* with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const addIdentityToWorkspace = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId, identityId },
|
|
||||||
body: {
|
|
||||||
role
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.AddIdentityToWorkspaceV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
let identityMembership = await IdentityMembership.findOne({
|
|
||||||
identity: new Types.ObjectId(identityId),
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (identityMembership) throw BadRequestError({
|
|
||||||
message: `Identity with id ${identityId} already exists in project with id ${workspaceId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
const workspace = await Workspace.findById(workspaceId);
|
|
||||||
if (!workspace) throw ResourceNotFoundError();
|
|
||||||
|
|
||||||
const identityMembershipOrg = await IdentityMembershipOrg.findOne({
|
|
||||||
identity: new Types.ObjectId(identityId),
|
|
||||||
organization: workspace.organization
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
|
||||||
message: `Failed to find identity with id ${identityId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!identityMembershipOrg.organization.equals(workspace.organization)) throw BadRequestError({
|
|
||||||
message: "Failed to add identity to project in another organization"
|
|
||||||
});
|
|
||||||
|
|
||||||
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
|
|
||||||
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
|
||||||
|
|
||||||
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to add identity to project with more privileged role"
|
|
||||||
});
|
|
||||||
|
|
||||||
let customRole;
|
|
||||||
if (role) {
|
|
||||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
|
||||||
if (isCustomRole) {
|
|
||||||
customRole = await Role.findOne({
|
|
||||||
slug: role,
|
|
||||||
isOrgRole: false,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
identityMembership = await new IdentityMembership({
|
|
||||||
identity: identityMembershipOrg.identity,
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
role: customRole ? CUSTOM : role,
|
|
||||||
customRole
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identityMembership
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update role of identity with id [identityId] in workspace
|
|
||||||
* with id [workspaceId] to [role]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const updateIdentityWorkspaceRole = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Update project identity membership'
|
|
||||||
#swagger.description = 'Update project identity membership'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['identityId'] = {
|
|
||||||
"description": "ID of identity whose membership to update in project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"role": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Role to update to for identity project membership",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"identityMembership": {
|
|
||||||
$ref: "#/components/schemas/IdentityMembership",
|
|
||||||
"description": "Updated identity membership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId, identityId },
|
|
||||||
body: {
|
|
||||||
role
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.UpdateIdentityWorkspaceRoleV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
let identityMembership = await IdentityMembership
|
|
||||||
.findOne({
|
|
||||||
identity: new Types.ObjectId(identityId),
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
})
|
|
||||||
.populate<{
|
|
||||||
identity: IIdentity,
|
|
||||||
customRole: IRole
|
|
||||||
}>("identity customRole");
|
|
||||||
|
|
||||||
if (!identityMembership) throw BadRequestError({
|
|
||||||
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const identityRolePermission = await getWorkspaceRolePermissions(
|
|
||||||
identityMembership?.customRole?.slug ?? identityMembership.role,
|
|
||||||
identityMembership.workspace.toString()
|
|
||||||
);
|
|
||||||
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
|
|
||||||
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to update role of more privileged identity"
|
|
||||||
});
|
|
||||||
|
|
||||||
const rolePermission = await getWorkspaceRolePermissions(role, workspaceId);
|
|
||||||
const isAsPrivilegedAsIntendedRole = isAtLeastAsPrivilegedWorkspace(permission, rolePermission);
|
|
||||||
|
|
||||||
if (!isAsPrivilegedAsIntendedRole) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to update identity to a more privileged role"
|
|
||||||
});
|
|
||||||
|
|
||||||
let customRole;
|
|
||||||
if (role) {
|
|
||||||
const isCustomRole = ![ADMIN, MEMBER, VIEWER, NO_ACCESS].includes(role);
|
|
||||||
if (isCustomRole) {
|
|
||||||
customRole = await Role.findOne({
|
|
||||||
slug: role,
|
|
||||||
isOrgRole: false,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
identityMembership = await IdentityMembership.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
identity: identityMembership.identity._id,
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: customRole ? CUSTOM : role,
|
|
||||||
customRole
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identityMembership
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete identity with id [identityId] from workspace
|
|
||||||
* with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const deleteIdentityFromWorkspace = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Delete project identity membership'
|
|
||||||
#swagger.description = 'Delete project identity membership'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['identityId'] = {
|
|
||||||
"description": "ID of identity whose membership to delete in project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"identityMembership": {
|
|
||||||
$ref: "#/components/schemas/IdentityMembership",
|
|
||||||
"description": "Deleted identity membership"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId, identityId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteIdentityFromWorkspaceV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const identityMembership = await IdentityMembership
|
|
||||||
.findOne({
|
|
||||||
identity: new Types.ObjectId(identityId),
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
})
|
|
||||||
.populate<{
|
|
||||||
identity: IIdentity,
|
|
||||||
customRole: IRole
|
|
||||||
}>("identity customRole");
|
|
||||||
|
|
||||||
if (!identityMembership) throw ResourceNotFoundError({
|
|
||||||
message: `Identity with id ${identityId} does not exist in project with id ${workspaceId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const identityRolePermission = await getWorkspaceRolePermissions(
|
|
||||||
identityMembership?.customRole?.slug ?? identityMembership.role,
|
|
||||||
identityMembership.workspace.toString()
|
|
||||||
);
|
|
||||||
const isAsPrivilegedAsIdentity = isAtLeastAsPrivilegedWorkspace(permission, identityRolePermission);
|
|
||||||
if (!isAsPrivilegedAsIdentity) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to remove more privileged identity from project"
|
|
||||||
});
|
|
||||||
|
|
||||||
await IdentityMembership.findByIdAndDelete(identityMembership._id);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identityMembership
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return list of identity memberships for workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaceIdentityMemberships = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return project identity memberships'
|
|
||||||
#swagger.description = 'Return project identity memberships'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['workspaceId'] = {
|
|
||||||
"description": "ID of project",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"identityMemberships": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/IdentityMembership"
|
|
||||||
},
|
|
||||||
"description": "Identity memberships of project"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceIdentityMembersV2, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const identityMemberships = await IdentityMembership.find({
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
}).populate("identity customRole");
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identityMemberships
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-var-requires */
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import jwt from "jsonwebtoken";
|
|
||||||
import * as bigintConversion from "bigint-conversion";
|
|
||||||
const jsrp = require("jsrp");
|
|
||||||
import { LoginSRPDetail, User } from "../../models";
|
|
||||||
import { createToken, issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
|
|
||||||
import { checkUserDevice } from "../../helpers/user";
|
|
||||||
import { sendMail } from "../../helpers/nodemailer";
|
|
||||||
import { TokenService } from "../../services";
|
|
||||||
import { BadRequestError, InternalServerError } from "../../utils/errors";
|
|
||||||
import { AuthTokenType, TOKEN_EMAIL_MFA } from "../../variables";
|
|
||||||
import { getAuthSecret, getHttpsEnabled, getJwtMfaLifetime } from "../../config";
|
|
||||||
import { AuthMethod } from "../../models/user";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/auth";
|
|
||||||
|
|
||||||
declare module "jsonwebtoken" {
|
|
||||||
export interface ProviderAuthJwtPayload extends jwt.JwtPayload {
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
authProvider: AuthMethod;
|
|
||||||
isUserCompleted: boolean;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log in user step 1: Return [salt] and [serverPublicKey] as part of step 1 of SRP protocol
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const login1 = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { email, clientPublicKey, providerAuthToken }
|
|
||||||
} = await validateRequest(reqValidator.Login1V3, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select("+salt +verifier");
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
|
||||||
await validateProviderAuthToken({
|
|
||||||
email,
|
|
||||||
providerAuthToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
// generate server-side public key
|
|
||||||
const serverPublicKey = server.getPublicKey();
|
|
||||||
await LoginSRPDetail.findOneAndReplace(
|
|
||||||
{
|
|
||||||
email: email
|
|
||||||
},
|
|
||||||
{
|
|
||||||
email,
|
|
||||||
userId: user.id,
|
|
||||||
clientPublicKey: clientPublicKey,
|
|
||||||
serverBInt: bigintConversion.bigintToBuf(server.bInt)
|
|
||||||
},
|
|
||||||
{ upsert: true, returnNewDocument: false }
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
serverPublicKey,
|
|
||||||
salt: user.salt
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Log in user step 2: complete step 2 of SRP protocol and return token and their (encrypted)
|
|
||||||
* private key
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const login2 = async (req: Request, res: Response) => {
|
|
||||||
if (!req.headers["user-agent"])
|
|
||||||
throw InternalServerError({ message: "User-Agent header is required" });
|
|
||||||
|
|
||||||
const {
|
|
||||||
body: { email, providerAuthToken, clientProof }
|
|
||||||
} = await validateRequest(reqValidator.Login2V3, req);
|
|
||||||
|
|
||||||
const user = await User.findOne({
|
|
||||||
email
|
|
||||||
}).select(
|
|
||||||
"+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag +devices"
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to find user");
|
|
||||||
|
|
||||||
if (!user.authMethods.includes(AuthMethod.EMAIL)) {
|
|
||||||
await validateProviderAuthToken({
|
|
||||||
email,
|
|
||||||
providerAuthToken
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email });
|
|
||||||
|
|
||||||
if (!loginSRPDetail) {
|
|
||||||
return BadRequestError(Error("Failed to find login details for SRP"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const server = new jsrp.server();
|
|
||||||
server.init(
|
|
||||||
{
|
|
||||||
salt: user.salt,
|
|
||||||
verifier: user.verifier,
|
|
||||||
b: loginSRPDetail.serverBInt
|
|
||||||
},
|
|
||||||
async () => {
|
|
||||||
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
|
|
||||||
|
|
||||||
// compare server and client shared keys
|
|
||||||
if (server.checkClientProof(clientProof)) {
|
|
||||||
if (user.isMfaEnabled) {
|
|
||||||
// case: user has MFA enabled
|
|
||||||
|
|
||||||
// generate temporary MFA token
|
|
||||||
const token = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.MFA_TOKEN,
|
|
||||||
userId: user._id.toString()
|
|
||||||
},
|
|
||||||
expiresIn: await getJwtMfaLifetime(),
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
const code = await TokenService.createToken({
|
|
||||||
type: TOKEN_EMAIL_MFA,
|
|
||||||
email
|
|
||||||
});
|
|
||||||
|
|
||||||
// send MFA code [code] to [email]
|
|
||||||
await sendMail({
|
|
||||||
template: "emailMfa.handlebars",
|
|
||||||
subjectLine: "Infisical MFA code",
|
|
||||||
recipients: [user.email],
|
|
||||||
substitutions: {
|
|
||||||
code
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
mfaEnabled: true,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await checkUserDevice({
|
|
||||||
user,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
const tokens = await issueAuthTokens({
|
|
||||||
userId: user._id,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie("jid", tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: await getHttpsEnabled()
|
|
||||||
});
|
|
||||||
|
|
||||||
// case: user does not have MFA enablgged
|
|
||||||
// return (access) token in response
|
|
||||||
|
|
||||||
interface ResponseData {
|
|
||||||
mfaEnabled: boolean;
|
|
||||||
encryptionVersion: any;
|
|
||||||
protectedKey?: string;
|
|
||||||
protectedKeyIV?: string;
|
|
||||||
protectedKeyTag?: string;
|
|
||||||
token: string;
|
|
||||||
publicKey?: string;
|
|
||||||
encryptedPrivateKey?: string;
|
|
||||||
iv?: string;
|
|
||||||
tag?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const response: ResponseData = {
|
|
||||||
mfaEnabled: false,
|
|
||||||
encryptionVersion: user.encryptionVersion,
|
|
||||||
token: tokens.token,
|
|
||||||
publicKey: user.publicKey,
|
|
||||||
encryptedPrivateKey: user.encryptedPrivateKey,
|
|
||||||
iv: user.iv,
|
|
||||||
tag: user.tag
|
|
||||||
};
|
|
||||||
|
|
||||||
if (user?.protectedKey && user?.protectedKeyIV && user?.protectedKeyTag) {
|
|
||||||
response.protectedKey = user.protectedKey;
|
|
||||||
response.protectedKeyIV = user.protectedKeyIV;
|
|
||||||
response.protectedKeyTag = user.protectedKeyTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to authenticate. Try again?"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import * as usersController from "./usersController";
|
|
||||||
import * as secretsController from "./secretsController";
|
|
||||||
import * as workspacesController from "./workspacesController";
|
|
||||||
import * as authController from "./authController";
|
|
||||||
import * as signupController from "./signupController";
|
|
||||||
|
|
||||||
export {
|
|
||||||
usersController,
|
|
||||||
authController,
|
|
||||||
secretsController,
|
|
||||||
signupController,
|
|
||||||
workspacesController
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,193 +0,0 @@
|
|||||||
import jwt from "jsonwebtoken";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import * as Sentry from "@sentry/node";
|
|
||||||
import { MembershipOrg, User } from "../../models";
|
|
||||||
import { completeAccount } from "../../helpers/user";
|
|
||||||
import { initializeDefaultOrg } from "../../helpers/signup";
|
|
||||||
import { issueAuthTokens, validateProviderAuthToken } from "../../helpers/auth";
|
|
||||||
import { ACCEPTED, AuthTokenType, INVITED } from "../../variables";
|
|
||||||
import { standardRequest } from "../../config/request";
|
|
||||||
import { getAuthSecret, getHttpsEnabled, getLoopsApiKey } from "../../config";
|
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
|
||||||
import { TelemetryService } from "../../services";
|
|
||||||
import { AuthMethod } from "../../models";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/auth";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Complete setting up user by adding their personal and auth information as part of the
|
|
||||||
* signup flow
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const completeAccountSignup = async (req: Request, res: Response) => {
|
|
||||||
let user, token;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
email,
|
|
||||||
publicKey,
|
|
||||||
salt,
|
|
||||||
lastName,
|
|
||||||
verifier,
|
|
||||||
firstName,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
organizationName,
|
|
||||||
providerAuthToken,
|
|
||||||
attributionSource,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.CompletedAccountSignupV3, req);
|
|
||||||
|
|
||||||
user = await User.findOne({ email });
|
|
||||||
|
|
||||||
if (!user || (user && user?.publicKey)) {
|
|
||||||
// case 1: user doesn't exist.
|
|
||||||
// case 2: user has already completed account
|
|
||||||
return res.status(403).send({
|
|
||||||
error: "Failed to complete account for complete user"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (providerAuthToken) {
|
|
||||||
await validateProviderAuthToken({
|
|
||||||
email,
|
|
||||||
providerAuthToken
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const [AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE] = <[string, string]>(
|
|
||||||
req.headers["authorization"]?.split(" ", 2)
|
|
||||||
) ?? [null, null];
|
|
||||||
if (AUTH_TOKEN_TYPE === null) {
|
|
||||||
throw BadRequestError({ message: "Missing Authorization Header in the request header." });
|
|
||||||
}
|
|
||||||
if (AUTH_TOKEN_TYPE.toLowerCase() !== "bearer") {
|
|
||||||
throw BadRequestError({
|
|
||||||
message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (AUTH_TOKEN_VALUE === null) {
|
|
||||||
throw BadRequestError({
|
|
||||||
message: "Missing Authorization Body in the request header"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const decodedToken = <jwt.UserIDJwtPayload>(
|
|
||||||
jwt.verify(AUTH_TOKEN_VALUE, await getAuthSecret())
|
|
||||||
);
|
|
||||||
|
|
||||||
if (decodedToken.authTokenType !== AuthTokenType.SIGNUP_TOKEN) throw UnauthorizedRequestError();
|
|
||||||
if (decodedToken.userId !== user.id) throw UnauthorizedRequestError();
|
|
||||||
}
|
|
||||||
|
|
||||||
// complete setting up user's account
|
|
||||||
user = await completeAccount({
|
|
||||||
userId: user._id.toString(),
|
|
||||||
firstName,
|
|
||||||
lastName,
|
|
||||||
encryptionVersion: 2,
|
|
||||||
protectedKey,
|
|
||||||
protectedKeyIV,
|
|
||||||
protectedKeyTag,
|
|
||||||
publicKey,
|
|
||||||
encryptedPrivateKey,
|
|
||||||
encryptedPrivateKeyIV,
|
|
||||||
encryptedPrivateKeyTag,
|
|
||||||
salt,
|
|
||||||
verifier
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) throw new Error("Failed to complete account for non-existent user"); // ensure user is non-null
|
|
||||||
|
|
||||||
const hasSamlEnabled = user.authMethods.some((authMethod: AuthMethod) =>
|
|
||||||
[AuthMethod.OKTA_SAML, AuthMethod.AZURE_SAML, AuthMethod.JUMPCLOUD_SAML].includes(authMethod)
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasSamlEnabled) {
|
|
||||||
// TODO: modify this part
|
|
||||||
// initialize default organization and workspace
|
|
||||||
await initializeDefaultOrg({
|
|
||||||
organizationName,
|
|
||||||
user
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// update organization membership statuses that are
|
|
||||||
// invited to completed with user attached
|
|
||||||
await MembershipOrg.updateMany(
|
|
||||||
{
|
|
||||||
inviteEmail: email,
|
|
||||||
status: INVITED
|
|
||||||
},
|
|
||||||
{
|
|
||||||
user,
|
|
||||||
status: ACCEPTED
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
// issue tokens
|
|
||||||
const tokens = await issueAuthTokens({
|
|
||||||
userId: user._id,
|
|
||||||
ip: req.realIP,
|
|
||||||
userAgent: req.headers["user-agent"] ?? ""
|
|
||||||
});
|
|
||||||
|
|
||||||
token = tokens.token;
|
|
||||||
|
|
||||||
// sending a welcome email to new users
|
|
||||||
if (await getLoopsApiKey()) {
|
|
||||||
await standardRequest.post(
|
|
||||||
"https://app.loops.so/api/v1/events/send",
|
|
||||||
{
|
|
||||||
email: email,
|
|
||||||
eventName: "Sign Up",
|
|
||||||
firstName: firstName,
|
|
||||||
lastName: lastName
|
|
||||||
},
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
Accept: "application/json",
|
|
||||||
Authorization: "Bearer " + (await getLoopsApiKey())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// store (refresh) token in httpOnly cookie
|
|
||||||
res.cookie("jid", tokens.refreshToken, {
|
|
||||||
httpOnly: true,
|
|
||||||
path: "/",
|
|
||||||
sameSite: "strict",
|
|
||||||
secure: await getHttpsEnabled()
|
|
||||||
});
|
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
|
||||||
if (postHogClient) {
|
|
||||||
postHogClient.capture({
|
|
||||||
event: "User Signed Up",
|
|
||||||
distinctId: email,
|
|
||||||
properties: {
|
|
||||||
email,
|
|
||||||
...(attributionSource ? { attributionSource } : {})
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
return res.status(400).send({
|
|
||||||
message: "Failed to complete account setup"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully set up account",
|
|
||||||
user,
|
|
||||||
token
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { APIKeyDataV2 } from "../../models";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return API keys belonging to current user.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getMyAPIKeys = async (req: Request, res: Response) => {
|
|
||||||
const apiKeyData = await APIKeyDataV2.find({
|
|
||||||
user: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { validateRequest } from "../../helpers/validation";
|
|
||||||
import { Membership, Secret, User } from "../../models";
|
|
||||||
import { SecretService } from "../../services";
|
|
||||||
import { getAuthDataProjectPermissions } from "../../ee/services/ProjectRoleService";
|
|
||||||
import { UnauthorizedRequestError } from "../../utils/errors";
|
|
||||||
import * as reqValidator from "../../validation/workspace";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether or not all secrets in workspace with id [workspaceId]
|
|
||||||
* are blind-indexed
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceBlinkIndexStatusV3, req);
|
|
||||||
|
|
||||||
await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof User) {
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
if (membership.role !== "admin")
|
|
||||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const secretsWithoutBlindIndex = await Secret.countDocuments({
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
secretBlindIndex: {
|
|
||||||
$exists: false
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send(secretsWithoutBlindIndex === 0);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get all secrets for workspace with id [workspaceId]
|
|
||||||
*/
|
|
||||||
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetWorkspaceSecretsV3, req);
|
|
||||||
|
|
||||||
await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof User) {
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
if (membership.role !== "admin")
|
|
||||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const secrets = await Secret.find({
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secrets
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update blind indices for secrets in workspace with id [workspaceId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId },
|
|
||||||
body: { secretsToUpdate }
|
|
||||||
} = await validateRequest(reqValidator.NameWorkspaceSecretsV3, req);
|
|
||||||
|
|
||||||
await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (req.authData.authPayload instanceof User) {
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
if (membership.role !== "admin")
|
|
||||||
throw UnauthorizedRequestError({ message: "User must be an admin" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// get secret blind index salt
|
|
||||||
const salt = await SecretService.getSecretBlindIndexSalt({
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
// update secret blind indices
|
|
||||||
const operations = await Promise.all(
|
|
||||||
secretsToUpdate.map(async (secretToUpdate) => {
|
|
||||||
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
|
|
||||||
secretName: secretToUpdate.secretName,
|
|
||||||
salt
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
updateOne: {
|
|
||||||
filter: {
|
|
||||||
_id: new Types.ObjectId(secretToUpdate._id)
|
|
||||||
},
|
|
||||||
update: {
|
|
||||||
secretBlindIndex
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
await Secret.bulkWrite(operations);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
message: "Successfully named workspace secrets"
|
|
||||||
});
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,36 +0,0 @@
|
|||||||
The Infisical Enterprise license (the “Enterprise License”)
|
|
||||||
Copyright (c) 2022 Infisical Inc
|
|
||||||
|
|
||||||
With regard to the Infisical Software:
|
|
||||||
|
|
||||||
This software and associated documentation files (the "Software") may only be
|
|
||||||
used in production, if you (and any entity that you represent) have agreed to,
|
|
||||||
and are in compliance with, the Infisical Subscription Terms of Service, available
|
|
||||||
at https://infisical.com/terms (the “Enterprise Terms”), or other
|
|
||||||
agreement governing the use of the Software, as agreed by you and Infisical,
|
|
||||||
and otherwise have a valid Infisical Enterprise License for the
|
|
||||||
correct number of user seats. Subject to the foregoing sentence, you are free to
|
|
||||||
modify this Software and publish patches to the Software. You agree that Infisical
|
|
||||||
and/or its licensors (as applicable) retain all right, title and interest in and
|
|
||||||
to all such modifications and/or patches, and all such modifications and/or
|
|
||||||
patches may only be used, copied, modified, displayed, distributed, or otherwise
|
|
||||||
exploited with a valid Infiscial Enterprise subscription for the correct
|
|
||||||
number of user seats. Notwithstanding the foregoing, you may copy and modify
|
|
||||||
the Software for development and testing purposes, without requiring a
|
|
||||||
subscription. You agree that Infisical and/or its licensors (as applicable) retain
|
|
||||||
all right, title and interest in and to all such modifications. You are not
|
|
||||||
granted any other rights beyond what is expressly stated herein. Subject to the
|
|
||||||
foregoing, it is forbidden to copy, merge, publish, distribute, sublicense,
|
|
||||||
and/or sell the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
|
|
||||||
For all third party components incorporated into the Infisical Software, those
|
|
||||||
components are licensed under the original license provided by the owner of the
|
|
||||||
applicable component.
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { EELicenseService } from "../../services";
|
|
||||||
import { getLicenseServerUrl } from "../../../config";
|
|
||||||
import { licenseServerKeyRequest } from "../../../config/request";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../../validation/cloudProducts";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return available cloud product information.
|
|
||||||
* Note: Nicely formatted to easily construct a table from
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getCloudProducts = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { "billing-cycle": billingCycle }
|
|
||||||
} = await validateRequest(reqValidator.GetCloudProductsV1, req);
|
|
||||||
|
|
||||||
if (EELicenseService.instanceType === "cloud") {
|
|
||||||
const { data } = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
head: [],
|
|
||||||
rows: []
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,460 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
|
||||||
IIdentity,
|
|
||||||
Identity,
|
|
||||||
IdentityAccessToken,
|
|
||||||
IdentityMembership,
|
|
||||||
IdentityMembershipOrg,
|
|
||||||
IdentityUniversalAuth,
|
|
||||||
IdentityUniversalAuthClientSecret,
|
|
||||||
Organization
|
|
||||||
} from "../../../models";
|
|
||||||
import {
|
|
||||||
EventType,
|
|
||||||
IRole,
|
|
||||||
Role
|
|
||||||
} from "../../models";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../../validation/identities";
|
|
||||||
import {
|
|
||||||
getAuthDataOrgPermissions,
|
|
||||||
getOrgRolePermissions,
|
|
||||||
isAtLeastAsPrivilegedOrg
|
|
||||||
} from "../../services/RoleService";
|
|
||||||
import {
|
|
||||||
BadRequestError,
|
|
||||||
ForbiddenRequestError,
|
|
||||||
ResourceNotFoundError,
|
|
||||||
} from "../../../utils/errors";
|
|
||||||
import { ADMIN, CUSTOM, MEMBER, NO_ACCESS } from "../../../variables";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects
|
|
||||||
} from "../../services/RoleService";
|
|
||||||
import { EEAuditLogService } from "../../services";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create identity
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createIdentity = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Create identity'
|
|
||||||
#swagger.description = 'Create identity'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of entity to create",
|
|
||||||
"example": "development"
|
|
||||||
},
|
|
||||||
"organizationId": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "ID of organization where to create identity",
|
|
||||||
"example": "dev-environment"
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Role to assume for organization membership",
|
|
||||||
"example": "no-access"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["name", "organizationId", "role"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"identity": {
|
|
||||||
$ref: '#/definitions/Identity'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Details of the created identity"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
name,
|
|
||||||
organizationId,
|
|
||||||
role
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.CreateIdentityV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const rolePermission = await getOrgRolePermissions(role, organizationId);
|
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to create a more privileged identity"
|
|
||||||
});
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) throw BadRequestError({ message: `Organization with id ${organizationId} not found` });
|
|
||||||
|
|
||||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
|
||||||
|
|
||||||
let customRole;
|
|
||||||
if (isCustomRole) {
|
|
||||||
customRole = await Role.findOne({
|
|
||||||
slug: role,
|
|
||||||
isOrgRole: true,
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const identity = await new Identity({
|
|
||||||
name
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
await new IdentityMembershipOrg({
|
|
||||||
identity: identity._id,
|
|
||||||
organization: new Types.ObjectId(organizationId),
|
|
||||||
role: isCustomRole ? CUSTOM : role,
|
|
||||||
customRole
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.CREATE_IDENTITY,
|
|
||||||
metadata: {
|
|
||||||
identityId: identity._id.toString(),
|
|
||||||
name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update identity with id [identityId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateIdentity = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Update identity'
|
|
||||||
#swagger.description = 'Update identity'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['identityId'] = {
|
|
||||||
"description": "ID of identity to update",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Name of entity to update to",
|
|
||||||
"example": "development"
|
|
||||||
},
|
|
||||||
"role": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Role to update to for organization membership",
|
|
||||||
"example": "no-access"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"identity": {
|
|
||||||
$ref: '#/definitions/Identity'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Details of the updated identity"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { identityId },
|
|
||||||
body: {
|
|
||||||
name,
|
|
||||||
role
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.UpdateIdentityV1, req);
|
|
||||||
|
|
||||||
const identityMembershipOrg = await IdentityMembershipOrg
|
|
||||||
.findOne({
|
|
||||||
identity: new Types.ObjectId(identityId)
|
|
||||||
})
|
|
||||||
.populate<{
|
|
||||||
identity: IIdentity,
|
|
||||||
customRole: IRole
|
|
||||||
}>("identity customRole");
|
|
||||||
|
|
||||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
|
||||||
message: `Failed to find identity with id ${identityId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: identityMembershipOrg.organization
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const identityRolePermission = await getOrgRolePermissions(
|
|
||||||
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
|
|
||||||
identityMembershipOrg.organization.toString()
|
|
||||||
);
|
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to update more privileged identity"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (role) {
|
|
||||||
const rolePermission = await getOrgRolePermissions(role, identityMembershipOrg.organization.toString());
|
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, rolePermission);
|
|
||||||
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to update identity to a more privileged role"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
let customRole;
|
|
||||||
if (role) {
|
|
||||||
const isCustomRole = ![ADMIN, MEMBER, NO_ACCESS].includes(role);
|
|
||||||
if (isCustomRole) {
|
|
||||||
customRole = await Role.findOne({
|
|
||||||
slug: role,
|
|
||||||
isOrgRole: true,
|
|
||||||
organization: identityMembershipOrg.organization
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!customRole) throw BadRequestError({ message: "Role not found" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const identity = await Identity.findByIdAndUpdate(
|
|
||||||
identityId,
|
|
||||||
{
|
|
||||||
name,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!identity) throw BadRequestError({
|
|
||||||
message: `Failed to update identity with id ${identityId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
await IdentityMembershipOrg.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
identity: identity._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
role: customRole ? CUSTOM : role,
|
|
||||||
...(customRole ? {
|
|
||||||
customRole
|
|
||||||
} : {}),
|
|
||||||
...(role && !customRole ? { // non-custom role
|
|
||||||
$unset: {
|
|
||||||
customRole: 1
|
|
||||||
}
|
|
||||||
} : {})
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.UPDATE_IDENTITY,
|
|
||||||
metadata: {
|
|
||||||
identityId: identity._id.toString(),
|
|
||||||
name: identity.name,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
organizationId: identityMembershipOrg.organization
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete identity with id [identityId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteIdentity = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Delete identity'
|
|
||||||
#swagger.description = 'Delete identity'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"bearerAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['identityId'] = {
|
|
||||||
"description": "ID of identity",
|
|
||||||
"required": true,
|
|
||||||
"type": "string",
|
|
||||||
"in": "path"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"identity": {
|
|
||||||
$ref: '#/definitions/Identity'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"description": "Details of the deleted identity"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { identityId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteIdentityV1, req);
|
|
||||||
|
|
||||||
const identityMembershipOrg = await IdentityMembershipOrg
|
|
||||||
.findOne({
|
|
||||||
identity: new Types.ObjectId(identityId)
|
|
||||||
})
|
|
||||||
.populate<{
|
|
||||||
identity: IIdentity,
|
|
||||||
customRole: IRole
|
|
||||||
}>("identity customRole");
|
|
||||||
|
|
||||||
if (!identityMembershipOrg) throw ResourceNotFoundError({
|
|
||||||
message: `Failed to find identity with id ${identityId}`
|
|
||||||
});
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: identityMembershipOrg.organization
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Delete,
|
|
||||||
OrgPermissionSubjects.Identity
|
|
||||||
);
|
|
||||||
|
|
||||||
const identityRolePermission = await getOrgRolePermissions(
|
|
||||||
identityMembershipOrg?.customRole?.slug ?? identityMembershipOrg.role,
|
|
||||||
identityMembershipOrg.organization.toString()
|
|
||||||
);
|
|
||||||
const hasRequiredPrivileges = isAtLeastAsPrivilegedOrg(permission, identityRolePermission);
|
|
||||||
if (!hasRequiredPrivileges) throw ForbiddenRequestError({
|
|
||||||
message: "Failed to delete more privileged identity"
|
|
||||||
});
|
|
||||||
|
|
||||||
const identity = await Identity.findByIdAndDelete(identityMembershipOrg.identity);
|
|
||||||
if (!identity) throw ResourceNotFoundError({
|
|
||||||
message: `Identity with id ${identityId} not found`
|
|
||||||
});
|
|
||||||
|
|
||||||
await IdentityMembershipOrg.findByIdAndDelete(identityMembershipOrg._id);
|
|
||||||
|
|
||||||
await IdentityMembership.deleteMany({
|
|
||||||
identity: identityMembershipOrg.identity
|
|
||||||
});
|
|
||||||
|
|
||||||
await IdentityUniversalAuth.deleteMany({
|
|
||||||
identity: identityMembershipOrg.identity
|
|
||||||
});
|
|
||||||
|
|
||||||
await IdentityUniversalAuthClientSecret.deleteMany({
|
|
||||||
identity: identityMembershipOrg.identity
|
|
||||||
});
|
|
||||||
|
|
||||||
await IdentityAccessToken.deleteMany({
|
|
||||||
identity: identityMembershipOrg.identity
|
|
||||||
});
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.DELETE_IDENTITY,
|
|
||||||
metadata: {
|
|
||||||
identityId: identity._id.toString()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
organizationId: identityMembershipOrg.organization
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
identity
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import * as identitiesController from "./identitiesController";
|
|
||||||
import * as secretController from "./secretController";
|
|
||||||
import * as secretSnapshotController from "./secretSnapshotController";
|
|
||||||
import * as organizationsController from "./organizationsController";
|
|
||||||
import * as ssoController from "./ssoController";
|
|
||||||
import * as usersController from "./usersController";
|
|
||||||
import * as workspaceController from "./workspaceController";
|
|
||||||
import * as membershipController from "./membershipController";
|
|
||||||
import * as cloudProductsController from "./cloudProductsController";
|
|
||||||
import * as roleController from "./roleController";
|
|
||||||
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
|
|
||||||
import * as secretApprovalRequestController from "./secretApprovalRequestsController";
|
|
||||||
import * as secretRotationProviderController from "./secretRotationProviderController";
|
|
||||||
import * as secretRotationController from "./secretRotationController";
|
|
||||||
|
|
||||||
export {
|
|
||||||
identitiesController,
|
|
||||||
secretController,
|
|
||||||
secretSnapshotController,
|
|
||||||
organizationsController,
|
|
||||||
ssoController,
|
|
||||||
usersController,
|
|
||||||
workspaceController,
|
|
||||||
membershipController,
|
|
||||||
cloudProductsController,
|
|
||||||
roleController,
|
|
||||||
secretApprovalPolicyController,
|
|
||||||
secretApprovalRequestController,
|
|
||||||
secretRotationProviderController,
|
|
||||||
secretRotationController
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { IUser, Membership, Workspace } from "../../../models";
|
|
||||||
import { EventType } from "../../../ee/models";
|
|
||||||
import { IMembershipPermission } from "../../../models/membership";
|
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
|
||||||
import { ADMIN, MEMBER } from "../../../variables/organization";
|
|
||||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../../variables";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { EEAuditLogService } from "../../services";
|
|
||||||
|
|
||||||
export const denyMembershipPermissions = async (req: Request, res: Response) => {
|
|
||||||
const { membershipId } = req.params;
|
|
||||||
const { permissions } = req.body;
|
|
||||||
const sanitizedMembershipPermissions: IMembershipPermission[] = permissions.map((permission: IMembershipPermission) => {
|
|
||||||
if (!permission.ability || !permission.environmentSlug || ![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" })
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
environmentSlug: permission.environmentSlug,
|
|
||||||
ability: permission.ability
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const sanitizedMembershipPermissionsUnique = _.uniqWith(sanitizedMembershipPermissions, _.isEqual)
|
|
||||||
|
|
||||||
const membershipToModify = await Membership.findById(membershipId)
|
|
||||||
if (!membershipToModify) {
|
|
||||||
throw BadRequestError({ message: "Unable to locate resource" })
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the user making the request is a admin of this project
|
|
||||||
if (![ADMIN, MEMBER].includes(membershipToModify.role)) {
|
|
||||||
throw UnauthorizedRequestError()
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if the requested slugs are indeed a part of this related workspace
|
|
||||||
const relatedWorkspace = await Workspace.findById(membershipToModify.workspace)
|
|
||||||
if (!relatedWorkspace) {
|
|
||||||
throw BadRequestError({ message: "Something went wrong when locating the related workspace" })
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueEnvironmentSlugs = new Set(_.uniq(_.map(relatedWorkspace.environments, "slug")));
|
|
||||||
|
|
||||||
sanitizedMembershipPermissionsUnique.forEach(permission => {
|
|
||||||
if (!uniqueEnvironmentSlugs.has(permission.environmentSlug)) {
|
|
||||||
throw BadRequestError({ message: "Unknown environment slug reference" })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// update the permissions
|
|
||||||
const updatedMembershipWithPermissions = await Membership.findByIdAndUpdate(
|
|
||||||
{ _id: membershipToModify._id },
|
|
||||||
{ $set: { deniedPermissions: sanitizedMembershipPermissionsUnique } },
|
|
||||||
{ new: true }
|
|
||||||
).populate<{ user: IUser }>("user");
|
|
||||||
|
|
||||||
if (!updatedMembershipWithPermissions) {
|
|
||||||
throw BadRequestError({ message: "The resource has been removed before it can be modified" })
|
|
||||||
}
|
|
||||||
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS,
|
|
||||||
metadata: {
|
|
||||||
userId: updatedMembershipWithPermissions.user._id.toString(),
|
|
||||||
email: updatedMembershipWithPermissions.user.email,
|
|
||||||
deniedPermissions: updatedMembershipWithPermissions.deniedPermissions.map(({
|
|
||||||
environmentSlug,
|
|
||||||
ability
|
|
||||||
}) => ({
|
|
||||||
environmentSlug,
|
|
||||||
ability
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: updatedMembershipWithPermissions.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
res.send({
|
|
||||||
permissionsDenied: updatedMembershipWithPermissions.deniedPermissions,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,550 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { getLicenseServerUrl } from "../../../config";
|
|
||||||
import { licenseServerKeyRequest } from "../../../config/request";
|
|
||||||
import { EELicenseService } from "../../services";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../../validation/organization";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
getAuthDataOrgPermissions,
|
|
||||||
} from "../../services/RoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { Organization } from "../../../models";
|
|
||||||
import { OrganizationNotFoundError } from "../../../utils/errors";
|
|
||||||
|
|
||||||
export const getOrganizationPlansTable = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { billingCycle },
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgPlansTablev1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the organization current plan's feature set
|
|
||||||
*/
|
|
||||||
export const getOrganizationPlan = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceId },
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgPlanv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(
|
|
||||||
new Types.ObjectId(organizationId),
|
|
||||||
new Types.ObjectId(workspaceId)
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
plan
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return checkout url for pro trial
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const startOrganizationTrial = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId },
|
|
||||||
body: { success_url }
|
|
||||||
} = await validateRequest(reqValidator.StartOrgTrailv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { url }
|
|
||||||
} = await licenseServerKeyRequest.post(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/session/trial`,
|
|
||||||
{
|
|
||||||
success_url
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
EELicenseService.delPlan(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
url
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the organization's current plan's billing info
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationPlanBillingInfo = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgPlanBillingInfov1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/cloud-plan/billing`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the organization's current plan's feature table
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationPlanTable = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgPlanTablev1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/cloud-plan/table`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getOrganizationBillingDetails = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgBillingDetailsv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateOrganizationBillingDetails = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId },
|
|
||||||
body: { name, email }
|
|
||||||
} = await validateRequest(reqValidator.UpdateOrgBillingDetailsv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.patch(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details`,
|
|
||||||
{
|
|
||||||
...(name ? { name } : {}),
|
|
||||||
...(email ? { email } : {})
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the organization's payment methods on file
|
|
||||||
*/
|
|
||||||
export const getOrganizationPmtMethods = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgPmtMethodsv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { pmtMethods }
|
|
||||||
} = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/payment-methods`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(pmtMethods);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return URL to add payment method for organization
|
|
||||||
*/
|
|
||||||
export const addOrganizationPmtMethod = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId },
|
|
||||||
body: { success_url, cancel_url }
|
|
||||||
} = await validateRequest(reqValidator.CreateOrgPmtMethodv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { url }
|
|
||||||
} = await licenseServerKeyRequest.post(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/payment-methods`,
|
|
||||||
{
|
|
||||||
success_url,
|
|
||||||
cancel_url
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
url
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete payment method with id [pmtMethodId] for organization
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteOrganizationPmtMethod = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId, pmtMethodId }
|
|
||||||
} = await validateRequest(reqValidator.DelOrgPmtMethodv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Delete,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.delete(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/payment-methods/${pmtMethodId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the organization's tax ids on file
|
|
||||||
*/
|
|
||||||
export const getOrganizationTaxIds = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgTaxIdsv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { tax_ids }
|
|
||||||
} = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/tax-ids`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(tax_ids);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add tax id to organization
|
|
||||||
*/
|
|
||||||
export const addOrganizationTaxId = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId },
|
|
||||||
body: { type, value }
|
|
||||||
} = await validateRequest(reqValidator.CreateOrgTaxId, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.post(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/tax-ids`,
|
|
||||||
{
|
|
||||||
type,
|
|
||||||
value
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete tax id with id [taxId] from organization tax ids on file
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const deleteOrganizationTaxId = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId, taxId }
|
|
||||||
} = await validateRequest(reqValidator.DelOrgTaxIdv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Delete,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const { data } = await licenseServerKeyRequest.delete(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/billing-details/tax-ids/${taxId}`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return organization's invoices on file
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationInvoices = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgInvoicesv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { invoices }
|
|
||||||
} = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/invoices`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(invoices);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return organization's licenses on file
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getOrganizationLicenses = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetOrgLicencesv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Billing
|
|
||||||
);
|
|
||||||
|
|
||||||
const organization = await Organization.findById(organizationId);
|
|
||||||
if (!organization) {
|
|
||||||
throw OrganizationNotFoundError({
|
|
||||||
message: "Failed to find organization"
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
data: { licenses }
|
|
||||||
} = await licenseServerKeyRequest.get(
|
|
||||||
`${await getLicenseServerUrl()}/api/license-server/v1/customers/${
|
|
||||||
organization.customerId
|
|
||||||
}/licenses`
|
|
||||||
);
|
|
||||||
|
|
||||||
return res.status(200).send(licenses);
|
|
||||||
};
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { Membership, User } from "../../../models";
|
|
||||||
import {
|
|
||||||
CreateRoleSchema,
|
|
||||||
DeleteRoleSchema,
|
|
||||||
GetRoleSchema,
|
|
||||||
GetUserPermission,
|
|
||||||
GetUserProjectPermission,
|
|
||||||
UpdateRoleSchema
|
|
||||||
} from "../../validation/role";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
adminProjectPermissions,
|
|
||||||
getAuthDataProjectPermissions,
|
|
||||||
memberProjectPermissions,
|
|
||||||
noAccessProjectPermissions,
|
|
||||||
viewerProjectPermission
|
|
||||||
} from "../../services/ProjectRoleService";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
adminPermissions,
|
|
||||||
getAuthDataOrgPermissions,
|
|
||||||
getUserOrgPermissions,
|
|
||||||
memberPermissions,
|
|
||||||
noAccessPermissions
|
|
||||||
} from "../../services/RoleService";
|
|
||||||
import { BadRequestError } from "../../../utils/errors";
|
|
||||||
import { Role } from "../../models";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import { packRules } from "@casl/ability/extra";
|
|
||||||
|
|
||||||
export const createRole = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { workspaceId, name, description, slug, permissions, orgId }
|
|
||||||
} = await validateRequest(CreateRoleSchema, req);
|
|
||||||
|
|
||||||
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
|
|
||||||
if (isOrgRole) {
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(orgId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (permission.cannot(OrgPermissionActions.Create, OrgPermissionSubjects.Role)) {
|
|
||||||
throw BadRequestError({ message: "user doesn't have the permission." });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
if (permission.cannot(ProjectPermissionActions.Create, ProjectPermissionSub.Role)) {
|
|
||||||
throw BadRequestError({ message: "User doesn't have the permission." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const existingRole = await Role.findOne({ organization: orgId, workspace: workspaceId, slug });
|
|
||||||
if (existingRole) {
|
|
||||||
throw BadRequestError({ message: "Role already exist" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const role = new Role({
|
|
||||||
organization: orgId,
|
|
||||||
workspace: workspaceId,
|
|
||||||
isOrgRole,
|
|
||||||
name,
|
|
||||||
slug,
|
|
||||||
permissions,
|
|
||||||
description
|
|
||||||
});
|
|
||||||
await role.save();
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
message: "Successfully created role",
|
|
||||||
data: {
|
|
||||||
role
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateRole = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { id },
|
|
||||||
body: { name, description, slug, permissions, workspaceId, orgId }
|
|
||||||
} = await validateRequest(UpdateRoleSchema, req);
|
|
||||||
const isOrgRole = !workspaceId; // if workspaceid is provided then its a workspace rule
|
|
||||||
|
|
||||||
if (isOrgRole) {
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(orgId)
|
|
||||||
});
|
|
||||||
if (permission.cannot(OrgPermissionActions.Edit, OrgPermissionSubjects.Role)) {
|
|
||||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (permission.cannot(ProjectPermissionActions.Edit, ProjectPermissionSub.Role)) {
|
|
||||||
throw BadRequestError({ message: "User doesn't have the workspace permission." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (slug) {
|
|
||||||
const existingRole = await Role.findOne({
|
|
||||||
organization: orgId,
|
|
||||||
slug,
|
|
||||||
isOrgRole,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
if (existingRole && existingRole.id !== id) {
|
|
||||||
throw BadRequestError({ message: "Role already exist" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const role = await Role.findByIdAndUpdate(
|
|
||||||
id,
|
|
||||||
{ name, description, slug, permissions },
|
|
||||||
{ returnDocument: "after" }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!role) {
|
|
||||||
throw BadRequestError({ message: "Role not found" });
|
|
||||||
}
|
|
||||||
res.status(200).json({
|
|
||||||
message: "Successfully updated role",
|
|
||||||
data: {
|
|
||||||
role
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteRole = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(DeleteRoleSchema, req);
|
|
||||||
|
|
||||||
const role = await Role.findById(id);
|
|
||||||
if (!role) {
|
|
||||||
throw BadRequestError({ message: "Role not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const isOrgRole = !role.workspace;
|
|
||||||
if (isOrgRole) {
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: role.organization
|
|
||||||
});
|
|
||||||
if (permission.cannot(OrgPermissionActions.Delete, OrgPermissionSubjects.Role)) {
|
|
||||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: role.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (permission.cannot(ProjectPermissionActions.Delete, ProjectPermissionSub.Role)) {
|
|
||||||
throw BadRequestError({ message: "User doesn't have the workspace permission." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await Role.findByIdAndDelete(role.id);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
message: "Successfully deleted role",
|
|
||||||
data: {
|
|
||||||
role
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getRoles = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceId, orgId }
|
|
||||||
} = await validateRequest(GetRoleSchema, req);
|
|
||||||
|
|
||||||
const isOrgRole = !workspaceId;
|
|
||||||
if (isOrgRole) {
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(orgId)
|
|
||||||
});
|
|
||||||
if (permission.cannot(OrgPermissionActions.Read, OrgPermissionSubjects.Role)) {
|
|
||||||
throw BadRequestError({ message: "User doesn't have the org permission." });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (permission.cannot(ProjectPermissionActions.Read, ProjectPermissionSub.Role)) {
|
|
||||||
throw BadRequestError({ message: "User doesn't have the workspace permission." });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const customRoles = await Role.find({ organization: orgId, isOrgRole, workspace: workspaceId });
|
|
||||||
// as this is shared between org and workspace switch the rule set based on it
|
|
||||||
const roles = [
|
|
||||||
{
|
|
||||||
_id: "admin",
|
|
||||||
name: "Admin",
|
|
||||||
slug: "admin",
|
|
||||||
description: "Complete administration access over the organization",
|
|
||||||
permissions: isOrgRole ? adminPermissions.rules : adminProjectPermissions.rules
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "no-access",
|
|
||||||
name: "No Access",
|
|
||||||
slug: "no-access",
|
|
||||||
description: "No access to any resources in the organization",
|
|
||||||
permissions: isOrgRole ? noAccessPermissions.rules : noAccessProjectPermissions.rules
|
|
||||||
},
|
|
||||||
{
|
|
||||||
_id: "member",
|
|
||||||
name: isOrgRole ? "Member" : "Developer",
|
|
||||||
slug: "member",
|
|
||||||
description: "Non-administrative role in an organization",
|
|
||||||
permissions: isOrgRole ? memberPermissions.rules : memberProjectPermissions.rules
|
|
||||||
},
|
|
||||||
// viewer role only for project level
|
|
||||||
...(isOrgRole
|
|
||||||
? []
|
|
||||||
: [
|
|
||||||
{
|
|
||||||
_id: "viewer",
|
|
||||||
name: "Viewer",
|
|
||||||
slug: "viewer",
|
|
||||||
description: "Non-administrative role in an organization",
|
|
||||||
permissions: viewerProjectPermission.rules
|
|
||||||
}
|
|
||||||
]),
|
|
||||||
...customRoles
|
|
||||||
];
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
message: "Successfully fetched role list",
|
|
||||||
data: {
|
|
||||||
roles
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserPermissions = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { orgId }
|
|
||||||
} = await validateRequest(GetUserPermission, req);
|
|
||||||
|
|
||||||
const { permission, membership } = await getUserOrgPermissions(req.user._id, orgId);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
data: {
|
|
||||||
permissions: packRules(permission.rules),
|
|
||||||
membership
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getUserWorkspacePermissions = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(GetUserProjectPermission, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
let membership;
|
|
||||||
if (req.authData.authPayload instanceof User) {
|
|
||||||
membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
data: {
|
|
||||||
permissions: packRules(permission.rules),
|
|
||||||
membership
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import { ForbiddenError, subject } from "@casl/ability";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { nanoid } from "nanoid";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../services/ProjectRoleService";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
|
||||||
import { getSecretPolicyOfBoard } from "../../services/SecretApprovalService";
|
|
||||||
import { BadRequestError } from "../../../utils/errors";
|
|
||||||
import * as reqValidator from "../../validation/secretApproval";
|
|
||||||
|
|
||||||
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
|
|
||||||
|
|
||||||
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { approvals, secretPath, approvers, environment, workspaceId, name }
|
|
||||||
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.SecretApproval
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretApproval = new SecretApprovalPolicy({
|
|
||||||
workspace: workspaceId,
|
|
||||||
name: name ?? `${environment}-${nanoid(3)}`,
|
|
||||||
secretPath,
|
|
||||||
environment,
|
|
||||||
approvals,
|
|
||||||
approvers
|
|
||||||
});
|
|
||||||
await secretApproval.save();
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
approval: secretApproval
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { approvals, approvers, secretPath, name },
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
|
|
||||||
|
|
||||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
|
||||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: secretApproval.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.SecretApproval
|
|
||||||
);
|
|
||||||
|
|
||||||
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
|
|
||||||
approvals,
|
|
||||||
approvers,
|
|
||||||
name: (name || secretApproval?.name) ?? `${secretApproval.environment}-${nanoid(3)}`,
|
|
||||||
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
approval: updatedDoc
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.DeleteSecretApprovalRule, req);
|
|
||||||
|
|
||||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
|
||||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: secretApproval.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.SecretApproval
|
|
||||||
);
|
|
||||||
|
|
||||||
const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id);
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
approval: deletedDoc
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.SecretApproval
|
|
||||||
);
|
|
||||||
|
|
||||||
const doc = await SecretApprovalPolicy.find({ workspace: workspaceId });
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
approvals: doc
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSecretApprovalPolicyOfBoard = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceId, environment, secretPath }
|
|
||||||
} = await validateRequest(reqValidator.GetSecretApprovalPolicyOfABoard, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { secretPath, environment })
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretApprovalPolicy = await getSecretPolicyOfBoard(workspaceId, environment, secretPath);
|
|
||||||
return res.send({ policy: secretApprovalPolicy });
|
|
||||||
};
|
|
||||||
@@ -1,366 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import { Folder, Membership, User } from "../../../models";
|
|
||||||
import { ApprovalStatus, SecretApprovalRequest } from "../../models/secretApprovalRequest";
|
|
||||||
import * as reqValidator from "../../validation/secretApprovalRequest";
|
|
||||||
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../../../utils/errors";
|
|
||||||
import { ISecretApprovalPolicy, SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
|
||||||
import { performSecretApprovalRequestMerge } from "../../services/SecretApprovalService";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { EEAuditLogService } from "../../services";
|
|
||||||
import { EventType } from "../../models";
|
|
||||||
|
|
||||||
export const getSecretApprovalRequestCount = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.getSecretApprovalRequestCount, req);
|
|
||||||
|
|
||||||
if (!(req.authData.authPayload instanceof User)) return;
|
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
const approvalRequestCount = await SecretApprovalRequest.aggregate([
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: SecretApprovalPolicy.collection.name,
|
|
||||||
localField: "policy",
|
|
||||||
foreignField: "_id",
|
|
||||||
as: "policy"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ $unwind: "$policy" },
|
|
||||||
...(membership.role !== "admin"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
$or: [
|
|
||||||
{ committer: new Types.ObjectId(membership.id) },
|
|
||||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: "$status",
|
|
||||||
count: { $sum: 1 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]);
|
|
||||||
const openRequests = approvalRequestCount.find(({ _id }) => _id === "open");
|
|
||||||
const closedRequests = approvalRequestCount.find(({ _id }) => _id === "close");
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
approvals: { open: openRequests?.count || 0, closed: closedRequests?.count || 0 }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSecretApprovalRequests = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { status, committer, workspaceId, environment, limit, offset }
|
|
||||||
} = await validateRequest(reqValidator.getSecretApprovalRequests, req);
|
|
||||||
|
|
||||||
if (!(req.authData.authPayload instanceof User)) return;
|
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
const query = {
|
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
|
||||||
environment,
|
|
||||||
committer: committer ? new Types.ObjectId(committer) : undefined,
|
|
||||||
status
|
|
||||||
};
|
|
||||||
// to strip of undefined in query we use es6 spread to ignore those fields
|
|
||||||
Object.entries(query).forEach(
|
|
||||||
([key, value]) => value === undefined && delete query[key as keyof typeof query]
|
|
||||||
);
|
|
||||||
const approvalRequests = await SecretApprovalRequest.aggregate([
|
|
||||||
{
|
|
||||||
$match: query
|
|
||||||
},
|
|
||||||
{ $sort: { createdAt: -1 } },
|
|
||||||
{
|
|
||||||
$lookup: {
|
|
||||||
from: SecretApprovalPolicy.collection.name,
|
|
||||||
localField: "policy",
|
|
||||||
foreignField: "_id",
|
|
||||||
as: "policy"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{ $unwind: "$policy" },
|
|
||||||
...(membership.role !== "admin"
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
$or: [
|
|
||||||
{ committer: new Types.ObjectId(membership.id) },
|
|
||||||
{ "policy.approvers": new Types.ObjectId(membership.id) }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
: []),
|
|
||||||
{ $skip: offset },
|
|
||||||
{ $limit: limit }
|
|
||||||
]);
|
|
||||||
if (!approvalRequests.length) return res.send({ approvals: [] });
|
|
||||||
|
|
||||||
const unqiueEnvs = environment ?? {
|
|
||||||
$in: [...new Set(approvalRequests.map(({ environment }) => environment))]
|
|
||||||
};
|
|
||||||
const approvalRootFolders = await Folder.find({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: unqiueEnvs
|
|
||||||
}).lean();
|
|
||||||
|
|
||||||
const formatedApprovals = approvalRequests.map((el) => {
|
|
||||||
let secretPath = "/";
|
|
||||||
const folders = approvalRootFolders.find(({ environment }) => environment === el.environment);
|
|
||||||
if (folders) {
|
|
||||||
secretPath = getFolderWithPathFromId(folders?.nodes, el.folderId)?.folderPath || "/";
|
|
||||||
}
|
|
||||||
return { ...el, secretPath };
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
approvals: formatedApprovals
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSecretApprovalRequestDetails = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.getSecretApprovalRequestDetails, req);
|
|
||||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id)
|
|
||||||
.populate<{ policy: ISecretApprovalPolicy }>("policy")
|
|
||||||
.populate({
|
|
||||||
path: "commits.secretVersion",
|
|
||||||
populate: {
|
|
||||||
path: "tags"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.populate("commits.secret", "version")
|
|
||||||
.populate("commits.newVersion.tags")
|
|
||||||
.lean();
|
|
||||||
if (!secretApprovalRequest)
|
|
||||||
throw BadRequestError({ message: "Secret approval request not found" });
|
|
||||||
|
|
||||||
if (!(req.authData.authPayload instanceof User)) return;
|
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: secretApprovalRequest.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
// allow to fetch only if its admin or is the committer or approver
|
|
||||||
if (
|
|
||||||
membership.role !== "admin" &&
|
|
||||||
!secretApprovalRequest.committer.equals(membership.id) &&
|
|
||||||
!secretApprovalRequest.policy.approvers.find(
|
|
||||||
(approverId) => approverId.toString() === membership._id.toString()
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
|
||||||
}
|
|
||||||
|
|
||||||
let secretPath = "/";
|
|
||||||
const approvalRootFolders = await Folder.findOne({
|
|
||||||
workspace: secretApprovalRequest.workspace,
|
|
||||||
environment: secretApprovalRequest.environment
|
|
||||||
}).lean();
|
|
||||||
if (approvalRootFolders) {
|
|
||||||
secretPath =
|
|
||||||
getFolderWithPathFromId(approvalRootFolders?.nodes, secretApprovalRequest.folderId)
|
|
||||||
?.folderPath || "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.send({
|
|
||||||
approval: { ...secretApprovalRequest, secretPath }
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSecretApprovalReviewStatus = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { status },
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.updateSecretApprovalReviewStatus, req);
|
|
||||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
|
||||||
policy: ISecretApprovalPolicy;
|
|
||||||
}>("policy");
|
|
||||||
if (!secretApprovalRequest)
|
|
||||||
throw BadRequestError({ message: "Secret approval request not found" });
|
|
||||||
|
|
||||||
if (!(req.authData.authPayload instanceof User)) return;
|
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: secretApprovalRequest.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
if (
|
|
||||||
membership.role !== "admin" &&
|
|
||||||
secretApprovalRequest.committer !== membership.id &&
|
|
||||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const reviewerPos = secretApprovalRequest.reviewers.findIndex(
|
|
||||||
({ member }) => member.toString() === membership._id.toString()
|
|
||||||
);
|
|
||||||
if (reviewerPos !== -1) {
|
|
||||||
secretApprovalRequest.reviewers[reviewerPos].status = status;
|
|
||||||
} else {
|
|
||||||
secretApprovalRequest.reviewers.push({ member: membership._id, status });
|
|
||||||
}
|
|
||||||
await secretApprovalRequest.save();
|
|
||||||
|
|
||||||
return res.send({ status });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const mergeSecretApprovalRequest = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.mergeSecretApprovalRequest, req);
|
|
||||||
|
|
||||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
|
||||||
policy: ISecretApprovalPolicy;
|
|
||||||
}>("policy");
|
|
||||||
|
|
||||||
if (!secretApprovalRequest)
|
|
||||||
throw BadRequestError({ message: "Secret approval request not found" });
|
|
||||||
|
|
||||||
if (!(req.authData.authPayload instanceof User)) return;
|
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: secretApprovalRequest.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
if (
|
|
||||||
membership.role !== "admin" &&
|
|
||||||
secretApprovalRequest.committer !== membership.id &&
|
|
||||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership.id))
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const reviewers = secretApprovalRequest.reviewers.reduce<Record<string, ApprovalStatus>>(
|
|
||||||
(prev, curr) => ({ ...prev, [curr.member.toString()]: curr.status }),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
const hasMinApproval =
|
|
||||||
secretApprovalRequest.policy.approvals <=
|
|
||||||
secretApprovalRequest.policy.approvers.filter(
|
|
||||||
(approverId) => reviewers[approverId.toString()] === ApprovalStatus.APPROVED
|
|
||||||
).length;
|
|
||||||
|
|
||||||
if (!hasMinApproval) throw BadRequestError({ message: "Doesn't have minimum approvals needed" });
|
|
||||||
|
|
||||||
const approval = await performSecretApprovalRequestMerge(
|
|
||||||
id,
|
|
||||||
req.authData,
|
|
||||||
membership._id.toString()
|
|
||||||
);
|
|
||||||
return res.send({ approval });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const updateSecretApprovalRequestStatus = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { status },
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.updateSecretApprovalRequestStatus, req);
|
|
||||||
|
|
||||||
const secretApprovalRequest = await SecretApprovalRequest.findById(id).populate<{
|
|
||||||
policy: ISecretApprovalPolicy;
|
|
||||||
}>("policy");
|
|
||||||
|
|
||||||
if (!secretApprovalRequest)
|
|
||||||
throw BadRequestError({ message: "Secret approval request not found" });
|
|
||||||
|
|
||||||
if (!(req.authData.authPayload instanceof User)) return;
|
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
|
||||||
user: req.authData.authPayload._id,
|
|
||||||
workspace: secretApprovalRequest.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!membership) throw UnauthorizedRequestError();
|
|
||||||
|
|
||||||
if (
|
|
||||||
membership.role !== "admin" &&
|
|
||||||
secretApprovalRequest.committer !== membership.id &&
|
|
||||||
!secretApprovalRequest.policy.approvers.find((approverId) => approverId.equals(membership._id))
|
|
||||||
) {
|
|
||||||
throw UnauthorizedRequestError({ message: "User has no access" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (secretApprovalRequest.hasMerged)
|
|
||||||
throw BadRequestError({ message: "Approval request has been merged" });
|
|
||||||
if (secretApprovalRequest.status === "close" && status === "close")
|
|
||||||
throw BadRequestError({ message: "Approval request is already closed" });
|
|
||||||
if (secretApprovalRequest.status === "open" && status === "open")
|
|
||||||
throw BadRequestError({ message: "Approval request is already open" });
|
|
||||||
|
|
||||||
const updatedRequest = await SecretApprovalRequest.findByIdAndUpdate(
|
|
||||||
id,
|
|
||||||
{ status, statusChangeBy: membership._id },
|
|
||||||
{ new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status === "close") {
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.SECRET_APPROVAL_CLOSED,
|
|
||||||
metadata: {
|
|
||||||
closedBy: membership._id.toString(),
|
|
||||||
secretApprovalRequestId: id,
|
|
||||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: secretApprovalRequest.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await EEAuditLogService.createAuditLog(
|
|
||||||
req.authData,
|
|
||||||
{
|
|
||||||
type: EventType.SECRET_APPROVAL_REOPENED,
|
|
||||||
metadata: {
|
|
||||||
reopenedBy: membership._id.toString(),
|
|
||||||
secretApprovalRequestId: id,
|
|
||||||
secretApprovalRequestSlug: secretApprovalRequest.slug
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
workspaceId: secretApprovalRequest.workspace
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return res.send({ approval: updatedRequest });
|
|
||||||
};
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
import { ForbiddenError, subject } from "@casl/ability";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import { Folder, Secret } from "../../../models";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../services/ProjectRoleService";
|
|
||||||
import { BadRequestError } from "../../../utils/errors";
|
|
||||||
import * as reqValidator from "../../../validation";
|
|
||||||
import { SecretVersion } from "../../models";
|
|
||||||
import { EESecretService } from "../../services";
|
|
||||||
import { getFolderWithPathFromId } from "../../../services/FolderService";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return secret versions for secret with id [secretId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const getSecretVersions = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Return secret versions'
|
|
||||||
#swagger.description = 'Return secret versions'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['secretId'] = {
|
|
||||||
"description": "ID of secret",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['offset'] = {
|
|
||||||
"description": "Number of versions to skip",
|
|
||||||
"required": false,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.parameters['limit'] = {
|
|
||||||
"description": "Maximum number of versions to return",
|
|
||||||
"required": false,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"secretVersions": {
|
|
||||||
"type": "array",
|
|
||||||
"items": {
|
|
||||||
$ref: "#/components/schemas/SecretVersion"
|
|
||||||
},
|
|
||||||
"description": "Secret versions"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
const {
|
|
||||||
params: { secretId },
|
|
||||||
query: { offset, limit }
|
|
||||||
} = await validateRequest(reqValidator.GetSecretVersionsV1, req);
|
|
||||||
|
|
||||||
const secret = await Secret.findById(secretId);
|
|
||||||
if (!secret) {
|
|
||||||
throw BadRequestError({ message: "Failed to find secret" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: secret.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.SecretRollback
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretVersions = await SecretVersion.find({
|
|
||||||
secret: secretId
|
|
||||||
})
|
|
||||||
.sort({ createdAt: -1 })
|
|
||||||
.skip(offset)
|
|
||||||
.limit(limit);
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secretVersions
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Roll back secret with id [secretId] to version [version]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|
||||||
/*
|
|
||||||
#swagger.summary = 'Roll back secret to a version.'
|
|
||||||
#swagger.description = 'Roll back secret to a version.'
|
|
||||||
|
|
||||||
#swagger.security = [{
|
|
||||||
"apiKeyAuth": []
|
|
||||||
}]
|
|
||||||
|
|
||||||
#swagger.parameters['secretId'] = {
|
|
||||||
"description": "ID of secret",
|
|
||||||
"required": true,
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.requestBody = {
|
|
||||||
"required": true,
|
|
||||||
"content": {
|
|
||||||
"application/json": {
|
|
||||||
"schema": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"version": {
|
|
||||||
"type": "integer",
|
|
||||||
"description": "Version of secret to roll back to"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#swagger.responses[200] = {
|
|
||||||
content: {
|
|
||||||
"application/json": {
|
|
||||||
schema: {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"secret": {
|
|
||||||
"type": "object",
|
|
||||||
$ref: "#/components/schemas/Secret",
|
|
||||||
"description": "Secret rolled back to"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
const {
|
|
||||||
params: { secretId },
|
|
||||||
body: { version }
|
|
||||||
} = await validateRequest(reqValidator.RollbackSecretVersionV1, req);
|
|
||||||
|
|
||||||
const toBeUpdatedSec = await Secret.findById(secretId);
|
|
||||||
if (!toBeUpdatedSec) {
|
|
||||||
throw BadRequestError({ message: "Failed to find secret" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: toBeUpdatedSec.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.SecretRollback
|
|
||||||
);
|
|
||||||
|
|
||||||
// validate secret version
|
|
||||||
const oldSecretVersion = await SecretVersion.findOne({
|
|
||||||
secret: secretId,
|
|
||||||
version
|
|
||||||
}).select("+secretBlindIndex");
|
|
||||||
|
|
||||||
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
|
||||||
|
|
||||||
const {
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
secretBlindIndex,
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
algorithm,
|
|
||||||
folder,
|
|
||||||
keyEncoding
|
|
||||||
} = oldSecretVersion;
|
|
||||||
|
|
||||||
let secretPath = "/";
|
|
||||||
const folders = await Folder.findOne({ workspace, environment });
|
|
||||||
if (folders)
|
|
||||||
secretPath = getFolderWithPathFromId(folders.nodes, folder || "root")?.folderPath || "/";
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
subject(ProjectPermissionSub.Secrets, { environment: toBeUpdatedSec.environment, secretPath })
|
|
||||||
);
|
|
||||||
|
|
||||||
// update secret
|
|
||||||
const secret = await Secret.findByIdAndUpdate(
|
|
||||||
secretId,
|
|
||||||
{
|
|
||||||
$inc: {
|
|
||||||
version: 1
|
|
||||||
},
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
folderId: folder,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!secret) throw new Error("Failed to find and update secret");
|
|
||||||
|
|
||||||
// add new secret version
|
|
||||||
await new SecretVersion({
|
|
||||||
secret: secretId,
|
|
||||||
version: secret.version,
|
|
||||||
workspace,
|
|
||||||
type,
|
|
||||||
user,
|
|
||||||
environment,
|
|
||||||
isDeleted: false,
|
|
||||||
...(secretBlindIndex ? { secretBlindIndex } : {}),
|
|
||||||
secretKeyCiphertext,
|
|
||||||
secretKeyIV,
|
|
||||||
secretKeyTag,
|
|
||||||
secretValueCiphertext,
|
|
||||||
secretValueIV,
|
|
||||||
secretValueTag,
|
|
||||||
folder,
|
|
||||||
algorithm,
|
|
||||||
keyEncoding
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
// take secret snapshot
|
|
||||||
await EESecretService.takeSecretSnapshot({
|
|
||||||
workspaceId: secret.workspace,
|
|
||||||
environment,
|
|
||||||
folderId: folder
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secret
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,110 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/secretRotation";
|
|
||||||
import * as secretRotationService from "../../secretRotation/service";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
export const createSecretRotation = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
provider,
|
|
||||||
customProvider,
|
|
||||||
interval,
|
|
||||||
outputs,
|
|
||||||
secretPath,
|
|
||||||
environment,
|
|
||||||
workspaceId,
|
|
||||||
inputs
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.createSecretRotationV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Create,
|
|
||||||
ProjectPermissionSub.SecretRotation
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretRotation = await secretRotationService.createSecretRotation({
|
|
||||||
workspaceId,
|
|
||||||
inputs,
|
|
||||||
environment,
|
|
||||||
secretPath,
|
|
||||||
outputs,
|
|
||||||
interval,
|
|
||||||
customProvider,
|
|
||||||
provider
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send({ secretRotation });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const restartSecretRotations = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { id }
|
|
||||||
} = await validateRequest(reqValidator.restartSecretRotationV1, req);
|
|
||||||
|
|
||||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: doc.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Edit,
|
|
||||||
ProjectPermissionSub.SecretRotation
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretRotation = await secretRotationService.restartSecretRotation({ id });
|
|
||||||
return res.send({ secretRotation });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteSecretRotations = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { id }
|
|
||||||
} = await validateRequest(reqValidator.removeSecretRotationV1, req);
|
|
||||||
|
|
||||||
const doc = await secretRotationService.getSecretRotationById({ id });
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: doc.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Delete,
|
|
||||||
ProjectPermissionSub.SecretRotation
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretRotations = await secretRotationService.deleteSecretRotation({ id });
|
|
||||||
return res.send({ secretRotations });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getSecretRotations = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.getSecretRotationV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.SecretRotation
|
|
||||||
);
|
|
||||||
|
|
||||||
const secretRotations = await secretRotationService.getSecretRotationOfWorkspace(workspaceId);
|
|
||||||
return res.send({ secretRotations });
|
|
||||||
};
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import * as reqValidator from "../../validation/secretRotationProvider";
|
|
||||||
import * as secretRotationProviderService from "../../secretRotation/service";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../services/ProjectRoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
export const getProviderTemplates = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { workspaceId }
|
|
||||||
} = await validateRequest(reqValidator.getSecretRotationProvidersV1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.SecretRotation
|
|
||||||
);
|
|
||||||
|
|
||||||
const rotationProviderList = await secretRotationProviderService.getProviderTemplate({
|
|
||||||
workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.send(rotationProviderList);
|
|
||||||
};
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
import { Request, Response } from "express";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import {
|
|
||||||
ProjectPermissionActions,
|
|
||||||
ProjectPermissionSub,
|
|
||||||
getAuthDataProjectPermissions
|
|
||||||
} from "../../services/ProjectRoleService";
|
|
||||||
import * as reqValidator from "../../../validation/secretSnapshot";
|
|
||||||
import { ISecretVersion, SecretSnapshot, TFolderRootVersionSchema } from "../../models";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return secret snapshot with id [secretSnapshotId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getSecretSnapshot = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { secretSnapshotId }
|
|
||||||
} = await validateRequest(reqValidator.GetSecretSnapshotV1, req);
|
|
||||||
|
|
||||||
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
|
|
||||||
.lean()
|
|
||||||
.populate<{ secretVersions: ISecretVersion[] }>({
|
|
||||||
path: "secretVersions",
|
|
||||||
populate: {
|
|
||||||
path: "tags",
|
|
||||||
model: "Tag"
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
|
||||||
|
|
||||||
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataProjectPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
workspaceId: secretSnapshot.workspace
|
|
||||||
});
|
|
||||||
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
ProjectPermissionActions.Read,
|
|
||||||
ProjectPermissionSub.SecretRollback
|
|
||||||
);
|
|
||||||
|
|
||||||
const folderId = secretSnapshot.folderId;
|
|
||||||
// to show only the folder required secrets
|
|
||||||
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
|
|
||||||
({ folder }) => folder === folderId
|
|
||||||
);
|
|
||||||
|
|
||||||
secretSnapshot.folderVersion = secretSnapshot?.folderVersion?.nodes?.children?.map(
|
|
||||||
({ id, name }) => ({
|
|
||||||
id,
|
|
||||||
name
|
|
||||||
})
|
|
||||||
) as any;
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
secretSnapshot
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,268 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { BotOrgService } from "../../../services";
|
|
||||||
import { SSOConfig } from "../../models";
|
|
||||||
import { AuthMethod, MembershipOrg, User } from "../../../models";
|
|
||||||
import { getSSOConfigHelper } from "../../helpers/organizations";
|
|
||||||
import { client } from "../../../config";
|
|
||||||
import { ResourceNotFoundError } from "../../../utils/errors";
|
|
||||||
import { getSiteURL } from "../../../config";
|
|
||||||
import { EELicenseService } from "../../services";
|
|
||||||
import * as reqValidator from "../../../validation/sso";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import {
|
|
||||||
OrgPermissionActions,
|
|
||||||
OrgPermissionSubjects,
|
|
||||||
getAuthDataOrgPermissions
|
|
||||||
} from "../../services/RoleService";
|
|
||||||
import { ForbiddenError } from "@casl/ability";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Redirect user to appropriate SSO endpoint after successful authentication
|
|
||||||
* to finish inputting their master key for logging in or signing up
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const redirectSSO = async (req: Request, res: Response) => {
|
|
||||||
if (req.isUserCompleted) {
|
|
||||||
return res.redirect(
|
|
||||||
`${await getSiteURL()}/login/sso?token=${encodeURIComponent(req.providerAuthToken)}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.redirect(
|
|
||||||
`${await getSiteURL()}/signup/sso?token=${encodeURIComponent(req.providerAuthToken)}`
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return organization SAML SSO configuration
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getSSOConfig = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
query: { organizationId }
|
|
||||||
} = await validateRequest(reqValidator.GetSsoConfigv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Read,
|
|
||||||
OrgPermissionSubjects.Sso
|
|
||||||
);
|
|
||||||
|
|
||||||
const data = await getSSOConfigHelper({
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update organization SAML SSO configuration
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const updateSSOConfig = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
|
|
||||||
} = await validateRequest(reqValidator.UpdateSsoConfigv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Edit,
|
|
||||||
OrgPermissionSubjects.Sso
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
if (!plan.samlSSO)
|
|
||||||
return res.status(400).send({
|
|
||||||
message:
|
|
||||||
"Failed to update SAML SSO configuration due to plan restriction. Upgrade plan to update SSO configuration."
|
|
||||||
});
|
|
||||||
|
|
||||||
interface PatchUpdate {
|
|
||||||
authProvider?: string;
|
|
||||||
isActive?: boolean;
|
|
||||||
encryptedEntryPoint?: string;
|
|
||||||
entryPointIV?: string;
|
|
||||||
entryPointTag?: string;
|
|
||||||
encryptedIssuer?: string;
|
|
||||||
issuerIV?: string;
|
|
||||||
issuerTag?: string;
|
|
||||||
encryptedCert?: string;
|
|
||||||
certIV?: string;
|
|
||||||
certTag?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const update: PatchUpdate = {};
|
|
||||||
|
|
||||||
if (authProvider) {
|
|
||||||
update.authProvider = authProvider;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isActive !== undefined) {
|
|
||||||
update.isActive = isActive;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
if (entryPoint) {
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedEntryPoint,
|
|
||||||
iv: entryPointIV,
|
|
||||||
tag: entryPointTag
|
|
||||||
} = client.encryptSymmetric(entryPoint, key);
|
|
||||||
|
|
||||||
update.encryptedEntryPoint = encryptedEntryPoint;
|
|
||||||
update.entryPointIV = entryPointIV;
|
|
||||||
update.entryPointTag = entryPointTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (issuer) {
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedIssuer,
|
|
||||||
iv: issuerIV,
|
|
||||||
tag: issuerTag
|
|
||||||
} = client.encryptSymmetric(issuer, key);
|
|
||||||
|
|
||||||
update.encryptedIssuer = encryptedIssuer;
|
|
||||||
update.issuerIV = issuerIV;
|
|
||||||
update.issuerTag = issuerTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cert) {
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedCert,
|
|
||||||
iv: certIV,
|
|
||||||
tag: certTag
|
|
||||||
} = client.encryptSymmetric(cert, key);
|
|
||||||
|
|
||||||
update.encryptedCert = encryptedCert;
|
|
||||||
update.certIV = certIV;
|
|
||||||
update.certTag = certTag;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ssoConfig = await SSOConfig.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
},
|
|
||||||
update,
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!ssoConfig)
|
|
||||||
throw ResourceNotFoundError({
|
|
||||||
message: "Failed to find SSO config to update"
|
|
||||||
});
|
|
||||||
|
|
||||||
if (update.isActive !== undefined) {
|
|
||||||
const membershipOrgs = await MembershipOrg.find({
|
|
||||||
organization: new Types.ObjectId(organizationId)
|
|
||||||
}).select("user");
|
|
||||||
|
|
||||||
if (update.isActive) {
|
|
||||||
await User.updateMany(
|
|
||||||
{
|
|
||||||
_id: {
|
|
||||||
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
authMethods: [ssoConfig.authProvider]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
await User.updateMany(
|
|
||||||
{
|
|
||||||
_id: {
|
|
||||||
$in: membershipOrgs.map((membershipOrg) => membershipOrg.user)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
authMethods: [AuthMethod.EMAIL]
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).send(ssoConfig);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create organization SAML SSO configuration
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const createSSOConfig = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: { organizationId, authProvider, isActive, entryPoint, issuer, cert }
|
|
||||||
} = await validateRequest(reqValidator.CreateSsoConfigv1, req);
|
|
||||||
|
|
||||||
const { permission } = await getAuthDataOrgPermissions({
|
|
||||||
authData: req.authData,
|
|
||||||
organizationId: new Types.ObjectId(organizationId)
|
|
||||||
});
|
|
||||||
ForbiddenError.from(permission).throwUnlessCan(
|
|
||||||
OrgPermissionActions.Create,
|
|
||||||
OrgPermissionSubjects.Sso
|
|
||||||
);
|
|
||||||
|
|
||||||
const plan = await EELicenseService.getPlan(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
if (!plan.samlSSO)
|
|
||||||
return res.status(400).send({
|
|
||||||
message:
|
|
||||||
"Failed to create SAML SSO configuration due to plan restriction. Upgrade plan to add SSO configuration."
|
|
||||||
});
|
|
||||||
|
|
||||||
const key = await BotOrgService.getSymmetricKey(new Types.ObjectId(organizationId));
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedEntryPoint,
|
|
||||||
iv: entryPointIV,
|
|
||||||
tag: entryPointTag
|
|
||||||
} = client.encryptSymmetric(entryPoint, key);
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedIssuer,
|
|
||||||
iv: issuerIV,
|
|
||||||
tag: issuerTag
|
|
||||||
} = client.encryptSymmetric(issuer, key);
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: encryptedCert,
|
|
||||||
iv: certIV,
|
|
||||||
tag: certTag
|
|
||||||
} = client.encryptSymmetric(cert, key);
|
|
||||||
|
|
||||||
const ssoConfig = await new SSOConfig({
|
|
||||||
organization: new Types.ObjectId(organizationId),
|
|
||||||
authProvider,
|
|
||||||
isActive,
|
|
||||||
encryptedEntryPoint,
|
|
||||||
entryPointIV,
|
|
||||||
entryPointTag,
|
|
||||||
encryptedIssuer,
|
|
||||||
issuerIV,
|
|
||||||
issuerTag,
|
|
||||||
encryptedCert,
|
|
||||||
certIV,
|
|
||||||
certTag
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
return res.status(200).send(ssoConfig);
|
|
||||||
};
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the ip address of the current user
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
export const getMyIp = (req: Request, res: Response) => {
|
|
||||||
return res.status(200).send({
|
|
||||||
ip: req.authData.ipAddress
|
|
||||||
});
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,101 +0,0 @@
|
|||||||
import { Request, Response } from "express";
|
|
||||||
import { Types } from "mongoose";
|
|
||||||
import { APIKeyDataV2 } from "../../../models/apiKeyDataV2";
|
|
||||||
import { validateRequest } from "../../../helpers/validation";
|
|
||||||
import { BadRequestError } from "../../../utils/errors";
|
|
||||||
import * as reqValidator from "../../../validation";
|
|
||||||
import { createToken } from "../../../helpers";
|
|
||||||
import { AuthTokenType } from "../../../variables";
|
|
||||||
import { getAuthSecret } from "../../../config";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create API key data v2
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const createAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
body: {
|
|
||||||
name
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.CreateAPIKeyV3, req);
|
|
||||||
|
|
||||||
const apiKeyData = await new APIKeyDataV2({
|
|
||||||
name,
|
|
||||||
user: req.user._id,
|
|
||||||
usageCount: 0,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
const apiKey = createToken({
|
|
||||||
payload: {
|
|
||||||
authTokenType: AuthTokenType.API_KEY,
|
|
||||||
apiKeyDataId: apiKeyData._id.toString(),
|
|
||||||
userId: req.user._id.toString()
|
|
||||||
},
|
|
||||||
secret: await getAuthSecret()
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData,
|
|
||||||
apiKey
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update API key data v2 with id [apiKeyDataId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const updateAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { apiKeyDataId },
|
|
||||||
body: {
|
|
||||||
name,
|
|
||||||
}
|
|
||||||
} = await validateRequest(reqValidator.UpdateAPIKeyV3, req);
|
|
||||||
|
|
||||||
const apiKeyData = await APIKeyDataV2.findOneAndUpdate(
|
|
||||||
{
|
|
||||||
_id: new Types.ObjectId(apiKeyDataId),
|
|
||||||
user: req.user._id
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!apiKeyData) throw BadRequestError({
|
|
||||||
message: "Failed to update API key"
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete API key data v2 with id [apiKeyDataId]
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const deleteAPIKeyData = async (req: Request, res: Response) => {
|
|
||||||
const {
|
|
||||||
params: { apiKeyDataId }
|
|
||||||
} = await validateRequest(reqValidator.DeleteAPIKeyV3, req);
|
|
||||||
|
|
||||||
const apiKeyData = await APIKeyDataV2.findOneAndDelete({
|
|
||||||
_id: new Types.ObjectId(apiKeyDataId),
|
|
||||||
user: req.user._id
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!apiKeyData) throw BadRequestError({
|
|
||||||
message: "Failed to delete API key"
|
|
||||||
});
|
|
||||||
|
|
||||||
return res.status(200).send({
|
|
||||||
apiKeyData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
import * as apiKeyDataController from "./apiKeyDataController";
|
|
||||||
|
|
||||||
export {
|
|
||||||
apiKeyDataController
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import _ from "lodash";
|
|
||||||
import { Membership } from "../../models";
|
|
||||||
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from "../../variables";
|
|
||||||
|
|
||||||
export const userHasWorkspaceAccess = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string, action: any) => {
|
|
||||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
|
||||||
if (!membershipForWorkspace) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
|
||||||
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: action });
|
|
||||||
|
|
||||||
if (isDisallowed) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
export const userHasWriteOnlyAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
|
||||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
|
||||||
if (!membershipForWorkspace) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
|
||||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
|
||||||
const isReadDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
|
||||||
|
|
||||||
// case: you have write only if read is blocked and write is not
|
|
||||||
if (isReadDisallowed && !isWriteDisallowed) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
export const userHasNoAbility = async (userId: Types.ObjectId, workspaceId: Types.ObjectId, environment: string) => {
|
|
||||||
const membershipForWorkspace = await Membership.findOne({ workspace: workspaceId, user: userId })
|
|
||||||
if (!membershipForWorkspace) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
const deniedMembershipPermissions = membershipForWorkspace.deniedPermissions;
|
|
||||||
const isWriteDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
|
|
||||||
const isReadBlocked = _.some(deniedMembershipPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
|
|
||||||
|
|
||||||
if (isReadBlocked && isWriteDisallowed) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import {
|
|
||||||
SSOConfig
|
|
||||||
} from "../models";
|
|
||||||
import {
|
|
||||||
BotOrgService
|
|
||||||
} from "../../services";
|
|
||||||
import { client } from "../../config";
|
|
||||||
import { ValidationError } from "../../utils/errors";
|
|
||||||
|
|
||||||
export const getSSOConfigHelper = async ({
|
|
||||||
organizationId,
|
|
||||||
ssoConfigId
|
|
||||||
}: {
|
|
||||||
organizationId?: Types.ObjectId;
|
|
||||||
ssoConfigId?: Types.ObjectId;
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
if (!organizationId && !ssoConfigId) throw ValidationError({
|
|
||||||
message: "Getting SSO data requires either id of organization or SSO data"
|
|
||||||
});
|
|
||||||
|
|
||||||
const ssoConfig = await SSOConfig.findOne({
|
|
||||||
...(organizationId ? { organization: organizationId } : {}),
|
|
||||||
...(ssoConfigId ? { _id: ssoConfigId } : {})
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!ssoConfig) throw new Error("Failed to find organization SSO data");
|
|
||||||
|
|
||||||
const key = await BotOrgService.getSymmetricKey(
|
|
||||||
ssoConfig.organization
|
|
||||||
);
|
|
||||||
|
|
||||||
const entryPoint = client.decryptSymmetric(
|
|
||||||
ssoConfig.encryptedEntryPoint,
|
|
||||||
key,
|
|
||||||
ssoConfig.entryPointIV,
|
|
||||||
ssoConfig.entryPointTag
|
|
||||||
);
|
|
||||||
|
|
||||||
const issuer = client.decryptSymmetric(
|
|
||||||
ssoConfig.encryptedIssuer,
|
|
||||||
key,
|
|
||||||
ssoConfig.issuerIV,
|
|
||||||
ssoConfig.issuerTag
|
|
||||||
);
|
|
||||||
|
|
||||||
const cert = client.decryptSymmetric(
|
|
||||||
ssoConfig.encryptedCert,
|
|
||||||
key,
|
|
||||||
ssoConfig.certIV,
|
|
||||||
ssoConfig.certTag
|
|
||||||
);
|
|
||||||
|
|
||||||
return ({
|
|
||||||
_id: ssoConfig._id,
|
|
||||||
organization: ssoConfig.organization,
|
|
||||||
authProvider: ssoConfig.authProvider,
|
|
||||||
isActive: ssoConfig.isActive,
|
|
||||||
entryPoint,
|
|
||||||
issuer,
|
|
||||||
cert
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@@ -1,122 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import { Secret } from "../../models";
|
|
||||||
import {
|
|
||||||
FolderVersion,
|
|
||||||
ISecretVersion,
|
|
||||||
SecretSnapshot,
|
|
||||||
SecretVersion,
|
|
||||||
} from "../models";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
|
||||||
* [workspaceId] under a new snapshot with incremented version under the
|
|
||||||
* secretsnapshots collection.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.workspaceId
|
|
||||||
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
|
|
||||||
*/
|
|
||||||
const takeSecretSnapshotHelper = async ({
|
|
||||||
workspaceId,
|
|
||||||
environment,
|
|
||||||
folderId = "root",
|
|
||||||
}: {
|
|
||||||
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);
|
|
||||||
|
|
||||||
const latestSecretVersions = (
|
|
||||||
await SecretVersion.aggregate([
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
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 latestSecretSnapshot = await SecretSnapshot.findOne({
|
|
||||||
workspace: workspaceId,
|
|
||||||
}).sort({ version: -1 });
|
|
||||||
|
|
||||||
const secretSnapshot = await new SecretSnapshot({
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment,
|
|
||||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
|
||||||
secretVersions: latestSecretVersions,
|
|
||||||
folderId,
|
|
||||||
folderVersion: latestFolderVersion,
|
|
||||||
}).save();
|
|
||||||
|
|
||||||
return secretSnapshot;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add secret versions [secretVersions] to the SecretVersion collection.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object[]} obj.secretVersions
|
|
||||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
|
||||||
*/
|
|
||||||
const addSecretVersionsHelper = async ({
|
|
||||||
secretVersions,
|
|
||||||
}: {
|
|
||||||
secretVersions: ISecretVersion[];
|
|
||||||
}) => {
|
|
||||||
const newSecretVersions = await SecretVersion.insertMany(secretVersions);
|
|
||||||
|
|
||||||
return newSecretVersions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const markDeletedSecretVersionsHelper = async ({
|
|
||||||
secretIds,
|
|
||||||
}: {
|
|
||||||
secretIds: Types.ObjectId[];
|
|
||||||
}) => {
|
|
||||||
await SecretVersion.updateMany(
|
|
||||||
{
|
|
||||||
secret: { $in: secretIds },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
isDeleted: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
new: true,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export {
|
|
||||||
takeSecretSnapshotHelper,
|
|
||||||
addSecretVersionsHelper,
|
|
||||||
markDeletedSecretVersionsHelper,
|
|
||||||
};
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
import { Types } from "mongoose";
|
|
||||||
import { SecretVersion } from "../models";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return latest secret versions for secrets with ids [secretIds]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getLatestSecretVersionIds = async ({
|
|
||||||
secretIds,
|
|
||||||
}: {
|
|
||||||
secretIds: Types.ObjectId[];
|
|
||||||
}) => {
|
|
||||||
const latestSecretVersionIds = await SecretVersion.aggregate([
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
secret: {
|
|
||||||
$in: secretIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: "$secret",
|
|
||||||
version: { $max: "$version" },
|
|
||||||
versionId: { $max: "$_id" }, // id of latest secret version
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 },
|
|
||||||
},
|
|
||||||
]).exec();
|
|
||||||
|
|
||||||
return latestSecretVersionIds;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return latest [n] secret versions for secrets with ids [secretIds]
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {Object} obj.secretIds = ids of secrets to get latest versions for
|
|
||||||
* @param {Number} obj.n - number of latest secret versions to return for each secret
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
const getLatestNSecretSecretVersionIds = async ({
|
|
||||||
secretIds,
|
|
||||||
n,
|
|
||||||
}: {
|
|
||||||
secretIds: Types.ObjectId[];
|
|
||||||
n: number;
|
|
||||||
}) => {
|
|
||||||
// TODO: optimize query
|
|
||||||
const latestNSecretVersions = await SecretVersion.aggregate([
|
|
||||||
{
|
|
||||||
$match: {
|
|
||||||
secret: {
|
|
||||||
$in: secretIds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$sort: { version: -1 },
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$group: {
|
|
||||||
_id: "$secret",
|
|
||||||
versions: { $push: "$$ROOT" },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
$project: {
|
|
||||||
_id: 0,
|
|
||||||
secret: "$_id",
|
|
||||||
versions: { $slice: ["$versions", n] },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return latestNSecretVersions;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds };
|
|
||||||
@@ -1,70 +0,0 @@
|
|||||||
import { Schema, Types, model } from "mongoose";
|
|
||||||
import { ActorType, EventType, UserAgentType } from "./enums";
|
|
||||||
import { Actor, Event } from "./types";
|
|
||||||
|
|
||||||
export interface IAuditLog {
|
|
||||||
actor: Actor;
|
|
||||||
organization: Types.ObjectId;
|
|
||||||
workspace: Types.ObjectId;
|
|
||||||
ipAddress: string;
|
|
||||||
event: Event;
|
|
||||||
userAgent: string;
|
|
||||||
userAgentType: UserAgentType;
|
|
||||||
expiresAt?: Date;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auditLogSchema = new Schema<IAuditLog>(
|
|
||||||
{
|
|
||||||
actor: {
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
enum: ActorType,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: Schema.Types.Mixed
|
|
||||||
}
|
|
||||||
},
|
|
||||||
organization: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
required: false
|
|
||||||
},
|
|
||||||
workspace: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
required: false,
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
ipAddress: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
type: {
|
|
||||||
type: String,
|
|
||||||
enum: EventType,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
metadata: {
|
|
||||||
type: Schema.Types.Mixed
|
|
||||||
}
|
|
||||||
},
|
|
||||||
userAgent: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
userAgentType: {
|
|
||||||
type: String,
|
|
||||||
enum: UserAgentType,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
expiresAt: {
|
|
||||||
type: Date,
|
|
||||||
expires: 0
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamps: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
export const AuditLog = model<IAuditLog>("AuditLog", auditLogSchema);
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
export enum ActorType { // would extend to AWS, Azure, ...
|
|
||||||
USER = "user", // userIdentity
|
|
||||||
SERVICE = "service",
|
|
||||||
IDENTITY = "identity"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum UserAgentType {
|
|
||||||
WEB = "web",
|
|
||||||
CLI = "cli",
|
|
||||||
K8_OPERATOR = "k8-operator",
|
|
||||||
TERRAFORM = "terraform",
|
|
||||||
OTHER = "other",
|
|
||||||
PYTHON_SDK = "InfisicalPythonSDK",
|
|
||||||
NODE_SDK = "InfisicalNodeSDK"
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum EventType {
|
|
||||||
GET_SECRETS = "get-secrets",
|
|
||||||
GET_SECRET = "get-secret",
|
|
||||||
REVEAL_SECRET = "reveal-secret",
|
|
||||||
CREATE_SECRET = "create-secret",
|
|
||||||
CREATE_SECRETS = "create-secrets",
|
|
||||||
UPDATE_SECRET = "update-secret",
|
|
||||||
UPDATE_SECRETS = "update-secrets",
|
|
||||||
DELETE_SECRET = "delete-secret",
|
|
||||||
DELETE_SECRETS = "delete-secrets",
|
|
||||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
|
||||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
|
||||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
|
||||||
CREATE_INTEGRATION = "create-integration",
|
|
||||||
DELETE_INTEGRATION = "delete-integration",
|
|
||||||
ADD_TRUSTED_IP = "add-trusted-ip",
|
|
||||||
UPDATE_TRUSTED_IP = "update-trusted-ip",
|
|
||||||
DELETE_TRUSTED_IP = "delete-trusted-ip",
|
|
||||||
CREATE_SERVICE_TOKEN = "create-service-token", // v2
|
|
||||||
DELETE_SERVICE_TOKEN = "delete-service-token", // v2
|
|
||||||
CREATE_IDENTITY = "create-identity",
|
|
||||||
UPDATE_IDENTITY = "update-identity",
|
|
||||||
DELETE_IDENTITY = "delete-identity",
|
|
||||||
LOGIN_IDENTITY_UNIVERSAL_AUTH = "login-identity-universal-auth",
|
|
||||||
ADD_IDENTITY_UNIVERSAL_AUTH = "add-identity-universal-auth",
|
|
||||||
UPDATE_IDENTITY_UNIVERSAL_AUTH = "update-identity-universal-auth",
|
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH = "get-identity-universal-auth",
|
|
||||||
CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "create-identity-universal-auth-client-secret",
|
|
||||||
REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET = "revoke-identity-universal-auth-client-secret",
|
|
||||||
GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS = "get-identity-universal-auth-client-secret",
|
|
||||||
CREATE_ENVIRONMENT = "create-environment",
|
|
||||||
UPDATE_ENVIRONMENT = "update-environment",
|
|
||||||
DELETE_ENVIRONMENT = "delete-environment",
|
|
||||||
ADD_WORKSPACE_MEMBER = "add-workspace-member",
|
|
||||||
ADD_BATCH_WORKSPACE_MEMBER = "add-workspace-members",
|
|
||||||
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
|
|
||||||
CREATE_FOLDER = "create-folder",
|
|
||||||
UPDATE_FOLDER = "update-folder",
|
|
||||||
DELETE_FOLDER = "delete-folder",
|
|
||||||
CREATE_WEBHOOK = "create-webhook",
|
|
||||||
UPDATE_WEBHOOK_STATUS = "update-webhook-status",
|
|
||||||
DELETE_WEBHOOK = "delete-webhook",
|
|
||||||
GET_SECRET_IMPORTS = "get-secret-imports",
|
|
||||||
CREATE_SECRET_IMPORT = "create-secret-import",
|
|
||||||
UPDATE_SECRET_IMPORT = "update-secret-import",
|
|
||||||
DELETE_SECRET_IMPORT = "delete-secret-import",
|
|
||||||
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
|
|
||||||
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions",
|
|
||||||
SECRET_APPROVAL_MERGED = "secret-approval-merged",
|
|
||||||
SECRET_APPROVAL_REQUEST = "secret-approval-request",
|
|
||||||
SECRET_APPROVAL_CLOSED = "secret-approval-closed",
|
|
||||||
SECRET_APPROVAL_REOPENED = "secret-approval-reopened"
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export * from "./auditLog";
|
|
||||||
export * from "./enums";
|
|
||||||
export * from "./types";
|
|
||||||
@@ -1,585 +0,0 @@
|
|||||||
import { ActorType, EventType } from "./enums";
|
|
||||||
import { IIdentityTrustedIp } from "../../../models";
|
|
||||||
|
|
||||||
interface UserActorMetadata {
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ServiceActorMetadata {
|
|
||||||
serviceId: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IdentityActorMetadata {
|
|
||||||
identityId: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface UserActor {
|
|
||||||
type: ActorType.USER;
|
|
||||||
metadata: UserActorMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ServiceActor {
|
|
||||||
type: ActorType.SERVICE;
|
|
||||||
metadata: ServiceActorMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IdentityActor {
|
|
||||||
type: ActorType.IDENTITY;
|
|
||||||
metadata: IdentityActorMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Actor = UserActor | ServiceActor | IdentityActor;
|
|
||||||
|
|
||||||
interface GetSecretsEvent {
|
|
||||||
type: EventType.GET_SECRETS;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
numberOfSecrets: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetSecretEvent {
|
|
||||||
type: EventType.GET_SECRET;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secretId: string;
|
|
||||||
secretKey: string;
|
|
||||||
secretVersion: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateSecretEvent {
|
|
||||||
type: EventType.CREATE_SECRET;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secretId: string;
|
|
||||||
secretKey: string;
|
|
||||||
secretVersion: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateSecretBatchEvent {
|
|
||||||
type: EventType.CREATE_SECRETS;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateSecretEvent {
|
|
||||||
type: EventType.UPDATE_SECRET;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secretId: string;
|
|
||||||
secretKey: string;
|
|
||||||
secretVersion: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateSecretBatchEvent {
|
|
||||||
type: EventType.UPDATE_SECRETS;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteSecretEvent {
|
|
||||||
type: EventType.DELETE_SECRET;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secretId: string;
|
|
||||||
secretKey: string;
|
|
||||||
secretVersion: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteSecretBatchEvent {
|
|
||||||
type: EventType.DELETE_SECRETS;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetWorkspaceKeyEvent {
|
|
||||||
type: EventType.GET_WORKSPACE_KEY;
|
|
||||||
metadata: {
|
|
||||||
keyId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AuthorizeIntegrationEvent {
|
|
||||||
type: EventType.AUTHORIZE_INTEGRATION;
|
|
||||||
metadata: {
|
|
||||||
integration: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UnauthorizeIntegrationEvent {
|
|
||||||
type: EventType.UNAUTHORIZE_INTEGRATION;
|
|
||||||
metadata: {
|
|
||||||
integration: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateIntegrationEvent {
|
|
||||||
type: EventType.CREATE_INTEGRATION;
|
|
||||||
metadata: {
|
|
||||||
integrationId: string;
|
|
||||||
integration: string; // TODO: fix type
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
url?: string;
|
|
||||||
app?: string;
|
|
||||||
appId?: string;
|
|
||||||
targetEnvironment?: string;
|
|
||||||
targetEnvironmentId?: string;
|
|
||||||
targetService?: string;
|
|
||||||
targetServiceId?: string;
|
|
||||||
path?: string;
|
|
||||||
region?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteIntegrationEvent {
|
|
||||||
type: EventType.DELETE_INTEGRATION;
|
|
||||||
metadata: {
|
|
||||||
integrationId: string;
|
|
||||||
integration: string; // TODO: fix type
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
url?: string;
|
|
||||||
app?: string;
|
|
||||||
appId?: string;
|
|
||||||
targetEnvironment?: string;
|
|
||||||
targetEnvironmentId?: string;
|
|
||||||
targetService?: string;
|
|
||||||
targetServiceId?: string;
|
|
||||||
path?: string;
|
|
||||||
region?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddTrustedIPEvent {
|
|
||||||
type: EventType.ADD_TRUSTED_IP;
|
|
||||||
metadata: {
|
|
||||||
trustedIpId: string;
|
|
||||||
ipAddress: string;
|
|
||||||
prefix?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateTrustedIPEvent {
|
|
||||||
type: EventType.UPDATE_TRUSTED_IP;
|
|
||||||
metadata: {
|
|
||||||
trustedIpId: string;
|
|
||||||
ipAddress: string;
|
|
||||||
prefix?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteTrustedIPEvent {
|
|
||||||
type: EventType.DELETE_TRUSTED_IP;
|
|
||||||
metadata: {
|
|
||||||
trustedIpId: string;
|
|
||||||
ipAddress: string;
|
|
||||||
prefix?: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateServiceTokenEvent {
|
|
||||||
type: EventType.CREATE_SERVICE_TOKEN;
|
|
||||||
metadata: {
|
|
||||||
name: string;
|
|
||||||
scopes: Array<{
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteServiceTokenEvent {
|
|
||||||
type: EventType.DELETE_SERVICE_TOKEN;
|
|
||||||
metadata: {
|
|
||||||
name: string;
|
|
||||||
scopes: Array<{
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
}>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateIdentityEvent { // note: currently not logging org-role
|
|
||||||
type: EventType.CREATE_IDENTITY;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateIdentityEvent {
|
|
||||||
type: EventType.UPDATE_IDENTITY;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
name?: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteIdentityEvent {
|
|
||||||
type: EventType.DELETE_IDENTITY;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LoginIdentityUniversalAuthEvent {
|
|
||||||
type: EventType.LOGIN_IDENTITY_UNIVERSAL_AUTH ;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
identityUniversalAuthId: string;
|
|
||||||
clientSecretId: string;
|
|
||||||
identityAccessTokenId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddIdentityUniversalAuthEvent {
|
|
||||||
type: EventType.ADD_IDENTITY_UNIVERSAL_AUTH;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
clientSecretTrustedIps: Array<IIdentityTrustedIp>;
|
|
||||||
accessTokenTTL: number;
|
|
||||||
accessTokenMaxTTL: number;
|
|
||||||
accessTokenNumUsesLimit: number;
|
|
||||||
accessTokenTrustedIps: Array<IIdentityTrustedIp>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateIdentityUniversalAuthEvent {
|
|
||||||
type: EventType.UPDATE_IDENTITY_UNIVERSAL_AUTH;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
clientSecretTrustedIps?: Array<IIdentityTrustedIp>;
|
|
||||||
accessTokenTTL?: number;
|
|
||||||
accessTokenMaxTTL?: number;
|
|
||||||
accessTokenNumUsesLimit?: number;
|
|
||||||
accessTokenTrustedIps?: Array<IIdentityTrustedIp>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetIdentityUniversalAuthEvent {
|
|
||||||
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateIdentityUniversalAuthClientSecretEvent {
|
|
||||||
type: EventType.CREATE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
clientSecretId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetIdentityUniversalAuthClientSecretsEvent {
|
|
||||||
type: EventType.GET_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRETS;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
interface RevokeIdentityUniversalAuthClientSecretEvent {
|
|
||||||
type: EventType.REVOKE_IDENTITY_UNIVERSAL_AUTH_CLIENT_SECRET ;
|
|
||||||
metadata: {
|
|
||||||
identityId: string;
|
|
||||||
clientSecretId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateEnvironmentEvent {
|
|
||||||
type: EventType.CREATE_ENVIRONMENT;
|
|
||||||
metadata: {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateEnvironmentEvent {
|
|
||||||
type: EventType.UPDATE_ENVIRONMENT;
|
|
||||||
metadata: {
|
|
||||||
oldName: string;
|
|
||||||
newName: string;
|
|
||||||
oldSlug: string;
|
|
||||||
newSlug: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteEnvironmentEvent {
|
|
||||||
type: EventType.DELETE_ENVIRONMENT;
|
|
||||||
metadata: {
|
|
||||||
name: string;
|
|
||||||
slug: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddWorkspaceMemberEvent {
|
|
||||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
|
||||||
metadata: {
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AddBatchWorkspaceMemberEvent {
|
|
||||||
type: EventType.ADD_BATCH_WORKSPACE_MEMBER;
|
|
||||||
metadata: Array<{
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
}>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface RemoveWorkspaceMemberEvent {
|
|
||||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
|
||||||
metadata: {
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateFolderEvent {
|
|
||||||
type: EventType.CREATE_FOLDER;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
folderId: string;
|
|
||||||
folderName: string;
|
|
||||||
folderPath: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateFolderEvent {
|
|
||||||
type: EventType.UPDATE_FOLDER;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
folderId: string;
|
|
||||||
oldFolderName: string;
|
|
||||||
newFolderName: string;
|
|
||||||
folderPath: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteFolderEvent {
|
|
||||||
type: EventType.DELETE_FOLDER;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
folderId: string;
|
|
||||||
folderName: string;
|
|
||||||
folderPath: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateWebhookEvent {
|
|
||||||
type: EventType.CREATE_WEBHOOK;
|
|
||||||
metadata: {
|
|
||||||
webhookId: string;
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
webhookUrl: string;
|
|
||||||
isDisabled: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateWebhookStatusEvent {
|
|
||||||
type: EventType.UPDATE_WEBHOOK_STATUS;
|
|
||||||
metadata: {
|
|
||||||
webhookId: string;
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
webhookUrl: string;
|
|
||||||
isDisabled: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteWebhookEvent {
|
|
||||||
type: EventType.DELETE_WEBHOOK;
|
|
||||||
metadata: {
|
|
||||||
webhookId: string;
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
webhookUrl: string;
|
|
||||||
isDisabled: boolean;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetSecretImportsEvent {
|
|
||||||
type: EventType.GET_SECRET_IMPORTS;
|
|
||||||
metadata: {
|
|
||||||
environment: string;
|
|
||||||
secretImportId: string;
|
|
||||||
folderId: string;
|
|
||||||
numberOfImports: number;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CreateSecretImportEvent {
|
|
||||||
type: EventType.CREATE_SECRET_IMPORT;
|
|
||||||
metadata: {
|
|
||||||
secretImportId: string;
|
|
||||||
folderId: string;
|
|
||||||
importFromEnvironment: string;
|
|
||||||
importFromSecretPath: string;
|
|
||||||
importToEnvironment: string;
|
|
||||||
importToSecretPath: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateSecretImportEvent {
|
|
||||||
type: EventType.UPDATE_SECRET_IMPORT;
|
|
||||||
metadata: {
|
|
||||||
secretImportId: string;
|
|
||||||
folderId: string;
|
|
||||||
importToEnvironment: string;
|
|
||||||
importToSecretPath: string;
|
|
||||||
orderBefore: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
}[];
|
|
||||||
orderAfter: {
|
|
||||||
environment: string;
|
|
||||||
secretPath: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DeleteSecretImportEvent {
|
|
||||||
type: EventType.DELETE_SECRET_IMPORT;
|
|
||||||
metadata: {
|
|
||||||
secretImportId: string;
|
|
||||||
folderId: string;
|
|
||||||
importFromEnvironment: string;
|
|
||||||
importFromSecretPath: string;
|
|
||||||
importToEnvironment: string;
|
|
||||||
importToSecretPath: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateUserRole {
|
|
||||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
|
|
||||||
metadata: {
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
oldRole: string;
|
|
||||||
newRole: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface UpdateUserDeniedPermissions {
|
|
||||||
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
|
|
||||||
metadata: {
|
|
||||||
userId: string;
|
|
||||||
email: string;
|
|
||||||
deniedPermissions: {
|
|
||||||
environmentSlug: string;
|
|
||||||
ability: string;
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
interface SecretApprovalMerge {
|
|
||||||
type: EventType.SECRET_APPROVAL_MERGED;
|
|
||||||
metadata: {
|
|
||||||
mergedBy: string;
|
|
||||||
secretApprovalRequestSlug: string;
|
|
||||||
secretApprovalRequestId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecretApprovalClosed {
|
|
||||||
type: EventType.SECRET_APPROVAL_CLOSED;
|
|
||||||
metadata: {
|
|
||||||
closedBy: string;
|
|
||||||
secretApprovalRequestSlug: string;
|
|
||||||
secretApprovalRequestId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecretApprovalReopened {
|
|
||||||
type: EventType.SECRET_APPROVAL_REOPENED;
|
|
||||||
metadata: {
|
|
||||||
reopenedBy: string;
|
|
||||||
secretApprovalRequestSlug: string;
|
|
||||||
secretApprovalRequestId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SecretApprovalRequest {
|
|
||||||
type: EventType.SECRET_APPROVAL_REQUEST;
|
|
||||||
metadata: {
|
|
||||||
committedBy: string;
|
|
||||||
secretApprovalRequestSlug: string;
|
|
||||||
secretApprovalRequestId: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Event =
|
|
||||||
| GetSecretsEvent
|
|
||||||
| GetSecretEvent
|
|
||||||
| CreateSecretEvent
|
|
||||||
| CreateSecretBatchEvent
|
|
||||||
| UpdateSecretEvent
|
|
||||||
| UpdateSecretBatchEvent
|
|
||||||
| DeleteSecretEvent
|
|
||||||
| DeleteSecretBatchEvent
|
|
||||||
| GetWorkspaceKeyEvent
|
|
||||||
| AuthorizeIntegrationEvent
|
|
||||||
| UnauthorizeIntegrationEvent
|
|
||||||
| CreateIntegrationEvent
|
|
||||||
| DeleteIntegrationEvent
|
|
||||||
| AddTrustedIPEvent
|
|
||||||
| UpdateTrustedIPEvent
|
|
||||||
| DeleteTrustedIPEvent
|
|
||||||
| CreateServiceTokenEvent
|
|
||||||
| DeleteServiceTokenEvent
|
|
||||||
| CreateIdentityEvent
|
|
||||||
| UpdateIdentityEvent
|
|
||||||
| DeleteIdentityEvent
|
|
||||||
| LoginIdentityUniversalAuthEvent
|
|
||||||
| AddIdentityUniversalAuthEvent
|
|
||||||
| UpdateIdentityUniversalAuthEvent
|
|
||||||
| GetIdentityUniversalAuthEvent
|
|
||||||
| CreateIdentityUniversalAuthClientSecretEvent
|
|
||||||
| GetIdentityUniversalAuthClientSecretsEvent
|
|
||||||
| RevokeIdentityUniversalAuthClientSecretEvent
|
|
||||||
| CreateEnvironmentEvent
|
|
||||||
| UpdateEnvironmentEvent
|
|
||||||
| DeleteEnvironmentEvent
|
|
||||||
| AddWorkspaceMemberEvent
|
|
||||||
| AddBatchWorkspaceMemberEvent
|
|
||||||
| RemoveWorkspaceMemberEvent
|
|
||||||
| CreateFolderEvent
|
|
||||||
| UpdateFolderEvent
|
|
||||||
| DeleteFolderEvent
|
|
||||||
| CreateWebhookEvent
|
|
||||||
| UpdateWebhookStatusEvent
|
|
||||||
| DeleteWebhookEvent
|
|
||||||
| GetSecretImportsEvent
|
|
||||||
| CreateSecretImportEvent
|
|
||||||
| UpdateSecretImportEvent
|
|
||||||
| DeleteSecretImportEvent
|
|
||||||
| UpdateUserRole
|
|
||||||
| UpdateUserDeniedPermissions
|
|
||||||
| SecretApprovalMerge
|
|
||||||
| SecretApprovalClosed
|
|
||||||
| SecretApprovalRequest
|
|
||||||
| SecretApprovalReopened;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user