Compare commits

..

6 Commits

857 changed files with 24779 additions and 47320 deletions

View File

@ -1,6 +1,5 @@
# Keys
# Required key for platform encryption/decryption ops
# THIS IS A SAMPLE ENCRYPTION KEY AND SHOULD NOT BE USED FOR PRODUCTION
ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
# JWT
@ -31,12 +30,14 @@ MONGO_PASSWORD=example
# Required
SITE_URL=http://localhost:8080
# Mail/SMTP
SMTP_HOST=
SMTP_PORT=
SMTP_NAME=
SMTP_USERNAME=
SMTP_PASSWORD=
# Mail/SMTP
SMTP_HOST= # required
SMTP_USERNAME= # required
SMTP_PASSWORD= # required
SMTP_PORT=587
SMTP_SECURE=false
SMTP_FROM_ADDRESS= # required
SMTP_FROM_NAME=Infisical
# Integration
# Optional only if integration is used
@ -65,4 +66,4 @@ STRIPE_WEBHOOK_SECRET=
STRIPE_PRODUCT_STARTER=
STRIPE_PRODUCT_TEAM=
STRIPE_PRODUCT_PRO=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=

View File

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

View File

@ -1,25 +1,14 @@
name: Build, Publish and Deploy to Gamma
on:
push:
tags:
- "infisical/v*.*.*"
on: [workflow_dispatch]
jobs:
backend-image:
name: Build backend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: 📦 Install dependencies to test all dependencies
run: npm ci --only-production
working-directory: backend
- name: 🧪 Run tests
run: npm run test:ci
working-directory: backend
- name: Save commit hashes for tag
id: commit
uses: pr-mpt/actions-commit-hash@v2
@ -56,19 +45,15 @@ jobs:
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
push: true
context: backend
tags: |
infisical/backend:${{ steps.commit.outputs.short }}
infisical/backend:latest
infisical/backend:${{ steps.extract_version.outputs.version }}
tags: infisical/backend:${{ steps.commit.outputs.short }},
infisical/backend:latest
platforms: linux/amd64,linux/arm64
frontend-image:
name: Build frontend image
runs-on: ubuntu-latest
steps:
- name: Extract version from tag
id: extract_version
run: echo "::set-output name=version::${GITHUB_REF_NAME#infisical/}"
- name: ☁️ Checkout source
uses: actions/checkout@v3
- name: Save commit hashes for tag
@ -109,10 +94,8 @@ jobs:
push: true
token: ${{ secrets.DEPOT_PROJECT_TOKEN }}
context: frontend
tags: |
infisical/frontend:${{ steps.commit.outputs.short }}
infisical/frontend:latest
infisical/frontend:${{ steps.extract_version.outputs.version }}
tags: infisical/frontend:${{ steps.commit.outputs.short }},
infisical/frontend:latest
platforms: linux/amd64,linux/arm64
build-args: |
POSTHOG_API_KEY=${{ secrets.PUBLIC_POSTHOG_API_KEY }}
@ -139,7 +122,7 @@ jobs:
token: ${{ secrets.DIGITALOCEAN_ACCESS_TOKEN }}
- name: Save DigitalOcean kubeconfig with short-lived credentials
run: doctl kubernetes cluster kubeconfig save --expiry-seconds 600 k8s-1-25-4-do-0-nyc1-1670645170179
- name: switch to gamma namespace
- name: switch to gamma namespace
run: kubectl config set-context --current --namespace=gamma
- name: test kubectl
run: kubectl get ingress
@ -152,4 +135,4 @@ jobs:
exit 1
else
echo "Helm upgrade was successful"
fi
fi

View File

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

View File

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

View File

@ -11,10 +11,6 @@ before:
- ./cli/scripts/completions.sh
- ./cli/scripts/manpages.sh
monorepo:
tag_prefix: infisical-cli/
dir: cli
builds:
- id: darwin-build
binary: infisical
@ -65,10 +61,10 @@ archives:
- goos: windows
format: zip
files:
- ../README*
- ../LICENSE*
- ../manpages/*
- ../completions/*
- README*
- LICENSE*
- manpages/*
- completions/*
release:
replace_existing_draft: true
@ -78,7 +74,14 @@ checksum:
name_template: "checksums.txt"
snapshot:
name_template: "{{ .Version }}-devel"
name_template: "{{ incpatch .Version }}-devel"
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
# publishers:
# - name: fury.io
@ -161,7 +164,7 @@ aurs:
mkdir -p "${pkgdir}/usr/share/zsh/site-functions/"
mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/"
install -Dm644 "./completions/infisical.bash" "${pkgdir}/usr/share/bash-completion/completions/infisical"
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/_infisical"
install -Dm644 "./completions/infisical.zsh" "${pkgdir}/usr/share/zsh/site-functions/infisical"
install -Dm644 "./completions/infisical.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/infisical.fish"
# man pages
install -Dm644 "./manpages/infisical.1.gz" "${pkgdir}/usr/share/man/man1/infisical.1.gz"

View File

@ -1,102 +0,0 @@
ARG POSTHOG_HOST=https://app.posthog.com
ARG POSTHOG_API_KEY=posthog-api-key
FROM node:16-alpine AS frontend-dependencies
WORKDIR /app
COPY frontend/package.json frontend/package-lock.json frontend/next.config.js ./
# Install dependencies
RUN npm ci --only-production --ignore-scripts
# Rebuild the source code only when needed
FROM node:16-alpine AS frontend-builder
WORKDIR /app
# Copy dependencies
COPY --from=frontend-dependencies /app/node_modules ./node_modules
# Copy all files
COPY /frontend .
ENV NODE_ENV production
ENV NEXT_PUBLIC_ENV production
ARG POSTHOG_HOST
ENV NEXT_PUBLIC_POSTHOG_HOST $POSTHOG_HOST
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY $POSTHOG_API_KEY
# Build
RUN npm run build
# Production image
FROM node:16-alpine AS frontend-runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
RUN mkdir -p /app/.next/cache/images && chown nextjs:nodejs /app/.next/cache/images
VOLUME /app/.next/cache/images
ARG POSTHOG_API_KEY
ENV NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY \
BAKED_NEXT_PUBLIC_POSTHOG_API_KEY=$POSTHOG_API_KEY
COPY --chown=nextjs:nodejs --chmod=555 frontend/scripts ./scripts
COPY --from=frontend-builder /app/public ./public
RUN chown nextjs:nodejs ./public/data
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=frontend-builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
ENV NEXT_TELEMETRY_DISABLED 1
##
## BACKEND
##
FROM node:16-alpine AS backend-build
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY /backend .
RUN npm run build
# Production stage
FROM node:16-alpine AS backend-runner
WORKDIR /app
COPY backend/package*.json ./
RUN npm ci --only-production
COPY --from=backend-build /app .
# Production stage
FROM node:14-alpine AS production
WORKDIR /
# Install PM2
RUN npm install -g pm2
# Copy ecosystem.config.js
COPY ecosystem.config.js .
RUN apk add --no-cache nginx
COPY nginx/default-stand-alone-docker.conf /etc/nginx/nginx.conf
COPY --from=backend-runner /app /backend
COPY --from=frontend-runner /app/ /app/
EXPOSE 80
ENV HTTPS_ENABLED false
CMD ["pm2-runtime", "start", "ecosystem.config.js"]

369
README.md

File diff suppressed because one or more lines are too long

View File

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

2220
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,18 @@
{
"dependencies": {
"@aws-sdk/client-secrets-manager": "^3.312.0",
"@godaddy/terminus": "^4.12.0",
"@aws-sdk/client-secrets-manager": "^3.267.0",
"@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.41.0",
"@sentry/tracing": "^7.48.0",
"@sentry/node": "^7.39.0",
"@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"argon2": "^0.30.3",
"await-to-js": "^3.0.0",
"aws-sdk": "^2.1360.0",
"axios": "^1.3.5",
"aws-sdk": "^2.1311.0",
"axios": "^1.1.3",
"axios-retry": "^3.4.0",
"bcrypt": "^5.1.0",
"bigint-conversion": "^2.4.0",
"bigint-conversion": "^2.2.2",
"builder-pattern": "^2.2.0",
"cookie-parser": "^1.4.6",
"cors": "^2.8.5",
@ -24,15 +23,15 @@
"express-validator": "^6.14.2",
"handlebars": "^4.7.7",
"helmet": "^5.1.1",
"infisical-node": "^1.1.3",
"infisical-node": "^1.0.37",
"js-yaml": "^4.1.0",
"jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"lodash": "^4.17.21",
"mongoose": "^6.10.5",
"mongoose": "^6.7.2",
"nodemailer": "^6.8.0",
"posthog-node": "^2.6.0",
"posthog-node": "^2.5.4",
"query-string": "^7.1.3",
"request-ip": "^3.3.0",
"rimraf": "^3.0.2",
@ -58,7 +57,7 @@
"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": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
},
@ -81,7 +80,7 @@
"@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12",
"@types/express": "^4.17.14",
"@types/jest": "^29.5.0",
"@types/jest": "^29.2.4",
"@types/jsonwebtoken": "^8.5.9",
"@types/lodash": "^4.14.191",
"@types/node": "^18.11.3",

View File

@ -1532,15 +1532,7 @@
"/api/v1/invite-org/signup": {
"post": {
"description": "",
"parameters": [
{
"name": "host",
"in": "header",
"schema": {
"type": "string"
}
}
],
"parameters": [],
"responses": {
"200": {
"description": "OK"
@ -2079,15 +2071,6 @@
"targetEnvironment": {
"example": "any"
},
"targetEnvironmentId": {
"example": "any"
},
"targetService": {
"example": "any"
},
"targetServiceId": {
"example": "any"
},
"owner": {
"example": "any"
},
@ -2314,13 +2297,6 @@
"schema": {
"type": "string"
}
},
{
"name": "teamId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
@ -2333,107 +2309,6 @@
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/teams": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/vercel/branches": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/railway/environments": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v1/integration-auth/{integrationAuthId}/railway/services": {
"get": {
"description": "",
"parameters": [
{
"name": "integrationAuthId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "appId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/signup/complete-account/signup": {
"post": {
"description": "",
@ -2995,6 +2870,9 @@
}
}
}
},
"400": {
"description": "Bad Request"
}
},
"security": [
@ -3004,26 +2882,6 @@
]
}
},
"/api/v2/organizations/{organizationId}/service-accounts": {
"get": {
"description": "",
"parameters": [
{
"name": "organizationId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/workspace/{workspaceId}/environments": {
"post": {
"description": "",
@ -4160,6 +4018,9 @@
"responses": {
"200": {
"description": "OK"
},
"400": {
"description": "Bad Request"
}
},
"requestBody": {
@ -4212,138 +4073,6 @@
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/me": {
"get": {
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"delete": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/": {
"post": {
"description": "",
"parameters": [],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/name": {
"patch": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"name": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"post": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
@ -4351,90 +4080,6 @@
"400": {
"description": "Bad Request"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"environment": {
"example": "any"
},
"workspaceId": {
"example": "any"
},
"read": {
"example": "any"
},
"write": {
"example": "any"
},
"encryptedKey": {
"example": "any"
},
"nonce": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/permissions/workspace/{serviceAccountWorkspacePermissionId}": {
"delete": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "serviceAccountWorkspacePermissionId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v2/service-accounts/{serviceAccountId}/keys": {
"get": {
"description": "",
"parameters": [
{
"name": "serviceAccountId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
@ -4504,297 +4149,6 @@
}
}
},
"/api/v3/secrets/": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "environment",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/secrets/{secretName}": {
"post": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
},
"secretKeyCiphertext": {
"example": "any"
},
"secretKeyIV": {
"example": "any"
},
"secretKeyTag": {
"example": "any"
},
"secretValueCiphertext": {
"example": "any"
},
"secretValueIV": {
"example": "any"
},
"secretValueTag": {
"example": "any"
},
"secretCommentCiphertext": {
"example": "any"
},
"secretCommentIV": {
"example": "any"
},
"secretCommentTag": {
"example": "any"
}
}
}
}
}
}
},
"get": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "workspaceId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "environment",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "type",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
},
"patch": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
},
"secretValueCiphertext": {
"example": "any"
},
"secretValueIV": {
"example": "any"
},
"secretValueTag": {
"example": "any"
}
}
}
}
}
}
},
"delete": {
"description": "",
"parameters": [
{
"name": "secretName",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"workspaceId": {
"example": "any"
},
"environment": {
"example": "any"
},
"type": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets/blind-index-status": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets": {
"get": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
}
}
},
"/api/v3/workspaces/{workspaceId}/secrets/names": {
"post": {
"description": "",
"parameters": [
{
"name": "workspaceId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "OK"
}
},
"requestBody": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"secretsToUpdate": {
"example": "any"
}
}
}
}
}
}
}
},
"/api/status": {
"get": {
"description": "",

View File

@ -1,69 +1,51 @@
import InfisicalClient from 'infisical-node';
const client = new InfisicalClient({
token: process.env.INFISICAL_TOKEN!
});
export const getPort = async () => (await client.getSecret('PORT')).secretValue || 4000;
export const getInviteOnlySignup = async () => (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue == undefined ? false : (await client.getSecret('INVITE_ONLY_SIGNUP')).secretValue;
export const getEncryptionKey = async () => (await client.getSecret('ENCRYPTION_KEY')).secretValue;
export const getSaltRounds = async () => parseInt((await client.getSecret('SALT_ROUNDS')).secretValue) || 10;
export const getJwtAuthLifetime = async () => (await client.getSecret('JWT_AUTH_LIFETIME')).secretValue || '10d';
export const getJwtAuthSecret = async () => (await client.getSecret('JWT_AUTH_SECRET')).secretValue;
export const getJwtMfaLifetime = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
export const getJwtMfaSecret = async () => (await client.getSecret('JWT_MFA_LIFETIME')).secretValue || '5m';
export const getJwtRefreshLifetime = async () => (await client.getSecret('JWT_REFRESH_LIFETIME')).secretValue || '90d';
export const getJwtRefreshSecret = async () => (await client.getSecret('JWT_REFRESH_SECRET')).secretValue;
export const getJwtServiceSecret = async () => (await client.getSecret('JWT_SERVICE_SECRET')).secretValue;
export const getJwtSignupLifetime = async () => (await client.getSecret('JWT_SIGNUP_LIFETIME')).secretValue || '15m';
export const getJwtSignupSecret = async () => (await client.getSecret('JWT_SIGNUP_SECRET')).secretValue;
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 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 getClientSlugVercel = async () => (await client.getSecret('CLIENT_SLUG_VERCEL')).secretValue;
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 getStripeProductStarter = async () => (await client.getSecret('STRIPE_PRODUCT_STARTER')).secretValue;
export const getStripeProductPro = async () => (await client.getSecret('STRIPE_PRODUCT_PRO')).secretValue;
export const getStripeProductTeam = async () => (await client.getSecret('STRIPE_PRODUCT_TEAM')).secretValue;
export const getStripePublishableKey = async () => (await client.getSecret('STRIPE_PUBLISHABLE_KEY')).secretValue;
export const getStripeSecretKey = async () => (await client.getSecret('STRIPE_SECRET_KEY')).secretValue;
export const getStripeWebhookSecret = async () => (await client.getSecret('STRIPE_WEBHOOK_SECRET')).secretValue;
export const getTelemetryEnabled = async () => (await client.getSecret('TELEMETRY_ENABLED')).secretValue !== 'false' && true;
export const getLoopsApiKey = async () => (await client.getSecret('LOOPS_API_KEY')).secretValue;
export const getSmtpConfigured = async () => (await client.getSecret('SMTP_HOST')).secretValue == '' || (await client.getSecret('SMTP_HOST')).secretValue == undefined ? false : true
export const getHttpsEnabled = async () => {
if ((await getNodeEnv()) != "production") {
// no https for anything other than prod
return false
}
if ((await client.getSecret('HTTPS_ENABLED')).secretValue == undefined || (await client.getSecret('HTTPS_ENABLED')).secretValue == "") {
// default when no value present
return true
}
return (await client.getSecret('HTTPS_ENABLED')).secretValue === 'true' && true
}
import infisical from 'infisical-node';
export const getPort = () => infisical.get('PORT')! || 4000;
export const getInviteOnlySignup = () => infisical.get('INVITE_ONLY_SIGNUP')! == undefined ? false : infisical.get('INVITE_ONLY_SIGNUP');
export const getEncryptionKey = () => infisical.get('ENCRYPTION_KEY')!;
export const getSaltRounds = () => parseInt(infisical.get('SALT_ROUNDS')!) || 10;
export const getJwtAuthLifetime = () => infisical.get('JWT_AUTH_LIFETIME')! || '10d';
export const getJwtAuthSecret = () => infisical.get('JWT_AUTH_SECRET')!;
export const getJwtMfaLifetime = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
export const getJwtMfaSecret = () => infisical.get('JWT_MFA_LIFETIME')! || '5m';
export const getJwtRefreshLifetime = () => infisical.get('JWT_REFRESH_LIFETIME')! || '90d';
export const getJwtRefreshSecret = () => infisical.get('JWT_REFRESH_SECRET')!;
export const getJwtServiceSecret = () => infisical.get('JWT_SERVICE_SECRET')!;
export const getJwtSignupLifetime = () => infisical.get('JWT_SIGNUP_LIFETIME')! || '15m';
export const getJwtSignupSecret = () => infisical.get('JWT_SIGNUP_SECRET')!;
export const getMongoURL = () => infisical.get('MONGO_URL')!;
export const getNodeEnv = () => infisical.get('NODE_ENV')!;
export const getVerboseErrorOutput = () => infisical.get('VERBOSE_ERROR_OUTPUT')! === 'true' && true;
export const getLokiHost = () => infisical.get('LOKI_HOST')!;
export const getClientIdAzure = () => infisical.get('CLIENT_ID_AZURE')!;
export const getClientIdHeroku = () => infisical.get('CLIENT_ID_HEROKU')!;
export const getClientIdVercel = () => infisical.get('CLIENT_ID_VERCEL')!;
export const getClientIdNetlify = () => infisical.get('CLIENT_ID_NETLIFY')!;
export const getClientIdGitHub = () => infisical.get('CLIENT_ID_GITHUB')!;
export const getClientIdGitLab = () => infisical.get('CLIENT_ID_GITLAB')!;
export const getClientSecretAzure = () => infisical.get('CLIENT_SECRET_AZURE')!;
export const getClientSecretHeroku = () => infisical.get('CLIENT_SECRET_HEROKU')!;
export const getClientSecretVercel = () => infisical.get('CLIENT_SECRET_VERCEL')!;
export const getClientSecretNetlify = () => infisical.get('CLIENT_SECRET_NETLIFY')!;
export const getClientSecretGitHub = () => infisical.get('CLIENT_SECRET_GITHUB')!;
export const getClientSecretGitLab = () => infisical.get('CLIENT_SECRET_GITLAB')!;
export const getClientSlugVercel = () => infisical.get('CLIENT_SLUG_VERCEL')!;
export const getPostHogHost = () => infisical.get('POSTHOG_HOST')! || 'https://app.posthog.com';
export const getPostHogProjectApiKey = () => infisical.get('POSTHOG_PROJECT_API_KEY')! || 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
export const getSentryDSN = () => infisical.get('SENTRY_DSN')!;
export const getSiteURL = () => infisical.get('SITE_URL')!;
export const getSmtpHost = () => infisical.get('SMTP_HOST')!;
export const getSmtpSecure = () => infisical.get('SMTP_SECURE')! === 'true' || false;
export const getSmtpPort = () => parseInt(infisical.get('SMTP_PORT')!) || 587;
export const getSmtpUsername = () => infisical.get('SMTP_USERNAME')!;
export const getSmtpPassword = () => infisical.get('SMTP_PASSWORD')!;
export const getSmtpFromAddress = () => infisical.get('SMTP_FROM_ADDRESS')!;
export const getSmtpFromName = () => infisical.get('SMTP_FROM_NAME')! || 'Infisical';
export const getStripeProductStarter = () => infisical.get('STRIPE_PRODUCT_STARTER')!;
export const getStripeProductPro = () => infisical.get('STRIPE_PRODUCT_PRO')!;
export const getStripeProductTeam = () => infisical.get('STRIPE_PRODUCT_TEAM')!;
export const getStripePublishableKey = () => infisical.get('STRIPE_PUBLISHABLE_KEY')!;
export const getStripeSecretKey = () => infisical.get('STRIPE_SECRET_KEY')!;
export const getStripeWebhookSecret = () => infisical.get('STRIPE_WEBHOOK_SECRET')!;
export const getTelemetryEnabled = () => infisical.get('TELEMETRY_ENABLED')! !== 'false' && true;
export const getLoopsApiKey = () => infisical.get('LOOPS_API_KEY')!;
export const getSmtpConfigured = () => infisical.get('SMTP_HOST') == '' || infisical.get('SMTP_HOST') == undefined ? false : true

View File

@ -15,10 +15,10 @@ import { BadRequestError } from '../../utils/errors';
import { EELogService } from '../../ee/services';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtRefreshSecret,
getJwtAuthLifetime,
getJwtAuthSecret,
getHttpsEnabled
getJwtAuthSecret
} from '../../config';
declare module 'jsonwebtoken' {
@ -126,21 +126,21 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
secure: getNodeEnv() === 'production' ? true : false
});
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
// return (access) token in response
return res.status(200).send({
token: tokens.token,
@ -182,14 +182,14 @@ export const logout = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean
secure: getNodeEnv() === 'production' ? true : false
});
const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT,
userId: req.user._id
});
logoutAction && await EELogService.createLog({
userId: req.user._id,
actions: [logoutAction],
@ -237,7 +237,7 @@ export const getNewToken = async (req: Request, res: Response) => {
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
jwt.verify(refreshToken, getJwtRefreshSecret())
);
const user = await User.findOne({
@ -252,8 +252,8 @@ export const getNewToken = async (req: Request, res: Response) => {
payload: {
userId: decodedToken.userId
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});
return res.status(200).send({

View File

@ -1,5 +1,4 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot';
@ -30,7 +29,7 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
// -> create a new bot and return it
bot = await createBot({
name: 'Infisical Bot',
workspaceId: new Types.ObjectId(workspaceId)
workspaceId
});
}
} catch (err) {

View File

@ -14,6 +14,7 @@ import * as stripeController from './stripeController';
import * as userActionController from './userActionController';
import * as userController from './userController';
import * as workspaceController from './workspaceController';
import * as secretsFolderController from './secretsFolderController'
export {
authController,
@ -31,5 +32,6 @@ export {
stripeController,
userActionController,
userController,
workspaceController
workspaceController,
secretsFolderController
};

View File

@ -12,11 +12,6 @@ import {
getTeams,
revokeAccess
} from '../../integrations';
import {
INTEGRATION_VERCEL_API_URL,
INTEGRATION_RAILWAY_API_URL
} from '../../variables';
import request from '../../config/request';
/***
* Return integration authorization with id [integrationAuthId]
@ -44,7 +39,7 @@ export const getIntegrationAuth = async (req: Request, res: Response) => {
}
export const getIntegrationOptions = async (req: Request, res: Response) => {
const INTEGRATION_OPTIONS = await getIntegrationOptionsFunc();
const INTEGRATION_OPTIONS = getIntegrationOptionsFunc();
return res.status(200).send({
integrationOptions: INTEGRATION_OPTIONS,
@ -193,206 +188,25 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
* @returns
*/
export const getIntegrationAuthTeams = async (req: Request, res: Response) => {
const teams = await getTeams({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
let teams;
try {
teams = await getTeams({
integrationAuth: req.integrationAuth,
accessToken: req.accessToken
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization teams"
});
}
return res.status(200).send({
teams
});
}
/**
* Return list of available Vercel (preview) branches for Vercel project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthVercelBranches = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface VercelBranch {
ref: string;
lastCommit: string;
isProtected: boolean;
}
const params = new URLSearchParams({
projectId: appId,
...(req.integrationAuth.teamId ? {
teamId: req.integrationAuth.teamId
} : {})
});
let branches: string[] = [];
if (appId && appId !== '') {
const { data }: { data: VercelBranch[] } = await request.get(
`${INTEGRATION_VERCEL_API_URL}/v1/integrations/git-branches`,
{
params,
headers: {
Authorization: `Bearer ${req.accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
branches = data.map((b) => b.ref);
}
return res.status(200).send({
branches
});
}
/**
* Return list of Railway environments for Railway project with
* id [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayEnvironments = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayEnvironment {
node: {
id: string;
name: string;
isEphemeral: boolean;
}
}
interface Environment {
environmentId: string;
name: string;
}
let environments: Environment[] = [];
if (appId && appId !== '') {
const query = `
query GetEnvironments($projectId: String!, $after: String, $before: String, $first: Int, $isEphemeral: Boolean, $last: Int) {
environments(projectId: $projectId, after: $after, before: $before, first: $first, isEphemeral: $isEphemeral, last: $last) {
edges {
node {
id
name
isEphemeral
}
}
}
}
`;
const variables = {
projectId: appId
}
const { data: { data: { environments: { edges } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables,
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
environments = edges.map((e: RailwayEnvironment) => {
return ({
name: e.node.name,
environmentId: e.node.id
});
});
}
return res.status(200).send({
environments
});
}
/**
* Return list of Railway services for Railway project with id
* [appId]
* @param req
* @param res
*/
export const getIntegrationAuthRailwayServices = async (req: Request, res: Response) => {
const { integrationAuthId } = req.params;
const appId = req.query.appId as string;
interface RailwayService {
node: {
id: string;
name: string;
}
}
interface Service {
name: string;
serviceId: string;
}
let services: Service[] = [];
const query = `
query project($id: String!) {
project(id: $id) {
createdAt
deletedAt
id
description
expiredAt
isPublic
isTempProject
isUpdatable
name
prDeploys
teamId
updatedAt
upstreamUrl
services {
edges {
node {
id
name
}
}
}
}
}
`;
if (appId && appId !== '') {
const variables = {
id: appId
}
const { data: { data: { project: { services: { edges } } } } } = await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables
}, {
headers: {
'Authorization': `Bearer ${req.accessToken}`,
'Content-Type': 'application/json',
},
});
services = edges.map((e: RailwayService) => ({
name: e.node.name,
serviceId: e.node.id
}));
}
return res.status(200).send({
services
});
}
/**
* Delete integration authorization with id [integrationAuthId]
* @param req

View File

@ -24,9 +24,6 @@ export const createIntegration = async (req: Request, res: Response) => {
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
@ -42,22 +39,18 @@ export const createIntegration = async (req: Request, res: Response) => {
app,
appId,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
workspaceId: integration.workspace.toString()
})
});
}
@ -118,8 +111,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
workspaceId: integration.workspace.toString(),
}),
});
}

View File

@ -215,7 +215,7 @@ export const inviteUserToWorkspace = async (req: Request, res: Response) => {
inviterFirstName: req.user.firstName,
inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name,
callback_url: (await getSiteURL()) + '/login'
callback_url: getSiteURL() + '/login'
}
});
} catch (err) {

View File

@ -1,4 +1,3 @@
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { MembershipOrg, Organization, User } from '../../models';
@ -140,7 +139,7 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
inviteEmail: inviteeEmail,
organization: organizationId,
role: MEMBER,
status: INVITED
status: invitee?.publicKey ? ACCEPTED : INVITED
}).save();
}
} else {
@ -165,7 +164,6 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
const organization = await Organization.findOne({ _id: organizationId });
if (organization) {
const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail,
@ -181,14 +179,13 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
inviterEmail: req.user.email,
organizationName: organization.name,
email: inviteeEmail,
organizationId: organization._id.toString(),
token,
callback_url: (await getSiteURL()) + '/signupinvite'
callback_url: getSiteURL() + '/signupinvite'
}
});
if (!(await getSmtpConfigured())) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
if (!getSmtpConfigured()) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}`
}
}
@ -217,18 +214,13 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user, token;
try {
const {
email,
organizationId,
code
} = req.body;
const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey');
const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email,
status: INVITED,
organization: new Types.ObjectId(organizationId)
status: INVITED
});
if (!membershipOrg)
@ -265,8 +257,8 @@ export const verifyUserToOrganization = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);

View File

@ -85,7 +85,7 @@ export const createOrganization = async (req: Request, res: Response) => {
export const getOrganization = async (req: Request, res: Response) => {
let organization;
try {
organization = req.organization
organization = req.membershipOrg.organization;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
@ -317,29 +317,29 @@ export const createOrganizationPortalSession = async (
) => {
let session;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
// check if there is a payment method on file
const paymentMethods = await stripe.paymentMethods.list({
customer: req.organization.customerId,
customer: req.membershipOrg.organization.customerId,
type: 'card'
});
if (paymentMethods.data.length < 1) {
// case: no payment method on file
session = await stripe.checkout.sessions.create({
customer: req.organization.customerId,
customer: req.membershipOrg.organization.customerId,
mode: 'setup',
payment_method_types: ['card'],
success_url: (await getSiteURL()) + '/dashboard',
cancel_url: (await getSiteURL()) + '/dashboard'
success_url: getSiteURL() + '/dashboard',
cancel_url: getSiteURL() + '/dashboard'
});
} else {
session = await stripe.billingPortal.sessions.create({
customer: req.organization.customerId,
return_url: (await getSiteURL()) + '/dashboard'
customer: req.membershipOrg.organization.customerId,
return_url: getSiteURL() + '/dashboard'
});
}
@ -365,12 +365,12 @@ export const getOrganizationSubscriptions = async (
) => {
let subscriptions;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
subscriptions = await stripe.subscriptions.list({
customer: req.organization.customerId
customer: req.membershipOrg.organization.customerId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });

View File

@ -44,7 +44,7 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
substitutions: {
email,
token,
callback_url: (await getSiteURL()) + '/password-reset'
callback_url: getSiteURL() + '/password-reset'
}
});
} catch (err) {
@ -91,8 +91,8 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);

View File

@ -1,6 +1,5 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Key, Secret } from '../../models';
import {
v1PushSecrets as push,
@ -10,7 +9,7 @@ import {
import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../../events';
import { EventService } from '../../services';
import { TelemetryService } from '../../services';
import { getPostHogClient } from '../../services';
interface PushSecret {
ciphertextKey: string;
@ -39,7 +38,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@ -85,8 +84,7 @@ export const pushSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
workspaceId
})
});
@ -114,7 +112,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
@ -183,7 +181,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;

View File

@ -86,22 +86,4 @@ export const deleteFolder = async (req: Request, res: Response) => {
}
res.send()
}
// TODO: validate workspace
export const getFolderById = async (req: Request, res: Response) => {
const { folderId } = req.params
const folder = await Folder.findById(folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" })
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId: folder.workspace as any,
acceptedRoles: [ADMIN, MEMBER]
});
res.send({ folder })
}

View File

@ -61,7 +61,7 @@ export const createServiceToken = async (req: Request, res: Response) => {
workspaceId
},
expiresIn: expiresIn,
secret: await getJwtServiceSecret()
secret: getJwtServiceSecret()
});
} catch (err) {
return res.status(400).send({

View File

@ -21,7 +21,7 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
try {
email = req.body.email;
if (await getInviteOnlySignup()) {
if (getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({})
if (userCount != 0) {
@ -75,7 +75,7 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
}
// verify email
if (await getSmtpConfigured()) {
if (getSmtpConfigured()) {
await checkEmailVerification({
email,
code
@ -93,8 +93,8 @@ export const verifyEmailSignup = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret()
expiresIn: getJwtSignupLifetime(),
secret: getJwtSignupSecret()
});
} catch (err) {
Sentry.setUser(null);

View File

@ -13,7 +13,7 @@ export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
// check request for valid stripe signature
const stripe = new Stripe(await getStripeSecretKey(), {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
@ -21,7 +21,7 @@ export const handleWebhook = async (req: Request, res: Response) => {
event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });

View File

@ -43,14 +43,13 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const secretHash = await bcrypt.hash(secret, getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({
name,
lastUsed: new Date(),
expiresAt,
user: req.user._id,
secretHash

View File

@ -17,9 +17,9 @@ import {
} from '../../variables';
import { getChannelFromUserAgent } from '../../utils/posthog'; // TODO: move this
import {
getNodeEnv,
getJwtMfaLifetime,
getJwtMfaSecret,
getHttpsEnabled
getJwtMfaSecret
} from '../../config';
declare module 'jsonwebtoken' {
@ -124,8 +124,8 @@ export const login2 = async (req: Request, res: Response) => {
payload: {
userId: user._id.toString()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
expiresIn: getJwtMfaLifetime(),
secret: getJwtMfaSecret()
});
const code = await TokenService.createToken({
@ -163,7 +163,7 @@ export const login2 = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
secure: getNodeEnv() === 'production' ? true : false
});
// case: user does not have MFA enablgged
@ -302,7 +302,7 @@ export const verifyMfaToken = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
secure: getNodeEnv() === 'production' ? true : false
});
interface VerifyMfaTokenRes {

View File

@ -11,7 +11,7 @@ import {
import { SecretVersion } from '../../ee/models';
import { BadRequestError } from '../../utils/errors';
import _ from 'lodash';
import { PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS } from '../../variables';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
/**
* Create new workspace environment named [environmentName] under workspace with id
@ -244,8 +244,8 @@ export const getAllAccessibleEnvironmentsOfWorkspace = async (
throw BadRequestError()
}
relatedWorkspace.environments.forEach(environment => {
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_READ_SECRETS })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: PERMISSION_WRITE_SECRETS })
const isReadBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_READ })
const isWriteBlocked = _.some(deniedPermission, { environmentSlug: environment.slug, ability: ABILITY_WRITE })
if (isReadBlocked && isWriteBlocked) {
return
} else {

View File

@ -7,7 +7,6 @@ import * as serviceTokenDataController from './serviceTokenDataController';
import * as apiKeyDataController from './apiKeyDataController';
import * as secretController from './secretController';
import * as secretsController from './secretsController';
import * as serviceAccountsController from './serviceAccountsController';
import * as environmentController from './environmentController';
import * as tagController from './tagController';
@ -21,7 +20,6 @@ export {
apiKeyDataController,
secretController,
secretsController,
serviceAccountsController,
environmentController,
tagController
}

View File

@ -1,11 +1,9 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
MembershipOrg,
Membership,
Workspace,
ServiceAccount
Workspace
} from '../../models';
import { deleteMembershipOrg } from '../../helpers/membershipOrg';
import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
@ -262,45 +260,37 @@ export const getOrganizationWorkspaces = async (req: Request, res: Response) =>
}
}
*/
const { organizationId } = req.params;
let workspaces;
try {
const { organizationId } = req.params;
const workspacesSet = new Set(
(
await Workspace.find(
{
organization: organizationId
},
'_id'
)
).map((w) => w._id.toString())
);
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
});
}
/**
* Return service accounts for organization with id [organizationId]
* @param req
* @param res
*/
export const getOrganizationServiceAccounts = async (req: Request, res: Response) => {
const { organizationId } = req.params;
const serviceAccounts = await ServiceAccount.find({
organization: new Types.ObjectId(organizationId)
});
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
)
.filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization workspaces'
});
}
return res.status(200).send({
serviceAccounts
workspaces
});
}

View File

@ -7,9 +7,7 @@ const { ValidationError } = mongoose.Error;
import { BadRequestError, InternalServerError, UnauthorizedRequestError, ValidationError as RouteValidationError } from '../../utils/errors';
import { AnyBulkWriteOperation } from 'mongodb';
import { SECRET_PERSONAL, SECRET_SHARED } from "../../variables";
import { TelemetryService } from '../../services';
import { User } from "../../models";
import { AccountNotFoundError } from '../../utils/errors';
import { getPostHogClient } from '../../services';
/**
* Create secret for workspace with id [workspaceId] and environment [environment]
@ -17,7 +15,7 @@ import { AccountNotFoundError } from '../../utils/errors';
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const secretToCreate: CreateSecretRequestBody = req.body.secret;
const { workspaceId, environment } = req.params
const sanitizedSecret: SanitizedSecretForCreate = {
@ -70,7 +68,7 @@ export const createSecret = async (req: Request, res: Response) => {
* @param res
*/
export const createSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const secretsToCreate: CreateSecretRequestBody[] = req.body.secrets;
const { workspaceId, environment } = req.params
const sanitizedSecretesToCreate: SanitizedSecretForCreate[] = []
@ -132,7 +130,7 @@ export const createSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretIdsToDelete: string[] = req.body.secretIds
@ -186,7 +184,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
* @param res
*/
export const deleteSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
await Secret.findByIdAndDelete(req._secret._id)
if (postHogClient) {
@ -215,7 +213,7 @@ export const deleteSecret = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretsModificationsRequested: ModifySecretRequestBody[] = req.body.secrets;
const [secretIdsUserCanModifyError, secretIdsUserCanModify] = await to(Secret.find({ workspace: workspaceId, environment: environmentName }, { _id: 1 }).then())
@ -283,7 +281,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
* @returns
*/
export const updateSecret = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const { workspaceId, environmentName } = req.params
const secretModificationsRequested: ModifySecretRequestBody = req.body.secret;
@ -337,23 +335,20 @@ export const updateSecret = async (req: Request, res: Response) => {
* @returns
*/
export const getSecrets = async (req: Request, res: Response) => {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = 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
let userEmail: Types.ObjectId | undefined = undefined // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user;
const user = await User.findById(req.serviceTokenData.user, 'email');
if (!user) throw AccountNotFoundError();
userEmail = user.email;
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
const [err, secrets] = await to(Secret.find(

View File

@ -2,7 +2,7 @@ import to from 'await-to-js';
import { Types } from 'mongoose';
import { Request, Response } from 'express';
import { ISecret, Secret } from '../../models';
import { IAction, SecretVersion } from '../../ee/models';
import { IAction } from '../../ee/models';
import {
SECRET_PERSONAL,
SECRET_SHARED,
@ -11,33 +11,31 @@ import {
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
} from '../../variables';
import { UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { BadRequestError, UnauthorizedRequestError, ValidationError } from '../../utils/errors';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { EESecretService, EELogService } from '../../ee/services';
import { TelemetryService, SecretService } from '../../services';
import { getPostHogClient } from '../../services';
import { getChannelFromUserAgent } from '../../utils/posthog';
import { PERMISSION_WRITE_SECRETS } from '../../variables';
import { ABILITY_READ, ABILITY_WRITE } from '../../variables/organization';
import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions';
import Tag from '../../models/tag';
import _, { eq } from 'lodash';
import _ from 'lodash';
import {
BatchSecretRequest,
BatchSecret
} from '../../types/secret';
import { getFolderPath, getFoldersInDirectory, normalizePath } from '../../utils/folder';
import { ROOT_FOLDER_PATH } from '../../utils/folder';
import Folder from '../../models/folder';
/**
* Peform a batch of any specified CUD secret operations
* (used by dashboard)
* @param req
* @param res
*/
export const batchSecrets = async (req: Request, res: Response) => {
const channel = getChannelFromUserAgent(req.headers['user-agent']);
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const {
workspaceId,
@ -54,48 +52,30 @@ export const batchSecrets = async (req: Request, res: Response) => {
const deleteSecrets: Types.ObjectId[] = [];
const actions: IAction[] = [];
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
for (const request of requests) {
const folderId = request.secret.folder
for await (const request of requests) {
const folderId = request.secret.folderId
// TODO: need to auth folder
// need to auth folder
const fullFolderPath = await getFolderPath(folderId)
let secretBlindIndex = '';
switch (request.method) {
case 'POST':
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: request.secret.secretName,
salt
});
createSecrets.push({
...request.secret,
path: fullFolderPath,
folder: folderId,
version: 1,
user: request.secret.type === SECRET_PERSONAL ? req.user : undefined,
environment,
workspace: new Types.ObjectId(workspaceId),
path: fullFolderPath,
folder: folderId,
secretBlindIndex
workspace: new Types.ObjectId(workspaceId)
});
break;
case 'PATCH':
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: request.secret.secretName,
salt,
});
updateSecrets.push({
...request.secret,
_id: new Types.ObjectId(request.secret._id),
secretBlindIndex,
folder: folderId,
path: fullFolderPath,
_id: new Types.ObjectId(request.secret._id)
});
break;
case 'DELETE':
@ -109,6 +89,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
if (createSecrets.length > 0) {
createdSecrets = await Secret.insertMany(createSecrets);
// (EE) add secret versions for new secrets
await EESecretService.addSecretVersions({
secretVersions: createdSecrets.map((n: any) => {
return ({
@ -122,9 +103,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: createdSecrets.map((n) => n._id)
}) as IAction;
@ -163,10 +142,7 @@ export const batchSecrets = async (req: Request, res: Response) => {
const updateOperations = updateSecrets.map((u) => ({
updateOne: {
filter: {
_id: new Types.ObjectId(u._id),
workspace: new Types.ObjectId(workspaceId)
},
filter: { _id: new Types.ObjectId(u._id) },
update: {
$inc: {
version: 1
@ -179,14 +155,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
await Secret.bulkWrite(updateOperations);
const secretVersions = updateSecrets.map((u) => new SecretVersion({
const secretVersions = updateSecrets.map((u) => ({
secret: new Types.ObjectId(u._id),
version: listedSecretsObj[u._id.toString()].version,
workspace: new Types.ObjectId(workspaceId),
type: listedSecretsObj[u._id.toString()].type,
environment,
isDeleted: false,
secretBlindIndex: u.secretBlindIndex,
secretKeyCiphertext: u.secretKeyCiphertext,
secretKeyIV: u.secretKeyIV,
secretKeyTag: u.secretKeyTag,
@ -281,13 +256,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
// // trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId)
workspaceId
})
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId)
workspaceId
});
const resObj: { [key: string]: ISecret[] | string[] } = {}
@ -365,15 +340,14 @@ export const createSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body;
if (req.user) {
const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, PERMISSION_WRITE_SECRETS)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_WRITE)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
let listOfSecretsToCreate;
@ -385,30 +359,31 @@ export const createSecrets = async (req: Request, res: Response) => {
listOfSecretsToCreate = [req.body.secrets];
}
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
type secretsToCreateType = {
type: string;
secretName?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: string[]
}
const secretsToInsert: ISecret[] = [];
const secretsToInsert: ISecret[] = await Promise.all(
listOfSecretsToCreate.map(async ({
for (const {
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags,
folder
} of listOfSecretsToCreate) {
const fullFolderPath = await getFolderPath(folder)
const secret: any = {
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
secretName,
user: type === SECRET_PERSONAL ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -418,36 +393,13 @@ export const createSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}: secretsToCreateType) => {
let secretBlindIndex;
if (secretName) {
secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName,
salt
});
}
tags,
folder: folder,
path: fullFolderPath
};
return ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
type,
...(secretBlindIndex ? { secretBlindIndex } : {}),
user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
});
})
);
secretsToInsert.push(secret);
}
const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject());
@ -455,7 +407,7 @@ export const createSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId)
workspaceId
})
});
}, 5000);
@ -469,45 +421,48 @@ export const createSecrets = async (req: Request, res: Response) => {
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
}) => new SecretVersion({
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
secretBlindIndex,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
}))
});
const addAction = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: req.user._id,
workspaceId: new Types.ObjectId(workspaceId),
secretIds: newlyCreatedSecrets.map((n) => n._id)
});
// (EE) create (audit) log
addAction && await EELogService.createLog({
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: req.user._id.toString(),
workspaceId: new Types.ObjectId(workspaceId),
actions: [addAction],
channel,
@ -516,16 +471,13 @@ export const createSecrets = async (req: Request, res: Response) => {
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId)
workspaceId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
distinctId: req.user.email,
properties: {
numberOfSecrets: listOfSecretsToCreate.length,
environment,
@ -555,7 +507,7 @@ export const getSecrets = async (req: Request, res: Response) => {
#swagger.security = [{
"apiKeyAuth": []
}]
}]
#swagger.parameters['workspaceId'] = {
"description": "ID of project",
@ -589,139 +541,100 @@ export const getSecrets = async (req: Request, res: Response) => {
}
*/
const { tagSlugs, secretsPath } = req.query;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const postHogClient = getPostHogClient();
const { workspaceId, environment, tagSlugs, secretsPath } = req.query;
const normalizedPath = normalizePath(secretsPath as string)
const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath)
// secrets to return
let secrets: ISecret[] = [];
// query tags table to get all tags ids for the tag names for the given workspace
let tagIds = [];
const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : [];
if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId });
tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
}
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
// case: client authorization is via JWT
const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment)
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped to env and project
let hasWriteOnlyAccess
if (!req.serviceTokenData) {
hasWriteOnlyAccess = await userHasWriteOnlyAbility(userId, workspaceId, environment)
const hasNoAccess = await userHasNoAbility(userId, workspaceId, environment)
if (hasNoAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
const secretQuery: any = {
let secrets: any
let secretQuery: any
if (tagNamesList != undefined && tagNamesList.length != 0) {
const workspaceFromDB = await Tag.find({ workspace: workspaceId })
const tagIds = _.map(tagNamesList, (tagName) => {
const tag = _.find(workspaceFromDB, { slug: tagName });
return tag ? tag.id : null;
});
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: req.user._id }, // personal secrets for this user
{ user: { $exists: false } } // shared secrets from workspace
]
{ user: userId },
{ user: { $exists: false } }
],
tags: { $in: tagIds },
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
if (normalizedPath == ROOT_FOLDER_PATH) {
secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] }
} else if (normalizedPath) {
secretQuery.path = normalizedPath
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
if (hasWriteOnlyAccess) {
// only return the secret keys and not the values since user does not have right to see values
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
}
}
// case: client authorization is via service token
if (req.serviceTokenData) {
const userId = req.serviceTokenData.user;
const secretQuery: any = {
} else {
secretQuery = {
workspace: workspaceId,
environment,
$or: [
{ user: userId }, // personal secrets for this user
{ user: { $exists: false } } // shared secrets from workspace
]
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
// TODO: check if user can query for given path
if (normalizedPath == ROOT_FOLDER_PATH) {
secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] }
} else if (normalizedPath) {
secretQuery.path = normalizedPath
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
// TODO check if service token has write only permission
secrets = await Secret.find(secretQuery).populate("tags");
}
// case: client authorization is via service account
if (req.serviceAccount) {
const secretQuery: any = {
workspace: workspaceId,
environment,
user: { $exists: false } // shared secrets only from workspace
}
// Add path to secrets query
secretQuery.path = normalizedPath
if (normalizedPath == ROOT_FOLDER_PATH) {
secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] }
} else if (normalizedPath) {
secretQuery.path = normalizedPath
}
if (tagIds.length > 0) {
secretQuery.tags = { $in: tagIds };
}
secrets = await Secret.find(secretQuery).populate("tags");
if (hasWriteOnlyAccess) {
secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag")
} else {
secrets = await Secret.find(secretQuery).populate("tags")
}
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const readAction = await EELogService.createAction({
name: ACTION_READ_SECRETS,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId as string),
secretIds: secrets.map((n: any) => n._id)
});
readAction && await EELogService.createLog({
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: new Types.ObjectId(userId),
workspaceId: new Types.ObjectId(workspaceId as string),
actions: [readAction],
channel,
ipAddress: req.ip
});
const postHogClient = await TelemetryService.getPostHogClient();
const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath)
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
distinctId: userEmail,
properties: {
numberOfSecrets: secrets.length,
environment,
@ -738,6 +651,58 @@ export const getSecrets = async (req: Request, res: Response) => {
});
}
export const getOnlySecretKeys = async (req: Request, res: Response) => {
const { workspaceId, environment } = req.query;
let userId = "" // used for getting personal secrets for user
let userEmail = "" // used for posthog
if (req.user) {
userId = req.user._id;
userEmail = req.user.email;
}
if (req.serviceTokenData) {
userId = req.serviceTokenData.user._id
userEmail = req.serviceTokenData.user.email;
}
// none service token case as service tokens are already scoped
if (!req.serviceTokenData) {
const hasAccess = await userHasWorkspaceAccess(userId, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
}
const [err, secretKeys] = await to(Secret.find(
{
workspace: workspaceId,
environment,
$or: [
{ user: userId },
{ user: { $exists: false } }
],
type: { $in: [SECRET_SHARED, SECRET_PERSONAL] }
}
)
.select("secretKeyIV secretKeyTag secretKeyCiphertext")
.then())
if (err) throw ValidationError({ message: 'Failed to get secrets', stack: err.stack });
// readAction && await EELogService.createLog({
// userId: new Types.ObjectId(userId),
// workspaceId: new Types.ObjectId(workspaceId as string),
// actions: [readAction],
// channel,
// ipAddress: req.ip
// });
return res.status(200).send({
secretKeys
});
}
/**
* Update secret(s)
* @param req
@ -788,8 +753,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli';
// TODO: move type
interface PatchSecret {
id: string;
secretKeyCiphertext: string;
@ -804,7 +771,9 @@ export const updateSecrets = async (req: Request, res: Response) => {
tags: string[]
}
const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => {
const updateOperationsToPerform = [];
for (const secret of req.body.secrets) {
const {
secretKeyCiphertext,
secretKeyIV,
@ -815,10 +784,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
tags
tags,
folder
} = secret;
return ({
const fullFolderPath = await getFolderPath(folder)
const updateOperation = {
updateOne: {
filter: { _id: new Types.ObjectId(secret.id) },
update: {
@ -832,6 +804,8 @@ export const updateSecrets = async (req: Request, res: Response) => {
secretValueIV,
secretValueTag,
tags,
path: fullFolderPath,
folder: folder,
...((
secretCommentCiphertext !== undefined &&
secretCommentIV &&
@ -843,8 +817,10 @@ export const updateSecrets = async (req: Request, res: Response) => {
} : {}),
}
}
});
});
};
updateOperationsToPerform.push(updateOperation);
}
await Secret.bulkWrite(updateOperationsToPerform);
@ -908,25 +884,21 @@ export const updateSecrets = async (req: Request, res: Response) => {
setTimeout(async () => {
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(key)
workspaceId: key
})
});
}, 10000);
const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: req.user._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
updateAction && await EELogService.createLog({
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: req.user._id.toString(),
workspaceId: new Types.ObjectId(key),
actions: [updateAction],
channel,
@ -935,16 +907,13 @@ export const updateSecrets = async (req: Request, res: Response) => {
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(key)
workspaceId: key
})
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
distinctId: req.user.email,
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,
@ -966,7 +935,7 @@ export const updateSecrets = async (req: Request, res: Response) => {
}
/**
* Delete secret(s)
* Delete secret(s) with id [workspaceId] and environment [environment]
* @param req
* @param res
*/
@ -1015,7 +984,7 @@ export const deleteSecrets = async (req: Request, res: Response) => {
}
}
*/
const postHogClient = getPostHogClient();
const channel = getChannelFromUserAgent(req.headers['user-agent'])
const toDelete = req.secrets.map((s: any) => s._id);
@ -1044,23 +1013,19 @@ export const deleteSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(key)
workspaceId: key
})
});
const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: req.user._id,
workspaceId: new Types.ObjectId(key),
secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id)
});
// (EE) create (audit) log
deleteAction && await EELogService.createLog({
userId: req.user?._id,
serviceAccountId: req.serviceAccount?._id,
serviceTokenDataId: req.serviceTokenData?._id,
userId: req.user._id.toString(),
workspaceId: new Types.ObjectId(key),
actions: [deleteAction],
channel,
@ -1069,16 +1034,13 @@ export const deleteSecrets = async (req: Request, res: Response) => {
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(key)
});
workspaceId: key
})
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: await TelemetryService.getDistinctId({
authData: req.authData
}),
distinctId: req.user.email,
properties: {
numberOfSecrets: workspaceSecretObj[key].length,
environment: workspaceSecretObj[key][0].environment,

View File

@ -1,306 +0,0 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
ServiceAccount,
ServiceAccountKey,
ServiceAccountOrganizationPermission,
ServiceAccountWorkspacePermission
} from '../../models';
import {
CreateServiceAccountDto
} from '../../interfaces/serviceAccounts/dto';
import { BadRequestError, ServiceAccountNotFoundError } from '../../utils/errors';
import { getSaltRounds } from '../../config';
/**
* Return service account tied to the request (service account) client
* @param req
* @param res
*/
export const getCurrentServiceAccount = async (req: Request, res: Response) => {
const serviceAccount = await ServiceAccount.findById(req.serviceAccount._id);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
return res.status(200).send({
serviceAccount
});
}
/**
* Return service account with id [serviceAccountId]
* @param req
* @param res
*/
export const getServiceAccountById = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
return res.status(200).send({
serviceAccount
});
}
/**
* Create a new service account under organization with id [organizationId]
* that has access to workspaces [workspaces]
* @param req
* @param res
* @returns
*/
export const createServiceAccount = async (req: Request, res: Response) => {
const {
name,
organizationId,
publicKey,
expiresIn,
}: CreateServiceAccountDto = req.body;
let expiresAt;
if (expiresIn) {
expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
}
const secret = crypto.randomBytes(16).toString('base64');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
// create service account
const serviceAccount = await new ServiceAccount({
name,
organization: new Types.ObjectId(organizationId),
user: req.user,
publicKey,
lastUsed: new Date(),
expiresAt,
secretHash
}).save();
const serviceAccountObj = serviceAccount.toObject();
delete serviceAccountObj.secretHash;
// provision default org-level permission for service account
await new ServiceAccountOrganizationPermission({
serviceAccount: serviceAccount._id
}).save();
const secretId = Buffer.from(serviceAccount._id.toString(), 'hex').toString('base64');
return res.status(200).send({
serviceAccountAccessKey: `sa.${secretId}.${secret}`,
serviceAccount: serviceAccountObj
});
}
/**
* Change name of service account with id [serviceAccountId] to [name]
* @param req
* @param res
* @returns
*/
export const changeServiceAccountName = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const { name } = req.body;
const serviceAccount = await ServiceAccount.findOneAndUpdate(
{
_id: new Types.ObjectId(serviceAccountId)
},
{
name
},
{
new: true
}
);
return res.status(200).send({
serviceAccount
});
}
/**
* Add a service account key to service account with id [serviceAccountId]
* for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const addServiceAccountKey = async (req: Request, res: Response) => {
const {
workspaceId,
encryptedKey,
nonce
} = req.body;
const serviceAccountKey = await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: req.serviceAccount._d,
workspace: new Types.ObjectId(workspaceId)
}).save();
return serviceAccountKey;
}
/**
* Return workspace-level permission for service account with id [serviceAccountId]
* @param req
* @param res
*/
export const getServiceAccountWorkspacePermissions = async (req: Request, res: Response) => {
const serviceAccountWorkspacePermissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: req.serviceAccount._id
}).populate('workspace');
return res.status(200).send({
serviceAccountWorkspacePermissions
});
}
/**
* Add a workspace permission to service account with id [serviceAccountId]
* @param req
* @param res
*/
export const addServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const {
environment,
workspaceId,
read = false,
write = false,
encryptedKey,
nonce
} = req.body;
if (!req.membership.workspace.environments.some((e: { name: string; slug: string }) => e.slug === environment)) {
return res.status(400).send({
message: 'Failed to validate workspace environment'
});
}
const existingPermission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment
});
if (existingPermission) throw BadRequestError({ message: 'Failed to add workspace permission to service account due to already-existing ' });
const serviceAccountWorkspacePermission = await new ServiceAccountWorkspacePermission({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId),
environment,
read,
write
}).save();
const existingServiceAccountKey = await ServiceAccountKey.findOne({
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
});
if (!existingServiceAccountKey) {
await new ServiceAccountKey({
encryptedKey,
nonce,
sender: req.user._id,
serviceAccount: new Types.ObjectId(serviceAccountId),
workspace: new Types.ObjectId(workspaceId)
}).save();
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete workspace permission from service account with id [serviceAccountId]
* @param req
* @param res
*/
export const deleteServiceAccountWorkspacePermission = async (req: Request, res: Response) => {
const { serviceAccountWorkspacePermissionId } = req.params;
const serviceAccountWorkspacePermission = await ServiceAccountWorkspacePermission.findByIdAndDelete(serviceAccountWorkspacePermissionId);
if (serviceAccountWorkspacePermission) {
const { serviceAccount, workspace } = serviceAccountWorkspacePermission;
const count = await ServiceAccountWorkspacePermission.countDocuments({
serviceAccount,
workspace
});
if (count === 0) {
await ServiceAccountKey.findOneAndDelete({
serviceAccount,
workspace
});
}
}
return res.status(200).send({
serviceAccountWorkspacePermission
});
}
/**
* Delete service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const deleteServiceAccount = async (req: Request, res: Response) => {
const { serviceAccountId } = req.params;
const serviceAccount = await ServiceAccount.findByIdAndDelete(serviceAccountId);
if (serviceAccount) {
await ServiceAccountKey.deleteMany({
serviceAccount: serviceAccount._id
});
await ServiceAccountOrganizationPermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
await ServiceAccountWorkspacePermission.deleteMany({
serviceAccount: new Types.ObjectId(serviceAccountId)
});
}
return res.status(200).send({
serviceAccount
});
}
/**
* Return service account keys for service account with id [serviceAccountId]
* @param req
* @param res
* @returns
*/
export const getServiceAccountKeys = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const serviceAccountKeys = await ServiceAccountKey.find({
serviceAccount: req.serviceAccount._id,
...(workspaceId ? { workspace: new Types.ObjectId(workspaceId) } : {})
});
return res.status(200).send({
serviceAccountKeys
});
}

View File

@ -3,19 +3,11 @@ import { Request, Response } from 'express';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
User,
ServiceAccount,
ServiceTokenData
} from '../../models';
import { userHasWorkspaceAccess } from '../../ee/helpers/checkMembershipPermissions';
import {
PERMISSION_READ_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN
} from '../../variables';
import { ABILITY_READ } from '../../variables/organization';
import { getSaltRounds } from '../../config';
import { BadRequestError } from '../../utils/errors';
/**
* Return service token data associated with service token on request
@ -50,16 +42,7 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
}
*/
if (!(req.authData.authPayload instanceof ServiceTokenData)) throw BadRequestError({
message: 'Failed accepted client validation for service token data'
});
const serviceTokenData = await ServiceTokenData
.findById(req.authData.authPayload._id)
.select('+encryptedKey +iv +tag')
.populate('user');
return res.status(200).json(serviceTokenData);
return res.status(200).json(req.serviceTokenData);
}
/**
@ -70,60 +53,59 @@ export const getServiceTokenData = async (req: Request, res: Response) => {
* @returns
*/
export const createServiceTokenData = async (req: Request, res: Response) => {
let serviceTokenData;
let serviceToken, serviceTokenData;
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
try {
const {
name,
workspaceId,
environment,
encryptedKey,
iv,
tag,
expiresIn,
permissions
} = req.body;
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const hasAccess = await userHasWorkspaceAccess(req.user, workspaceId, environment, ABILITY_READ)
if (!hasAccess) {
throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" })
}
let expiresAt;
if (expiresIn) {
expiresAt = new Date()
const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, getSaltRounds());
const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user: req.user._id,
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create service token data'
});
}
let user, serviceAccount;
if (req.authData.authMode === AUTH_MODE_JWT && req.authData.authPayload instanceof User) {
user = req.authData.authPayload._id;
}
if (req.authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && req.authData.authPayload instanceof ServiceAccount) {
serviceAccount = req.authData.authPayload._id;
}
serviceTokenData = await new ServiceTokenData({
name,
workspace: workspaceId,
environment,
user,
serviceAccount,
lastUsed: new Date(),
expiresAt,
secretHash,
encryptedKey,
iv,
tag,
permissions
}).save();
// return service token data without sensitive data
serviceTokenData = await ServiceTokenData.findById(serviceTokenData._id);
if (!serviceTokenData) throw new Error('Failed to find service token data');
const serviceToken = `st.${serviceTokenData._id.toString()}.${secret}`;
return res.status(200).send({
serviceToken,
serviceTokenData
@ -137,11 +119,25 @@ export const createServiceTokenData = async (req: Request, res: Response) => {
* @returns
*/
export const deleteServiceTokenData = async (req: Request, res: Response) => {
const { serviceTokenDataId } = req.params;
let serviceTokenData;
try {
const { serviceTokenDataId } = req.params;
const serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
serviceTokenData = await ServiceTokenData.findByIdAndDelete(serviceTokenDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete service token data'
});
}
return res.status(200).send({
serviceTokenData
});
}
function UnauthorizedRequestError(arg0: { message: string; }) {
throw new Error('Function not implemented.');
}

View File

@ -8,7 +8,7 @@ import {
import { issueAuthTokens } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../../variables';
import request from '../../config/request';
import { getLoopsApiKey, getHttpsEnabled } from '../../config';
import { getNodeEnv, getLoopsApiKey } from '../../config';
/**
* Complete setting up user by adding their personal and auth information as part of the
@ -24,9 +24,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -38,9 +38,9 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
email: string;
firstName: string;
lastName: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
protectedKey: string;
protectedKeyIV: string;
protectedKeyTag: string;
publicKey: string;
encryptedPrivateKey: string;
encryptedPrivateKeyIV: string;
@ -48,11 +48,11 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
salt: string;
verifier: string;
organizationName: string;
} = req.body;
} = 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
@ -66,10 +66,10 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -108,7 +108,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
token = tokens.token;
// sending a welcome email to new users
if (await getLoopsApiKey()) {
if (getLoopsApiKey()) {
await request.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
@ -117,7 +117,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey())
"Authorization": "Bearer " + getLoopsApiKey()
},
});
}
@ -127,7 +127,7 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
secure: getNodeEnv() === 'production' ? true : false
});
} catch (err) {
Sentry.setUser(null);
@ -158,9 +158,9 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
email,
firstName,
lastName,
protectedKey,
protectedKeyIV,
protectedKeyTag,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -192,10 +192,10 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
userId: user._id.toString(),
firstName,
lastName,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
encryptionVersion: 2,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
encryptedPrivateKeyIV,
@ -232,7 +232,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
secure: getNodeEnv() === 'production' ? true : false
});
} catch (err) {
Sentry.setUser(null);
@ -241,7 +241,7 @@ export const completeAccountInvite = async (req: Request, res: Response) => {
message: 'Failed to complete account setup'
});
}
return res.status(200).send({
message: 'Successfully set up account',
user,

View File

@ -19,7 +19,7 @@ import {
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { TelemetryService, EventService } from '../../services';
import { getPostHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
interface V2PushSecret {
@ -48,7 +48,7 @@ interface V2PushSecret {
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
@ -95,8 +95,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
workspaceId
})
});
@ -123,7 +122,7 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
export const pullSecrets = async (req: Request, res: Response) => {
let secrets;
try {
const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = getPostHogClient();
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
@ -132,7 +131,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
if (req.user) {
userId = req.user._id.toString();
} else if (req.serviceTokenData) {
userId = req.serviceTokenData.user.toString();
userId = req.serviceTokenData.user._id
}
// validate environment
const workspaceEnvs = req.membership.workspace.environments;
@ -507,4 +506,5 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
message: 'Successfully changed autoCapitalization setting',
workspace
});
};
};

View File

@ -1,7 +0,0 @@
import * as secretsController from './secretsController';
import * as workspacesController from './workspacesController';
export {
secretsController,
workspacesController
}

View File

@ -1,183 +0,0 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import {
SecretService,
TelemetryService,
EventService
} from '../../services';
import { eventPushSecrets } from '../../events';
import { getAuthDataPayloadIdObj } from '../../utils/auth';
import { BadRequestError } from '../../utils/errors';
/**
* Get secrets for workspace with id [workspaceId] and environment
* [environment]
* @param req
* @param res
*/
export const getSecrets = async (req: Request, res: Response) => {
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const secrets = await SecretService.getSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment,
authData: req.authData
});
return res.status(200).send({
secrets
});
}
/**
* Get secret with name [secretName]
* @param req
* @param res
*/
export const getSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const workspaceId = req.query.workspaceId as string;
const environment = req.query.environment as string;
const type = req.query.type as 'shared' | 'personal' | undefined;
const secret = await SecretService.getSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData
});
return res.status(200).send({
secret
});
}
/**
* Create secret with name [secretName]
* @param req
* @param res
*/
export const createSecret = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} = req.body;
const secret = await SecretService.createSecret({
secretName,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
authData: req.authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
...((secretCommentCiphertext && secretCommentIV && secretCommentTag) ? {
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
} : {})
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({
secret: secretWithoutBlindIndex
});
}
/**
* Update secret with name [secretName]
* @param req
* @param res
*/
export const updateSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type,
secretValueCiphertext,
secretValueIV,
secretValueTag
} = req.body;
const secret = await SecretService.updateSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
return res.status(200).send({
secret
});
}
/**
* Delete secret with name [secretName]
* @param req
* @param res
*/
export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params;
const {
workspaceId,
environment,
type
} = req.body;
const { secret, secrets } = await SecretService.deleteSecret({
secretName,
workspaceId,
environment,
type,
authData: req.authData
});
await EventService.handleEvent({
event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId),
environment
})
});
return res.status(200).send({
secret
});
}

View File

@ -1,90 +0,0 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import { Secret } from '../../models';
import { SecretService } from'../../services';
/**
* Return whether or not all secrets in workspace with id [workspaceId]
* are blind-indexed
* @param req
* @param res
* @returns
*/
export const getWorkspaceBlindIndexStatus = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const secretsWithoutBlindIndex = await Secret.countDocuments({
workspace: new Types.ObjectId(workspaceId),
secretBlindIndex: {
$exists: false
}
});
return res.status(200).send(secretsWithoutBlindIndex === 0);
}
/**
* Get all secrets for workspace with id [workspaceId]
*/
export const getWorkspaceSecrets = async (req: Request, res: Response) => {
const { workspaceId } = req.params;
const secrets = await Secret.find({
workspace: new Types.ObjectId (workspaceId)
});
return res.status(200).send({
secrets
});
}
/**
* Update blind indices for secrets in workspace with id [workspaceId]
* @param req
* @param res
*/
export const nameWorkspaceSecrets = async (req: Request, res: Response) => {
interface SecretToUpdate {
secretName: string;
_id: string;
}
const { workspaceId } = req.params;
const {
secretsToUpdate
}: {
secretsToUpdate: SecretToUpdate[];
} = req.body;
// get secret blind index salt
const salt = await SecretService.getSecretBlindIndexSalt({
workspaceId: new Types.ObjectId(workspaceId)
});
// update secret blind indices
const operations = await Promise.all(
secretsToUpdate.map(async (secretToUpdate: SecretToUpdate) => {
const secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({
secretName: secretToUpdate.secretName,
salt
});
return ({
updateOne: {
filter: {
_id: new Types.ObjectId(secretToUpdate._id)
},
update: {
secretBlindIndex
}
}
});
})
);
await Secret.bulkWrite(operations);
return res.status(200).send({
message: 'Successfully named workspace secrets'
});
}

View File

@ -2,8 +2,7 @@ import { Request, Response } from "express";
import { Membership, Workspace } from "../../../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 { ABILITY_READ, ABILITY_WRITE, ADMIN, MEMBER } from "../../../variables/organization";
import { Builder } from "builder-pattern"
import _ from "lodash";
@ -11,7 +10,7 @@ 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)) {
if (!permission.ability || !permission.environmentSlug || ![ABILITY_READ, ABILITY_WRITE].includes(permission.ability)) {
throw BadRequestError({ message: "One or more required fields are missing from the request or have incorrect type" })
}

View File

@ -146,7 +146,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
const oldSecretVersion = await SecretVersion.findOne({
secret: secretId,
version
}).select('+secretBlindIndex')
});
if (!oldSecretVersion) throw new Error('Failed to find secret version');
@ -155,7 +155,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -175,7 +174,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -199,7 +197,6 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
user,
environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -210,7 +207,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace
workspaceId: secret.workspace.toString()
});
} catch (err) {

View File

@ -15,10 +15,16 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
secretSnapshot = await SecretSnapshot
.findById(secretSnapshotId)
.populate('secretVersions');
.populate({
path: 'secretVersions',
populate: {
path: 'tags',
model: 'Tag',
}
});
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);

View File

@ -12,7 +12,7 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
export const handleWebhook = async (req: Request, res: Response) => {
let event;
try {
const stripe = new Stripe(await getStripeSecretKey(), {
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
@ -21,7 +21,7 @@ export const handleWebhook = async (req: Request, res: Response) => {
event = stripe.webhooks.constructEvent(
req.body,
sig,
await getStripeWebhookSecret()
getStripeWebhookSecret()
);
} catch (err) {
Sentry.setUser({ email: req.user.email });

View File

@ -173,7 +173,6 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
}
}
*/
let secrets;
try {
const { workspaceId } = req.params;
@ -183,10 +182,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
version
}).populate<{ secretVersions: ISecretVersion[]}>({
path: 'secretVersions',
select: '+secretBlindIndex'
});
}).populate<{ secretVersions: ISecretVersion[]}>('secretVersions');
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
@ -226,7 +222,6 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -245,7 +240,6 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
type,
user,
environment,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -263,7 +257,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
);
// add secret versions
const secretV = await SecretVersion.insertMany(
await SecretVersion.insertMany(
secrets.map(({
_id,
version,
@ -271,7 +265,6 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -289,7 +282,6 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
user,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
@ -312,7 +304,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId)
workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
@ -426,7 +418,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
.skip(offset)
.limit(limit)
.populate('actions')
.populate('user serviceAccount serviceTokenData');
.populate('user');
} catch (err) {
Sentry.setUser({ email: req.user.email });

View File

@ -1,3 +1,4 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { Action } from '../models';
import {
@ -23,37 +24,39 @@ import {
const createActionUpdateSecret = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
const action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
let action;
try {
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
secretIds,
n: 2
}))
.map((s) => ({
oldSecretVersion: s.versions[0]._id,
newSecretVersion: s.versions[1]._id
}));
action = await new Action({
name,
user: userId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create update secret action');
}
return action;
}
@ -69,66 +72,68 @@ const createActionUpdateSecret = async ({
const createActionSecret = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[];
}) => {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
const latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
}))
.map((s) => ({
newSecretVersion: s.versionId
}));
const action = await new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
let action;
try {
// case: action is adding, deleting, or reading secrets
// -> add new secret versions
const latestSecretVersions = (await getLatestSecretVersionIds({
secretIds
}))
.map((s) => ({
newSecretVersion: s.versionId
}));
action = await new Action({
name,
user: userId,
workspace: workspaceId,
payload: {
secretVersions: latestSecretVersions
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action create/read/delete secret action');
}
return action;
}
/**
* Create an (audit) action for client with id [userId],
* [serviceAccountId], or [serviceTokenDataId]
* Create an (audit) action for user with id [userId]
* @param {Object} obj
* @param {String} obj.name - name of action
* @param {String} obj.userId - id of user associated with action
* @returns
*/
const createActionClient = ({
const createActionUser = ({
name,
userId,
serviceAccountId,
serviceTokenDataId
userId
}: {
name: string;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
userId: Types.ObjectId;
}) => {
const action = new Action({
name,
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId
}).save();
let action;
try {
action = new Action({
name,
user: userId
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create user action');
}
return action;
}
@ -144,47 +149,49 @@ const createActionClient = ({
const createActionHelper = async ({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds,
}: {
name: string;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) => {
let action;
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionClient({
name,
userId
});
break;
case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS:
case ACTION_DELETE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionSecret({
name,
userId,
workspaceId,
secretIds
});
break;
case ACTION_UPDATE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionUpdateSecret({
name,
userId,
workspaceId,
secretIds
});
break;
try {
switch (name) {
case ACTION_LOGIN:
case ACTION_LOGOUT:
action = await createActionUser({
name,
userId
});
break;
case ACTION_ADD_SECRETS:
case ACTION_READ_SECRETS:
case ACTION_DELETE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionSecret({
name,
userId,
workspaceId,
secretIds
});
break;
case ACTION_UPDATE_SECRETS:
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
action = await createActionUpdateSecret({
name,
userId,
workspaceId,
secretIds
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action');
}
return action;
@ -192,4 +199,4 @@ const createActionHelper = async ({
export {
createActionHelper
};
};

View File

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

View File

@ -1,9 +1,9 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Log,
IAction
} from '../models';
/**
* Create an (audit) log
* @param {Object} obj
@ -16,35 +16,36 @@ import {
*/
const createLogHelper = async ({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
ipAddress: string;
}) => {
const log = await new Log({
user: userId,
serviceAccount: serviceAccountId,
serviceTokenData: serviceTokenDataId,
workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,
channel,
ipAddress
}).save();
let log;
try {
log = await new Log({
user: userId,
workspace: workspaceId ?? undefined,
actionNames: actions.map((a) => a.name),
actions,
channel,
ipAddress
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create log');
}
return log;
}
export {
createLogHelper
}
}

View File

@ -1,6 +1,14 @@
import { Types } from "mongoose";
import { Secret, ISecret } from "../../models";
import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import {
Secret,
ISecret
} from '../../models';
import {
SecretSnapshot,
SecretVersion,
ISecretVersion
} from '../models';
/**
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
@ -11,53 +19,56 @@ import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
* @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/
const takeSecretSnapshotHelper = async ({
workspaceId,
workspaceId
}: {
workspaceId: Types.ObjectId;
workspaceId: string;
}) => {
const secretIds = (
await Secret.find(
{
workspace: workspaceId,
},
"_id"
)
).map((s) => s._id);
const latestSecretVersions = (
await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
},
{
$group: {
_id: "$secret",
version: { $max: "$version" },
versionId: { $max: "$_id" }, // secret version id
},
},
{
$sort: { version: -1 },
},
]).exec()
).map((s) => s.versionId);
let secretSnapshot;
try {
const secretIds = (await Secret.find({
workspace: workspaceId
}, '_id')).map((s) => s._id);
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId,
}).sort({ version: -1 });
const latestSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // secret version id
}
},
{
$sort: { version: -1 }
}
])
.exec())
.map((s) => s.versionId);
const secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions,
}).save();
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId
}).sort({ version: -1 });
return secretSnapshot;
};
secretSnapshot = await new SecretSnapshot({
workspace: workspaceId,
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot');
}
return secretSnapshot;
}
/**
* Add secret versions [secretVersions] to the SecretVersion collection.
@ -66,79 +77,93 @@ const takeSecretSnapshotHelper = async ({
* @returns {SecretVersion[]} newSecretVersions - new secret versions
*/
const addSecretVersionsHelper = async ({
secretVersions,
secretVersions
}: {
secretVersions: ISecretVersion[];
secretVersions: ISecretVersion[]
}) => {
const newSecretVersions = await SecretVersion.insertMany(secretVersions);
let newSecretVersions;
try {
newSecretVersions = await SecretVersion.insertMany(secretVersions);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(`Failed to add secret versions [err=${err}]`);
}
return newSecretVersions;
};
return newSecretVersions;
}
const markDeletedSecretVersionsHelper = async ({
secretIds,
secretIds
}: {
secretIds: Types.ObjectId[];
secretIds: Types.ObjectId[];
}) => {
await SecretVersion.updateMany(
{
secret: { $in: secretIds },
},
{
isDeleted: true,
},
{
new: true,
}
);
};
try {
await SecretVersion.updateMany({
secret: { $in: secretIds }
}, {
isDeleted: true
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to mark secret versions as deleted');
}
}
/**
* Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions.
*/
const initSecretVersioningHelper = async () => {
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
try {
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: "secretversions",
localField: "_id",
foreignField: "secret",
as: "versions",
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map(
(s, idx) =>
new SecretVersion({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment,
})
),
});
}
};
const unversionedSecrets: ISecret[] = await Secret.aggregate([
{
$lookup: {
from: 'secretversions',
localField: '_id',
foreignField: 'secret',
as: 'versions',
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
if (unversionedSecrets.length > 0) {
await addSecretVersionsHelper({
secretVersions: unversionedSecrets.map((s, idx) => ({
...s,
secret: s._id,
version: s.version ? s.version : 1,
isDeleted: false,
workspace: s.workspace,
environment: s.environment
}))
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to ensure that secrets are versioned');
}
}
export {
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper,
};
takeSecretSnapshotHelper,
addSecretVersionsHelper,
markDeletedSecretVersionsHelper,
initSecretVersioningHelper
}

View File

@ -1,3 +1,4 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { SecretVersion } from '../models';
@ -12,32 +13,41 @@ const getLatestSecretVersionIds = async ({
}: {
secretIds: Types.ObjectId[];
}) => {
interface LatestSecretVersionId {
_id: Types.ObjectId;
version: number;
versionId: Types.ObjectId;
}
const latestSecretVersionIds = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
let latestSecretVersionIds: LatestSecretVersionId[];
try {
latestSecretVersionIds = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // id of latest secret version
}
},
{
$sort: { version: -1 }
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // id of latest secret version
}
},
{
$sort: { version: -1 }
}
])
.exec());
])
.exec());
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest secret versions');
}
return latestSecretVersionIds;
}
@ -56,32 +66,40 @@ const getLatestNSecretSecretVersionIds = async ({
secretIds: Types.ObjectId[];
n: number;
}) => {
// TODO: optimize query
const latestNSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
let latestNSecretVersions;
try {
latestNSecretVersions = (await SecretVersion.aggregate([
{
$match: {
secret: {
$in: secretIds,
},
},
},
{
$sort: { version: -1 },
},
},
{
$sort: { version: -1 },
},
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
{
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
},
},
},
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", n] },
},
}
]));
{
$project: {
_id: 0,
secret: "$_id",
versions: { $slice: ["$versions", n] },
},
}
]));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest n secret versions');
}
return latestNSecretVersions;
}

View File

@ -15,28 +15,32 @@ import {
const requireSecretSnapshotAuth = ({
acceptedRoles,
}: {
acceptedRoles: Array<'admin' | 'member'>;
acceptedRoles: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
}));
}
await validateMembership({
userId: req.user._id,
workspaceId: secretSnapshot.workspace,
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
try {
const { secretSnapshotId } = req.params;
const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId);
if (!secretSnapshot) {
return next(SecretSnapshotNotFoundError({
message: 'Failed to find secret snapshot'
}));
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: secretSnapshot.workspace.toString(),
acceptedRoles
});
req.secretSnapshot = secretSnapshot as any;
next();
next();
} catch (err) {
return next(UnauthorizedRequestError({ message: 'Unable to authenticate secret snapshot' }));
}
}
}

View File

@ -11,8 +11,6 @@ import {
export interface IAction {
name: string;
user?: Types.ObjectId,
serviceAccount?: Types.ObjectId,
serviceTokenData?: Types.ObjectId,
workspace?: Types.ObjectId,
payload?: {
secretVersions?: Types.ObjectId[]
@ -35,15 +33,8 @@ const actionSchema = new Schema<IAction>(
},
user: {
type: Schema.Types.ObjectId,
ref: 'User'
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
ref: 'User',
required: true
},
workspace: {
type: Schema.Types.ObjectId,

View File

@ -11,8 +11,6 @@ import {
export interface ILog {
_id: Types.ObjectId;
user?: Types.ObjectId;
serviceAccount?: Types.ObjectId;
serviceTokenData?: Types.ObjectId;
workspace?: Types.ObjectId;
actionNames: string[];
actions: Types.ObjectId[];
@ -26,14 +24,6 @@ const logSchema = new Schema<ILog>(
type: Schema.Types.ObjectId,
ref: 'User'
},
serviceAccount: {
type: Schema.Types.ObjectId,
ref: 'ServiceAccount'
},
serviceTokenData: {
type: Schema.Types.ObjectId,
ref: 'ServiceTokenData'
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace'

View File

@ -5,7 +5,6 @@ import {
} from '../../variables';
export interface ISecretVersion {
_id: Types.ObjectId;
secret: Types.ObjectId;
version: number;
workspace: Types.ObjectId; // new
@ -13,13 +12,13 @@ export interface ISecretVersion {
user?: Types.ObjectId; // new
environment: string; // new
isDeleted: boolean;
secretBlindIndex?: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
tags?: string[];
}
const secretVersionSchema = new Schema<ISecretVersion>(
@ -58,10 +57,6 @@ const secretVersionSchema = new Schema<ISecretVersion>(
default: false,
required: true
},
secretBlindIndex: {
type: String,
select: false
},
secretKeyCiphertext: {
type: String,
required: true
@ -85,7 +80,12 @@ const secretVersionSchema = new Schema<ISecretVersion>(
secretValueTag: {
type: String, // symmetric
required: true
}
},
tags: {
ref: 'Tag',
type: [Schema.Types.ObjectId],
default: []
},
},
{
timestamps: true

View File

@ -7,12 +7,7 @@ import {
} from '../../../middleware';
import { query, param, body } from 'express-validator';
import { secretController } from '../../controllers/v1';
import {
ADMIN,
MEMBER,
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../../../variables';
import { ADMIN, MEMBER } from '../../../variables';
router.get(
'/:secretId/secret-versions',
@ -20,8 +15,7 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS]
acceptedRoles: [ADMIN, MEMBER]
}),
param('secretId').exists().trim(),
query('offset').exists().isInt(),
@ -36,8 +30,7 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireSecretAuth({
acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_READ_SECRETS, PERMISSION_WRITE_SECRETS]
acceptedRoles: [ADMIN, MEMBER]
}),
param('secretId').exists().trim(),
body('version').exists().isInt(),

View File

@ -15,8 +15,7 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
@ -31,8 +30,7 @@ router.get(
acceptedAuthModes: ['jwt']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
validateRequest,
@ -45,8 +43,7 @@ router.post(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
body('version').exists().isInt(),
@ -60,8 +57,7 @@ router.get(
acceptedAuthModes: ['jwt', 'apiKey']
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: 'params'
acceptedRoles: [ADMIN, MEMBER]
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),

View File

@ -26,16 +26,12 @@ class EELogService {
*/
static async createLog({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
ipAddress
}: {
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
actions: IAction[];
channel: string;
@ -44,8 +40,6 @@ class EELogService {
if (!EELicenseService.isLicenseValid) return null;
return await createLogHelper({
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
actions,
channel,
@ -65,23 +59,17 @@ class EELogService {
static async createAction({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
}: {
name: string;
userId?: Types.ObjectId;
serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId;
userId: Types.ObjectId;
workspaceId?: Types.ObjectId;
secretIds?: Types.ObjectId[];
}) {
return await createActionHelper({
name,
userId,
serviceAccountId,
serviceTokenDataId,
workspaceId,
secretIds
});

View File

@ -25,7 +25,7 @@ class EESecretService {
static async takeSecretSnapshot({
workspaceId
}: {
workspaceId: Types.ObjectId;
workspaceId: string;
}) {
if (!EELicenseService.isLicenseValid) return;
return await takeSecretSnapshotHelper({ workspaceId });

View File

@ -1,4 +1,3 @@
import { Types } from 'mongoose';
import {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
@ -23,16 +22,13 @@ interface PushSecret {
* @returns
*/
const eventPushSecrets = ({
workspaceId,
environment
workspaceId
}: {
workspaceId: Types.ObjectId;
environment?: string;
workspaceId: string;
}) => {
return ({
name: EVENT_PUSH_SECRETS,
workspaceId,
environment,
payload: {
}

View File

@ -1,17 +1,15 @@
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import {
IUser,
User,
ServiceTokenData,
ServiceAccount,
APIKeyData
} from '../models';
import {
AccountNotFoundError,
ServiceTokenDataNotFoundError,
ServiceAccountNotFoundError,
APIKeyDataNotFoundError,
UnauthorizedRequestError,
BadRequestError
@ -22,12 +20,6 @@ import {
getJwtRefreshLifetime,
getJwtRefreshSecret
} from '../config';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
*
@ -45,7 +37,7 @@ const validateAuthMode = ({
const apiKey = headers['x-api-key'];
const authHeader = headers['authorization'];
let authMode, authTokenValue;
let authTokenType, authTokenValue;
if (apiKey === undefined && authHeader === undefined) {
// case: no auth or X-API-KEY header present
throw BadRequestError({ message: 'Missing Authorization or X-API-KEY in request header.' });
@ -53,7 +45,7 @@ const validateAuthMode = ({
if (typeof apiKey === 'string') {
// case: treat request authentication type as via X-API-KEY (i.e. API Key)
authMode = AUTH_MODE_API_KEY;
authTokenType = 'apiKey';
authTokenValue = apiKey;
}
@ -69,24 +61,20 @@ const validateAuthMode = ({
switch (tokenValue.split('.', 1)[0]) {
case 'st':
authMode = AUTH_MODE_SERVICE_TOKEN;
break;
case 'sa':
authMode = AUTH_MODE_SERVICE_ACCOUNT;
authTokenType = 'serviceToken';
break;
default:
authMode = AUTH_MODE_JWT;
authTokenType = 'jwt';
}
authTokenValue = tokenValue;
}
if (!authMode || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!authTokenType || !authTokenValue) throw BadRequestError({ message: 'Missing valid Authorization or X-API-KEY in request header.' });
if (!acceptedAuthModes.includes(authMode)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
if (!acceptedAuthModes.includes(authTokenType)) throw BadRequestError({ message: 'The provided authentication type is not supported.' });
return ({
authMode,
authTokenType,
authTokenValue
});
}
@ -102,17 +90,25 @@ const getAuthUserPayload = async ({
}: {
authTokenValue: string;
}) => {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, await getJwtAuthSecret())
);
let user;
try {
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(authTokenValue, getJwtAuthSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user) throw AccountNotFoundError({ message: 'Failed to find User' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
if (!user?.publicKey) throw UnauthorizedRequestError({ message: 'Failed to authenticate User with partially set up account' });
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate JWT token'
});
}
return user;
}
@ -128,70 +124,45 @@ const getAuthSTDPayload = async ({
}: {
authTokenValue: string;
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
let serviceTokenData;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
let serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
// TODO: optimize double query
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt');
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
if (!serviceTokenData) {
throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} else if (serviceTokenData?.expiresAt && new Date(serviceTokenData.expiresAt) < new Date()) {
// case: service token expired
await ServiceTokenData.findByIdAndDelete(serviceTokenData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired service token'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findById(TOKEN_IDENTIFIER)
.select('+encryptedKey +iv +tag')
.populate<{user: IUser}>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired service token'
message: 'Failed to authenticate service token'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, serviceTokenData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate service token'
});
serviceTokenData = await ServiceTokenData
.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
})
.select('+encryptedKey +iv +tag');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({ message: 'Failed to find service token data' });
return serviceTokenData;
}
/**
* Return service account access key payload
* @param {Object} obj
* @param {String} obj.authTokenValue - service account access token value
* @returns {ServiceAccount} serviceAccount
*/
const getAuthSAAKPayload = async ({
authTokenValue
}: {
authTokenValue: string;
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
const serviceAccount = await ServiceAccount.findById(
Buffer.from(TOKEN_IDENTIFIER, 'base64').toString('hex')
).select('+secretHash');
if (!serviceAccount) {
throw ServiceAccountNotFoundError({ message: 'Failed to find service account' });
}
const result = await bcrypt.compare(TOKEN_SECRET, serviceAccount.secretHash);
if (!result) throw UnauthorizedRequestError({
message: 'Failed to authenticate service account access key'
});
return serviceAccount;
}
/**
* Return API key data payload corresponding to API key [authTokenValue]
* @param {Object} obj
@ -203,44 +174,33 @@ const getAuthAPIKeyPayload = async ({
}: {
authTokenValue: string;
}) => {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
let user;
try {
const [_, TOKEN_IDENTIFIER, TOKEN_SECRET] = <[string, string, string]>authTokenValue.split('.', 3);
let apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate<{ user: IUser }>('user', '+publicKey');
const apiKeyData = await APIKeyData
.findById(TOKEN_IDENTIFIER, '+secretHash +expiresAt')
.populate('user', '+publicKey');
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired API key'
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
} else if (apiKeyData?.expiresAt && new Date(apiKeyData.expiresAt) < new Date()) {
// case: API key expired
await APIKeyData.findByIdAndDelete(apiKeyData._id);
throw UnauthorizedRequestError({
message: 'Failed to authenticate expired API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
}
const isMatch = await bcrypt.compare(TOKEN_SECRET, apiKeyData.secretHash);
if (!isMatch) throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
apiKeyData = await APIKeyData.findOneAndUpdate({
_id: new Types.ObjectId(TOKEN_IDENTIFIER)
}, {
lastUsed: new Date()
}, {
new: true
});
if (!apiKeyData) {
throw APIKeyDataNotFoundError({ message: 'Failed to find API key data' });
}
const user = await User.findById(apiKeyData.user).select('+publicKey');
if (!user) {
throw AccountNotFoundError({
message: 'Failed to find user'
user = apiKeyData.user;
} catch (err) {
throw UnauthorizedRequestError({
message: 'Failed to authenticate API key'
});
}
@ -256,23 +216,30 @@ const getAuthAPIKeyPayload = async ({
* @return {String} obj.refreshToken - issued refresh token
*/
const issueAuthTokens = async ({ userId }: { userId: string }) => {
let token: string;
let refreshToken: string;
try {
// issue tokens
token = createToken({
payload: {
userId
},
expiresIn: getJwtAuthLifetime(),
secret: getJwtAuthSecret()
});
// issue tokens
const token = createToken({
payload: {
userId
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
});
const refreshToken = createToken({
payload: {
userId
},
expiresIn: await getJwtRefreshLifetime(),
secret: await getJwtRefreshSecret()
});
refreshToken = createToken({
payload: {
userId
},
expiresIn: getJwtRefreshLifetime(),
secret: getJwtRefreshSecret()
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to issue tokens');
}
return {
token,
@ -286,14 +253,19 @@ const issueAuthTokens = async ({ userId }: { userId: string }) => {
* @param {String} obj.userId - id of user whose tokens are cleared.
*/
const clearTokens = async ({ userId }: { userId: string }): Promise<void> => {
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
}, {
$inc: {
refreshVersion: 1
}
});
try {
// increment refreshVersion on user by 1
User.findOneAndUpdate({
_id: userId
}, {
$inc: {
refreshVersion: 1
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
};
/**
@ -313,16 +285,21 @@ const createToken = ({
expiresIn: string | number;
secret: string;
}) => {
return jwt.sign(payload, secret, {
expiresIn
});
try {
return jwt.sign(payload, secret, {
expiresIn
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create a token');
}
};
export {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthSAAKPayload,
getAuthAPIKeyPayload,
createToken,
issueAuthTokens,

View File

@ -1,142 +1,58 @@
import { Types } from "mongoose";
import * as Sentry from '@sentry/node';
import {
Bot,
BotKey,
Secret,
ISecret,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
} from "../models";
import {
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric,
} from "../utils/crypto";
import {
SECRET_SHARED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
} from "../variables";
import { getEncryptionKey } from "../config";
import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors";
import { validateMembership } from "../helpers/membership";
import { validateUserClientForWorkspace } from "../helpers/user";
import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount";
/**
* Validate authenticated clients for bot with id [botId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.botId - id of bot to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForBot = async ({
authData,
botId,
acceptedRoles,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
botId: Types.ObjectId;
acceptedRoles: Array<"admin" | "member">;
}) => {
const bot = await Bot.findById(botId);
if (!bot) throw BotNotFoundError();
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: bot.workspace,
});
return bot;
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for bot",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles,
});
return bot;
}
throw BotNotFoundError({
message: "Failed client authorization for bot",
});
};
Bot,
BotKey,
Secret,
ISecret,
IUser
} from '../models';
import {
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
import { SECRET_SHARED } from '../variables';
import { getEncryptionKey } from '../config';
/**
* Create an inactive bot with name [name] for workspace with id [workspaceId]
* @param {Object} obj
* @param {Object} obj
* @param {String} obj.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to
*/
const createBot = async ({
name,
workspaceId,
}: {
name: string;
workspaceId: Types.ObjectId;
}) => {
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey,
key: await getEncryptionKey(),
});
const bot = await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag,
}).save();
workspaceId,
}: {
name: string;
workspaceId: string;
}) => {
let bot;
try {
const { publicKey, privateKey } = generateKeyPair();
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext: privateKey,
key: getEncryptionKey()
});
return bot;
};
bot = await new Bot({
name,
workspace: workspaceId,
isActive: false,
publicKey,
encryptedPrivateKey: ciphertext,
iv,
tag
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create bot');
}
return bot;
}
/**
* Return decrypted secrets for workspace with id [workspaceId]
@ -146,105 +62,125 @@ const createBot = async ({
* @param {String} obj.environment - environment
*/
const getSecretsHelper = async ({
workspaceId,
environment,
workspaceId,
environment
}: {
workspaceId: Types.ObjectId;
environment: string;
workspaceId: string;
environment: string;
}) => {
const content = {} as any;
const key = await getKey({ workspaceId: workspaceId.toString() });
const secrets = await Secret.find({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
});
const content = {} as any;
try {
const key = await getKey({ workspaceId });
const secrets = await Secret.find({
workspace: workspaceId,
environment,
type: SECRET_SHARED
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key,
});
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key,
});
content[secretKey] = secretValue;
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get secrets');
}
content[secretKey] = secretValue;
});
return content;
};
return content;
}
/**
* Return bot's copy of the workspace key for workspace
* Return bot's copy of the workspace key for workspace
* with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
const botKey = await BotKey.findOne({
workspace: workspaceId,
}).populate<{ sender: IUser }>("sender", "publicKey");
if (!botKey) throw new Error("Failed to find bot key");
const bot = await Bot.findOne({
workspace: workspaceId,
}).select("+encryptedPrivateKey +iv +tag");
if (!bot) throw new Error("Failed to find bot");
if (!bot.isActive) throw new Error("Bot is not active");
const privateKeyBot = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: await getEncryptionKey(),
});
const key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot,
});
return key;
};
let key;
try {
const botKey = await BotKey.findOne({
workspace: workspaceId
}).populate<{ sender: IUser }>('sender', 'publicKey');
if (!botKey) throw new Error('Failed to find bot key');
const bot = await Bot.findOne({
workspace: workspaceId
}).select('+encryptedPrivateKey +iv +tag');
if (!bot) throw new Error('Failed to find bot');
if (!bot.isActive) throw new Error('Bot is not active');
const privateKeyBot = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: getEncryptionKey()
});
key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get workspace key');
}
return key;
}
/**
* Return symmetrically encrypted [plaintext] using the
* key for workspace with id [workspaceId]
* key for workspace with id [workspaceId]
* @param {Object} obj1
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt
*/
const encryptSymmetricHelper = async ({
workspaceId,
plaintext,
workspaceId,
plaintext
}: {
workspaceId: Types.ObjectId;
plaintext: string;
workspaceId: string;
plaintext: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext,
key,
});
return {
ciphertext,
iv,
tag,
};
};
try {
const key = await getKey({ workspaceId });
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext,
key
});
return ({
ciphertext,
iv,
tag
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption with bot');
}
}
/**
* Return symmetrically decrypted [ciphertext] using the
* key for workspace with id [workspaceId]
@ -255,31 +191,39 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.tag - tag
*/
const decryptSymmetricHelper = async ({
workspaceId,
ciphertext,
iv,
tag,
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;
}) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
const plaintext = decryptSymmetric({
workspaceId,
ciphertext,
iv,
tag,
key,
});
return plaintext;
};
tag
}: {
workspaceId: string;
ciphertext: string;
iv: string;
tag: string;
}) => {
let plaintext;
try {
const key = await getKey({ workspaceId });
const plaintext = decryptSymmetric({
ciphertext,
iv,
tag,
key
});
return plaintext;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption with bot');
}
return plaintext;
}
export {
validateClientForBot,
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper,
};
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
}

View File

@ -1,6 +1,5 @@
import mongoose from 'mongoose';
import { EESecretService } from '../ee/services';
import { SecretService } from '../services';
import { getLogger } from '../utils/logger';
/**
@ -20,12 +19,11 @@ const initDatabaseHelper = async ({
// allow empty strings to pass the required validator
mongoose.Schema.Types.String.checkRequired(v => typeof v === 'string');
(await getLogger("database")).info("Database connection established");
getLogger("database").info("Database connection established");
await EESecretService.initSecretVersioning();
await SecretService.initSecretBlindIndexDataHelper();
} catch (err) {
(await getLogger("database")).error(`Unable to establish Database connection due to the error.\n${err}`);
getLogger("database").error(`Unable to establish Database connection due to the error.\n${err}`);
}
return mongoose.connection;

View File

@ -1,13 +1,12 @@
import { Types } from "mongoose";
import { Bot, IBot } from "../models";
import { EVENT_PUSH_SECRETS } from "../variables";
import { IntegrationService } from "../services";
import { Bot, IBot } from '../models';
import * as Sentry from '@sentry/node';
import { EVENT_PUSH_SECRETS } from '../variables';
import { IntegrationService } from '../services';
interface Event {
name: string;
workspaceId: Types.ObjectId;
environment?: string;
payload: any;
name: string;
workspaceId: string;
payload: any;
}
/**
@ -18,25 +17,35 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event)
*/
const handleEventHelper = async ({ event }: { event: Event }) => {
const { workspaceId, environment } = event;
const handleEventHelper = async ({
event
}: {
event: Event;
}) => {
const { workspaceId } = event;
// TODO: moduralize bot check into separate function
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
if (!bot) return;
try {
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
}
// TODO: moduralize bot check into separate function
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true,
});
if (!bot) return;
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId,
environment,
});
break;
}
};
export { handleEventHelper };
export {
handleEventHelper
}

View File

@ -1,42 +1,17 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Bot,
Integration,
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
import {
UnauthorizedRequestError,
IntegrationAuthNotFoundError,
IntegrationNotFoundError
} from '../utils/errors';
import { UnauthorizedRequestError } from '../utils/errors';
import RequestError from '../utils/requestError';
import {
validateClientForIntegrationAuth
} from '../helpers/integrationAuth';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import { IntegrationService } from '../services';
interface Update {
workspace: string;
@ -45,84 +20,6 @@ interface Update {
accountId?: string;
}
/**
* Validate authenticated clients for integration with id [integrationId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.integrationId - id of integration to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForIntegration = async ({
authData,
integrationId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
integrationId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const integration = await Integration.findById(integrationId);
if (!integration) throw IntegrationNotFoundError();
const integrationAuth = await IntegrationAuth
.findById(integration.integrationAuth)
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) throw IntegrationAuthNotFoundError();
const accessToken = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id
})).accessToken;
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integration.workspace,
acceptedRoles
});
return ({ integration, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: integration.workspace
});
return ({ integration, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for integration'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integration.workspace,
acceptedRoles
});
return ({ integration, accessToken });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for integration'
});
}
/**
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
* named [integration]
@ -217,19 +114,14 @@ const handleOAuthExchangeHelper = async ({
* @param {Object} obj.workspaceId - id of workspace
*/
const syncIntegrationsHelper = async ({
workspaceId,
environment
workspaceId
}: {
workspaceId: Types.ObjectId;
environment?: string;
workspaceId: string;
}) => {
let integrations;
try {
integrations = await Integration.find({
workspace: workspaceId,
...(environment ? {
environment
} : {}),
isActive: true,
app: { $ne: null }
});
@ -239,7 +131,7 @@ const syncIntegrationsHelper = async ({
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace,
workspaceId: integration.workspace.toString(),
environment: integration.environment
});
@ -248,7 +140,7 @@ const syncIntegrationsHelper = async ({
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth
integrationAuthId: integration.integrationAuth.toString()
});
// sync secrets to integration
@ -256,7 +148,7 @@ const syncIntegrationsHelper = async ({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessId: access.accessId,
accessToken: access.accessToken
});
}
@ -275,7 +167,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let refreshToken;
try {
@ -286,7 +178,7 @@ const syncIntegrationsHelper = async ({
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
workspaceId: integrationAuth.workspace.toString(),
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
@ -312,7 +204,7 @@ const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessId;
let accessToken;
try {
@ -323,7 +215,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
workspaceId: integrationAuth.workspace.toString(),
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
@ -345,7 +237,7 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
workspaceId: integrationAuth.workspace.toString(),
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
@ -391,7 +283,7 @@ const setIntegrationAuthRefreshHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
workspaceId: integrationAuth.workspace.toString(),
plaintext: refreshToken
});
@ -440,14 +332,14 @@ const setIntegrationAuthAccessHelper = async ({
if (!integrationAuth) throw new Error('Failed to find integration auth');
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessId
});
}
@ -475,11 +367,10 @@ const setIntegrationAuthAccessHelper = async ({
}
export {
validateClientForIntegration,
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
}
}

View File

@ -1,108 +0,0 @@
import { Types } from 'mongoose';
import {
IntegrationAuth,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
IWorkspace
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
IntegrationAuthNotFoundError,
UnauthorizedRequestError
} from '../utils/errors';
import { IntegrationService } from '../services';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
/**
* Validate authenticated clients for integration authorization with id [integrationAuthId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.integrationAuthId - id of integration authorization to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForIntegrationAuth = async ({
authData,
integrationAuthId,
acceptedRoles,
attachAccessToken
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
integrationAuthId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
attachAccessToken?: boolean;
}) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) throw IntegrationAuthNotFoundError();
let accessToken;
if (attachAccessToken) {
accessToken = (await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id
})).accessToken;
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: integrationAuth.workspace._id
});
return ({ integrationAuth, accessToken });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for integration authorization'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: integrationAuth.workspace._id,
acceptedRoles
});
return ({ integrationAuth, accessToken });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for integration authorization'
});
}
export {
validateClientForIntegrationAuth
};

View File

@ -1,3 +1,4 @@
import * as Sentry from '@sentry/node';
import { Key, IKey } from '../models';
interface Key {
@ -26,30 +27,36 @@ const pushKeys = async ({
workspaceId: string;
keys: Key[];
}): Promise<void> => {
// filter out already-inserted keys
const keysSet = new Set(
(
await Key.find(
{
workspace: workspaceId
},
'receiver'
)
).map((k: IKey) => k.receiver.toString())
);
try {
// filter out already-inserted keys
const keysSet = new Set(
(
await Key.find(
{
workspace: workspaceId
},
'receiver'
)
).map((k: IKey) => k.receiver.toString())
);
keys = keys.filter((key) => !keysSet.has(key.userId));
keys = keys.filter((key) => !keysSet.has(key.userId));
// add new shared keys only
await Key.insertMany(
keys.map((k) => ({
encryptedKey: k.encryptedKey,
nonce: k.nonce,
sender: userId,
receiver: k.userId,
workspace: workspaceId
}))
);
// add new shared keys only
await Key.insertMany(
keys.map((k) => ({
encryptedKey: k.encryptedKey,
nonce: k.nonce,
sender: userId,
receiver: k.userId,
workspace: workspaceId
}))
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to push access keys');
}
};
export { pushKeys };

View File

@ -1,106 +1,5 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipNotFoundError,
BadRequestError,
UnauthorizedRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForWorkspace
} from '../helpers/user';
import {
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
/**
* Validate authenticated clients for membership with id [membershipId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipId - id of membership to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspaceRoles
* @returns {Membership} - validated membership
*/
const validateClientForMembership = async ({
authData,
membershipId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const membership = await Membership.findById(membershipId);
if (!membership) throw MembershipNotFoundError({
message: 'Failed to find membership'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: membership.workspace
});
return membership;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: new Types.ObjectId(membership.workspace)
});
return membership;
}
if (authData.authMode == AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: membership.workspace,
acceptedRoles
});
return membership;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for membership'
});
}
import { Membership, Key } from '../models';
/**
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
@ -115,24 +14,28 @@ const validateMembership = async ({
workspaceId,
acceptedRoles,
}: {
userId: Types.ObjectId;
workspaceId: Types.ObjectId;
acceptedRoles?: Array<'admin' | 'member'>;
userId: string;
workspaceId: string;
acceptedRoles: string[];
}) => {
const membership = await Membership.findOne({
user: userId,
workspace: workspaceId
}).populate("workspace");
if (!membership) {
throw MembershipNotFoundError({ message: 'Failed to find workspace membership' });
}
if (acceptedRoles) {
let membership;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try {
membership = await Membership.findOne({
user: userId,
workspace: workspaceId
}).populate("workspace");
if (!membership) throw new Error('Failed to find membership');
if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({ message: 'Failed authorization for membership role' });
throw new Error('Failed to validate membership role');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate membership');
}
return membership;
@ -230,7 +133,6 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
};
export {
validateClientForMembership,
validateMembership,
addMemberships,
findMembership,

View File

@ -1,139 +1,40 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import {
MembershipOrg,
Workspace,
Membership,
Key,
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData
} from '../models';
import {
MembershipOrgNotFoundError,
BadRequestError,
UnauthorizedRequestError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
/**
* Validate authenticated clients for organization membership with id [membershipOrgId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.membershipOrgId - id of organization membership to validate against
* @param {Array<'owner' | 'admin' | 'member'>} obj.acceptedRoles - accepted organization roles
* @param {MembershipOrg} - validated organization membership
*/
const validateClientForMembershipOrg = async ({
authData,
membershipOrgId,
acceptedRoles,
acceptedStatuses
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
membershipOrgId: Types.ObjectId;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await MembershipOrg.findById(membershipOrgId);
if (!membershipOrg) throw MembershipOrgNotFoundError({
message: 'Failed to find organization membership '
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
if (!authData.authPayload.organization.equals(membershipOrg.organization)) throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
return membershipOrg;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service account client authorization for organization membership'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateMembershipOrg({
userId: authData.authPayload._id,
organizationId: membershipOrg.organization,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for organization membership'
});
}
import { MembershipOrg, Workspace, Membership, Key } from '../models';
/**
* Validate that user with id [userId] is a member of organization with id [organizationId]
* and has at least one of the roles in [acceptedRoles]
* @param {Object} obj
* @param {Types.ObjectId} obj.userId
* @param {Types.ObjectId} obj.organizationId
* @param {String[]} obj.acceptedRoles
*
*/
const validateMembershipOrg = async ({
const validateMembership = async ({
userId,
organizationId,
acceptedRoles,
acceptedStatuses
acceptedRoles
}: {
userId: Types.ObjectId;
organizationId: Types.ObjectId;
acceptedRoles?: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses?: Array<'invited' | 'accepted'>;
userId: string;
organizationId: string;
acceptedRoles: string[];
}) => {
const membershipOrg = await MembershipOrg.findOne({
user: userId,
organization: organizationId
});
if (!membershipOrg) {
throw MembershipOrgNotFoundError({ message: 'Failed to find organization membership' });
}
if (acceptedRoles) {
if (!acceptedRoles.includes(membershipOrg.role)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership role' });
let membership;
try {
membership = await MembershipOrg.findOne({
user: new Types.ObjectId(userId),
organization: new Types.ObjectId(organizationId)
});
if (!membership) throw new Error('Failed to find organization membership');
if (!acceptedRoles.includes(membership.role)) {
throw new Error('Failed to validate organization membership role');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to validate organization membership');
}
if (acceptedStatuses) {
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw UnauthorizedRequestError({ message: 'Failed to validate organization membership status' });
}
}
return membershipOrg;
return membership;
}
/**
@ -143,7 +44,15 @@ const validateMembershipOrg = async ({
* @return {Object} membershipOrg - membership
*/
const findMembershipOrg = (queryObj: any) => {
const membershipOrg = MembershipOrg.findOne(queryObj);
let membershipOrg;
try {
membershipOrg = MembershipOrg.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find organization membership');
}
return membershipOrg;
};
@ -166,27 +75,33 @@ const addMembershipsOrg = async ({
roles: string[];
statuses: string[];
}) => {
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
update: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
upsert: true
}
};
});
try {
const operations = userIds.map((userId, idx) => {
return {
updateOne: {
filter: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
update: {
user: userId,
organization: organizationId,
role: roles[idx],
status: statuses[idx]
},
upsert: true
}
};
});
await MembershipOrg.bulkWrite(operations as any);
await MembershipOrg.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to organization');
}
};
/**
@ -199,43 +114,49 @@ const deleteMembershipOrg = async ({
}: {
membershipOrgId: string;
}) => {
const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
_id: membershipOrgId
});
let deletedMembershipOrg;
try {
deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
_id: membershipOrgId
});
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership');
// delete keys associated with organization membership
if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user
// delete keys associated with organization membership
if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user
const workspaces = (
await Workspace.find({
organization: deletedMembershipOrg.organization
})
).map((w) => w._id.toString());
const workspaces = (
await Workspace.find({
organization: deletedMembershipOrg.organization
})
).map((w) => w._id.toString());
await Membership.deleteMany({
user: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
await Membership.deleteMany({
user: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
await Key.deleteMany({
receiver: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
}
await Key.deleteMany({
receiver: deletedMembershipOrg.user,
workspace: {
$in: workspaces
}
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete organization membership');
}
return deletedMembershipOrg;
};
export {
validateClientForMembershipOrg,
validateMembershipOrg,
validateMembership,
findMembershipOrg,
addMembershipsOrg,
deleteMembershipOrg

View File

@ -25,7 +25,7 @@ const sendMail = async ({
recipients: string[];
substitutions: any;
}) => {
if (await getSmtpConfigured()) {
if (getSmtpConfigured()) {
try {
const html = fs.readFileSync(
path.resolve(__dirname, '../templates/' + template),
@ -35,7 +35,7 @@ const sendMail = async ({
const htmlToSend = temp(substitutions);
await smtpTransporter.sendMail({
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
from: `"${getSmtpFromName()}" <${getSmtpFromAddress()}>`,
to: recipients.join(', '),
subject: subjectLine,
html: htmlToSend

View File

@ -1,116 +1,14 @@
import Stripe from "stripe";
import { Types } from "mongoose";
import {
IUser,
User,
IServiceAccount,
ServiceAccount,
IServiceTokenData,
ServiceTokenData,
} from "../models";
import { Organization, MembershipOrg } from "../models";
import {
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER,
} from "../variables";
import {
getStripeSecretKey,
getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter,
} from "../config";
import {
UnauthorizedRequestError,
OrganizationNotFoundError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "../helpers/user";
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
/**
* Validate accepted clients for organization with id [organizationId]
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against
*/
const validateClientForOrganization = async ({
authData,
organizationId,
acceptedRoles,
acceptedStatuses,
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
organizationId: Types.ObjectId;
acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<"invited" | "accepted">;
}) => {
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: "Failed to find organization",
});
}
if (
authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
return { organization, membershipOrg };
}
if (
authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
authData.authPayload instanceof ServiceAccount
) {
await validateServiceAccountClientForOrganization({
serviceAccount: authData.authPayload,
organization,
});
return { organization };
}
if (
authData.authMode === AUTH_MODE_SERVICE_TOKEN &&
authData.authPayload instanceof ServiceTokenData
) {
throw UnauthorizedRequestError({
message: "Failed service token authorization for organization",
});
}
if (
authData.authMode === AUTH_MODE_API_KEY &&
authData.authPayload instanceof User
) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses,
});
return { organization, membershipOrg };
}
throw UnauthorizedRequestError({
message: "Failed client authorization for organization",
});
};
import * as Sentry from '@sentry/node';
import Stripe from 'stripe';
import { Types } from 'mongoose';
import { ACCEPTED } from '../variables';
import { Organization, MembershipOrg } from '../models';
import {
getStripeSecretKey,
getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter
} from '../config';
/**
* Create an organization with name [name]
@ -120,37 +18,43 @@ const validateClientForOrganization = async ({
* @param {Object} organization - new organization
*/
const createOrganization = async ({
name,
email,
name,
email
}: {
name: string;
email: string;
name: string;
email: string;
}) => {
let organization;
// register stripe account
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: "2022-08-01",
});
let organization;
try {
// register stripe account
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
if (await getStripeSecretKey()) {
const customer = await stripe.customers.create({
email,
description: name,
});
if (getStripeSecretKey()) {
const customer = await stripe.customers.create({
email,
description: name
});
organization = await new Organization({
name,
customerId: customer.id,
}).save();
} else {
organization = await new Organization({
name,
}).save();
}
organization = await new Organization({
name,
customerId: customer.id
}).save();
} else {
organization = await new Organization({
name
}).save();
}
await initSubscriptionOrg({ organizationId: organization._id });
await initSubscriptionOrg({ organizationId: organization._id });
} catch (err) {
Sentry.setUser({ email });
Sentry.captureException(err);
throw new Error(`Failed to create organization [err=${err}]`);
}
return organization;
return organization;
};
/**
@ -162,52 +66,57 @@ const createOrganization = async ({
* @return {Subscription} obj.subscription - new subscription
*/
const initSubscriptionOrg = async ({
organizationId,
organizationId
}: {
organizationId: Types.ObjectId;
organizationId: Types.ObjectId;
}) => {
let stripeSubscription;
let subscription;
let stripeSubscription;
let subscription;
try {
// find organization
const organization = await Organization.findOne({
_id: organizationId
});
// find organization
const organization = await Organization.findOne({
_id: organizationId,
});
if (organization) {
if (organization.customerId) {
// initialize starter subscription with quantity of 0
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
if (organization) {
if (organization.customerId) {
// initialize starter subscription with quantity of 0
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: "2022-08-01",
});
const productToPriceMap = {
starter: getStripeProductStarter(),
team: getStripeProductTeam(),
pro: getStripeProductPro()
};
const productToPriceMap = {
starter: await getStripeProductStarter(),
team: await getStripeProductTeam(),
pro: await getStripeProductPro(),
};
stripeSubscription = await stripe.subscriptions.create({
customer: organization.customerId,
items: [
{
price: productToPriceMap['starter'],
quantity: 1
}
],
payment_behavior: 'default_incomplete',
proration_behavior: 'none',
expand: ['latest_invoice.payment_intent']
});
}
} else {
throw new Error('Failed to initialize free organization subscription');
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to initialize free organization subscription');
}
stripeSubscription = await stripe.subscriptions.create({
customer: organization.customerId,
items: [
{
price: productToPriceMap["starter"],
quantity: 1,
},
],
payment_behavior: "default_incomplete",
proration_behavior: "none",
expand: ["latest_invoice.payment_intent"],
});
}
} else {
throw new Error("Failed to initialize free organization subscription");
}
return {
stripeSubscription,
subscription,
};
return {
stripeSubscription,
subscription
};
};
/**
@ -217,49 +126,53 @@ const initSubscriptionOrg = async ({
* @param {Number} obj.organizationId - id of subscription's organization
*/
const updateSubscriptionOrgQuantity = async ({
organizationId,
organizationId
}: {
organizationId: string;
organizationId: string;
}) => {
let stripeSubscription;
// find organization
const organization = await Organization.findOne({
_id: organizationId,
});
let stripeSubscription;
try {
// find organization
const organization = await Organization.findOne({
_id: organizationId
});
if (organization && organization.customerId) {
const quantity = await MembershipOrg.countDocuments({
organization: organizationId,
status: ACCEPTED,
});
if (organization && organization.customerId) {
const quantity = await MembershipOrg.countDocuments({
organization: organizationId,
status: ACCEPTED
});
const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: "2022-08-01",
});
const stripe = new Stripe(getStripeSecretKey(), {
apiVersion: '2022-08-01'
});
const subscription = (
await stripe.subscriptions.list({
customer: organization.customerId,
})
).data[0];
const subscription = (
await stripe.subscriptions.list({
customer: organization.customerId
})
).data[0];
stripeSubscription = await stripe.subscriptions.update(subscription.id, {
items: [
{
id: subscription.items.data[0].id,
price: subscription.items.data[0].price.id,
quantity,
},
],
});
}
stripeSubscription = await stripe.subscriptions.update(subscription.id, {
items: [
{
id: subscription.items.data[0].id,
price: subscription.items.data[0].price.id,
quantity
}
]
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
return stripeSubscription;
return stripeSubscription;
};
export {
validateClientForOrganization,
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity,
};
createOrganization,
initSubscriptionOrg,
updateSubscriptionOrgQuantity
};

View File

@ -15,7 +15,7 @@ const apiLimiter = rateLimit({
});
// 10 requests per minute
const authLimit = rateLimit({
const authLimiter = rateLimit({
windowMs: 60 * 1000,
max: 10,
standardHeaders: true,
@ -36,16 +36,8 @@ const passwordLimiter = rateLimit({
}
});
const authLimiter = (req: any, res: any, next: any) => {
if (process.env.NODE_ENV === 'production') {
authLimit(req, res, next);
} else {
next();
}
};
export {
apiLimiter,
export {
apiLimiter,
authLimiter,
passwordLimiter
passwordLimiter
};

File diff suppressed because it is too large Load Diff

View File

@ -1,947 +0,0 @@
import { Types } from 'mongoose';
import {
CreateSecretParams,
GetSecretsParams,
GetSecretParams,
UpdateSecretParams,
DeleteSecretParams
} from '../interfaces/services/SecretService';
import {
AuthData
} from '../interfaces/middleware';
import {
User,
Workspace,
ServiceAccount,
ServiceTokenData,
Secret,
ISecret,
SecretBlindIndexData,
} from '../models';
import { SecretVersion } from '../ee/models';
import {
validateMembership
} from '../helpers/membership';
import {
validateUserClientForSecret,
validateUserClientForSecrets
} from '../helpers/user';
import {
validateServiceTokenDataClientForSecrets,
validateServiceTokenDataClientForWorkspace
} from '../helpers/serviceTokenData';
import {
validateServiceAccountClientForSecrets,
validateServiceAccountClientForWorkspace
} from '../helpers/serviceAccount';
import {
BadRequestError,
UnauthorizedRequestError,
SecretNotFoundError,
SecretBlindIndexDataNotFoundError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
SECRET_PERSONAL,
SECRET_SHARED,
ACTION_ADD_SECRETS,
ACTION_READ_SECRETS,
ACTION_UPDATE_SECRETS,
ACTION_DELETE_SECRETS
} from '../variables';
import crypto from 'crypto';
import * as argon2 from 'argon2';
import {
encryptSymmetric,
decryptSymmetric
} from '../utils/crypto';
import { getEncryptionKey } from '../config';
import { TelemetryService } from '../services';
import {
EESecretService,
EELogService
} from '../ee/services';
import {
getAuthDataPayloadIdObj,
getAuthDataPayloadUserObj
} from '../utils/auth';
/**
* Validate authenticated clients for secrets with id [secretId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.secretId - id of secret to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecret = async ({
authData,
secretId,
acceptedRoles,
requiredPermissions
}: {
authData: AuthData;
secretId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions: string[];
}) => {
const secret = await Secret.findById(secretId);
if (!secret) throw SecretNotFoundError({
message: 'Failed to find secret'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment,
requiredPermissions
});
return secret;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId: secret.workspace,
environment: secret.environment
});
return secret;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecret({
user: authData.authPayload,
secret,
acceptedRoles,
requiredPermissions
});
return secret;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secret'
});
}
/**
* Validate authenticated clients for secrets with ids [secretIds] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId[]} obj.secretIds - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForSecrets = async ({
authData,
secretIds,
requiredPermissions
}: {
authData: AuthData;
secretIds: Types.ObjectId[];
requiredPermissions: string[];
}) => {
let secrets: ISecret[] = [];
secrets = await Secret.find({
_id: {
$in: secretIds
}
});
if (secrets.length != secretIds.length) {
throw BadRequestError({ message: 'Failed to validate non-existent secrets' })
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForSecrets({
serviceAccount: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForSecrets({
serviceTokenData: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForSecrets({
user: authData.authPayload,
secrets,
requiredPermissions
});
return secrets;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for secrets resource'
});
}
/**
* Initialize secret blind index data by setting previously
* un-initialized projects to have secret blind index data
* (Ensures that all projects have associated blind index data)
*/
const initSecretBlindIndexDataHelper = async () => {
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
_id: {
$nin: workspaceIdsBlindIndexed
}
});
const secretBlindIndexDataToInsert = await Promise.all(
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric({
plaintext: salt,
key: await getEncryptionKey()
});
const secretBlindIndexData = new SecretBlindIndexData({
workspace: workspaceToBlindIndex,
encryptedSaltCiphertext,
saltIV,
saltTag
})
return secretBlindIndexData;
})
);
if (secretBlindIndexDataToInsert.length > 0) {
await SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
}
}
/**
* Create secret blind index data containing encrypted blind index [salt]
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId
*/
const createSecretBlindIndexDataHelper = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
// initialize random blind index salt for workspace
const salt = crypto.randomBytes(16).toString('base64');
const {
ciphertext: encryptedSaltCiphertext,
iv: saltIV,
tag: saltTag
} = encryptSymmetric({
plaintext: salt,
key: await getEncryptionKey()
});
const secretBlindIndexData = await new SecretBlindIndexData({
workspace: workspaceId,
encryptedSaltCiphertext,
saltIV,
saltTag
}).save();
return secretBlindIndexData;
}
/**
* Get secret blind index salt for workspace with id [workspaceId]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns
*/
const getSecretBlindIndexSaltHelper = async ({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) => {
// check if workspace blind index data exists
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId
});
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt
const salt = decryptSymmetric({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: await getEncryptionKey()
});
return salt;
}
/**
* Generate blind index for secret with name [secretName]
* and salt [salt]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to generate blind index for
* @param {String} obj.salt - base64-salt
*/
const generateSecretBlindIndexWithSaltHelper = async ({
secretName,
salt
}: {
secretName: string;
salt: string;
}) => {
// generate secret blind index
const secretBlindIndex = (await argon2.hash(secretName, {
type: argon2.argon2id,
salt: Buffer.from(salt, 'base64'),
saltLength: 16, // default 16 bytes
memoryCost: 65536, // default pool of 64 MiB per thread.
hashLength: 32,
parallelism: 1,
raw: true
})).toString('base64');
return secretBlindIndex;
}
/**
* Generate blind index for secret with name [secretName]
* for workspace with id [workspaceId]
* @param {Object} obj
* @param {Stringj} obj.secretName - name of secret to generate blind index for
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
*/
const generateSecretBlindIndexHelper = async ({
secretName,
workspaceId
}: {
secretName: string;
workspaceId: Types.ObjectId;
}) => {
// check if workspace blind index data exists
const secretBlindIndexData = await SecretBlindIndexData.findOne({
workspace: workspaceId
});
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
// decrypt workspace salt
const salt = decryptSymmetric({
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
iv: secretBlindIndexData.saltIV,
tag: secretBlindIndexData.saltTag,
key: await getEncryptionKey()
});
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
return secretBlindIndex;
}
/**
* Create secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to create
* @param {Types.ObjectId} obj.workspaceId - id of workspace to create secret for
* @param {String} obj.environment - environment in workspace to create secret for
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const createSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
const exists = await Secret.exists({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
});
if (exists) throw BadRequestError({
message: 'Failed to create secret that already exists'
});
if (type === SECRET_PERSONAL) {
// case: secret type is personal -> check if a corresponding shared secret
// with the same blind index [secretBlindIndex] exists
const exists = await Secret.exists({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
type: SECRET_SHARED
});
if (!exists) throw BadRequestError({
message: 'Failed to create personal secret override for no corresponding shared secret'
});
}
// create secret
const secret = await new Secret({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
}).save();
const secretVersion = new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
// // (EE) add version for new secret
await EESecretService.addSecretVersions({
secretVersions: [secretVersion]
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: [secret._id]
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets added',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secret;
}
/**
* Get secrets for workspace with id [workspaceId] and environment [environment]
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment in workspace
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretsHelper = async ({
workspaceId,
environment,
authData
}: GetSecretsParams) => {
let secrets: ISecret[] = [];
// get personal secrets first
secrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_PERSONAL,
...getAuthDataPayloadUserObj(authData)
});
// concat with shared secrets
secrets = secrets.concat(await Secret.find({
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED,
secretBlindIndex: {
$nin: secrets.map((secret) => secret.secretBlindIndex)
}
}));
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: secrets.map((secret) => secret._id)
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets pulled',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secrets;
}
/**
* Get secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to get
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const getSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData
}: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secret: ISecret | null = null;
// try getting personal secret first (if exists)
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type: type ?? SECRET_PERSONAL,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
});
if (!secret) {
// case: failed to find personal secret matching criteria
// -> find shared secret matching criteria
secret = await Secret.findOne({
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type: SECRET_SHARED
});
}
if (!secret) throw SecretNotFoundError();
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_READ_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: [secret._id]
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets pull',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secret;
}
/**
* Update secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to update
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {String} obj.secretValueCiphertext - ciphertext of secret value
* @param {String} obj.secretValueIV - IV of secret value
* @param {String} obj.secretValueTag - tag of secret value
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const updateSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData,
secretValueCiphertext,
secretValueIV,
secretValueTag
}: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secret: ISecret | null = null;
if (type === SECRET_SHARED) {
// case: update shared secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type
},
{
secretValueCiphertext,
secretValueIV,
secretValueTag,
$inc: { version: 1 }
},
{
new: true
}
);
} else {
// case: update personal secret
secret = await Secret.findOneAndUpdate(
{
secretBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
...getAuthDataPayloadUserObj(authData)
},
{
secretValueCiphertext,
secretValueIV,
secretValueTag,
$inc: { version: 1 }
},
{
new: true
}
);
}
if (!secret) throw SecretNotFoundError();
const secretVersion = new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag
});
// (EE) add version for new secret
await EESecretService.addSecretVersions({
secretVersions: [secretVersion]
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: [secret._id]
});
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets modified',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return secret;
}
/**
* Delete secret with name [secretName]
* @param {Object} obj
* @param {String} obj.secretName - name of secret to delete
* @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to
* @param {String} obj.environment - environment in workspace that secret belongs to
* @param {'shared' | 'personal'} obj.type - type of secret
* @param {AuthData} obj.authData - authentication data on request
* @returns
*/
const deleteSecretHelper = async ({
secretName,
workspaceId,
environment,
type,
authData
}: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName,
workspaceId: new Types.ObjectId(workspaceId)
});
let secrets: ISecret[] = [];
let secret: ISecret | null = null;
if (type === SECRET_SHARED) {
secrets = await Secret.find({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment
});
secret = await Secret.findOneAndDelete({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type
});
await Secret.deleteMany({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment
});
} else {
secret = await Secret.findOneAndDelete({
secretBlindIndex,
workspaceId: new Types.ObjectId(workspaceId),
environment,
type,
...getAuthDataPayloadUserObj(authData)
});
if (secret) {
secrets = [secret];
}
}
if (!secret) throw SecretNotFoundError();
await EESecretService.markDeletedSecretVersions({
secretIds: secrets.map((secret) => secret._id)
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: secrets.map((secret) => secret._id)
});
// (EE) take a secret snapshot
action && await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.authChannel,
ipAddress: authData.authIP
});
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: 'secrets deleted',
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: authData.authChannel,
userAgent: authData.authUserAgent
}
});
}
return ({
secrets,
secret
});
}
export {
validateClientForSecret,
validateClientForSecrets,
initSecretBlindIndexDataHelper,
createSecretBlindIndexDataHelper,
getSecretBlindIndexSaltHelper,
generateSecretBlindIndexWithSaltHelper,
generateSecretBlindIndexHelper,
createSecretHelper,
getSecretsHelper,
getSecretHelper,
updateSecretHelper,
deleteSecretHelper
}

View File

@ -1,271 +0,0 @@
import _ from 'lodash';
import { Types } from 'mongoose';
import {
User,
IUser,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
ISecret,
IOrganization,
IServiceAccountWorkspacePermission,
ServiceAccountWorkspacePermission
} from '../models';
import {
BadRequestError,
UnauthorizedRequestError,
ServiceAccountNotFoundError
} from '../utils/errors';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import {
validateUserClientForServiceAccount
} from '../helpers/user';
const validateClientForServiceAccount = async ({
authData,
serviceAccountId,
requiredPermissions
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
},
serviceAccountId: Types.ObjectId;
requiredPermissions?: string[];
}) => {
const serviceAccount = await ServiceAccount.findById(serviceAccountId);
if (!serviceAccount) {
throw ServiceAccountNotFoundError({
message: 'Failed to find service account'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForServiceAccount({
user: authData.authPayload,
serviceAccount,
requiredPermissions
});
return serviceAccount;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForServiceAccount({
serviceAccount: authData.authPayload,
targetServiceAccount: serviceAccount,
requiredPermissions
});
return serviceAccount;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for service account resource'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForServiceAccount({
user: authData.authPayload,
serviceAccount,
requiredPermissions
});
return serviceAccount;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for service account resource'
});
}
/**
* Validate that service account (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForWorkspace = async ({
serviceAccount,
workspaceId,
environment,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (environment) {
// case: environment specified ->
// evaluate service account authorization for workspace
// in the context of a specific environment [environment]
const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount,
workspace: new Types.ObjectId(workspaceId),
environment
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace environment'
});
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
if (!permission.read) runningIsDisallowed = true;
break;
case PERMISSION_WRITE_SECRETS:
if (!permission.write) runningIsDisallowed = true;
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
} else {
// case: no environment specified ->
// evaluate service account authorization for workspace
// without need of environment [environment]
const permission = await ServiceAccountWorkspacePermission.findOne({
serviceAccount,
workspace: new Types.ObjectId(workspaceId)
});
if (!permission) throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given workspace'
});
}
}
/**
* Validate that service account (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceAccount} obj.serviceAccount - service account client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForSecrets = async ({
serviceAccount,
secrets,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
const permissions = await ServiceAccountWorkspacePermission.find({
serviceAccount: serviceAccount._id
});
const permissionsObj = _.keyBy(permissions, (p) => {
return `${p.workspace.toString()}-${p.environment}`
});
secrets.forEach((secret: ISecret) => {
const permission = permissionsObj[`${secret.workspace.toString()}-${secret.environment}`];
if (!permission) throw BadRequestError({
message: 'Failed to find any permission for the secret workspace and environment'
});
requiredPermissions?.forEach((requiredPermission: string) => {
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
if (!permission.read) runningIsDisallowed = true;
break;
case PERMISSION_WRITE_SECRETS:
if (!permission.write) runningIsDisallowed = true;
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
});
});
}
/**
* Validate that service account (client) can access target service
* account [serviceAccount] with required permissions [requiredPermissions]
* @param {Object} obj
* @param {SerivceAccount} obj.serviceAccount - service account client
* @param {ServiceAccount} targetServiceAccount - target service account to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceAccountClientForServiceAccount = ({
serviceAccount,
targetServiceAccount,
requiredPermissions
}: {
serviceAccount: IServiceAccount;
targetServiceAccount: IServiceAccount;
requiredPermissions?: string[];
}) => {
if (!serviceAccount.organization.equals(targetServiceAccount.organization)) {
throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given service account'
});
}
}
/**
* Validate that service account (client) can access organization [organization]
* @param {Object} obj
* @param {User} obj.user - service account client
* @param {Organization} obj.organization - organization to validate against
*/
const validateServiceAccountClientForOrganization = async ({
serviceAccount,
organization
}: {
serviceAccount: IServiceAccount;
organization: IOrganization;
}) => {
if (!serviceAccount.organization.equals(organization._id)) {
throw UnauthorizedRequestError({
message: 'Failed service account authorization for the given organization'
});
}
}
export {
validateClientForServiceAccount,
validateServiceAccountClientForWorkspace,
validateServiceAccountClientForSecrets,
validateServiceAccountClientForServiceAccount,
validateServiceAccountClientForOrganization
}

View File

@ -1,188 +0,0 @@
import { Types } from 'mongoose';
import {
ISecret,
IServiceTokenData,
ServiceTokenData,
IUser,
User,
IServiceAccount,
ServiceAccount,
} from '../models';
import {
UnauthorizedRequestError,
ServiceTokenDataNotFoundError
} from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
/**
* Validate authenticated clients for service token with id [serviceTokenId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.serviceTokenData - id of service token to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/
const validateClientForServiceTokenData = async ({
authData,
serviceTokenDataId,
acceptedRoles
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
serviceTokenDataId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>;
}) => {
const serviceTokenData = await ServiceTokenData
.findById(serviceTokenDataId)
.select('+encryptedKey +iv +tag')
.populate<{ user: IUser }>('user');
if (!serviceTokenData) throw ServiceTokenDataNotFoundError({
message: 'Failed to find service token data'
});
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: serviceTokenData.workspace,
acceptedRoles
});
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: serviceTokenData.workspace
});
return serviceTokenData;
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
throw UnauthorizedRequestError({
message: 'Failed service token authorization for service token data'
});
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: serviceTokenData.workspace,
acceptedRoles
});
return serviceTokenData;
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for service token data'
});
}
/**
* Validate that service token (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForWorkspace = async ({
serviceTokenData,
workspaceId,
environment,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
workspaceId: Types.ObjectId;
environment?: string;
requiredPermissions?: string[];
}) => {
if (!serviceTokenData.workspace.equals(workspaceId)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (environment) {
// case: environment is specified
if (serviceTokenData.environment !== environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
}
}
/**
* Validate that service token (client) can access secrets
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {ServiceTokenData} obj.serviceTokenData - service token client
* @param {Secret[]} secrets - secrets to validate against
* @param {string[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateServiceTokenDataClientForSecrets = async ({
serviceTokenData,
secrets,
requiredPermissions
}: {
serviceTokenData: IServiceTokenData;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
secrets.forEach((secret: ISecret) => {
if (!serviceTokenData.workspace.equals(secret.workspace)) {
// case: invalid workspaceId passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace'
});
}
if (serviceTokenData.environment !== secret.environment) {
// case: invalid environment passed
throw UnauthorizedRequestError({
message: 'Failed service token authorization for the given workspace environment'
});
}
requiredPermissions?.forEach((permission) => {
if (!serviceTokenData.permissions.includes(permission)) {
throw UnauthorizedRequestError({
message: `Failed service token authorization for the given workspace environment action: ${permission}`
});
}
});
});
}
export {
validateClientForServiceTokenData,
validateServiceTokenDataClientForWorkspace,
validateServiceTokenDataClientForSecrets
}

View File

@ -1,15 +1,16 @@
import { Types } from "mongoose";
import { TokenData } from "../models";
import crypto from "crypto";
import bcrypt from "bcrypt";
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose';
import { TokenData } from '../models';
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import {
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET,
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
import { getSaltRounds } from "../config";
TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET
} from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import { getSaltRounds } from '../config';
/**
* Create and store a token in the database for purpose [type]
@ -21,197 +22,194 @@ import { getSaltRounds } from "../config";
* @returns {String} token - the created token
*/
const createTokenHelper = async ({
type,
email,
phoneNumber,
organizationId,
}: {
type:
| "emailConfirmation"
| "emailMfa"
| "organizationInvitation"
| "passwordReset";
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
}) => {
let token, expiresAt, triesLeft;
// generate random token based on specified token use-case
// type [type]
switch (type) {
case TOKEN_EMAIL_CONFIRMATION:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
expiresAt = new Date(new Date().getTime() + 86400000);
break;
case TOKEN_EMAIL_MFA:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
triesLeft = 5;
expiresAt = new Date(new Date().getTime() + 300000);
break;
case TOKEN_EMAIL_ORG_INVITATION:
// generate random hex
token = crypto.randomBytes(16).toString("hex");
expiresAt = new Date(new Date().getTime() + 259200000);
break;
case TOKEN_EMAIL_PASSWORD_RESET:
// generate random hex
token = crypto.randomBytes(16).toString("hex");
expiresAt = new Date(new Date().getTime() + 86400000);
break;
default:
token = crypto.randomBytes(16).toString("hex");
expiresAt = new Date();
break;
}
interface TokenDataQuery {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
interface TokenDataUpdate {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
tokenHash: string;
triesLeft?: number;
expiresAt: Date;
}
const query: TokenDataQuery = { type };
const update: TokenDataUpdate = {
type,
tokenHash: await bcrypt.hash(token, await getSaltRounds()),
expiresAt,
};
email,
phoneNumber,
organizationId
}: {
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId
}) => {
let token, expiresAt, triesLeft;
try {
// generate random token based on specified token use-case
// type [type]
switch (type) {
case TOKEN_EMAIL_CONFIRMATION:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
expiresAt = new Date((new Date()).getTime() + 86400000);
break;
case TOKEN_EMAIL_MFA:
// generate random 6-digit code
token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1));
triesLeft = 5;
expiresAt = new Date((new Date()).getTime() + 300000);
break;
case TOKEN_EMAIL_ORG_INVITATION:
// generate random hex
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date((new Date()).getTime() + 259200000);
break;
case TOKEN_EMAIL_PASSWORD_RESET:
// generate random hex
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date((new Date()).getTime() + 86400000);
break;
default:
token = crypto.randomBytes(16).toString('hex');
expiresAt = new Date();
break;
}
interface TokenDataQuery {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
interface TokenDataUpdate {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
tokenHash: string;
triesLeft?: number;
expiresAt: Date;
}
if (email) {
query.email = email;
update.email = email;
}
if (phoneNumber) {
query.phoneNumber = phoneNumber;
update.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId;
update.organization = organizationId;
}
const query: TokenDataQuery = { type };
const update: TokenDataUpdate = {
type,
tokenHash: await bcrypt.hash(token, getSaltRounds()),
expiresAt
}
if (triesLeft) {
update.triesLeft = triesLeft;
}
await TokenData.findOneAndUpdate(query, update, {
new: true,
upsert: true,
});
return token;
};
if (email) {
query.email = email;
update.email = email;
}
if (phoneNumber) {
query.phoneNumber = phoneNumber;
update.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId
update.organization = organizationId
}
if (triesLeft) {
update.triesLeft = triesLeft;
}
await TokenData.findOneAndUpdate(
query,
update,
{
new: true,
upsert: true
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
"Failed to create token"
);
}
return token;
}
/**
*
*
* @param {Object} obj
* @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token
*/
const validateTokenHelper = async ({
type,
email,
phoneNumber,
organizationId,
token,
type,
email,
phoneNumber,
organizationId,
token
}: {
type:
| "emailConfirmation"
| "emailMfa"
| "organizationInvitation"
| "passwordReset";
email?: string;
phoneNumber?: string;
organizationId?: Types.ObjectId;
token: string;
}) => {
interface Query {
type: string;
type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset';
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
const query: Query = { type };
if (email) {
query.email = email;
}
if (phoneNumber) {
query.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId;
}
const tokenData = await TokenData.findOne(query).select("+tokenHash");
if (!tokenData) throw new Error("Failed to find token to validate");
if (tokenData.expiresAt < new Date()) {
// case: token expired
await TokenData.findByIdAndDelete(tokenData._id);
throw UnauthorizedRequestError({
message: "MFA session expired. Please log in again",
context: {
code: "mfa_expired",
},
});
}
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
if (!isValid) {
// case: token is not valid
if (tokenData?.triesLeft !== undefined) {
// case: token has a try-limit
if (tokenData.triesLeft === 1) {
// case: token is out of tries
await TokenData.findByIdAndDelete(tokenData._id);
} else {
// case: token has more than 1 try left
await TokenData.findByIdAndUpdate(
tokenData._id,
{
triesLeft: tokenData.triesLeft - 1,
},
{
new: true,
}
);
}
throw UnauthorizedRequestError({
message: "MFA code is invalid",
context: {
code: "mfa_invalid",
triesLeft: tokenData.triesLeft - 1,
},
});
organizationId?: Types.ObjectId;
token: string;
}) => {
interface Query {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
throw UnauthorizedRequestError({
message: "MFA code is invalid",
context: {
code: "mfa_invalid",
},
});
}
const query: Query = { type };
// case: token is valid
await TokenData.findByIdAndDelete(tokenData._id);
};
if (email) { query.email = email; }
if (phoneNumber) { query.phoneNumber = phoneNumber; }
if (organizationId) { query.organization = organizationId; }
export { createTokenHelper, validateTokenHelper };
const tokenData = await TokenData.findOne(query).select('+tokenHash');
if (!tokenData) throw new Error('Failed to find token to validate');
if (tokenData.expiresAt < new Date()) {
// case: token expired
await TokenData.findByIdAndDelete(tokenData._id);
throw UnauthorizedRequestError({
message: 'MFA session expired. Please log in again',
context: {
code: 'mfa_expired'
}
});
}
const isValid = await bcrypt.compare(token, tokenData.tokenHash);
if (!isValid) {
// case: token is not valid
if (tokenData?.triesLeft !== undefined) {
// case: token has a try-limit
if (tokenData.triesLeft === 1) {
// case: token is out of tries
await TokenData.findByIdAndDelete(tokenData._id);
} else {
// case: token has more than 1 try left
await TokenData.findByIdAndUpdate(tokenData._id, {
triesLeft: tokenData.triesLeft - 1
}, {
new: true
});
}
throw UnauthorizedRequestError({
message: 'MFA code is invalid',
context: {
code: 'mfa_invalid',
triesLeft: tokenData.triesLeft - 1
}
});
}
throw UnauthorizedRequestError({
message: 'MFA code is invalid',
context: {
code: 'mfa_invalid'
}
});
}
// case: token is valid
await TokenData.findByIdAndDelete(tokenData._id);
}
export {
createTokenHelper,
validateTokenHelper
}

View File

@ -1,24 +1,6 @@
import { Types } from 'mongoose';
import {
IUser,
ISecret,
IServiceAccount,
User,
Membership,
IOrganization,
Organization,
} from '../models';
import * as Sentry from '@sentry/node';
import { IUser, User } from '../models';
import { sendMail } from './nodemailer';
import { validateMembership } from './membership';
import _ from 'lodash';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
import {
validateMembershipOrg
} from '../helpers/membershipOrg';
import {
PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS
} from '../variables';
/**
* Initialize a user under email [email]
@ -27,9 +9,16 @@ import {
* @returns {Object} user - the initialized user
*/
const setupAccount = async ({ email }: { email: string }) => {
const user = await new User({
email
}).save();
let user;
try {
user = await new User({
email
}).save();
} catch (err) {
Sentry.setUser({ email });
Sentry.captureException(err);
throw new Error('Failed to set up account');
}
return user;
};
@ -81,27 +70,34 @@ const completeAccount = async ({
salt: string;
verifier: string;
}) => {
const options = {
new: true
};
const user = await User.findByIdAndUpdate(
userId,
{
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
options
);
let user;
try {
const options = {
new: true
};
user = await User.findByIdAndUpdate(
userId,
{
firstName,
lastName,
encryptionVersion,
protectedKey,
protectedKeyIV,
protectedKeyTag,
publicKey,
encryptedPrivateKey,
iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag,
salt,
verifier
},
options
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to complete account set up');
}
return user;
};
@ -150,204 +146,4 @@ const checkUserDevice = async ({
}
}
/**
* Validate that user (client) can access workspace
* with id [workspaceId] and its environment [environment] with required permissions
* [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} environment - (optional) environment in workspace to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForWorkspace = async ({
user,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
// validate user membership in workspace
const membership = await validateMembership({
userId: user._id,
workspaceId,
acceptedRoles
});
let runningIsDisallowed = false;
requiredPermissions?.forEach((requiredPermission: string) => {
switch (requiredPermission) {
case PERMISSION_READ_SECRETS:
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_READ_SECRETS });
break;
case PERMISSION_WRITE_SECRETS:
runningIsDisallowed = _.some(membership.deniedPermissions, { environmentSlug: environment, ability: PERMISSION_WRITE_SECRETS });
break;
default:
break;
}
if (runningIsDisallowed) {
throw UnauthorizedRequestError({
message: `Failed permissions authorization for workspace environment action : ${requiredPermission}`
});
}
});
return membership;
}
/**
* Validate that user (client) can access secret [secret]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecret = async ({
user,
secret,
acceptedRoles,
requiredPermissions
}: {
user: IUser;
secret: ISecret;
acceptedRoles?: Array<'admin' | 'member'>;
requiredPermissions?: string[];
}) => {
const membership = await validateMembership({
userId: user._id,
workspaceId: secret.workspace,
acceptedRoles
});
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
const isDisallowed = _.some(membership.deniedPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
}
}
/**
* Validate that user (client) can access secrets [secrets]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Secret[]} obj.secrets - secrets to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForSecrets = async ({
user,
secrets,
requiredPermissions
}: {
user: IUser;
secrets: ISecret[];
requiredPermissions?: string[];
}) => {
// TODO: add acceptedRoles?
const userMemberships = await Membership.find({ user: user._id })
const userMembershipById = _.keyBy(userMemberships, 'workspace');
const workspaceIdsSet = new Set(userMemberships.map((m) => m.workspace.toString()));
// for each secret check if the secret belongs to a workspace the user is a member of
secrets.forEach((secret: ISecret) => {
if (!workspaceIdsSet.has(secret.workspace.toString())) {
throw BadRequestError({
message: 'Failed authorization for the secret'
});
}
if (requiredPermissions?.includes(PERMISSION_WRITE_SECRETS)) {
const deniedMembershipPermissions = userMembershipById[secret.workspace.toString()].deniedPermissions;
const isDisallowed = _.some(deniedMembershipPermissions, { environmentSlug: secret.environment, ability: PERMISSION_WRITE_SECRETS });
if (isDisallowed) {
throw UnauthorizedRequestError({
message: 'You do not have the required permissions to perform this action'
});
}
}
});
}
/**
* Validate that user (client) can access service account [serviceAccount]
* with required permissions [requiredPermissions]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {ServiceAccount} obj.serviceAccount - service account to validate against
* @param {String[]} requiredPermissions - required permissions as part of the endpoint
*/
const validateUserClientForServiceAccount = async ({
user,
serviceAccount,
requiredPermissions
}: {
user: IUser;
serviceAccount: IServiceAccount;
requiredPermissions?: string[];
}) => {
if (!serviceAccount.user.equals(user._id)) {
// case: user who created service account is not the
// same user that is on the request
await validateMembershipOrg({
userId: user._id,
organizationId: serviceAccount.organization,
acceptedRoles: [],
acceptedStatuses: []
});
}
}
/**
* Validate that user (client) can access organization [organization]
* @param {Object} obj
* @param {User} obj.user - user client
* @param {Organization} obj.organization - organization to validate against
*/
const validateUserClientForOrganization = async ({
user,
organization,
acceptedRoles,
acceptedStatuses
}: {
user: IUser;
organization: IOrganization;
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
}) => {
const membershipOrg = await validateMembershipOrg({
userId: user._id,
organizationId: organization._id,
acceptedRoles,
acceptedStatuses
});
return membershipOrg;
}
export {
setupAccount,
completeAccount,
checkUserDevice,
validateUserClientForWorkspace,
validateUserClientForSecrets,
validateUserClientForServiceAccount,
validateUserClientForOrganization,
validateUserClientForSecret
};
export { setupAccount, completeAccount, checkUserDevice };

View File

@ -1,135 +1,12 @@
import * as Sentry from '@sentry/node';
import crypto from 'crypto';
import { Types } from 'mongoose';
import {
Workspace,
Bot,
Membership,
Key,
Secret,
User,
IUser,
ServiceAccountWorkspacePermission,
ServiceAccount,
IServiceAccount,
ServiceTokenData,
IServiceTokenData,
SecretBlindIndexData
Secret
} from '../models';
import { createBot } from '../helpers/bot';
import { validateUserClientForWorkspace } from '../helpers/user';
import { validateServiceAccountClientForWorkspace } from '../helpers/serviceAccount';
import { validateServiceTokenDataClientForWorkspace } from '../helpers/serviceTokenData';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../utils/errors';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { encryptSymmetric } from '../utils/crypto';
import { SecretService } from '../services';
/**
* Validate authenticated clients for workspace with id [workspaceId] based
* on any known permissions.
* @param {Object} obj
* @param {Object} obj.authData - authenticated client details
* @param {Types.ObjectId} obj.workspaceId - id of workspace to validate against
* @param {String} obj.environment - (optional) environment in workspace to validate against
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
* @param {String[]} obj.requiredPermissions - required permissions as part of the endpoint
*/
const validateClientForWorkspace = async ({
authData,
workspaceId,
environment,
acceptedRoles,
requiredPermissions,
requireBlindIndicesEnabled
}: {
authData: {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
};
workspaceId: Types.ObjectId;
environment?: string;
acceptedRoles: Array<'admin' | 'member'>;
requiredPermissions?: string[];
requireBlindIndicesEnabled: boolean;
}) => {
const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({
message: 'Failed to find workspace'
});
if (requireBlindIndicesEnabled) {
// case: blind indices are not enabled for secrets in this workspace
// (i.e. workspace was created before blind indices were introduced
// and no admin has enabled it)
const secretBlindIndexData = await SecretBlindIndexData.exists({
workspace: new Types.ObjectId(workspaceId)
});
if (!secretBlindIndexData) throw UnauthorizedRequestError({
message: 'Failed workspace authorization due to blind indices not being enabled'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) {
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: authData.authPayload,
workspaceId,
environment,
requiredPermissions
});
return {};
}
if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) {
const membership = await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId,
environment,
acceptedRoles,
requiredPermissions
});
return ({ membership });
}
throw UnauthorizedRequestError({
message: 'Failed client authorization for workspace'
});
}
/**
* Create a workspace with name [name] in organization with id [organizationId]
@ -150,21 +27,13 @@ const createWorkspace = async ({
// create workspace
workspace = await new Workspace({
name,
organization: organizationId,
autoCapitalization: true
organization: organizationId
}).save();
// initialize bot for workspace
await createBot({
const bot = await createBot({
name: 'Infisical Bot',
workspaceId: workspace._id
workspaceId: workspace._id.toString()
});
// initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
@ -202,8 +71,4 @@ const deleteWorkspace = async ({ id }: { id: string }) => {
}
};
export {
validateClientForWorkspace,
createWorkspace,
deleteWorkspace
};
export { createWorkspace, deleteWorkspace };

View File

@ -1,6 +1,7 @@
import mongoose from 'mongoose';
import dotenv from 'dotenv';
dotenv.config();
import infisical from 'infisical-node';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
@ -8,7 +9,7 @@ import * as Sentry from '@sentry/node';
import { DatabaseService } from './services';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { TelemetryService } from './services';
import { logTelemetryMessage } from './services';
import { setTransporter } from './helpers/nodemailer';
import { createTestUserForDevelopment } from './utils/addDevelopmentUser';
// eslint-disable-next-line @typescript-eslint/no-var-requires
@ -56,15 +57,10 @@ import {
secret as v2SecretRouter, // begin to phase out
secrets as v2SecretsRouter,
serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter,
apiKeyData as v2APIKeyDataRouter,
environment as v2EnvironmentRouter,
tags as v2TagsRouter,
} from './routes/v2';
import {
secrets as v3SecretsRouter,
workspaces as v3WorkspacesRouter
} from './routes/v3';
import { healthCheck } from './routes/status';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
@ -74,21 +70,26 @@ import {
getNodeEnv,
getPort,
getSentryDSN,
getSiteURL,
getSmtpHost
getSiteURL
} from './config';
const main = async () => {
TelemetryService.logTelemetryMessage();
setTransporter(await initSmtp());
if (process.env.INFISICAL_TOKEN != "" || process.env.INFISICAL_TOKEN != undefined) {
await infisical.connect({
token: process.env.INFISICAL_TOKEN!
});
}
await DatabaseService.initDatabase(await getMongoURL());
if ((await getNodeEnv()) !== 'test') {
logTelemetryMessage();
setTransporter(initSmtp());
await DatabaseService.initDatabase(getMongoURL());
if (getNodeEnv() !== 'test') {
Sentry.init({
dsn: await getSentryDSN(),
dsn: getSentryDSN(),
tracesSampleRate: 1.0,
debug: await getNodeEnv() === 'production' ? false : true,
environment: await getNodeEnv()
debug: getNodeEnv() === 'production' ? false : true,
environment: getNodeEnv()
});
}
@ -100,13 +101,13 @@ const main = async () => {
app.use(
cors({
credentials: true,
origin: await getSiteURL()
origin: getSiteURL()
})
);
app.use(requestIp.mw());
if ((await getNodeEnv()) === 'production') {
if (getNodeEnv() === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
@ -120,7 +121,7 @@ const main = async () => {
app.use('/api/v1/workspace', eeWorkspaceRouter);
app.use('/api/v1/action', eeActionRouter);
// v1 routes (default)
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
@ -140,7 +141,7 @@ const main = async () => {
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
app.use('/api/v1/folder', v1SecretsFolder)
// v2 routes (improvements)
// v2 routes
app.use('/api/v2/signup', v2SignupRouter);
app.use('/api/v2/auth', v2AuthRouter);
app.use('/api/v2/users', v2UsersRouter);
@ -151,13 +152,8 @@ const main = async () => {
app.use('/api/v2/secret', v2SecretRouter); // deprecated
app.use('/api/v2/secrets', v2SecretsRouter);
app.use('/api/v2/service-token', v2ServiceTokenDataRouter); // TODO: turn into plural route
app.use('/api/v2/service-accounts', v2ServiceAccountsRouter); // new
app.use('/api/v2/api-key', v2APIKeyDataRouter);
// v3 routes (experimental)
app.use('/api/v3/secrets', v3SecretsRouter);
app.use('/api/v3/workspaces', v3WorkspacesRouter);
// api docs
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
@ -172,11 +168,11 @@ const main = async () => {
app.use(requestErrorHandler)
const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`)
const server = app.listen(getPort(), () => {
getLogger("backend-main").info(`Server started listening at port ${getPort()}`)
});
await createTestUserForDevelopment();
createTestUserForDevelopment();
setUpHealthEndpoint(server);
server.on('close', async () => {

View File

@ -1,6 +1,7 @@
import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models";
import request from "../config/request";
import request from '../config/request';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE,
@ -11,21 +12,17 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL,
} from "../variables";
interface App {
@ -46,80 +43,77 @@ interface App {
const getApps = async ({
integrationAuth,
accessToken,
teamId,
teamId
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
teamId?: string;
}) => {
let apps: App[] = [];
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = [];
break;
case INTEGRATION_AWS_PARAMETER_STORE:
apps = [];
break;
case INTEGRATION_AWS_SECRET_MANAGER:
apps = [];
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken,
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken,
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
accessToken,
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken,
});
break;
case INTEGRATION_GITLAB:
apps = await getAppsGitlab({
accessToken,
teamId,
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken,
});
break;
case INTEGRATION_RAILWAY:
apps = await getAppsRailway({
accessToken,
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleCI({
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
apps = await getAppsTravisCI({
accessToken,
});
break;
case INTEGRATION_SUPABASE:
apps = await getAppsSupabase({
accessToken,
});
break;
try {
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
apps = [];
break;
case INTEGRATION_AWS_PARAMETER_STORE:
apps = [];
break;
case INTEGRATION_AWS_SECRET_MANAGER:
apps = [];
break;
case INTEGRATION_HEROKU:
apps = await getAppsHeroku({
accessToken,
});
break;
case INTEGRATION_VERCEL:
apps = await getAppsVercel({
integrationAuth,
accessToken,
});
break;
case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({
accessToken,
});
break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
accessToken,
});
break;
case INTEGRATION_GITLAB:
apps = await getAppsGitlab({
accessToken,
teamId
});
break;
case INTEGRATION_RENDER:
apps = await getAppsRender({
accessToken,
});
break;
case INTEGRATION_FLYIO:
apps = await getAppsFlyio({
accessToken,
});
break;
case INTEGRATION_CIRCLECI:
apps = await getAppsCircleCI({
accessToken,
});
break;
case INTEGRATION_TRAVISCI:
apps = await getAppsTravisCI({
accessToken,
})
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get integration apps");
}
return apps;
@ -133,18 +127,25 @@ const getApps = async ({
* @returns {String} apps.name - name of Heroku app
*/
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
})
).data;
let apps;
try {
const res = (
await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: {
Accept: "application/vnd.heroku+json; version=3",
Authorization: `Bearer ${accessToken}`,
},
})
).data;
const apps = res.map((a: any) => ({
name: a.name,
}));
apps = res.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Heroku integration apps");
}
return apps;
};
@ -163,26 +164,32 @@ const getAppsVercel = async ({
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
const res = (
await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
let apps;
try {
const res = (
await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
},
...(integrationAuth?.teamId
? {
params: {
teamId: integrationAuth.teamId,
},
}
: {}),
})
).data;
const apps = res.projects.map((a: any) => ({
name: a.name,
appId: a.id,
}));
apps = res.projects.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Vercel integration apps");
}
return apps;
};
@ -196,41 +203,43 @@ const getAppsVercel = async ({
*/
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
const apps: any = [];
let page = 1;
const perPage = 10;
let hasMorePages = true;
try {
let page = 1;
const perPage = 10;
let hasMorePages = true;
// paginate through all sites
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
// paginate through all sites
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
filter: 'all'
});
const { data } = await request.get(
`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`,
{
const { data } = await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.site_id,
'Accept-Encoding': 'application/json'
}
});
});
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.site_id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
if (data.length < perPage) {
hasMorePages = false;
page++;
}
page++;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Netlify integration apps");
}
return apps;
@ -244,58 +253,32 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of Github site
*/
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
interface GitHubApp {
id: string;
name: string;
permissions: {
admin: boolean;
};
owner: {
login: string;
};
}
let apps;
try {
const octokit = new Octokit({
auth: accessToken,
});
const octokit = new Octokit({
auth: accessToken,
});
const getAllRepos = async () => {
let repos: GitHubApp[] = [];
let page = 1;
const per_page = 100;
let hasMore = true;
while (hasMore) {
const response = await octokit.request(
const repos = (
await octokit.request(
"GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}",
{
per_page,
page,
per_page: 100,
}
);
)
).data;
if (response.data.length > 0) {
repos = repos.concat(response.data);
page++;
} else {
hasMore = false;
}
}
return repos;
};
const repos = await getAllRepos();
const apps = repos
.filter((a: GitHubApp) => a.permissions.admin === true)
.map((a: GitHubApp) => {
return {
appId: a.id,
apps = repos
.filter((a: any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name,
owner: a.owner.login,
};
});
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Github repos");
}
return apps;
};
@ -309,74 +292,29 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.appId - id of Render service
*/
const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res.map((a: any) => ({
name: a.service.name,
appId: a.service.id,
}));
return apps;
};
/**
* Return list of projects for Railway integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Railway API
* @returns {Object[]} apps - names and ids of Railway services
* @returns {String} apps.name - name of Railway project
* @returns {String} apps.appId - id of Railway project
*
*/
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
const query = `
query GetProjects($userId: String, $teamId: String) {
projects(userId: $userId, teamId: $teamId) {
edges {
node {
id
name
}
}
}
}
`;
const variables = {};
const {
data: {
data: {
projects: { edges },
},
},
} = await request.post(
INTEGRATION_RAILWAY_API_URL,
{
query,
variables,
},
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Content-Type": "application/json",
"Accept-Encoding": "application/json",
},
}
);
const apps = edges.map((e: any) => ({
name: e.node.name,
appId: e.node.id,
}));
let apps: any;
try {
const res = (
await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Accept-Encoding': 'application/json',
},
})
).data;
apps = res
.map((a: any) => ({
name: a.service.name,
appId: a.service.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Render services");
}
return apps;
};
@ -389,40 +327,41 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of Fly.io apps
*/
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
const query = `
query($role: String) {
apps(type: "container", first: 400, role: $role) {
nodes {
id
name
hostname
let apps;
try {
const query = `
query($role: String) {
apps(type: "container", first: 400, role: $role) {
nodes {
id
name
hostname
}
}
}
}
`;
`;
const res = (
await request.post(
INTEGRATION_FLYIO_API_URL,
{
query,
variables: {
role: null,
},
const res = (await request.post(INTEGRATION_FLYIO_API_URL, {
query,
variables: {
role: null,
},
{
headers: {
Authorization: "Bearer " + accessToken,
Accept: "application/json",
"Accept-Encoding": "application/json",
},
}
)
).data.data.apps.nodes;
}, {
headers: {
Authorization: "Bearer " + accessToken,
'Accept': 'application/json',
'Accept-Encoding': 'application/json',
},
})).data.data.apps.nodes;
const apps = res.map((a: any) => ({
name: a.name,
}));
apps = res.map((a: any) => ({
name: a.name,
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Fly.io apps");
}
return apps;
};
@ -435,43 +374,63 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of CircleCI apps
*/
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, {
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res?.map((a: any) => {
return {
name: a?.reponame,
};
});
let apps: any;
try {
const res = (
await request.get(
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`,
{
headers: {
"Circle-Token": accessToken,
"Accept-Encoding": "application/json",
},
}
)
).data
apps = res?.map((a: any) => {
return {
name: a?.reponame
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get CircleCI projects");
}
return apps;
};
const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
const res = (
await request.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, {
headers: {
Authorization: `token ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
const apps = res?.map((a: any) => {
return {
name: a?.slug?.split("/")[1],
appId: a?.id,
};
});
let apps: any;
try {
const res = (
await request.get(
`${INTEGRATION_TRAVISCI_API_URL}/repos`,
{
headers: {
"Authorization": `token ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
apps = res?.map((a: any) => {
return {
name: a?.slug?.split("/")[1],
appId: a?.id,
}
});
}catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get TravisCI projects");
}
return apps;
};
}
/**
* Return list of repositories for GitLab integration
@ -480,125 +439,110 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
* @returns {Object[]} apps - names of GitLab sites
* @returns {String} apps.name - name of GitLab site
*/
const getAppsGitlab = async ({
const getAppsGitlab = async ({
accessToken,
teamId,
teamId
}: {
accessToken: string;
teamId?: string;
}) => {
const apps: App[] = [];
let page = 1;
const perPage = 10;
let hasMorePages = true;
try {
if (teamId) {
// case: fetch projects for group with id [teamId] in GitLab
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
});
const { data } = await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id,
if (teamId) {
// case: fetch projects for group with id [teamId] in GitLab
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
});
if (data.length < perPage) {
hasMorePages = false;
}
const { data } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
page++;
}
} else {
// case: fetch projects for individual in GitLab
const { id } = (
await request.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, {
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
})
).data;
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage),
});
const { data } = await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
{
params,
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id,
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
});
if (data.length < perPage) {
hasMorePages = false;
if (data.length < perPage) {
hasMorePages = false;
}
page++;
}
} else {
// case: fetch projects for individual in GitLab
const { id } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/user`,
{
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
).data;
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
page++;
const { data } = (
await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`,
{
params,
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.id
});
});
if (data.length < perPage) {
hasMorePages = false;
}
page++;
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab projects");
}
return apps;
};
/**
* Return list of projects for Supabase integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Supabase API
* @returns {Object[]} apps - names of Supabase apps
* @returns {String} apps.name - name of Supabase app
*/
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
const { data } = await request.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
);
const apps = data.map((a: any) => {
return {
name: a.name,
appId: a.id,
};
});
return apps;
};
}
export { getApps };

View File

@ -1,4 +1,5 @@
import request from "../config/request";
import * as Sentry from '@sentry/node';
import request from '../config/request';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
@ -11,8 +12,8 @@ import {
INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL,
} from "../variables";
INTEGRATION_GITLAB_TOKEN_URL
} from '../variables';
import {
getSiteURL,
getClientIdAzure,
@ -25,8 +26,8 @@ import {
getClientIdGitHub,
getClientSecretGitHub,
getClientIdGitLab,
getClientSecretGitLab,
} from "../config";
getClientSecretGitLab
} from '../config';
interface ExchangeCodeAzureResponse {
token_type: string;
@ -92,43 +93,49 @@ interface ExchangeCodeGitlabResponse {
*/
const exchangeCode = async ({
integration,
code,
code
}: {
integration: string;
code: string;
}) => {
let obj = {} as any;
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code,
});
break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code,
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code,
});
break;
case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({
code,
});
break;
case INTEGRATION_GITHUB:
obj = await exchangeCodeGithub({
code,
});
break;
case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({
code,
});
try {
switch (integration) {
case INTEGRATION_AZURE_KEY_VAULT:
obj = await exchangeCodeAzure({
code
});
break;
case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({
code
});
break;
case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({
code
});
break;
case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({
code
});
break;
case INTEGRATION_GITHUB:
obj = await exchangeCodeGithub({
code
});
break;
case INTEGRATION_GITLAB:
obj = await exchangeCodeGitlab({
code
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange');
}
return obj;
@ -136,33 +143,43 @@ const exchangeCode = async ({
/**
* Return [accessToken] for Azure OAuth2 code-token exchange
* @param param0
* @param param0
*/
const exchangeCodeAzure = async ({ code }: { code: string }) => {
const exchangeCodeAzure = async ({
code
}: {
code: string;
}) => {
const accessExpiresAt = new Date();
const res: ExchangeCodeAzureResponse = (
await request.post(
let res: ExchangeCodeAzureResponse;
try {
res = (await request.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
grant_type: 'authorization_code',
code: code,
scope: "https://vault.azure.net/.default openid offline_access",
client_id: await getClientIdAzure(),
client_secret: await getClientSecretAzure(),
redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback`,
scope: 'https://vault.azure.net/.default openid offline_access',
client_id: getClientIdAzure(),
client_secret: getClientSecretAzure(),
redirect_uri: `${getSiteURL()}/integrations/azure-key-vault/oauth2/callback`
} as any)
)
).data;
)).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
}
return {
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
};
};
accessExpiresAt
});
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
@ -174,28 +191,38 @@ const exchangeCodeAzure = async ({ code }: { code: string }) => {
* @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeHeroku = async ({ code }: { code: string }) => {
const exchangeCodeHeroku = async ({
code
}: {
code: string;
}) => {
let res: ExchangeCodeHerokuResponse;
const accessExpiresAt = new Date();
const res: ExchangeCodeHerokuResponse = (
await request.post(
try {
res = (await request.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
grant_type: 'authorization_code',
code: code,
client_secret: await getClientSecretHeroku(),
client_secret: getClientSecretHeroku()
} as any)
)
).data;
)).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Heroku');
}
return {
return ({
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
};
};
accessExpiresAt
});
}
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@ -208,23 +235,30 @@ const exchangeCodeHeroku = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeVercel = async ({ code }: { code: string }) => {
const res: ExchangeCodeVercelResponse = (
await request.post(
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: await getClientIdVercel(),
client_secret: await getClientSecretVercel(),
redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`,
} as any)
)
).data;
let res: ExchangeCodeVercelResponse;
try {
res = (
await request.post(
INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({
code: code,
client_id: getClientIdVercel(),
client_secret: getClientSecretVercel(),
redirect_uri: `${getSiteURL()}/integrations/vercel/oauth2/callback`
} as any)
)
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`);
}
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null,
teamId: res.team_id,
teamId: res.team_id
};
};
@ -239,39 +273,47 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeNetlify = async ({ code }: { code: string }) => {
const res: ExchangeCodeNetlifyResponse = (
await request.post(
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdNetlify(),
client_secret: await getClientSecretNetlify(),
redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`,
} as any)
)
).data;
let res: ExchangeCodeNetlifyResponse;
let accountId;
try {
res = (
await request.post(
INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: getClientIdNetlify(),
client_secret: getClientSecretNetlify(),
redirect_uri: `${getSiteURL()}/integrations/netlify/oauth2/callback`
} as any)
)
).data;
const res2 = await request.get("https://api.netlify.com/api/v1/sites", {
headers: {
Authorization: `Bearer ${res.access_token}`,
},
});
const res3 = (
await request.get("https://api.netlify.com/api/v1/accounts", {
const res2 = await request.get('https://api.netlify.com/api/v1/sites', {
headers: {
Authorization: `Bearer ${res.access_token}`,
},
})
).data;
Authorization: `Bearer ${res.access_token}`
}
});
const accountId = res3[0].id;
const res3 = (
await request.get('https://api.netlify.com/api/v1/accounts', {
headers: {
Authorization: `Bearer ${res.access_token}`
}
})
).data;
accountId = res3[0].id;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify');
}
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accountId,
accountId
};
};
@ -286,25 +328,33 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGithub = async ({ code }: { code: string }) => {
const res: ExchangeCodeGithubResponse = (
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: await getClientIdGitHub(),
client_secret: await getClientSecretGitHub(),
code: code,
redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`,
},
headers: {
Accept: "application/json",
"Accept-Encoding": "application/json",
},
})
).data;
let res: ExchangeCodeGithubResponse;
try {
res = (
await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: getClientIdGitHub(),
client_secret: getClientSecretGitHub(),
code: code,
redirect_uri: `${getSiteURL()}/integrations/github/oauth2/callback`
},
headers: {
'Accept': 'application/json',
'Accept-Encoding': 'application/json'
}
})
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Github');
}
return {
accessToken: res.access_token,
refreshToken: null,
accessExpiresAt: null,
accessExpiresAt: null
};
};
@ -319,32 +369,42 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGitlab = async ({ code }: { code: string }) => {
let res: ExchangeCodeGitlabResponse;
const accessExpiresAt = new Date();
const res: ExchangeCodeGitlabResponse = (
await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: await getClientIdGitLab(),
client_secret: await getClientSecretGitLab(),
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
)
).data;
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
try {
res = (
await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: 'authorization_code',
code: code,
client_id: getClientIdGitLab(),
client_secret: getClientSecretGitLab(),
redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any),
{
headers: {
"Accept-Encoding": "application/json",
}
}
)
).data;
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Gitlab');
}
return {
accessToken: res.access_token,
refreshToken: res.refresh_token,
accessExpiresAt,
accessExpiresAt
};
};
}
export { exchangeCode };

View File

@ -1,24 +1,29 @@
import request from "../config/request";
import { IIntegrationAuth } from "../models";
import * as Sentry from '@sentry/node';
import request from '../config/request';
import {
INTEGRATION_AZURE_KEY_VAULT,
IIntegrationAuth
} from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU,
INTEGRATION_GITLAB,
} from "../variables";
} from '../variables';
import {
INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL,
} from "../variables";
import { IntegrationService } from "../services";
INTEGRATION_GITLAB_TOKEN_URL
} from '../variables';
import {
IntegrationService
} from '../services';
import {
getSiteURL,
getClientIdAzure,
getClientSecretAzure,
getClientSecretHeroku,
getClientIdGitLab,
getClientSecretGitLab,
} from "../config";
getClientSecretGitLab
} from '../config';
interface RefreshTokenAzureResponse {
token_type: string;
@ -55,57 +60,60 @@ interface RefreshTokenGitLabResponse {
*/
const exchangeRefresh = async ({
integrationAuth,
refreshToken,
refreshToken
}: {
integrationAuth: IIntegrationAuth;
refreshToken: string;
}) => {
interface TokenDetails {
accessToken: string;
refreshToken: string;
accessExpiresAt: Date;
}
let tokenDetails: TokenDetails;
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
tokenDetails = await exchangeRefreshAzure({
refreshToken,
try {
switch (integrationAuth.integration) {
case INTEGRATION_AZURE_KEY_VAULT:
tokenDetails = await exchangeRefreshAzure({
refreshToken
});
break;
case INTEGRATION_HEROKU:
tokenDetails = await exchangeRefreshHeroku({
refreshToken
});
break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
refreshToken
});
break;
default:
throw new Error('Failed to exchange token for incompatible integration');
}
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt
});
break;
case INTEGRATION_HEROKU:
tokenDetails = await exchangeRefreshHeroku({
refreshToken,
await IntegrationService.setIntegrationAuthRefresh({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: tokenDetails.refreshToken
});
break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
refreshToken,
});
break;
default:
throw new Error("Failed to exchange token for incompatible integration");
}
return tokenDetails.accessToken;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
}
if (
tokenDetails?.accessToken &&
tokenDetails?.refreshToken &&
tokenDetails?.accessExpiresAt
) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt,
});
await IntegrationService.setIntegrationAuthRefresh({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: tokenDetails.refreshToken,
});
}
return tokenDetails.accessToken;
};
/**
@ -116,30 +124,38 @@ const exchangeRefresh = async ({
* @returns
*/
const exchangeRefreshAzure = async ({
refreshToken,
refreshToken
}: {
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: await getClientIdAzure(),
scope: "openid offline_access",
refresh_token: refreshToken,
grant_type: "refresh_token",
client_secret: await getClientSecretAzure(),
} as any)
);
try {
const accessExpiresAt = new Date();
const { data }: { data: RefreshTokenAzureResponse } = await request.post(
INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({
client_id: getClientIdAzure(),
scope: 'openid offline_access',
refresh_token: refreshToken,
grant_type: 'refresh_token',
client_secret: getClientSecretAzure()
} as any)
);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
};
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get refresh OAuth2 access token for Azure');
}
}
/**
* Return new access token by exchanging refresh token [refreshToken] for the
@ -149,31 +165,39 @@ const exchangeRefreshAzure = async ({
* @returns
*/
const exchangeRefreshHeroku = async ({
refreshToken,
refreshToken
}: {
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenHerokuResponse;
} = await request.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_secret: await getClientSecretHeroku(),
} as any)
);
try {
const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenHerokuResponse
} = await request.post(
INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: getClientSecretHeroku()
} as any)
);
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for Heroku');
}
};
/**
@ -184,38 +208,45 @@ const exchangeRefreshHeroku = async ({
* @returns
*/
const exchangeRefreshGitLab = async ({
refreshToken,
refreshToken
}: {
refreshToken: string;
}) => {
const accessExpiresAt = new Date();
const {
data,
}: {
data: RefreshTokenGitLabResponse;
} = await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: await getClientIdGitLab(),
client_secret: await getClientSecretGitLab(),
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
} as any),
{
headers: {
"Accept-Encoding": "application/json",
},
}
);
try {
const accessExpiresAt = new Date();
const {
data
}: {
data: RefreshTokenGitLabResponse
} = await request.post(
INTEGRATION_GITLAB_TOKEN_URL,
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_id: getClientIdGitLab,
client_secret: getClientSecretGitLab(),
redirect_uri: `${getSiteURL()}/integrations/gitlab/oauth2/callback`
} as any),
{
headers: {
"Accept-Encoding": "application/json",
}
});
accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return {
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt,
};
return ({
accessToken: data.access_token,
refreshToken: data.refresh_token,
accessExpiresAt
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for GitLab');
}
};
export { exchangeRefresh };

View File

@ -1,3 +1,4 @@
import * as Sentry from '@sentry/node';
import {
IIntegrationAuth,
IntegrationAuth,
@ -21,28 +22,34 @@ const revokeAccess = async ({
accessToken: string;
}) => {
let deletedIntegrationAuth;
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
break;
case INTEGRATION_VERCEL:
break;
case INTEGRATION_NETLIFY:
break;
case INTEGRATION_GITHUB:
break;
case INTEGRATION_GITLAB:
break;
}
try {
// add any integration-specific revocation logic
switch (integrationAuth.integration) {
case INTEGRATION_HEROKU:
break;
case INTEGRATION_VERCEL:
break;
case INTEGRATION_NETLIFY:
break;
case INTEGRATION_GITHUB:
break;
case INTEGRATION_GITLAB:
break;
}
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
});
if (deletedIntegrationAuth) {
await Integration.deleteMany({
integrationAuth: deletedIntegrationAuth._id
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id
});
if (deletedIntegrationAuth) {
await Integration.deleteMany({
integrationAuth: deletedIntegrationAuth._id
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete integration authorization');
}
return deletedIntegrationAuth;

View File

@ -21,24 +21,19 @@ import {
INTEGRATION_GITHUB,
INTEGRATION_GITLAB,
INTEGRATION_RENDER,
INTEGRATION_RAILWAY,
INTEGRATION_FLYIO,
INTEGRATION_CIRCLECI,
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_HEROKU_API_URL,
INTEGRATION_GITLAB_API_URL,
INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL,
INTEGRATION_RENDER_API_URL,
INTEGRATION_RAILWAY_API_URL,
INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL
} from "../variables";
import request from '../config/request';
import axios from "axios";
/**
* Sync/push [secrets] to [app] in integration named [integration]
@ -131,13 +126,6 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_RAILWAY:
await syncSecretsRailway({
integration,
secrets,
accessToken
});
break;
case INTEGRATION_FLYIO:
await syncSecretsFlyio({
integration,
@ -159,13 +147,6 @@ const syncSecrets = async ({
accessToken,
});
break;
case INTEGRATION_SUPABASE:
await syncSecretsSupabase({
integration,
secrets,
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
@ -627,7 +608,6 @@ const syncSecretsVercel = async ({
key: string;
value: string;
target: string[];
gitBranch?: string;
}
try {
@ -641,7 +621,46 @@ const syncSecretsVercel = async ({
}
: {}),
};
// const res = (
// await Promise.all(
// (
// await request.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// ))
// .data
// .envs
// .filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment))
// .map(async (secret: VercelSecret) => {
// if (secret.type === 'encrypted') {
// // case: secret is encrypted -> need to decrypt
// const decryptedSecret = (await request.get(
// `${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
// {
// params,
// headers: {
// Authorization: `Bearer ${accessToken}`,
// 'Accept-Encoding': 'application/json'
// }
// }
// )).data;
// return decryptedSecret;
// }
// return secret;
// }))).reduce((obj: any, secret: any) => ({
// ...obj,
// [secret.key]: secret
// }), {});
const vercelSecrets: VercelSecret[] = (await request.get(
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
{
@ -654,21 +673,7 @@ const syncSecretsVercel = async ({
))
.data
.envs
.filter((secret: VercelSecret) => {
if (!secret.target.includes(integration.targetEnvironment)) {
// case: secret does not have the same target environment
return false;
}
if (integration.targetEnvironment === 'preview' && integration.path && integration.path !== secret.gitBranch) {
// case: secret on preview environment does not have same target git branch
return false;
}
return true;
});
// return secret.target.includes(integration.targetEnvironment);
.filter((secret: VercelSecret) => secret.target.includes(integration.targetEnvironment));
const res: { [key: string]: VercelSecret } = {};
@ -691,7 +696,7 @@ const syncSecretsVercel = async ({
res[vercelSecret.key] = vercelSecret;
}
}
const updateSecrets: VercelSecret[] = [];
const deleteSecrets: VercelSecret[] = [];
const newSecrets: VercelSecret[] = [];
@ -705,9 +710,6 @@ const syncSecretsVercel = async ({
value: secrets[key],
type: "encrypted",
target: [integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
});
}
});
@ -724,10 +726,7 @@ const syncSecretsVercel = async ({
type: res[key].type,
target: res[key].target.includes(integration.targetEnvironment)
? [...res[key].target]
: [...res[key].target, integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
: [...res[key].target, integration.targetEnvironment]
});
}
} else {
@ -738,9 +737,6 @@ const syncSecretsVercel = async ({
value: res[key].value,
type: "encrypted", // value doesn't matter
target: [integration.targetEnvironment],
...(integration.path ? {
gitBranch: integration.path
} : {})
});
}
});
@ -1064,7 +1060,7 @@ const syncSecretsGitHub = async ({
"GET /repos/{owner}/{repo}/actions/secrets/public-key",
{
owner: integration.owner,
repo: integration.app
repo: integration.app,
}
)
).data;
@ -1171,58 +1167,6 @@ const syncSecretsRender = async ({
}
};
/**
* Sync/push [secrets] to Railway project with id [integration.appId]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Railway integration
*/
const syncSecretsRailway = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const query = `
mutation UpsertVariables($input: VariableCollectionUpsertInput!) {
variableCollectionUpsert(input: $input)
}
`;
const input = {
projectId: integration.appId,
environmentId: integration.targetEnvironmentId,
...(integration.targetServiceId ? { serviceId: integration.targetServiceId } : {}),
replace: true,
variables: secrets
};
await request.post(INTEGRATION_RAILWAY_API_URL, {
query,
variables: {
input,
},
}, {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to sync secrets to Railway");
}
}
/**
* Sync/push [secrets] to Fly.io app
* @param {Object} obj
@ -1627,79 +1571,4 @@ const syncSecretsGitLab = async ({
}
}
/**
* Sync/push [secrets] to Supabase with name [integration.app]
* @param {Object} obj
* @param {IIntegration} obj.integration - integration details
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
* @param {String} obj.accessToken - access token for Supabase integration
*/
const syncSecretsSupabase = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
const { data: getSecretsRes } = await request.get(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
// convert the secrets to [{}] format
const modifiedFormatForSecretInjection = Object.keys(secrets).map(
(key) => {
return {
name: key,
value: secrets[key]
};
}
);
await request.post(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
modifiedFormatForSecretInjection,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json'
}
}
);
const secretsToDelete: any = [];
getSecretsRes?.forEach((secretObj: any) => {
if (!(secretObj.name in secrets)) {
secretsToDelete.push(secretObj.name);
}
});
await request.delete(
`${INTEGRATION_SUPABASE_API_URL}/v1/projects/${integration.appId}/secrets`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json',
'Accept-Encoding': 'application/json'
},
data: secretsToDelete
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Supabase');
}
};
export { syncSecrets };

View File

@ -1,3 +1,4 @@
import * as Sentry from "@sentry/node";
import {
IIntegrationAuth
} from '../models';
@ -30,15 +31,21 @@ const getTeams = async ({
}) => {
let teams: Team[] = [];
switch (integrationAuth.integration) {
case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({
accessToken
});
break;
try {
switch (integrationAuth.integration) {
case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({
accessToken
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration teams');
}
return teams;
}
@ -56,24 +63,30 @@ const getTeamsGitLab = async ({
accessToken: string;
}) => {
let teams: Team[] = [];
const res = (await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
try {
const res = (await request.get(
`${INTEGRATION_GITLAB_API_URL}/v4/groups`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
}
}
)).data;
teams = res.map((t: any) => ({
name: t.name,
teamId: t.id
}));
)).data;
teams = res.map((t: any) => ({
name: t.name,
teamId: t.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab integration teams");
}
return teams;
}
export {
getTeams
}
}

View File

@ -1,13 +0,0 @@
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../../models';
export interface AuthData {
authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData;
authChannel: string;
authIP: string;
authUserAgent: string;
}

View File

@ -1,7 +0,0 @@
interface AddServiceAccountPermissionDto {
name: string;
workspaceId?: string;
environment?: string;
}
export default AddServiceAccountPermissionDto;

View File

@ -1,8 +0,0 @@
interface CreateServiceAccountDto {
organizationId: string;
name: string;
publicKey: string;
expiresIn: number;
}
export default CreateServiceAccountDto;

View File

@ -1,7 +0,0 @@
import CreateServiceAccountDto from './CreateServiceAccountDto';
import AddServiceAccountPermissionDto from './AddServiceAccountPermissionDto';
export {
CreateServiceAccountDto,
AddServiceAccountPermissionDto
}

View File

@ -1,52 +0,0 @@
import { Types } from 'mongoose';
import { AuthData } from '../../middleware';
export interface CreateSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal';
authData: AuthData;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
}
export interface GetSecretsParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
}
export interface GetSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type?: 'shared' | 'personal';
authData: AuthData;
}
export interface UpdateSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal',
authData: AuthData
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
}
export interface DeleteSecretParams {
secretName: string;
workspaceId: Types.ObjectId;
environment: string;
type: 'shared' | 'personal';
authData: AuthData;
}

View File

@ -10,8 +10,6 @@ import requireIntegrationAuth from './requireIntegrationAuth';
import requireIntegrationAuthorizationAuth from './requireIntegrationAuthorizationAuth';
import requireServiceTokenAuth from './requireServiceTokenAuth';
import requireServiceTokenDataAuth from './requireServiceTokenDataAuth';
import requireServiceAccountAuth from './requireServiceAccountAuth';
import requireServiceAccountWorkspacePermissionAuth from './requireServiceAccountWorkspacePermissionAuth';
import requireSecretAuth from './requireSecretAuth';
import requireSecretsAuth from './requireSecretsAuth';
import validateRequest from './validateRequest';
@ -29,8 +27,6 @@ export {
requireIntegrationAuthorizationAuth,
requireServiceTokenAuth,
requireServiceTokenDataAuth,
requireServiceAccountAuth,
requireServiceAccountWorkspacePermissionAuth,
requireSecretAuth,
requireSecretsAuth,
validateRequest

View File

@ -5,9 +5,9 @@ import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
import { getNodeEnv } from '../config';
export const requestErrorHandler: ErrorRequestHandler = async (error: RequestError | Error, req, res, next) => {
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError | Error, req, res, next) => {
if (res.headersSent) return next();
if ((await getNodeEnv()) !== "production") {
if (getNodeEnv() !== "production") {
/* eslint-disable no-console */
console.log(error)
/* eslint-enable no-console */
@ -15,8 +15,8 @@ export const requestErrorHandler: ErrorRequestHandler = async (error: RequestErr
//TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError
if (!(error instanceof RequestError)) {
error = InternalServerError({ context: { exception: error.message }, stack: error.stack });
(await getLogger('backend-main')).log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
error = InternalServerError({ context: { exception: error.message }, stack: error.stack })
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
}
//* Set Sentry user identification if req.user is populated

View File

@ -4,24 +4,11 @@ import {
validateAuthMode,
getAuthUserPayload,
getAuthSTDPayload,
getAuthAPIKeyPayload,
getAuthSAAKPayload
getAuthAPIKeyPayload
} from '../helpers/auth';
import {
UnauthorizedRequestError
} from '../utils/errors';
import {
IUser,
IServiceAccount,
IServiceTokenData
} from '../models';
import {
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY
} from '../variables';
import { getChannelFromUserAgent } from '../utils/posthog';
declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload {
@ -40,61 +27,50 @@ declare module 'jsonwebtoken' {
* @returns
*/
const requireAuth = ({
acceptedAuthModes = [AUTH_MODE_JWT],
acceptedAuthModes = ['jwt'],
requiredServiceTokenPermissions = []
}: {
acceptedAuthModes: string[];
requiredServiceTokenPermissions?: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// validate auth token against accepted auth modes [acceptedAuthModes]
// and return token type [authTokenType] and value [authTokenValue]
const { authMode, authTokenValue } = validateAuthMode({
const { authTokenType, authTokenValue } = validateAuthMode({
headers: req.headers,
acceptedAuthModes
});
let authPayload: IUser | IServiceAccount | IServiceTokenData;
switch (authMode) {
case AUTH_MODE_SERVICE_ACCOUNT:
authPayload = await getAuthSAAKPayload({
// attach auth payloads
let serviceTokenData: any;
switch (authTokenType) {
case 'serviceToken':
serviceTokenData = await getAuthSTDPayload({
authTokenValue
});
req.serviceAccount = authPayload;
requiredServiceTokenPermissions.forEach((requiredServiceTokenPermission) => {
if (!serviceTokenData.permissions.includes(requiredServiceTokenPermission)) {
return next(UnauthorizedRequestError({ message: 'Failed to authorize service token for endpoint' }));
}
});
req.serviceTokenData = serviceTokenData;
req.user = serviceTokenData?.user;
break;
case AUTH_MODE_SERVICE_TOKEN:
authPayload = await getAuthSTDPayload({
case 'apiKey':
req.user = await getAuthAPIKeyPayload({
authTokenValue
});
req.serviceTokenData = authPayload;
break;
case AUTH_MODE_API_KEY:
authPayload = await getAuthAPIKeyPayload({
authTokenValue
});
req.user = authPayload;
break;
default:
authPayload = await getAuthUserPayload({
req.user = await getAuthUserPayload({
authTokenValue
});
req.user = authPayload;
break;
}
req.requestData = {
...req.params,
...req.query,
...req.body,
}
req.authData = {
authMode,
authPayload, // User, ServiceAccount, ServiceTokenData
authChannel: getChannelFromUserAgent(req.headers['user-agent']),
authIP: req.ip,
authUserAgent: req.headers['user-agent'] ?? 'other'
}
return next();
}
}

View File

@ -1,28 +1,32 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Bot } from '../models';
import { validateMembership } from '../helpers/membership';
import { validateClientForBot } from '../helpers/bot';
import { AccountNotFoundError } from '../utils/errors';
type req = 'params' | 'body' | 'query';
const requireBotAuth = ({
acceptedRoles,
locationBotId = 'params'
location = 'params'
}: {
acceptedRoles: Array<'admin' | 'member'>;
locationBotId?: req;
acceptedRoles: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { botId } = req[locationBotId];
const bot = await Bot.findById(req[location].botId);
req.bot = await validateClientForBot({
authData: req.authData,
botId: new Types.ObjectId(botId),
if (!bot) {
return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles
});
req.bot = bot;
next();
}
}

View File

@ -1,9 +1,7 @@
import { Request, Response, NextFunction } from 'express';
import { Types } from 'mongoose';
import { Integration, IntegrationAuth } from '../models';
import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership';
import { validateClientForIntegration } from '../helpers/integration';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
/**
@ -15,25 +13,43 @@ import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/err
const requireIntegrationAuth = ({
acceptedRoles
}: {
acceptedRoles: Array<'admin' | 'member'>;
acceptedRoles: string[];
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
// integration authorization middleware
const { integrationId } = req.params;
const { integration, accessToken } = await validateClientForIntegration({
authData: req.authData,
integrationId: new Types.ObjectId(integrationId),
// validate integration accessibility
const integration = await Integration.findOne({
_id: integrationId
});
if (!integration) {
return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integration.workspace.toString(),
acceptedRoles
});
if (integration) {
req.integration = integration;
}
if (accessToken) {
req.accessToken = accessToken;
const integrationAuth = await IntegrationAuth.findOne({
_id: integration.integrationAuth
}).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}))
}
req.integration = integration;
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
return next();
};
};

View File

@ -1,8 +1,7 @@
import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models';
import { IntegrationService } from '../services';
import { validateClientForIntegrationAuth } from '../helpers/integrationAuth';
import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
@ -20,26 +19,36 @@ const requireIntegrationAuthorizationAuth = ({
attachAccessToken = true,
location = 'params'
}: {
acceptedRoles: Array<'admin' | 'member'>;
acceptedRoles: string[];
attachAccessToken?: boolean;
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { integrationAuthId } = req[location];
const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId
})
.populate<{ workspace: IWorkspace }>('workspace')
.select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
const { integrationAuth, accessToken } = await validateClientForIntegrationAuth({
authData: req.authData,
integrationAuthId: new Types.ObjectId(integrationAuthId),
acceptedRoles,
attachAccessToken
});
if (integrationAuth) {
req.integrationAuth = integrationAuth;
if (!integrationAuth) {
return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace._id.toString(),
acceptedRoles
});
if (accessToken) {
req.accessToken = accessToken;
req.integrationAuth = integrationAuth;
if (attachAccessToken) {
const access = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
req.accessToken = access.accessToken;
}
return next();

View File

@ -1,13 +1,9 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
Membership,
} from '../models';
import {
validateClientForMembership,
validateMembership
} from '../helpers/membership';
import { validateMembership } from '../helpers/membership';
type req = 'params' | 'body' | 'query';
@ -20,25 +16,43 @@ type req = 'params' | 'body' | 'query';
*/
const requireMembershipAuth = ({
acceptedRoles,
locationMembershipId = 'params'
location = 'params'
}: {
acceptedRoles: Array<'admin' | 'member'>;
locationMembershipId: req
acceptedRoles: string[];
location?: req;
}) => {
return async (
req: Request,
res: Response,
next: NextFunction
) => {
const { membershipId } = req[locationMembershipId];
req.targetMembership = await validateClientForMembership({
authData: req.authData,
membershipId: new Types.ObjectId(membershipId),
acceptedRoles
});
return next();
try {
const { membershipId } = req[location];
const membership = await Membership.findById(membershipId);
if (!membership) throw new Error('Failed to find target membership');
const userMembership = await Membership.findOne({
workspace: membership.workspace
});
if (!userMembership) throw new Error('Failed to validate own membership')
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
workspaceId: membership.workspace.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate workspace membership'
}));
}
}
}

View File

@ -1,17 +1,11 @@
import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express';
import { UnauthorizedRequestError } from '../utils/errors';
import {
MembershipOrg
} from '../models';
import {
validateClientForMembershipOrg,
validateMembershipOrg
} from '../helpers/membershipOrg';
import { validateMembership } from '../helpers/membershipOrg';
// TODO: transform
type req = 'params' | 'body' | 'query';
/**
@ -23,24 +17,32 @@ type req = 'params' | 'body' | 'query';
*/
const requireMembershipOrgAuth = ({
acceptedRoles,
acceptedStatuses,
locationMembershipOrgId = 'params'
location = 'params'
}: {
acceptedRoles: Array<'owner' | 'admin' | 'member'>;
acceptedStatuses: Array<'invited' | 'accepted'>;
locationMembershipOrgId?: req;
acceptedRoles: string[];
location?: req;
}) => {
return async (req: Request, res: Response, next: NextFunction) => {
const { membershipId } = req[locationMembershipOrgId];
req.membershipOrg = await validateClientForMembershipOrg({
authData: req.authData,
membershipOrgId: new Types.ObjectId(membershipId),
acceptedRoles,
acceptedStatuses
});
return next();
try {
const { membershipId } = req[location];
const membershipOrg = await MembershipOrg.findById(membershipId);
if (!membershipOrg) throw new Error('Failed to find target organization membership');
const targetMembership = await validateMembership({
userId: req.user._id.toString(),
organizationId: membershipOrg.organization.toString(),
acceptedRoles
});
req.targetMembership = targetMembership;
return next();
} catch (err) {
return next(UnauthorizedRequestError({
message: 'Unable to validate organization membership'
}));
}
}
}

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