Fix merge conflicts

This commit is contained in:
Tuan Dang
2022-12-27 09:34:07 -05:00
196 changed files with 8025 additions and 2831 deletions
+15 -15
View File
@@ -1,15 +1,13 @@
# Keys # Keys
# Required keys for platform encryption/decryption ops # Required key for platform encryption/decryption ops
PRIVATE_KEY=replace_with_nacl_sk ENCRYPTION_KEY=6c1fe4e407b8911c104518103505b218
PUBLIC_KEY=replace_with_nacl_pk
ENCRYPTION_KEY=replace_with_lengthy_secure_hex
# JWT # JWT
# Required secrets to sign JWT tokens # Required secrets to sign JWT tokens
JWT_SIGNUP_SECRET=replace_with_lengthy_secure_hex JWT_SIGNUP_SECRET=3679e04ca949f914c03332aaaeba805a
JWT_REFRESH_SECRET=replace_with_lengthy_secure_hex JWT_REFRESH_SECRET=5f2f3c8f0159068dc2bbb3a652a716ff
JWT_AUTH_SECRET=replace_with_lengthy_secure_hex JWT_AUTH_SECRET=4be6ba5602e0fa0ac6ac05c3cd4d247f
JWT_SERVICE_SECRET=replace_with_lengthy_secure_hex JWT_SERVICE_SECRET=f32f716d70a42c5703f4656015e76200
# JWT lifetime # JWT lifetime
# Optional lifetimes for JWT tokens expressed in seconds or a string # Optional lifetimes for JWT tokens expressed in seconds or a string
@@ -33,26 +31,28 @@ MONGO_PASSWORD=example
# Website URL # Website URL
# Required # Required
SITE_URL=http://localhost:8080 SITE_URL=http://localhost:8080
# Mail/SMTP # Mail/SMTP
# Required to send emails SMTP_HOST= # required
# By default, SMTP_HOST is set to smtp.gmail.com SMTP_USERNAME= # required
SMTP_HOST=smtp.gmail.com SMTP_PASSWORD= # required
SMTP_PORT=587 SMTP_PORT=587
SMTP_NAME=Team SMTP_SECURE=false
SMTP_USERNAME=[email protected] SMTP_FROM_ADDRESS= # required
SMTP_PASSWORD= SMTP_FROM_NAME=Infisical
# Integration # Integration
# Optional only if integration is used # Optional only if integration is used
CLIENT_ID_HEROKU= CLIENT_ID_HEROKU=
CLIENT_ID_VERCEL= CLIENT_ID_VERCEL=
CLIENT_ID_NETLIFY= CLIENT_ID_NETLIFY=
CLIENT_ID_GITHUB=
CLIENT_SECRET_HEROKU= CLIENT_SECRET_HEROKU=
CLIENT_SECRET_VERCEL= CLIENT_SECRET_VERCEL=
CLIENT_SECRET_NETLIFY= CLIENT_SECRET_NETLIFY=
CLIENT_SECRET_GITHUB=
CLIENT_SLUG_VERCEL=
# Sentry (optional) for monitoring errors # Sentry (optional) for monitoring errors
SENTRY_DSN= SENTRY_DSN=
+41
View File
@@ -0,0 +1,41 @@
name: "Backend Test Report"
on:
workflow_run:
workflows: ["Check Backend Pull Request"]
types:
- completed
jobs:
be-report:
name: Backend test report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: 📁 Download test results
id: download-artifact
uses: dawidd6/action-download-artifact@v2
with:
name: be-test-results
path: backend
workflow: check-be-pull-request.yml
workflow_conclusion: success
- name: 📋 Publish test results
uses: dorny/test-reporter@v1
with:
name: Test Results
path: reports/jest-*.xml
reporter: jest-junit
working-directory: backend
- name: 📋 Publish coverage
uses: ArtiomTr/jest-coverage-report-action@v2
id: coverage
with:
output: comment, report-markdown
coverage-file: coverage/report.json
github-token: ${{ secrets.GITHUB_TOKEN }}
working-directory: backend
- uses: marocchino/sticky-pull-request-comment@v2
with:
message: ${{ steps.coverage.outputs.report }}
+23 -22
View File
@@ -1,41 +1,42 @@
name: Check Backend Pull Request name: "Check Backend Pull Request"
on: on:
pull_request: pull_request:
types: [ opened, synchronize ] types: [opened, synchronize]
paths: paths:
- 'backend/**' - "backend/**"
- '!backend/README.md' - "!backend/README.md"
- '!backend/.*' - "!backend/.*"
- 'backend/.eslintrc.js' - "backend/.eslintrc.js"
jobs: jobs:
check-be-pr: check-be-pr:
name: Check name: Check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- - name: ☁️ Checkout source
name: ☁️ Checkout source
uses: actions/checkout@v3 uses: actions/checkout@v3
- - name: 🔧 Setup Node 16
name: 🔧 Setup Node 16
uses: actions/setup-node@v3 uses: actions/setup-node@v3
with: with:
node-version: '16' node-version: "16"
cache: 'npm' cache: "npm"
cache-dependency-path: backend/package-lock.json cache-dependency-path: backend/package-lock.json
- - name: 📦 Install dependencies
name: 📦 Install dependencies
run: npm ci --only-production --ignore-scripts run: npm ci --only-production --ignore-scripts
working-directory: backend working-directory: backend
# - - name: 🧪 Run tests
# name: 🧪 Run tests run: npm run test:ci
# run: npm run test:ci working-directory: backend
# working-directory: backend - name: 📁 Upload test results
- uses: actions/upload-artifact@v3
name: 🏗️ Run build if: always()
with:
name: be-test-results
path: |
./backend/reports
./backend/coverage
- name: 🏗️ Run build
run: npm run build run: npm run build
working-directory: backend working-directory: backend
@@ -1,22 +0,0 @@
name: Close inactive issues
on:
schedule:
- cron: "30 1 * * *"
jobs:
close-issues:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- uses: actions/stale@v4
with:
days-before-issue-stale: 30
days-before-issue-close: 14
stale-issue-label: "stale"
stale-issue-message: "This issue is stale because it has been open for 30 days with no activity."
close-issue-message: "This issue was closed because it has been inactive for 14 days since being marked as stale."
days-before-pr-stale: -1
days-before-pr-close: -1
repo-token: ${{ secrets.GITHUB_TOKEN }}
+11 -1
View File
@@ -13,7 +13,7 @@ permissions:
jobs: jobs:
goreleaser: goreleaser:
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
with: with:
@@ -24,6 +24,15 @@ jobs:
go-version: '>=1.19.3' go-version: '>=1.19.3'
cache: true cache: true
cache-dependency-path: cli/go.sum cache-dependency-path: cli/go.sum
- name: libssl1.1 => libssl1.0-dev for OSXCross
run: |
echo 'deb http://security.ubuntu.com/ubuntu bionic-security main' | sudo tee -a /etc/apt/sources.list
sudo apt update && apt-cache policy libssl1.0-dev
sudo apt-get install libssl1.0-dev
- name: OSXCross for CGO Support
run: |
mkdir ../../osxcross
git clone https://github.com/plentico/osxcross-target.git ../../osxcross/target
- uses: goreleaser/goreleaser-action@v2 - uses: goreleaser/goreleaser-action@v2
with: with:
distribution: goreleaser distribution: goreleaser
@@ -32,6 +41,7 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GO_RELEASER_GITHUB_TOKEN }}
FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }} FURY_TOKEN: ${{ secrets.FURYPUSHTOKEN }}
AUR_KEY: ${{ secrets.AUR_KEY }}
- uses: actions/setup-python@v4 - uses: actions/setup-python@v4
- run: pip install --upgrade cloudsmith-cli - run: pip install --upgrade cloudsmith-cli
- name: Publish to CloudSmith - name: Publish to CloudSmith
+3 -1
View File
@@ -25,7 +25,9 @@ node_modules
.env .env
# testing # testing
/coverage coverage
reports
junit.xml
# next.js # next.js
/.next/ /.next/
+36 -11
View File
@@ -7,28 +7,37 @@
# # you may remove this if you don't need go generate # # you may remove this if you don't need go generate
# - cd cli && go generate ./... # - cd cli && go generate ./...
builds: builds:
- env: - id: darwin-build
- CGO_ENABLED=0
binary: infisical binary: infisical
id: infisical env:
- CGO_ENABLED=1
- CC=/home/runner/work/osxcross/target/bin/o64-clang
- CXX=/home/runner/work/osxcross/target/bin/o64-clang++
goos: goos:
- darwin - darwin
ignore:
- goos: darwin
goarch: "386"
dir: ./cli
- id: all-other-builds
env:
- CGO_ENABLED=0
binary: infisical
goos:
- freebsd - freebsd
- linux - linux
- netbsd - netbsd
- openbsd - openbsd
- windows - windows
goarch: goarch:
- 386 - "386"
- amd64 - amd64
- arm - arm
- arm64 - arm64
goarm: goarm:
- 6 - "6"
- 7 - "7"
ignore: ignore:
- goos: darwin
goarch: "386"
- goos: windows - goos: windows
goarch: "386" goarch: "386"
- goos: freebsd - goos: freebsd
@@ -71,12 +80,12 @@ nfpms:
- id: infisical - id: infisical
package_name: infisical package_name: infisical
builds: builds:
- infisical - all-other-builds
vendor: Infisical, Inc vendor: Infisical, Inc
homepage: https://infisical.com/ homepage: https://infisical.com/
maintainer: Infisical, Inc maintainer: Infisical, Inc
description: The offical Infisical CLI description: The offical Infisical CLI
license: Apache 2.0 license: MIT
formats: formats:
- rpm - rpm
- deb - deb
@@ -92,7 +101,23 @@ scoop:
email: [email protected] email: [email protected]
homepage: "https://infisical.com" homepage: "https://infisical.com"
description: "The official Infisical CLI" description: "The official Infisical CLI"
license: Apache-2.0 license: MIT
aurs:
-
name: infisical-bin
homepage: "https://infisical.com"
description: "The official Infisical CLI"
maintainers:
- Infisical, Inc <[email protected]>
license: MIT
private_key: '{{ .Env.AUR_KEY }}'
git_url: 'ssh://[email protected]/infisical-bin.git'
package: |-
# bin
install -Dm755 "./infisical" "${pkgdir}/usr/bin/infisical"
# license
install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/infisical/LICENSE"
# dockers: # dockers:
# - dockerfile: goreleaser.dockerfile # - dockerfile: goreleaser.dockerfile
# goos: linux # goos: linux
+7 -3
View File
@@ -146,7 +146,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 AWS 🔜 AWS
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 GitHub Actions (https://github.com/Infisical/infisical/issues/54) <a href="https://infisical.com/docs/integrations/cicd/githubactions">
✔️ GitHub Actions
</a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 Railway 🔜 Railway
@@ -179,7 +181,9 @@ We're currently setting the foundation and building [integrations](https://infis
🔜 TravisCI 🔜 TravisCI
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 Netlify (https://github.com/Infisical/infisical/issues/55) <a href="https://infisical.com/docs/integrations/cloud/netlify">
✔️ Netlify
</a>
</td> </td>
<td align="left" valign="middle"> <td align="left" valign="middle">
🔜 Railway 🔜 Railway
@@ -317,4 +321,4 @@ Infisical officially launched as v.1.0 on November 21st, 2022. However, a lot of
<!-- prettier-ignore-start --> <!-- prettier-ignore-start -->
<!-- markdownlint-disable --> <!-- markdownlint-disable -->
<a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/dangtony98"><img src="https://avatars.githubusercontent.com/u/25857006?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/mv-turtle"><img src="https://avatars.githubusercontent.com/u/78047717?s=96&v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/maidul98"><img src="https://avatars.githubusercontent.com/u/9300960?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gangjun06"><img src="https://avatars.githubusercontent.com/u/50910815?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/reginaldbondoc"><img src="https://avatars.githubusercontent.com/u/7693108?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/SH5H"><img src="https://avatars.githubusercontent.com/u/25437192?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/gmgale"><img src="https://avatars.githubusercontent.com/u/62303146?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/asharonbaltazar"><img src="https://avatars.githubusercontent.com/u/58940073?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/edgarrmondragon"><img src="https://avatars.githubusercontent.com/u/16805946?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arjunyel"><img src="https://avatars.githubusercontent.com/u/11153289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/LemmyMwaura"><img src="https://avatars.githubusercontent.com/u/20738858?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/Zamion101"><img src="https://avatars.githubusercontent.com/u/8071263?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/akhilmhdh"><img src="https://avatars.githubusercontent.com/u/31166322?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/naorpeled"><img src="https://avatars.githubusercontent.com/u/6171622?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/jonerrr"><img src="https://avatars.githubusercontent.com/u/73760377?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/adrianmarinwork"><img src="https://avatars.githubusercontent.com/u/118568289?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/arthurzenika"><img src="https://avatars.githubusercontent.com/u/445200?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/hanywang2"><img src="https://avatars.githubusercontent.com/u/44352119?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/tobias-mintlify"><img src="https://avatars.githubusercontent.com/u/110702161?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wjhurley"><img src="https://avatars.githubusercontent.com/u/15939055?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/0xflotus"><img src="https://avatars.githubusercontent.com/u/26602940?v=4" width="50" height="50" alt=""/></a> <a href="https://github.com/wanjohiryan"><img src="https://avatars.githubusercontent.com/u/71614375?v=4" width="50" height="50" alt=""/></a>
+19
View File
@@ -0,0 +1,19 @@
import { server } from '../src/app';
import { describe, expect, it, beforeAll, afterAll } from '@jest/globals';
import supertest from 'supertest';
import { setUpHealthEndpoint } from '../src/services/health';
const requestWithSupertest = supertest(server);
describe('Healthcheck endpoint', () => {
beforeAll(async () => {
setUpHealthEndpoint(server);
});
afterAll(async () => {
server.close();
});
it('GET /healthcheck should return OK', async () => {
const res = await requestWithSupertest.get('/healthcheck');
expect(res.status).toEqual(200);
});
});
+2 -2
View File
@@ -14,6 +14,8 @@ declare global {
JWT_SIGNUP_SECRET: string; JWT_SIGNUP_SECRET: string;
MONGO_URL: string; MONGO_URL: string;
NODE_ENV: 'development' | 'staging' | 'testing' | 'production'; NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
VERBOSE_ERROR_OUTPUT: string;
LOKI_HOST: string;
CLIENT_ID_HEROKU: string; CLIENT_ID_HEROKU: string;
CLIENT_ID_VERCEL: string; CLIENT_ID_VERCEL: string;
CLIENT_ID_NETLIFY: string; CLIENT_ID_NETLIFY: string;
@@ -22,8 +24,6 @@ declare global {
CLIENT_SECRET_NETLIFY: string; CLIENT_SECRET_NETLIFY: string;
POSTHOG_HOST: string; POSTHOG_HOST: string;
POSTHOG_PROJECT_API_KEY: string; POSTHOG_PROJECT_API_KEY: string;
PRIVATE_KEY: string;
PUBLIC_KEY: string;
SENTRY_DSN: string; SENTRY_DSN: string;
SITE_URL: string; SITE_URL: string;
SMTP_HOST: string; SMTP_HOST: string;
+1538 -261
View File
File diff suppressed because it is too large Load Diff
+40 -4
View File
@@ -1,9 +1,11 @@
{ {
"dependencies": { "dependencies": {
"@godaddy/terminus": "^4.11.2", "@godaddy/terminus": "^4.11.2",
"@octokit/rest": "^19.0.5",
"@sentry/node": "^7.14.0", "@sentry/node": "^7.14.0",
"@sentry/tracing": "^7.19.0", "@sentry/tracing": "^7.19.0",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@types/libsodium-wrappers": "^0.7.10",
"axios": "^1.1.3", "axios": "^1.1.3",
"bigint-conversion": "^2.2.2", "bigint-conversion": "^2.2.2",
"cookie-parser": "^1.4.6", "cookie-parser": "^1.4.6",
@@ -15,17 +17,20 @@
"express-validator": "^6.14.2", "express-validator": "^6.14.2",
"handlebars": "^4.7.7", "handlebars": "^4.7.7",
"helmet": "^5.1.1", "helmet": "^5.1.1",
"jsonwebtoken": "^8.5.1", "jsonwebtoken": "^9.0.0",
"jsrp": "^0.2.4", "jsrp": "^0.2.4",
"libsodium-wrappers": "^0.7.10",
"mongoose": "^6.7.2", "mongoose": "^6.7.2",
"nodemailer": "^6.8.0", "nodemailer": "^6.8.0",
"posthog-node": "^2.1.0", "posthog-node": "^2.2.2",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"stripe": "^10.7.0", "stripe": "^10.7.0",
"tweetnacl": "^1.0.3", "tweetnacl": "^1.0.3",
"tweetnacl-util": "^0.15.1", "tweetnacl-util": "^0.15.1",
"typescript": "^4.9.3" "typescript": "^4.9.3",
"winston": "^3.8.2",
"winston-loki": "^6.0.6"
}, },
"name": "infisical-api", "name": "infisical-api",
"version": "1.0.0", "version": "1.0.0",
@@ -37,7 +42,11 @@
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build", "build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
"lint": "eslint . --ext .ts", "lint": "eslint . --ext .ts",
"lint-and-fix": "eslint . --ext .ts --fix", "lint-and-fix": "eslint . --ext .ts --fix",
"lint-staged": "lint-staged" "lint-staged": "lint-staged",
"pretest": "docker compose -f test-resources/docker-compose.test.yml up -d",
"test": "cross-env NODE_ENV=test jest --testTimeout=10000 --detectOpenHandles",
"test:ci": "npm test -- --watchAll=false --ci --reporters=default --reporters=jest-junit --reporters=github-actions --coverage --testLocationInResults --json --outputFile=coverage/report.json",
"posttest": "docker compose -f test-resources/docker-compose.test.yml down"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@@ -51,22 +60,49 @@
"homepage": "https://github.com/Infisical/infisical-api#readme", "homepage": "https://github.com/Infisical/infisical-api#readme",
"description": "", "description": "",
"devDependencies": { "devDependencies": {
"@jest/globals": "^29.3.1",
"@posthog/plugin-scaffold": "^1.3.4", "@posthog/plugin-scaffold": "^1.3.4",
"@types/cookie-parser": "^1.4.3", "@types/cookie-parser": "^1.4.3",
"@types/cors": "^2.8.12", "@types/cors": "^2.8.12",
"@types/express": "^4.17.14", "@types/express": "^4.17.14",
"@types/jest": "^29.2.4",
"@types/jsonwebtoken": "^8.5.9", "@types/jsonwebtoken": "^8.5.9",
"@types/node": "^18.11.3", "@types/node": "^18.11.3",
"@types/nodemailer": "^6.4.6", "@types/nodemailer": "^6.4.6",
"@types/supertest": "^2.0.12",
"@types/swagger-jsdoc": "^6.0.1", "@types/swagger-jsdoc": "^6.0.1",
"@types/swagger-ui-express": "^4.1.3", "@types/swagger-ui-express": "^4.1.3",
"@typescript-eslint/eslint-plugin": "^5.40.1", "@typescript-eslint/eslint-plugin": "^5.40.1",
"@typescript-eslint/parser": "^5.40.1", "@typescript-eslint/parser": "^5.40.1",
"cross-env": "^7.0.3",
"eslint": "^8.26.0", "eslint": "^8.26.0",
"install": "^0.13.0", "install": "^0.13.0",
"jest": "^29.3.1", "jest": "^29.3.1",
"jest-junit": "^15.0.0",
"nodemon": "^2.0.19", "nodemon": "^2.0.19",
"npm": "^8.19.3", "npm": "^8.19.3",
"supertest": "^6.3.3",
"ts-jest": "^29.0.3",
"ts-node": "^10.9.1" "ts-node": "^10.9.1"
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node",
"collectCoverageFrom": [
"src/*.{js,ts}",
"!**/node_modules/**"
],
"setupFiles": [
"<rootDir>/test-resources/env-vars.js"
]
},
"jest-junit": {
"outputDirectory": "reports",
"outputName": "jest-junit.xml",
"ancestorSeparator": " › ",
"uniqueOutputName": "false",
"suiteNameTemplate": "{filepath}",
"classNameTemplate": "{classname}",
"titleTemplate": "{title}"
} }
} }
+109
View File
@@ -0,0 +1,109 @@
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { patchRouterParam } = require('./utils/patchAsyncRoutes');
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import dotenv from 'dotenv';
dotenv.config();
import { PORT, NODE_ENV, SITE_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter';
import {
workspace as eeWorkspaceRouter,
secret as eeSecretRouter
} from './ee/routes/v1';
import {
signup as v1SignupRouter,
auth as v1AuthRouter,
bot as v1BotRouter,
organization as v1OrganizationRouter,
workspace as v1WorkspaceRouter,
membershipOrg as v1MembershipOrgRouter,
membership as v1MembershipRouter,
key as v1KeyRouter,
inviteOrg as v1InviteOrgRouter,
user as v1UserRouter,
userAction as v1UserActionRouter,
secret as v1SecretRouter,
serviceToken as v1ServiceTokenRouter,
password as v1PasswordRouter,
stripe as v1StripeRouter,
integration as v1IntegrationRouter,
integrationAuth as v1IntegrationAuthRouter
} from './routes/v1';
import {
secret as v2SecretRouter,
workspace as v2WorkspaceRouter
} from './routes/v2';
import { getLogger } from './utils/logger';
import { RouteNotFoundError } from './utils/errors';
import { requestErrorHandler } from './middleware/requestErrorHandler';
// patch async route params to handle Promise Rejections
patchRouterParam();
export const app = express();
app.enable('trust proxy');
app.use(express.json());
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: SITE_URL
})
);
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
}
// (EE) routes
app.use('/api/v1/secret', eeSecretRouter);
app.use('/api/v1/workspace', eeWorkspaceRouter);
// v1 routes
app.use('/api/v1/signup', v1SignupRouter);
app.use('/api/v1/auth', v1AuthRouter);
app.use('/api/v1/bot', v1BotRouter);
app.use('/api/v1/user', v1UserRouter);
app.use('/api/v1/user-action', v1UserActionRouter);
app.use('/api/v1/organization', v1OrganizationRouter);
app.use('/api/v1/workspace', v1WorkspaceRouter);
app.use('/api/v1/membership-org', v1MembershipOrgRouter);
app.use('/api/v1/membership', v1MembershipRouter);
app.use('/api/v1/key', v1KeyRouter);
app.use('/api/v1/invite-org', v1InviteOrgRouter);
app.use('/api/v1/secret', v1SecretRouter);
app.use('/api/v1/service-token', v1ServiceTokenRouter);
app.use('/api/v1/password', v1PasswordRouter);
app.use('/api/v1/stripe', v1StripeRouter);
app.use('/api/v1/integration', v1IntegrationRouter);
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
// v2 routes
app.use('/api/v2/workspace', v2WorkspaceRouter);
app.use('/api/v2/secret', v2SecretRouter);
//* Handle unrouted requests and respond with proper error message as well as status code
app.use((req, res, next)=>{
if(res.headersSent) return next();
next(RouteNotFoundError({message: `The requested source '(${req.method})${req.url}' was not found`}))
})
//* Error Handling Middleware (must be after all routing logic)
app.use(requestErrorHandler)
export const server = app.listen(PORT, () => {
getLogger("backend-main").info(`Server started listening at port ${PORT}`)
});
+19 -9
View File
@@ -10,26 +10,30 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!; const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
const MONGO_URL = process.env.MONGO_URL!; const MONGO_URL = process.env.MONGO_URL!;
const NODE_ENV = process.env.NODE_ENV! || 'production'; const NODE_ENV = process.env.NODE_ENV! || 'production';
const VERBOSE_ERROR_OUTPUT = process.env.VERBOSE_ERROR_OUTPUT! === 'true' && true;
const LOKI_HOST = process.env.LOKI_HOST || undefined;
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!; const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!; const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!; const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!; const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
const CLIENT_ID_GITHUB = process.env.CLIENT_ID_GITHUB!;
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!; const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!; const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
const CLIENT_SECRET_GITHUB = process.env.CLIENT_SECRET_GITHUB!;
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!; const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com'; const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
const POSTHOG_PROJECT_API_KEY = const POSTHOG_PROJECT_API_KEY =
process.env.POSTHOG_PROJECT_API_KEY! || process.env.POSTHOG_PROJECT_API_KEY! ||
'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE'; 'phc_nSin8j5q2zdhpFDI1ETmFNUIuTG4DwKVyIigrY10XiE';
const PRIVATE_KEY = process.env.PRIVATE_KEY!;
const PUBLIC_KEY = process.env.PUBLIC_KEY!;
const SENTRY_DSN = process.env.SENTRY_DSN!; const SENTRY_DSN = process.env.SENTRY_DSN!;
const SITE_URL = process.env.SITE_URL!; const SITE_URL = process.env.SITE_URL!;
const SMTP_HOST = process.env.SMTP_HOST! || 'smtp.gmail.com'; const SMTP_HOST = process.env.SMTP_HOST!;
const SMTP_PORT = process.env.SMTP_PORT! || 587; const SMTP_SECURE = process.env.SMTP_SECURE! === 'true' || false;
const SMTP_NAME = process.env.SMTP_NAME!; const SMTP_PORT = parseInt(process.env.SMTP_PORT!) || 587;
const SMTP_USERNAME = process.env.SMTP_USERNAME!; const SMTP_USERNAME = process.env.SMTP_USERNAME!;
const SMTP_PASSWORD = process.env.SMTP_PASSWORD!; const SMTP_PASSWORD = process.env.SMTP_PASSWORD!;
const SMTP_FROM_ADDRESS = process.env.SMTP_FROM_ADDRESS!;
const SMTP_FROM_NAME = process.env.SMTP_FROM_NAME! || 'Infisical';
const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!; const STRIPE_PRODUCT_CARD_AUTH = process.env.STRIPE_PRODUCT_CARD_AUTH!;
const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!; const STRIPE_PRODUCT_PRO = process.env.STRIPE_PRODUCT_PRO!;
const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!; const STRIPE_PRODUCT_STARTER = process.env.STRIPE_PRODUCT_STARTER!;
@@ -37,6 +41,7 @@ const STRIPE_PUBLISHABLE_KEY = process.env.STRIPE_PUBLISHABLE_KEY!;
const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!; const STRIPE_SECRET_KEY = process.env.STRIPE_SECRET_KEY!;
const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!; const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET!;
const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true; const TELEMETRY_ENABLED = process.env.TELEMETRY_ENABLED! !== 'false' && true;
const LICENSE_KEY = process.env.LICENSE_KEY!;
export { export {
PORT, PORT,
@@ -51,29 +56,34 @@ export {
JWT_SIGNUP_SECRET, JWT_SIGNUP_SECRET,
MONGO_URL, MONGO_URL,
NODE_ENV, NODE_ENV,
VERBOSE_ERROR_OUTPUT,
LOKI_HOST,
CLIENT_ID_HEROKU, CLIENT_ID_HEROKU,
CLIENT_ID_VERCEL, CLIENT_ID_VERCEL,
CLIENT_ID_NETLIFY, CLIENT_ID_NETLIFY,
CLIENT_ID_GITHUB,
CLIENT_SECRET_HEROKU, CLIENT_SECRET_HEROKU,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY, CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB,
CLIENT_SLUG_VERCEL, CLIENT_SLUG_VERCEL,
POSTHOG_HOST, POSTHOG_HOST,
POSTHOG_PROJECT_API_KEY, POSTHOG_PROJECT_API_KEY,
PRIVATE_KEY,
PUBLIC_KEY,
SENTRY_DSN, SENTRY_DSN,
SITE_URL, SITE_URL,
SMTP_HOST, SMTP_HOST,
SMTP_PORT, SMTP_PORT,
SMTP_NAME, SMTP_SECURE,
SMTP_USERNAME, SMTP_USERNAME,
SMTP_PASSWORD, SMTP_PASSWORD,
SMTP_FROM_ADDRESS,
SMTP_FROM_NAME,
STRIPE_PRODUCT_CARD_AUTH, STRIPE_PRODUCT_CARD_AUTH,
STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_STARTER, STRIPE_PRODUCT_STARTER,
STRIPE_PUBLISHABLE_KEY, STRIPE_PUBLISHABLE_KEY,
STRIPE_SECRET_KEY, STRIPE_SECRET_KEY,
STRIPE_WEBHOOK_SECRET, STRIPE_WEBHOOK_SECRET,
TELEMETRY_ENABLED TELEMETRY_ENABLED,
LICENSE_KEY
}; };
@@ -4,14 +4,14 @@ import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User } from '../models'; import { User } from '../../models';
import { createToken, issueTokens, clearTokens } from '../helpers/auth'; import { createToken, issueTokens, clearTokens } from '../../helpers/auth';
import { import {
NODE_ENV, NODE_ENV,
JWT_AUTH_LIFETIME, JWT_AUTH_LIFETIME,
JWT_AUTH_SECRET, JWT_AUTH_SECRET,
JWT_REFRESH_SECRET JWT_REFRESH_SECRET
} from '../config'; } from '../../config';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../models'; import { Bot, BotKey } from '../../models';
import { createBot } from '../helpers/bot'; import { createBot } from '../../helpers/bot';
interface BotKey { interface BotKey {
encryptedKey: string; encryptedKey: string;
@@ -2,10 +2,10 @@ import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import axios from 'axios'; import axios from 'axios';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import { IntegrationAuth, Integration } from '../models'; import { IntegrationAuth, Integration } from '../../models';
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables'; import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../../variables';
import { IntegrationService } from '../services'; import { IntegrationService } from '../../services';
import { getApps, revokeAccess } from '../integrations'; import { getApps, revokeAccess } from '../../integrations';
export const getIntegrationOptions = async ( export const getIntegrationOptions = async (
req: Request, req: Request,
@@ -1,9 +1,9 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { readFileSync } from 'fs'; import { readFileSync } from 'fs';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Integration, Bot, BotKey } from '../models'; import { Integration, Bot, BotKey } from '../../models';
import { EventService } from '../services'; import { EventService } from '../../services';
import { eventPushSecrets } from '../events'; import { eventPushSecrets } from '../../events';
interface Key { interface Key {
encryptedKey: string; encryptedKey: string;
@@ -1,9 +1,8 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Key } from '../models'; import { Key } from '../../models';
import { findMembership } from '../helpers/membership'; import { findMembership } from '../../helpers/membership';
import { PUBLIC_KEY } from '../config'; import { GRANTED } from '../../variables';
import { GRANTED } from '../variables';
/** /**
* Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with * Add (encrypted) copy of workspace key for workspace with id [workspaceId] for user with
@@ -84,16 +83,4 @@ export const getLatestKey = async (req: Request, res: Response) => {
} }
return res.status(200).send(resObj); return res.status(200).send(resObj);
}; };
/**
* Return public key of Infisical
* @param req
* @param res
* @returns
*/
export const getPublicKeyInfisical = async (req: Request, res: Response) => {
return res.status(200).send({
publicKey: PUBLIC_KEY
});
};
@@ -1,13 +1,13 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Membership, MembershipOrg, User, Key } from '../models'; import { Membership, MembershipOrg, User, Key } from '../../models';
import { import {
findMembership, findMembership,
deleteMembership as deleteMember deleteMembership as deleteMember
} from '../helpers/membership'; } from '../../helpers/membership';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { SITE_URL } from '../config'; import { SITE_URL } from '../../config';
import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../variables'; import { ADMIN, MEMBER, GRANTED, ACCEPTED } from '../../variables';
/** /**
* Check that user is a member of workspace with id [workspaceId] * Check that user is a member of workspace with id [workspaceId]
@@ -1,14 +1,14 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import crypto from 'crypto'; import crypto from 'crypto';
import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; import { SITE_URL, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { MembershipOrg, Organization, User, Token } from '../models'; import { MembershipOrg, Organization, User, Token } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../helpers/membershipOrg'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { checkEmailVerification } from '../helpers/signup'; import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../helpers/auth'; import { createToken } from '../../helpers/auth';
import { updateSubscriptionOrgQuantity } from '../helpers/organization'; import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED, INVITED } from '../../variables';
/** /**
* Delete organization membership with id [membershipOrgId] from organization * Delete organization membership with id [membershipOrgId] from organization
@@ -80,14 +80,14 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// TODO // TODO
let membershipToChangeRole; let membershipToChangeRole;
try { // try {
} catch (err) { // } catch (err) {
Sentry.setUser({ email: req.user.email }); // Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); // Sentry.captureException(err);
return res.status(400).send({ // return res.status(400).send({
message: 'Failed to change organization membership role' // message: 'Failed to change organization membership role'
}); // });
} // }
return res.status(200).send({ return res.status(200).send({
membershipOrg: membershipToChangeRole membershipOrg: membershipToChangeRole
@@ -6,7 +6,7 @@ import {
STRIPE_PRODUCT_STARTER, STRIPE_PRODUCT_STARTER,
STRIPE_PRODUCT_PRO, STRIPE_PRODUCT_PRO,
STRIPE_PRODUCT_CARD_AUTH STRIPE_PRODUCT_CARD_AUTH
} from '../config'; } from '../../config';
import Stripe from 'stripe'; import Stripe from 'stripe';
const stripe = new Stripe(STRIPE_SECRET_KEY, { const stripe = new Stripe(STRIPE_SECRET_KEY, {
@@ -18,10 +18,10 @@ import {
Organization, Organization,
Workspace, Workspace,
IncidentContactOrg IncidentContactOrg
} from '../models'; } from '../../models';
import { createOrganization as create } from '../helpers/organization'; import { createOrganization as create } from '../../helpers/organization';
import { addMembershipsOrg } from '../helpers/membershipOrg'; import { addMembershipsOrg } from '../../helpers/membershipOrg';
import { OWNER, ACCEPTED } from '../variables'; import { OWNER, ACCEPTED } from '../../variables';
const productToPriceMap = { const productToPriceMap = {
starter: STRIPE_PRODUCT_STARTER, starter: STRIPE_PRODUCT_STARTER,
@@ -1,13 +1,14 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import crypto from 'crypto'; import crypto from 'crypto';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
import { User, Token, BackupPrivateKey } from '../models'; import { User, Token, BackupPrivateKey } from '../../models';
import { checkEmailVerification } from '../helpers/signup'; import { checkEmailVerification } from '../../helpers/signup';
import { createToken } from '../helpers/auth'; import { createToken } from '../../helpers/auth';
import { sendMail } from '../helpers/nodemailer'; import { sendMail } from '../../helpers/nodemailer';
import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../config'; import { JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET, SITE_URL } from '../../config';
const clientPublicKeys: any = {}; const clientPublicKeys: any = {};
@@ -1,16 +1,16 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Key } from '../models'; import { Key, Secret } from '../../models';
import { import {
pushSecrets as push, v1PushSecrets as push,
pullSecrets as pull, pullSecrets as pull,
reformatPullSecrets reformatPullSecrets
} from '../helpers/secret'; } from '../../helpers/secret';
import { pushKeys } from '../helpers/key'; import { pushKeys } from '../../helpers/key';
import { eventPushSecrets } from '../events'; import { eventPushSecrets } from '../../events';
import { EventService } from '../services'; import { EventService } from '../../services';
import { ENV_SET } from '../variables'; import { ENV_SET } from '../../variables';
import { postHogClient } from '../services'; import { postHogClient } from '../../services';
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
@@ -21,6 +21,10 @@ interface PushSecret {
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal'; type: 'shared' | 'personal';
} }
@@ -169,9 +173,6 @@ export const pullSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecretsServiceToken = async (req: Request, res: Response) => { export const pullSecretsServiceToken = async (req: Request, res: Response) => {
// get (encrypted) secrets from workspace with id [workspaceId]
// service token route
let secrets; let secrets;
let key; let key;
try { try {
@@ -225,4 +226,4 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
secrets: reformatPullSecrets({ secrets }), secrets: reformatPullSecrets({ secrets }),
key key
}); });
}; };
@@ -1,8 +1,8 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { ServiceToken } from '../models'; import { ServiceToken } from '../../models';
import { createToken } from '../helpers/auth'; import { createToken } from '../../helpers/auth';
import { ENV_SET } from '../variables'; import { ENV_SET } from '../../variables';
import { JWT_SERVICE_SECRET } from '../config'; import { JWT_SERVICE_SECRET } from '../../config';
/** /**
* Return service token on request * Return service token on request
@@ -58,7 +58,8 @@ export const createServiceToken = async (req: Request, res: Response) => {
token = createToken({ token = createToken({
payload: { payload: {
serviceTokenId: serviceToken._id.toString() serviceTokenId: serviceToken._id.toString(),
workspaceId
}, },
expiresIn: expiresIn, expiresIn: expiresIn,
secret: JWT_SERVICE_SECRET secret: JWT_SERVICE_SECRET
@@ -1,15 +1,16 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../config'; import { NODE_ENV, JWT_SIGNUP_LIFETIME, JWT_SIGNUP_SECRET } from '../../config';
import { User, MembershipOrg } from '../models'; import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../helpers/user'; import { completeAccount } from '../../helpers/user';
import { import {
sendEmailVerification, sendEmailVerification,
checkEmailVerification, checkEmailVerification,
initializeDefaultOrg initializeDefaultOrg
} from '../helpers/signup'; } from '../../helpers/signup';
import { issueTokens, createToken } from '../helpers/auth'; import { issueTokens, createToken } from '../../helpers/auth';
import { INVITED, ACCEPTED } from '../variables'; import { INVITED, ACCEPTED } from '../../variables';
import axios from 'axios';
/** /**
* Signup step 1: Initialize account for user under email [email] and send a verification code * Signup step 1: Initialize account for user under email [email] and send a verification code
@@ -179,6 +180,21 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
token = tokens.token; token = tokens.token;
refreshToken = tokens.refreshToken; refreshToken = tokens.refreshToken;
// sending a welcome email to new users
if (process.env.LOOPS_API_KEY) {
await axios.post("https://app.loops.so/api/v1/events/send", {
"email": email,
"eventName": "Sign Up",
"firstName": firstName,
"lastName": lastName
}, {
headers: {
"Accept": "application/json",
"Authorization": "Bearer " + process.env.LOOPS_API_KEY
},
});
}
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -1,6 +1,6 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { UserAction } from '../models'; import { UserAction } from '../../models';
/** /**
* Add user action [action] * Add user action [action]
@@ -7,14 +7,14 @@ import {
Integration, Integration,
IntegrationAuth, IntegrationAuth,
IUser, IUser,
ServiceToken ServiceToken,
} from '../models'; } from '../../models';
import { import {
createWorkspace as create, createWorkspace as create,
deleteWorkspace as deleteWork deleteWorkspace as deleteWork
} from '../helpers/workspace'; } from '../../helpers/workspace';
import { addMemberships } from '../helpers/membership'; import { addMemberships } from '../../helpers/membership';
import { ADMIN, COMPLETED, GRANTED } from '../variables'; import { ADMIN, COMPLETED, GRANTED } from '../../variables';
/** /**
* Return public keys of members of workspace with id [workspaceId] * Return public keys of members of workspace with id [workspaceId]
+5
View File
@@ -0,0 +1,5 @@
import * as workspaceController from './workspaceController';
export {
workspaceController
}
@@ -0,0 +1,565 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import {
Workspace,
Membership,
MembershipOrg,
Integration,
IntegrationAuth,
Key,
IUser,
ServiceToken,
} from '../../models';
import {
createWorkspace as create,
deleteWorkspace as deleteWork
} from '../../helpers/workspace';
import {
v2PushSecrets as push,
pullSecrets as pull,
reformatPullSecrets
} from '../../helpers/secret';
import { pushKeys } from '../../helpers/key';
import { addMemberships } from '../../helpers/membership';
import { postHogClient, EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { ADMIN, COMPLETED, GRANTED, ENV_SET } from '../../variables';
interface V2PushSecret {
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
/**
* Return public keys of members of workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys;
try {
const { workspaceId } = req.params;
publicKeys = (
await Membership.find({
workspace: workspaceId
}).populate<{ user: IUser }>('user', 'publicKey')
)
.filter((m) => m.status === COMPLETED || m.status === GRANTED)
.map((member) => {
return {
publicKey: member.user.publicKey,
userId: member.user._id
};
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace member public keys'
});
}
return res.status(200).send({
publicKeys
});
};
/**
* Return memberships for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users;
try {
const { workspaceId } = req.params;
users = await Membership.find({
workspace: workspaceId
}).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace members'
});
}
return res.status(200).send({
users
});
};
/**
* Return workspaces that user is part of
* @param req
* @param res
* @returns
*/
export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces;
try {
workspaces = (
await Membership.find({
user: req.user._id
}).populate('workspace')
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspaces'
});
}
return res.status(200).send({
workspaces
});
};
/**
* Return workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
workspace = await Workspace.findOne({
_id: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace'
});
}
return res.status(200).send({
workspace
});
};
/**
* Create new workspace named [workspaceName] under organization with id
* [organizationId] and add user as admin
* @param req
* @param res
* @returns
*/
export const createWorkspace = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceName, organizationId } = req.body;
// validate organization membership
const membershipOrg = await MembershipOrg.findOne({
user: req.user._id,
organization: organizationId
});
if (!membershipOrg) {
throw new Error('Failed to validate organization membership');
}
if (workspaceName.length < 1) {
throw new Error('Workspace names must be at least 1-character long');
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
statuses: [GRANTED]
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create workspace'
});
}
return res.status(200).send({
workspace
});
};
/**
* Delete workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const deleteWorkspace = async (req: Request, res: Response) => {
try {
const { workspaceId } = req.params;
// delete workspace
await deleteWork({
id: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace'
});
}
return res.status(200).send({
message: 'Successfully deleted workspace'
});
};
/**
* Change name of workspace with id [workspaceId] to [name]
* @param req
* @param res
* @returns
*/
export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace;
try {
const { workspaceId } = req.params;
const { name } = req.body;
workspace = await Workspace.findOneAndUpdate(
{
_id: workspaceId
},
{
name
},
{
new: true
}
);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change workspace name'
});
}
return res.status(200).send({
message: 'Successfully changed workspace name',
workspace
});
};
/**
* Return integrations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations;
try {
const { workspaceId } = req.params;
integrations = await Integration.find({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integrations'
});
}
return res.status(200).send({
integrations
});
};
/**
* Return (integration) authorizations for workspace with id [workspaceId]
* @param req
* @param res
* @returns
*/
export const getWorkspaceIntegrationAuthorizations = async (
req: Request,
res: Response
) => {
let authorizations;
try {
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace integration authorizations'
});
}
return res.status(200).send({
authorizations
});
};
/**
* Return service service tokens for workspace [workspaceId] belonging to user
* @param req
* @param res
* @returns
*/
export const getWorkspaceServiceTokens = async (
req: Request,
res: Response
) => {
let serviceTokens;
try {
const { workspaceId } = req.params;
serviceTokens = await ServiceToken.find({
user: req.user._id,
workspace: workspaceId
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service tokens'
});
}
return res.status(200).send({
serviceTokens
});
}
/**
* Upload (encrypted) secrets to workspace with id [workspaceId]
* for environment [environment]
* @param req
* @param res
* @returns
*/
export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId]
try {
let { secrets }: { secrets: V2PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
throw new Error('Failed to validate environment');
}
// sanitize secrets
secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
);
await push({
userId: req.user._id,
workspaceId,
environment,
secrets
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) {
postHogClient.capture({
event: 'secrets pushed',
distinctId: req.user.email,
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId
})
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({
message: 'Successfully uploaded workspace secrets'
});
};
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key
* @param req
* @param res
* @returns
*/
export const pullSecrets = async (req: Request, res: Response) => {
// TODO: only return secrets, do not return workspace key
let secrets;
let key;
try {
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
throw new Error('Failed to validate environment');
}
secrets = await pull({
userId: req.user._id.toString(),
workspaceId,
environment
});
key = await Key.findOne({
workspace: workspaceId,
receiver: req.user._id
})
.sort({ createdAt: -1 })
.populate('sender', '+publicKey');
if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets });
}
if (postHogClient) {
// capture secrets pushed event in production
postHogClient.capture({
distinctId: req.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({
secrets,
key
});
};
// TODO: modify based on upcoming serviceTokenData changes
/**
* Return (encrypted) secrets for workspace with id [workspaceId]
* for environment [environment] and (encrypted) workspace key
* via service token
* @param req
* @param res
* @returns
*/
export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets;
let key;
try {
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment
if (!ENV_SET.has(environment)) {
throw new Error('Failed to validate environment');
}
secrets = await pull({
userId: req.serviceToken.user._id.toString(),
workspaceId,
environment
});
key = {
encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce,
sender: {
publicKey: req.serviceToken.publicKey
},
receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace
};
if (postHogClient) {
// capture secrets pulled event in production
postHogClient.capture({
distinctId: req.serviceToken.user.email,
event: 'secrets pulled',
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
channel: channel ? channel : 'cli'
}
});
}
} catch (err) {
Sentry.setUser({ email: req.serviceToken.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({
secrets: reformatPullSecrets({ secrets }),
key
});
};
-5
View File
@@ -1,5 +0,0 @@
import * as stripeController from './stripeController';
export {
stripeController
}
+9
View File
@@ -0,0 +1,9 @@
import * as stripeController from './stripeController';
import * as secretController from './secretController';
import * as workspaceController from './workspaceController';
export {
stripeController,
secretController,
workspaceController
}
@@ -0,0 +1,35 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretVersion } from '../../models';
/**
* Return secret versions for secret with id [secretId]
* @param req
* @param res
*/
export const getSecretVersions = async (req: Request, res: Response) => {
let secretVersions;
try {
const { secretId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretVersions = await SecretVersion.find({
secret: secretId
})
.skip(offset)
.limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret versions'
});
}
return res.status(200).send({
secretVersions
});
}
@@ -1,7 +1,7 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../config'; import { STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET } from '../../../config';
const stripe = new Stripe(STRIPE_SECRET_KEY, { const stripe = new Stripe(STRIPE_SECRET_KEY, {
apiVersion: '2022-08-01' apiVersion: '2022-08-01'
}); });
@@ -0,0 +1,35 @@
import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { SecretSnapshot } from '../../models';
/**
* Return secret snapshots for workspace with id [workspaceId]
* @param req
* @param res
*/
export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => {
let secretSnapshots;
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string);
secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId
})
.skip(offset)
.limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get secret snapshots'
});
}
return res.status(200).send({
secretSnapshots
});
}
-21
View File
@@ -1,21 +0,0 @@
/**
* @param {Object} obj
* @param {Object} obj.licenseKey - Infisical license key
*/
const checkLicenseKey = ({
licenseKey
}: {
licenseKey: string
}) => {
try {
// TODO
} catch (err) {
}
}
export {
checkLicenseKey
}
+74
View File
@@ -0,0 +1,74 @@
import * as Sentry from '@sentry/node';
import {
Secret
} from '../../models';
import {
SecretSnapshot,
SecretVersion,
ISecretVersion
} from '../models';
/**
* Save a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the
* secretsnapshots collection.
* @param {Object} obj
* @param {String} obj.workspaceId
*/
const takeSecretSnapshotHelper = async ({
workspaceId
}: {
workspaceId: string;
}) => {
try {
const secrets = await Secret.find({
workspace: workspaceId
});
const latestSecretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId
}).sort({ version: -1 });
if (!latestSecretSnapshot) {
// case: no snapshots exist for workspace -> create first snapshot
await new SecretSnapshot({
workspace: workspaceId,
version: 1,
secrets
}).save();
return;
}
// case: snapshots exist for workspace
await new SecretSnapshot({
workspace: workspaceId,
version: latestSecretSnapshot.version + 1,
secrets
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot');
}
}
const addSecretVersionsHelper = async ({
secretVersions
}: {
secretVersions: ISecretVersion[]
}) => {
try {
await SecretVersion.insertMany(secretVersions);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add secret versions');
}
}
export {
takeSecretSnapshotHelper,
addSecretVersionsHelper
}
+9
View File
@@ -0,0 +1,9 @@
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
import SecretVersion, { ISecretVersion } from "./secretVersion";
export {
SecretSnapshot,
ISecretSnapshot,
SecretVersion,
ISecretVersion
}
+109
View File
@@ -0,0 +1,109 @@
import { Schema, model, Types } from 'mongoose';
import {
SECRET_SHARED,
SECRET_PERSONAL,
ENV_DEV,
ENV_TESTING,
ENV_STAGING,
ENV_PROD
} from '../../variables';
export interface ISecretSnapshot {
workspace: Types.ObjectId;
version: number;
secrets: {
version: number;
workspace: Types.ObjectId;
type: string;
user: Types.ObjectId;
environment: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
}[]
}
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
version: {
type: Number,
required: true
},
secrets: [{
version: {
type: Number,
default: 1,
required: true
},
workspace: {
type: Schema.Types.ObjectId,
ref: 'Workspace',
required: true
},
type: {
type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true
},
user: {
// user associated with the personal secret
type: Schema.Types.ObjectId,
ref: 'User'
},
environment: {
type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretKeyHash: {
type: String,
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
secretValueHash: {
type: String,
required: true
}
}]
},
{
timestamps: true
}
);
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
export default SecretSnapshot;
+75
View File
@@ -0,0 +1,75 @@
import { Schema, model, Types } from 'mongoose';
export interface ISecretVersion {
_id?: Types.ObjectId;
secret: Types.ObjectId;
version: number;
isDeleted: boolean;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
}
const secretVersionSchema = new Schema<ISecretVersion>(
{
secret: { // could be deleted
type: Schema.Types.ObjectId,
ref: 'Secret',
required: true
},
version: {
type: Number,
default: 1,
required: true
},
isDeleted: {
type: Boolean,
default: false,
required: true
},
secretKeyCiphertext: {
type: String,
required: true
},
secretKeyIV: {
type: String, // symmetric
required: true
},
secretKeyTag: {
type: String, // symmetric
required: true
},
secretKeyHash: {
type: String,
required: true
},
secretValueCiphertext: {
type: String,
required: true
},
secretValueIV: {
type: String, // symmetric
required: true
},
secretValueTag: {
type: String, // symmetric
required: true
},
secretValueHash: {
type: String,
required: true
}
},
{
timestamps: true
}
);
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
export default SecretVersion;
+7
View File
@@ -0,0 +1,7 @@
import secret from './secret';
import workspace from './workspace';
export {
secret,
workspace
}
+26
View File
@@ -0,0 +1,26 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../../middleware';
import { body, query, param } from 'express-validator';
import { secretController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../../variables';
router.get(
'/:secretId/secret-versions',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('secretId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
validateRequest,
secretController.getSecretVersions
);
export default router;
@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { stripeController } from '../controllers'; import { stripeController } from '../../controllers/v1';
router.post('/webhook', stripeController.handleWebhook); router.post('/webhook', stripeController.handleWebhook);
+27
View File
@@ -0,0 +1,27 @@
import express from 'express';
const router = express.Router();
import {
requireAuth,
requireWorkspaceAuth,
validateRequest
} from '../../../middleware';
import { param, query } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../../../variables';
import { workspaceController } from '../../controllers/v1';
router.get(
'/:workspaceId/secret-snapshots',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
query('offset').exists().isInt(),
query('limit').exists().isInt(),
validateRequest,
workspaceController.getWorkspaceSecretSnapshots
);
export default router;
@@ -0,0 +1,19 @@
import { LICENSE_KEY } from '../../config';
/**
* Class to handle Enterprise Edition license actions
*/
class EELicenseService {
private readonly _isLicenseValid: boolean;
constructor(licenseKey: string) {
this._isLicenseValid = true;
}
public get isLicenseValid(): boolean {
return this._isLicenseValid;
}
}
export default new EELicenseService(LICENSE_KEY);
@@ -0,0 +1,47 @@
import { ISecretVersion } from '../models';
import {
takeSecretSnapshotHelper,
addSecretVersionsHelper
} from '../helpers/secret';
import EELicenseService from './EELicenseService';
/**
* Class to handle Enterprise Edition secret actions
*/
class EESecretService {
/**
* Save a copy of the current state of secrets in workspace with id
* [workspaceId] under a new snapshot with incremented version under the
* SecretSnapshot collection.
* Requires a valid license key [licenseKey]
* @param {Object} obj
* @param {String} obj.workspaceId
*/
static async takeSecretSnapshot({
workspaceId
}: {
workspaceId: string;
}) {
if (!EELicenseService.isLicenseValid) return;
await takeSecretSnapshotHelper({ workspaceId });
}
/**
* Adds secret versions [secretVersions] to the SecretVersion collection.
* @param {Object} obj
* @param {SecretVersion} obj.secretVersions
*/
static async addSecretVersions({
secretVersions
}: {
secretVersions: ISecretVersion[];
}) {
if (!EELicenseService.isLicenseValid) return;
await addSecretVersionsHelper({
secretVersions
});
}
}
export default EESecretService;
+7
View File
@@ -0,0 +1,7 @@
import EELicenseService from "./EELicenseService";
import EESecretService from "./EESecretService";
export {
EELicenseService,
EESecretService
}
-1
View File
@@ -12,7 +12,6 @@ import {
decryptSymmetric, decryptSymmetric,
decryptAsymmetric decryptAsymmetric
} from '../utils/crypto'; } from '../utils/crypto';
import { decryptSecrets } from '../helpers/secret';
import { ENCRYPTION_KEY } from '../config'; import { ENCRYPTION_KEY } from '../config';
import { SECRET_SHARED } from '../variables'; import { SECRET_SHARED } from '../variables';
+15 -7
View File
@@ -2,18 +2,18 @@ import * as Sentry from '@sentry/node';
import { import {
Bot, Bot,
Integration, Integration,
IIntegration,
IntegrationAuth, IntegrationAuth,
IIntegrationAuth
} from '../models'; } from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations'; import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService, IntegrationService } from '../services'; import { BotService } from '../services';
import { import {
ENV_DEV, ENV_DEV,
EVENT_PUSH_SECRETS, EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY
} from '../variables'; } from '../variables';
import { UnauthorizedRequestError } from '../utils/errors';
import RequestError from '../utils/requestError';
interface Update { interface Update {
workspace: string; workspace: string;
@@ -176,12 +176,13 @@ const syncIntegrationsHelper = async ({
*/ */
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let refreshToken; let refreshToken;
try { try {
const integrationAuth = await IntegrationAuth const integrationAuth = await IntegrationAuth
.findById(integrationAuthId) .findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag'); .select('+refreshCiphertext +refreshIV +refreshTag');
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
refreshToken = await BotService.decryptSymmetric({ refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(), workspaceId: integrationAuth.workspace.toString(),
@@ -193,7 +194,10 @@ const syncIntegrationsHelper = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get integration refresh token'); if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token');
} }
return refreshToken; return refreshToken;
@@ -209,12 +213,13 @@ const syncIntegrationsHelper = async ({
*/ */
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => { const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessToken; let accessToken;
try { try {
const integrationAuth = await IntegrationAuth const integrationAuth = await IntegrationAuth
.findById(integrationAuthId) .findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext'); .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
accessToken = await BotService.decryptSymmetric({ accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(), workspaceId: integrationAuth.workspace.toString(),
@@ -240,7 +245,10 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get integration access token'); if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration access token');
} }
return accessToken; return accessToken;
+1
View File
@@ -21,6 +21,7 @@ const validateMembership = async ({
}) => { }) => {
let membership; 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 { try {
membership = await Membership.findOne({ membership = await Membership.findOne({
user: userId, user: userId,
+9 -35
View File
@@ -2,40 +2,10 @@ import fs from 'fs';
import path from 'path'; import path from 'path';
import handlebars from 'handlebars'; import handlebars from 'handlebars';
import nodemailer from 'nodemailer'; import nodemailer from 'nodemailer';
import { import { SMTP_FROM_NAME, SMTP_FROM_ADDRESS } from '../config';
SMTP_HOST,
SMTP_PORT,
SMTP_NAME,
SMTP_USERNAME,
SMTP_PASSWORD
} from '../config';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
const mailOpts: SMTPConnection.Options = { let smtpTransporter: nodemailer.Transporter;
host: SMTP_HOST,
port: SMTP_PORT as number
};
if (SMTP_USERNAME && SMTP_PASSWORD) {
mailOpts.auth = {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
};
}
// create nodemailer transporter
const transporter = nodemailer.createTransport(mailOpts);
transporter
.verify()
.then(() => {
Sentry.setUser(null);
Sentry.captureMessage('SMTP - Successfully connected');
})
.catch((err) => {
Sentry.setUser(null);
Sentry.captureException(
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
);
});
/** /**
* @param {Object} obj * @param {Object} obj
@@ -63,8 +33,8 @@ const sendMail = async ({
const temp = handlebars.compile(html); const temp = handlebars.compile(html);
const htmlToSend = temp(substitutions); const htmlToSend = temp(substitutions);
await transporter.sendMail({ await smtpTransporter.sendMail({
from: `"${SMTP_NAME}" <${SMTP_USERNAME}>`, from: `"${SMTP_FROM_NAME}" <${SMTP_FROM_ADDRESS}>`,
to: recipients.join(', '), to: recipients.join(', '),
subject: subjectLine, subject: subjectLine,
html: htmlToSend html: htmlToSend
@@ -75,4 +45,8 @@ const sendMail = async ({
} }
}; };
export { sendMail }; const setTransporter = (transporter: nodemailer.Transporter) => {
smtpTransporter = transporter;
};
export { sendMail, setTransporter };
+417 -104
View File
@@ -1,12 +1,21 @@
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
Secret, Secret,
ISecret ISecret,
} from '../models'; } from '../models';
import {
EESecretService
} from '../ee/services';
import {
SecretVersion
} from '../ee/models';
import {
takeSecretSnapshotHelper
} from '../ee/helpers/secret';
import { decryptSymmetric } from '../utils/crypto'; import { decryptSymmetric } from '../utils/crypto';
import { SECRET_SHARED, SECRET_PERSONAL } from '../variables'; import { SECRET_SHARED, SECRET_PERSONAL } from '../variables';
interface PushSecret { interface V1PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
tagKey: string; tagKey: string;
@@ -15,11 +24,31 @@ interface PushSecret {
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
ciphertextComment: string;
ivComment: string;
tagComment: string;
hashComment: string;
type: 'shared' | 'personal'; type: 'shared' | 'personal';
} }
interface V2PushSecret {
type: string; // personal or shared
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretKeyHash: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
}
interface Update { interface Update {
[index: string]: string; [index: string]: any;
} }
type DecryptSecretType = 'text' | 'object' | 'expanded'; type DecryptSecretType = 'text' | 'object' | 'expanded';
@@ -35,7 +64,7 @@ type DecryptSecretType = 'text' | 'object' | 'expanded';
* @param {String} obj.environment - environment for secrets * @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push * @param {Object[]} obj.secrets - secrets to push
*/ */
const pushSecrets = async ({ const v1PushSecrets = async ({
userId, userId,
workspaceId, workspaceId,
environment, environment,
@@ -44,8 +73,9 @@ const pushSecrets = async ({
userId: string; userId: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
secrets: PushSecret[]; secrets: V1PushSecret[];
}): Promise<void> => { }): Promise<void> => {
// TODO: clean up function and fix up types
try { try {
// construct useful data structures // construct useful data structures
const oldSecrets = await pullSecrets({ const oldSecrets = await pullSecrets({
@@ -53,74 +83,133 @@ const pushSecrets = async ({
workspaceId, workspaceId,
environment environment
}); });
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => {
return { ...accumulator, [s.secretKeyHash]: s }; const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
}, {}); ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
const newSecretsObj = secrets.reduce((accumulator, s) => { , {});
return { ...accumulator, [s.hashKey]: s }; const newSecretsObj: any = secrets.reduce((accumulator, s) =>
}, {}); ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s })
, {});
// handle deleting secrets // handle deleting secrets
const toDelete = oldSecrets.filter( const toDelete = oldSecrets
(s: ISecret) => !(s.secretKeyHash in newSecretsObj) .filter(
); (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
)
.map((s) => s._id);
if (toDelete.length > 0) { if (toDelete.length > 0) {
await Secret.deleteMany({ await Secret.deleteMany({
_id: { $in: toDelete.map((s) => s._id) } _id: { $in: toDelete }
});
await SecretVersion.updateMany({
secret: { $in: toDelete }
}, {
isDeleted: true
}); });
} }
// handle modifying secrets where type or value changed const toUpdate = oldSecrets
const operations = secrets
.filter((s) => { .filter((s) => {
if (s.hashKey in oldSecretsObj) { if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.hashValue !== oldSecretsObj[s.hashKey].secretValueHash) { if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue
// case: filter secrets where value changed || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) {
// case: filter secrets where value or comment changed
return true; return true;
} }
if (s.type !== oldSecretsObj[s.hashKey].type) { if (!s.version) {
// case: filter secrets where type changed // case: filter (legacy) secrets that were not versioned
return true; return true;
} }
} }
return false; return false;
}) });
const operations = toUpdate
.map((s) => { .map((s) => {
const {
ciphertextValue,
ivValue,
tagValue,
hashValue,
ciphertextComment,
ivComment,
tagComment,
hashComment
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = { const update: Update = {
type: s.type, secretValueCiphertext: ciphertextValue,
secretValueCiphertext: s.ciphertextValue, secretValueIV: ivValue,
secretValueIV: s.ivValue, secretValueTag: tagValue,
secretValueTag: s.tagValue, secretValueHash: hashValue,
secretValueHash: s.hashValue secretCommentCiphertext: ciphertextComment,
}; secretCommentIV: ivComment,
secretCommentTag: tagComment,
secretCommentHash: hashComment,
}
if (!s.version) {
// case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) { if (s.type === SECRET_PERSONAL) {
// attach user assocaited with the personal secret // attach user associated with the personal secret
update['user'] = userId; update['user'] = userId;
} }
return { return {
updateOne: { updateOne: {
filter: { filter: {
workspace: workspaceId, _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
_id: oldSecretsObj[s.hashKey]._id
}, },
update update
} }
}; };
}); });
const a = await Secret.bulkWrite(operations as any); await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map(({
_id,
version,
type,
secretKeyHash,
}) => {
const newSecret = newSecretsObj[`${type}-${secretKeyHash}`];
return ({
secret: _id,
version: version ? version + 1 : 1,
isDeleted: false,
secretKeyCiphertext: newSecret.ciphertextKey,
secretKeyIV: newSecret.ivKey,
secretKeyTag: newSecret.tagKey,
secretKeyHash: newSecret.hashKey,
secretValueCiphertext: newSecret.ciphertextValue,
secretValueIV: newSecret.ivValue,
secretValueTag: newSecret.tagValue,
secretValueHash: newSecret.hashValue
})
})
});
// handle adding new secrets // handle adding new secrets
const toAdd = secrets.filter((s) => !(s.hashKey in oldSecretsObj)); const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj));
if (toAdd.length > 0) { if (toAdd.length > 0) {
// add secrets // add secrets
await Secret.insertMany( const newSecrets = await Secret.insertMany(
toAdd.map((s, idx) => { toAdd.map((s, idx) => {
let obj: any = { const obj: any = {
version: 1,
workspace: workspaceId, workspace: workspaceId,
type: toAdd[idx].type, type: toAdd[idx].type,
environment, environment,
@@ -131,7 +220,11 @@ const pushSecrets = async ({
secretValueCiphertext: s.ciphertextValue, secretValueCiphertext: s.ciphertextValue,
secretValueIV: s.ivValue, secretValueIV: s.ivValue,
secretValueTag: s.tagValue, secretValueTag: s.tagValue,
secretValueHash: s.hashValue secretValueHash: s.hashValue,
secretCommentCiphertext: s.ciphertextComment,
secretCommentIV: s.ivComment,
secretCommentTag: s.tagComment,
secretCommentHash: s.hashComment
}; };
if (toAdd[idx].type === 'personal') { if (toAdd[idx].type === 'personal') {
@@ -141,7 +234,282 @@ const pushSecrets = async ({
return obj; return obj;
}) })
); );
// (EE) add secret versions for new secrets
EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({
_id,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
secret: _id,
version: 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}))
});
} }
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
})
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to push shared and personal secrets');
}
};
/**
* Push secrets for user with id [userId] to workspace
* with id [workspaceId] with environment [environment]. Follow steps:
* 1. Handle shared secrets (insert, delete)
* 2. handle personal secrets (insert, delete)
* @param {Object} obj
* @param {String} obj.userId - id of user to push secrets for
* @param {String} obj.workspaceId - id of workspace to push to
* @param {String} obj.environment - environment for secrets
* @param {Object[]} obj.secrets - secrets to push
*/
const v2PushSecrets = async ({
userId,
workspaceId,
environment,
secrets
}: {
userId: string;
workspaceId: string;
environment: string;
secrets: V2PushSecret[];
}): Promise<void> => {
// TODO: clean up function and fix up types
try {
// construct useful data structures
const oldSecrets = await pullSecrets({
userId,
workspaceId,
environment
});
const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {});
const newSecretsObj: any = secrets.reduce((accumulator, s) =>
({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s })
, {});
// handle deleting secrets
const toDelete = oldSecrets
.filter(
(s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)
)
.map((s) => s._id);
if (toDelete.length > 0) {
await Secret.deleteMany({
_id: { $in: toDelete }
});
await SecretVersion.updateMany({
secret: { $in: toDelete }
}, {
isDeleted: true
});
}
const toUpdate = oldSecrets
.filter((s) => {
if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) {
if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash
|| s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) {
// case: filter secrets where value or comment changed
return true;
}
if (!s.version) {
// case: filter (legacy) secrets that were not versioned
return true;
}
}
return false;
});
const operations = toUpdate
.map((s) => {
const {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
const update: Update = {
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}
if (!s.version) {
// case: (legacy) secret was not versioned
update.version = 1;
} else {
update['$inc'] = {
version: 1
}
}
if (s.type === SECRET_PERSONAL) {
// attach user associated with the personal secret
update['user'] = userId;
}
return {
updateOne: {
filter: {
_id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id
},
update
}
};
});
await Secret.bulkWrite(operations as any);
// (EE) add secret versions for updated secrets
await EESecretService.addSecretVersions({
secretVersions: toUpdate.map((s) => {
const {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
} = newSecretsObj[`${s.type}-${s.secretKeyHash}`];
return ({
secret: s._id,
version: s.version ? s.version + 1 : 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
})
})
});
// handle adding new secrets
const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj));
if (toAdd.length > 0) {
// add secrets
const newSecrets = await Secret.insertMany(
toAdd.map(({
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash,
}, idx) => {
const obj: any = {
version: 1,
workspace: workspaceId,
type: toAdd[idx].type,
environment,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
secretCommentHash
};
if (toAdd[idx].type === 'personal') {
obj['user' as keyof typeof obj] = userId;
}
return obj;
})
);
// (EE) add secret versions for new secrets
EESecretService.addSecretVersions({
secretVersions: newSecrets.map(({
_id,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}) => ({
secret: _id,
version: 1,
isDeleted: false,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretKeyHash,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretValueHash
}))
});
}
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId
})
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -222,6 +590,13 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
iv: s.secretValueIV, iv: s.secretValueIV,
tag: s.secretValueTag, tag: s.secretValueTag,
hash: s.secretValueHash hash: s.secretValueHash
},
secretComment: {
workspace: s.workspace,
ciphertext: s.secretCommentCiphertext,
iv: s.secretCommentIV,
tag: s.secretCommentTag,
hash: s.secretCommentHash
} }
})); }));
} catch (err) { } catch (err) {
@@ -233,71 +608,9 @@ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => {
return reformatedSecrets; return reformatedSecrets;
}; };
/**
* Return decrypted secrets in format [format]
* @param {Object} obj
* @param {Object[]} obj.secrets - array of (encrypted) secret key-value pair objects
* @param {String} obj.key - symmetric key to decrypt secret key-value pairs
* @param {String} obj.format - desired return format that is either "text," "object," or "expanded"
* @return {String|Object} (decrypted) secrets also called the content
*/
const decryptSecrets = ({
secrets,
key,
format
}: {
secrets: PushSecret[];
key: string;
format: DecryptSecretType;
}) => {
// init content
let content: any = format === 'text' ? '' : {};
// decrypt secrets
secrets.forEach((s, idx) => {
const secretKey = decryptSymmetric({
ciphertext: s.ciphertextKey,
iv: s.ivKey,
tag: s.tagKey,
key
});
const secretValue = decryptSymmetric({
ciphertext: s.ciphertextValue,
iv: s.ivValue,
tag: s.tagValue,
key
});
switch (format) {
case 'text':
content += secretKey;
content += '=';
content += secretValue;
if (idx < secrets.length) {
content += '\n';
}
break;
case 'object':
content[secretKey] = secretValue;
break;
case 'expanded':
content[secretKey] = {
...s,
plaintextKey: secretKey,
plaintextValue: secretValue
};
break;
}
});
return content;
};
export { export {
pushSecrets, v1PushSecrets,
v2PushSecrets,
pullSecrets, pullSecrets,
reformatPullSecrets, reformatPullSecrets
decryptSecrets
}; };
+17 -123
View File
@@ -1,131 +1,25 @@
/* eslint-disable no-console */
import http from 'http';
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import cookieParser from 'cookie-parser';
import mongoose from 'mongoose';
import dotenv from 'dotenv'; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { PORT, SENTRY_DSN, NODE_ENV, MONGO_URL, SITE_URL } from './config'; import { SENTRY_DSN, NODE_ENV, MONGO_URL } from './config';
import { apiLimiter } from './helpers/rateLimiter'; import { server } from './app';
import { createTerminus } from '@godaddy/terminus'; import { initDatabase } from './services/database';
import { setUpHealthEndpoint } from './services/health';
import { initSmtp } from './services/smtp';
import { setTransporter } from './helpers/nodemailer';
const app = express(); initDatabase(MONGO_URL);
Sentry.init({ setUpHealthEndpoint(server);
dsn: SENTRY_DSN,
tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV
});
import { setTransporter(initSmtp());
signup as signupRouter,
auth as authRouter,
bot as botRouter,
organization as organizationRouter,
workspace as workspaceRouter,
membershipOrg as membershipOrgRouter,
membership as membershipRouter,
key as keyRouter,
inviteOrg as inviteOrgRouter,
user as userRouter,
userAction as userActionRouter,
secret as secretRouter,
serviceToken as serviceTokenRouter,
password as passwordRouter,
stripe as stripeRouter,
integration as integrationRouter,
integrationAuth as integrationAuthRouter,
log as logRouter
} from './routes';
const connectWithRetry = () => { if (NODE_ENV !== 'test') {
mongoose Sentry.init({
.connect(MONGO_URL) dsn: SENTRY_DSN,
.then(() => console.log('Successfully connected to DB')) tracesSampleRate: 1.0,
.catch((e) => { debug: NODE_ENV === 'production' ? false : true,
console.log('Failed to connect to DB ', e); environment: NODE_ENV
setTimeout(() => { });
console.log(e);
}, 5000);
});
return mongoose.connection;
};
const dbConnection = connectWithRetry();
app.enable('trust proxy');
app.use(cookieParser());
app.use(
cors({
credentials: true,
origin: SITE_URL
})
);
if (NODE_ENV === 'production') {
// enable app-wide rate-limiting + helmet security
// in production
app.disable('x-powered-by');
app.use(apiLimiter);
app.use(helmet());
} }
app.use(express.json());
// routers
app.use('/api/v1/signup', signupRouter);
app.use('/api/v1/auth', authRouter);
app.use('/api/v1/bot', botRouter);
app.use('/api/v1/user', userRouter);
app.use('/api/v1/user-action', userActionRouter);
app.use('/api/v1/organization', organizationRouter);
app.use('/api/v1/workspace', workspaceRouter);
app.use('/api/v1/membership-org', membershipOrgRouter);
app.use('/api/v1/membership', membershipRouter);
app.use('/api/v1/key', keyRouter);
app.use('/api/v1/invite-org', inviteOrgRouter);
app.use('/api/v1/secret', secretRouter);
app.use('/api/v1/service-token', serviceTokenRouter);
app.use('/api/v1/password', passwordRouter);
app.use('/api/v1/stripe', stripeRouter);
app.use('/api/v1/integration', integrationRouter);
app.use('/api/v1/integration-auth', integrationAuthRouter);
app.use('/api/v1/log', logRouter);
const server = http.createServer(app);
const onSignal = () => {
console.log('Server is starting clean-up');
return Promise.all([
() => {
dbConnection.close(() => {
console.info('Database connection closed');
});
}
]);
};
const healthCheck = () => {
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
return Promise
.resolve
// optionally include a resolve value to be included as
// info in the health check response
();
};
createTerminus(server, {
healthChecks: {
'/healthcheck': healthCheck,
onSignal
}
});
server.listen(PORT, () => {
console.log('Listening on PORT ' + PORT);
});
+167 -123
View File
@@ -1,17 +1,22 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
import { IIntegrationAuth } from '../models';
import { import {
IIntegrationAuth INTEGRATION_HEROKU,
} from '../models'; INTEGRATION_VERCEL,
import { INTEGRATION_NETLIFY,
INTEGRATION_HEROKU, INTEGRATION_GITHUB,
INTEGRATION_VERCEL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_NETLIFY, INTEGRATION_VERCEL_API_URL,
INTEGRATION_HEROKU_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_GITHUB_API_URL
INTEGRATION_NETLIFY_API_URL
} from '../variables'; } from '../variables';
interface GitHubApp {
name: string;
}
/** /**
* Return list of names of apps for integration named [integration] * Return list of names of apps for integration named [integration]
* @param {Object} obj * @param {Object} obj
@@ -21,47 +26,51 @@ import {
* @returns {String} apps.name - name of integration app * @returns {String} apps.name - name of integration app
*/ */
const getApps = async ({ const getApps = async ({
integrationAuth, integrationAuth,
accessToken accessToken
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
interface App {
interface App { name: string;
name: string; siteId?: string;
siteId?: string; }
}
let apps: App[]; // TODO: add type and define payloads for apps let apps: App[]; // TODO: add type and define payloads for apps
try { try {
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
apps = await getAppsHeroku({ apps = await getAppsHeroku({
accessToken accessToken
}); });
break; break;
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
apps = await getAppsVercel({ apps = await getAppsVercel({
accessToken accessToken
}); });
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
apps = await getAppsNetlify({ apps = await getAppsNetlify({
integrationAuth, integrationAuth,
accessToken accessToken
}); });
break; break;
} case INTEGRATION_GITHUB:
apps = await getAppsGithub({
} catch (err) { integrationAuth,
Sentry.setUser(null); accessToken
Sentry.captureException(err); });
throw new Error('Failed to get integration apps'); break;
} }
} catch (err) {
return apps; Sentry.setUser(null);
} Sentry.captureException(err);
throw new Error('Failed to get integration apps');
}
return apps;
};
/** /**
* Return list of names of apps for Heroku integration * Return list of names of apps for Heroku integration
@@ -70,31 +79,29 @@ const getApps = async ({
* @returns {Object[]} apps - names of Heroku apps * @returns {Object[]} apps - names of Heroku apps
* @returns {String} apps.name - name of Heroku app * @returns {String} apps.name - name of Heroku app
*/ */
const getAppsHeroku = async ({ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
accessToken let apps;
}: { try {
accessToken: string; const res = (
}) => { await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
let apps; headers: {
try { Accept: 'application/vnd.heroku+json; version=3',
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { Authorization: `Bearer ${accessToken}`
headers: { }
Accept: 'application/vnd.heroku+json; version=3', })
Authorization: `Bearer ${accessToken}` ).data;
}
})).data; apps = res.map((a: any) => ({
name: a.name
apps = res.map((a: any) => ({ }));
name: a.name } catch (err) {
})); Sentry.setUser(null);
} catch (err) { Sentry.captureException(err);
Sentry.setUser(null); throw new Error('Failed to get Heroku integration apps');
Sentry.captureException(err); }
throw new Error('Failed to get Heroku integration apps');
} return apps;
};
return apps;
}
/** /**
* Return list of names of apps for Vercel integration * Return list of names of apps for Vercel integration
@@ -103,30 +110,28 @@ const getAppsHeroku = async ({
* @returns {Object[]} apps - names of Vercel apps * @returns {Object[]} apps - names of Vercel apps
* @returns {String} apps.name - name of Vercel app * @returns {String} apps.name - name of Vercel app
*/ */
const getAppsVercel = async ({ const getAppsVercel = async ({ accessToken }: { accessToken: string }) => {
accessToken let apps;
}: { try {
accessToken: string; const res = (
}) => { await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
let apps; headers: {
try { Authorization: `Bearer ${accessToken}`
const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { }
headers: { })
Authorization: `Bearer ${accessToken}` ).data;
}
})).data; apps = res.projects.map((a: any) => ({
name: a.name
apps = res.projects.map((a: any) => ({ }));
name: a.name } catch (err) {
})); Sentry.setUser(null);
} catch (err) { Sentry.captureException(err);
Sentry.setUser(null); throw new Error('Failed to get Vercel integration apps');
Sentry.captureException(err); }
throw new Error('Failed to get Vercel integration apps');
} return apps;
};
return apps;
}
/** /**
* Return list of names of sites for Netlify integration * Return list of names of sites for Netlify integration
@@ -136,34 +141,73 @@ const getAppsVercel = async ({
* @returns {String} apps.name - name of Netlify site * @returns {String} apps.name - name of Netlify site
*/ */
const getAppsNetlify = async ({ const getAppsNetlify = async ({
integrationAuth, integrationAuth,
accessToken accessToken
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
let apps; let apps;
try { try {
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { const res = (
headers: { await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
Authorization: `Bearer ${accessToken}` headers: {
} Authorization: `Bearer ${accessToken}`
})).data; }
})
apps = res.map((a: any) => ({ ).data;
name: a.name,
siteId: a.site_id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
}
return apps;
}
export { apps = res.map((a: any) => ({
getApps name: a.name,
} siteId: a.site_id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Netlify integration apps');
}
return apps;
};
/**
* Return list of names of repositories for Github integration
* @param {Object} obj
* @param {String} obj.accessToken - access token for Netlify API
* @returns {Object[]} apps - names of Netlify sites
* @returns {String} apps.name - name of Netlify site
*/
const getAppsGithub = async ({
integrationAuth,
accessToken
}: {
integrationAuth: IIntegrationAuth;
accessToken: string;
}) => {
let apps;
try {
const octokit = new Octokit({
auth: accessToken
});
const repos = (await octokit.request(
'GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}',
{}
)).data;
apps = repos
.filter((a:any) => a.permissions.admin === true)
.map((a: any) => ({
name: a.name
})
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get Github repos');
}
return apps;
};
export { getApps };
+187 -143
View File
@@ -1,46 +1,56 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_GITHUB,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
ACTION_PUSH_TO_HEROKU INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITHUB_API_URL
} from '../variables'; } from '../variables';
import { import {
SITE_URL, SITE_URL,
CLIENT_SECRET_HEROKU, CLIENT_ID_VERCEL,
CLIENT_ID_VERCEL, CLIENT_ID_NETLIFY,
CLIENT_ID_NETLIFY, CLIENT_ID_GITHUB,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_HEROKU,
CLIENT_SECRET_NETLIFY CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config'; } from '../config';
interface ExchangeCodeHerokuResponse { interface ExchangeCodeHerokuResponse {
token_type: string; token_type: string;
access_token: string; access_token: string;
expires_in: number; expires_in: number;
refresh_token: string; refresh_token: string;
user_id: string; user_id: string;
session_nonce?: string; session_nonce?: string;
} }
interface ExchangeCodeVercelResponse { interface ExchangeCodeVercelResponse {
token_type: string; token_type: string;
access_token: string; access_token: string;
installation_id: string; installation_id: string;
user_id: string; user_id: string;
team_id?: string; team_id?: string;
} }
interface ExchangeCodeNetlifyResponse { interface ExchangeCodeNetlifyResponse {
access_token: string; access_token: string;
token_type: string; token_type: string;
refresh_token: string; refresh_token: string;
scope: string; scope: string;
created_at: number; created_at: number;
}
interface ExchangeCodeGithubResponse {
access_token: string;
scope: string;
token_type: string;
} }
/** /**
@@ -56,40 +66,45 @@ interface ExchangeCodeNetlifyResponse {
* @returns {String} obj.action - integration action for bot sequence * @returns {String} obj.action - integration action for bot sequence
*/ */
const exchangeCode = async ({ const exchangeCode = async ({
integration, integration,
code code
}: { }: {
integration: string; integration: string;
code: string; code: string;
}) => { }) => {
let obj = {} as any; let obj = {} as any;
try { try {
switch (integration) { switch (integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
obj = await exchangeCodeHeroku({ obj = await exchangeCodeHeroku({
code code
}); });
break; break;
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
obj = await exchangeCodeVercel({ obj = await exchangeCodeVercel({
code code
}); });
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
obj = await exchangeCodeNetlify({ obj = await exchangeCodeNetlify({
code code
}); });
break; break;
} case INTEGRATION_GITHUB:
} catch (err) { obj = await exchangeCodeGithub({
Sentry.setUser(null); code
Sentry.captureException(err); });
throw new Error('Failed OAuth2 code-token exchange'); break;
} }
} catch (err) {
return obj; Sentry.setUser(null);
} Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange');
}
return obj;
};
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
@@ -144,35 +159,33 @@ const exchangeCodeHeroku = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API * @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeVercel = async ({ const exchangeCodeVercel = async ({ code }: { code: string }) => {
code let res: ExchangeCodeVercelResponse;
}: { try {
code: string; res = (
}) => { await axios.post(
let res: ExchangeCodeVercelResponse; INTEGRATION_VERCEL_TOKEN_URL,
try { new URLSearchParams({
res = (await axios.post( code: code,
INTEGRATION_VERCEL_TOKEN_URL, client_id: CLIENT_ID_VERCEL,
new URLSearchParams({ client_secret: CLIENT_SECRET_VERCEL,
code: code, redirect_uri: `${SITE_URL}/vercel`
client_id: CLIENT_ID_VERCEL, } as any)
client_secret: CLIENT_SECRET_VERCEL, )
redirect_uri: `${SITE_URL}/vercel` ).data;
} as any) } catch (err) {
)).data; Sentry.setUser(null);
} catch (err) { Sentry.captureException(err);
Sentry.setUser(null); throw new Error('Failed OAuth2 code-token exchange with Vercel');
Sentry.captureException(err); }
throw new Error('Failed OAuth2 code-token exchange with Vercel');
} return {
accessToken: res.access_token,
return ({ refreshToken: null,
accessToken: res.access_token, accessExpiresAt: null,
refreshToken: null, teamId: res.team_id
accessExpiresAt: null, };
teamId: res.team_id };
});
}
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@@ -184,58 +197,89 @@ const exchangeCodeVercel = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API * @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeNetlify = async ({ const exchangeCodeNetlify = async ({ code }: { code: string }) => {
code let res: ExchangeCodeNetlifyResponse;
}: { let accountId;
code: string; try {
}) => { res = (
let res: ExchangeCodeNetlifyResponse; await axios.post(
let accountId; INTEGRATION_NETLIFY_TOKEN_URL,
try { new URLSearchParams({
res = (await axios.post( grant_type: 'authorization_code',
INTEGRATION_NETLIFY_TOKEN_URL, code: code,
new URLSearchParams({ client_id: CLIENT_ID_NETLIFY,
grant_type: 'authorization_code', client_secret: CLIENT_SECRET_NETLIFY,
code: code, redirect_uri: `${SITE_URL}/netlify`
client_id: CLIENT_ID_NETLIFY, } as any)
client_secret: CLIENT_SECRET_NETLIFY, )
redirect_uri: `${SITE_URL}/netlify` ).data;
} as any)
)).data;
const res2 = await axios.get( const res2 = await axios.get('https://api.netlify.com/api/v1/sites', {
'https://api.netlify.com/api/v1/sites', headers: {
{ Authorization: `Bearer ${res.access_token}`
headers: { }
Authorization: `Bearer ${res.access_token}`
}
}
);
const res3 = (await axios.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
}); });
}
export { const res3 = (
exchangeCode await axios.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
};
};
/**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Github
* code-token exchange
* @param {Object} obj1
* @param {Object} obj1.code - code for code-token exchange
* @returns {Object} obj2
* @returns {String} obj2.accessToken - access token for Github API
* @returns {String} obj2.refreshToken - refresh token for Github API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/
const exchangeCodeGithub = async ({ code }: { code: string }) => {
let res: ExchangeCodeGithubResponse;
try {
res = (
await axios.get(INTEGRATION_GITHUB_TOKEN_URL, {
params: {
client_id: CLIENT_ID_GITHUB,
client_secret: CLIENT_SECRET_GITHUB,
code: code,
redirect_uri: `${SITE_URL}/github`
},
headers: {
Accept: '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
};
};
export { exchangeCode };
+35 -36
View File
@@ -13,46 +13,47 @@ import {
* named [integration] * named [integration]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
*/ */
const exchangeRefresh = async ({ const exchangeRefresh = async ({
integration, integration,
refreshToken refreshToken
}: { }: {
integration: string; integration: string;
refreshToken: string; refreshToken: string;
}) => { }) => {
let accessToken; let accessToken;
try { try {
switch (integration) { switch (integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
accessToken = await exchangeRefreshHeroku({ accessToken = await exchangeRefreshHeroku({
refreshToken refreshToken
}); });
break; break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
} }
} catch (err) {
return accessToken; Sentry.setUser(null);
} Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token');
}
return accessToken;
};
/** /**
* Return new access token by exchanging refresh token [refreshToken] for the * Return new access token by exchanging refresh token [refreshToken] for the
* Heroku integration * Heroku integration
* @param {Object} obj * @param {Object} obj
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku * @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
* @returns * @returns
*/ */
const exchangeRefreshHeroku = async ({ const exchangeRefreshHeroku = async ({
refreshToken refreshToken
}: { }: {
refreshToken: string; refreshToken: string;
}) => { }) => {
let accessToken; let accessToken;
//TODO: Refactor code to take advantage of using RequestError. It's possible to create new types of errors for more detailed errors
try { try {
const res = await axios.post( const res = await axios.post(
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
@@ -63,16 +64,14 @@ const exchangeRefreshHeroku = async ({
} as any) } as any)
); );
accessToken = res.data.access_token; accessToken = res.data.access_token;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to get new OAuth2 access token for Heroku'); throw new Error('Failed to get new OAuth2 access token for Heroku');
} }
return accessToken;
}
export { return accessToken;
exchangeRefresh };
}
export { exchangeRefresh };
+37 -40
View File
@@ -1,50 +1,47 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
import { import {
IIntegrationAuth, INTEGRATION_HEROKU,
IntegrationAuth, INTEGRATION_VERCEL,
Integration INTEGRATION_NETLIFY,
} from '../models'; INTEGRATION_GITHUB
import {
INTEGRATION_HEROKU,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables'; } from '../variables';
const revokeAccess = async ({ const revokeAccess = async ({
integrationAuth, integrationAuth,
accessToken accessToken
}: { }: {
integrationAuth: IIntegrationAuth, integrationAuth: IIntegrationAuth;
accessToken: string accessToken: string;
}) => { }) => {
try { try {
// add any integration-specific revocation logic // add any integration-specific revocation logic
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
break; break;
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
break; break;
} case INTEGRATION_GITHUB:
break;
const 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');
} }
}
export { const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
revokeAccess _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');
}
};
export { revokeAccess };
+215 -91
View File
@@ -1,16 +1,21 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { Octokit } from '@octokit/rest';
// import * as sodium from 'libsodium-wrappers';
import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import { import {
IIntegration, IIntegrationAuth INTEGRATION_HEROKU,
} from '../models'; INTEGRATION_VERCEL,
import { INTEGRATION_NETLIFY,
INTEGRATION_HEROKU, INTEGRATION_GITHUB,
INTEGRATION_VERCEL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_NETLIFY, INTEGRATION_VERCEL_API_URL,
INTEGRATION_HEROKU_API_URL, INTEGRATION_NETLIFY_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_GITHUB_API_URL
INTEGRATION_NETLIFY_API_URL
} from '../variables'; } from '../variables';
import { access, appendFile } from 'fs';
// TODO: need a helper function in the future to handle integration // TODO: need a helper function in the future to handle integration
// envar priorities (i.e. prioritize secrets within integration or those on Infisical) // envar priorities (i.e. prioritize secrets within integration or those on Infisical)
@@ -26,49 +31,54 @@ import {
* @param {String} obj.accessToken - access token for integration * @param {String} obj.accessToken - access token for integration
*/ */
const syncSecrets = async ({ const syncSecrets = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken, accessToken
}: { }: {
integration: IIntegration; integration: IIntegration;
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
switch (integration.integration) { switch (integration.integration) {
case INTEGRATION_HEROKU: case INTEGRATION_HEROKU:
await syncSecretsHeroku({ await syncSecretsHeroku({
integration, integration,
secrets, secrets,
accessToken accessToken
}); });
break; break;
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
await syncSecretsVercel({ await syncSecretsVercel({
integration, integration,
secrets, secrets,
accessToken accessToken
}); });
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
await syncSecretsNetlify({ await syncSecretsNetlify({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken accessToken
}); });
break; break;
} case INTEGRATION_GITHUB:
await syncSecretsGitHub({
// TODO: set integration to inactive if it was not synced correctly (send alert?) integration,
} catch (err) { secrets,
Sentry.setUser(null); accessToken
Sentry.captureException(err); });
throw new Error('Failed to sync secrets to integration'); break;
} }
} } catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integration');
}
};
/** /**
* Sync/push [secrets] to Heroku [app] * Sync/push [secrets] to Heroku [app]
@@ -77,47 +87,49 @@ const syncSecrets = async ({
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values) * @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
*/ */
const syncSecretsHeroku = async ({ const syncSecretsHeroku = async ({
integration, integration,
secrets, secrets,
accessToken accessToken
}: { }: {
integration: IIntegration, integration: IIntegration;
secrets: any; secrets: any;
accessToken: string; accessToken: string;
}) => { }) => {
try { try {
const herokuSecrets = (await axios.get( const herokuSecrets = (
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, await axios.get(
{ `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
headers: { {
Accept: 'application/vnd.heroku+json; version=3', headers: {
Authorization: `Bearer ${accessToken}` Accept: 'application/vnd.heroku+json; version=3',
} Authorization: `Bearer ${accessToken}`
} }
)).data; }
)
Object.keys(herokuSecrets).forEach(key => { ).data;
if (!(key in secrets)) {
secrets[key] = null;
}
});
await axios.patch( Object.keys(herokuSecrets).forEach((key) => {
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, if (!(key in secrets)) {
secrets, secrets[key] = null;
{ }
headers: { });
Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}` await axios.patch(
} `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
} secrets,
); {
} catch (err) { headers: {
Sentry.setUser(null); Accept: 'application/vnd.heroku+json; version=3',
Sentry.captureException(err); Authorization: `Bearer ${accessToken}`
throw new Error('Failed to sync secrets to Heroku'); }
} }
} );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku');
}
};
/** /**
* Sync/push [secrets] to Heroku [app] * Sync/push [secrets] to Heroku [app]
@@ -474,8 +486,120 @@ const syncSecretsNetlify = async ({
throw new Error('Failed to sync secrets to Heroku'); throw new Error('Failed to sync secrets to Heroku');
} }
} }
export { /**
syncSecrets * Sync/push [secrets] to GitHub [repo]
} * @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)
*/
const syncSecretsGitHub = async ({
integration,
secrets,
accessToken
}: {
integration: IIntegration;
secrets: any;
accessToken: string;
}) => {
try {
interface GitHubRepoKey {
key_id: string;
key: string;
}
interface GitHubSecret {
name: string;
created_at: string;
updated_at: string;
}
interface GitHubSecretRes {
[index: string]: GitHubSecret;
}
const deleteSecrets: GitHubSecret[] = [];
const octokit = new Octokit({
auth: accessToken
});
const user = (await octokit.request('GET /user', {})).data;
const repoPublicKey: GitHubRepoKey = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets/public-key',
{
owner: user.login,
repo: integration.app
}
)).data;
// // Get local copy of decrypted secrets. We cannot decrypt them as we dont have access to GH private key
const encryptedSecrets: GitHubSecretRes = (await octokit.request(
'GET /repos/{owner}/{repo}/actions/secrets',
{
owner: user.login,
repo: integration.app
}
))
.data
.secrets
.reduce((obj: any, secret: any) => ({
...obj,
[secret.name]: secret
}), {});
Object.keys(encryptedSecrets).map(async (key) => {
if (!(key in secrets)) {
await octokit.request(
'DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: user.login,
repo: integration.app,
secret_name: key
}
);
}
});
Object.keys(secrets).map((key) => {
// let encryptedSecret;
sodium.ready.then(async () => {
// convert secret & base64 key to Uint8Array.
const binkey = sodium.from_base64(
repoPublicKey.key,
sodium.base64_variants.ORIGINAL
);
const binsec = sodium.from_string(secrets[key]);
// encrypt secret using libsodium
const encBytes = sodium.crypto_box_seal(binsec, binkey);
// convert encrypted Uint8Array to base64
const encryptedSecret = sodium.to_base64(
encBytes,
sodium.base64_variants.ORIGINAL
);
await octokit.request(
'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}',
{
owner: user.login,
repo: integration.app,
secret_name: key,
encrypted_value: encryptedSecret,
key_id: repoPublicKey.key_id
}
);
});
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to GitHub');
}
};
export { syncSecrets };
@@ -0,0 +1,29 @@
import { ErrorRequestHandler } from "express";
import * as Sentry from '@sentry/node';
import { InternalServerError } from "../utils/errors";
import { getLogger } from "../utils/logger";
import RequestError, { LogLevel } from "../utils/requestError";
export const requestErrorHandler: ErrorRequestHandler = (error: RequestError|Error, req, res, next) => {
if(res.headersSent) return next();
//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})
getLogger('backend-main').log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
}
//* Set Sentry user identification if req.user is populated
if(req.user !== undefined && req.user !== null){
Sentry.setUser({ email: req.user.email })
}
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
if([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)){
Sentry.captureException(error)
}
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
next()
}
+16 -23
View File
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../models'; import { User } from '../models';
import { JWT_AUTH_SECRET } from '../config'; import { JWT_AUTH_SECRET } from '../config';
import { AccountNotFoundError, BadRequestError, UnauthorizedRequestError } from '../utils/errors';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -20,32 +20,25 @@ declare module 'jsonwebtoken' {
*/ */
const requireAuth = async (req: Request, res: Response, next: NextFunction) => { const requireAuth = async (req: Request, res: Response, next: NextFunction) => {
// JWT authentication middleware // JWT authentication middleware
try { const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (!req.headers?.authorization) if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
throw new Error('Failed to locate authorization header'); if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const token = req.headers.authorization.split(' ')[1]; const decodedToken = <jwt.UserIDJwtPayload>(
const decodedToken = <jwt.UserIDJwtPayload>( jwt.verify(AUTH_TOKEN_VALUE, JWT_AUTH_SECRET)
jwt.verify(token, JWT_AUTH_SECRET) );
);
const user = await User.findOne({ const user = await User.findOne({
_id: decodedToken.userId _id: decodedToken.userId
}).select('+publicKey'); }).select('+publicKey');
if (!user) throw new Error('Failed to authenticate unfound user'); if (!user) return next(AccountNotFoundError({message: 'Failed to locate User account'}))
if (!user?.publicKey) if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account'); return next(UnauthorizedRequestError({message: 'Unable to authenticate due to partially set up account'}))
req.user = user; req.user = user;
return next(); return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed to authenticate user. Try logging in'
});
}
}; };
export default requireAuth; export default requireAuth;
+16 -24
View File
@@ -1,7 +1,7 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Bot } from '../models'; import { Bot } from '../models';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { AccountNotFoundError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -15,30 +15,22 @@ const requireBotAuth = ({
location?: req; location?: req;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { const bot = await Bot.findOne({ _id: req[location].botId });
const bot = await Bot.findOne({ _id: req[location].botId });
if (!bot) {
if (!bot) { return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
throw new Error('Failed to find bot');
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
req.bot = bot;
next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed bot authorization'
});
} }
await validateMembership({
userId: req.user._id.toString(),
workspaceId: bot.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
req.bot = bot;
next();
} }
} }
@@ -1,8 +1,8 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { Bot, Integration, IntegrationAuth, Membership } from '../models'; import { Integration, IntegrationAuth } from '../models';
import { IntegrationService } from '../services'; import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { IntegrationNotFoundError, UnauthorizedRequestError } from '../utils/errors';
/** /**
* Validate if user on request is a member of workspace with proper roles associated * Validate if user on request is a member of workspace with proper roles associated
@@ -21,48 +21,40 @@ const requireIntegrationAuth = ({
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// integration authorization middleware // integration authorization middleware
try { const { integrationId } = req.params;
const { integrationId } = req.params;
// validate integration accessibility // validate integration accessibility
const integration = await Integration.findOne({ const integration = await Integration.findOne({
_id: integrationId _id: integrationId
}); });
if (!integration) { if (!integration) {
throw new Error('Failed to find integration'); return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integration.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
const integrationAuth = await IntegrationAuth.findOne({
_id: integration.integrationAuth
}).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
);
if (!integrationAuth) {
throw new Error('Failed to find integration authorization');
}
req.integration = integration;
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString()
});
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed integration authorization'
});
} }
await validateMembership({
userId: req.user._id.toString(),
workspaceId: integration.workspace.toString(),
acceptedRoles,
acceptedStatuses
});
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();
}; };
}; };
@@ -3,6 +3,7 @@ import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth } from '../models'; import { IntegrationAuth } from '../models';
import { IntegrationService } from '../services'; import { IntegrationService } from '../services';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
/** /**
* Validate if user on request is a member of workspace with proper roles associated * Validate if user on request is a member of workspace with proper roles associated
@@ -22,41 +23,33 @@ const requireIntegrationAuthorizationAuth = ({
attachAccessToken?: boolean; attachAccessToken?: boolean;
}) => { }) => {
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
try { const { integrationAuthId } = req.params;
const { integrationAuthId } = req.params;
const integrationAuth = await IntegrationAuth.findOne({ const integrationAuth = await IntegrationAuth.findOne({
_id: integrationAuthId _id: integrationAuthId
}).select( }).select(
'+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt' '+refreshCiphertext +refreshIV +refreshTag +accessCiphertext +accessIV +accessTag +accessExpiresAt'
); );
if (!integrationAuth) { if (!integrationAuth) {
throw new Error('Failed to find integration authorization'); return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authorization credentials'}))
} }
await validateMembership({ await validateMembership({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId: integrationAuth.workspace.toString(), workspaceId: integrationAuth.workspace.toString(),
acceptedRoles, acceptedRoles,
acceptedStatuses acceptedStatuses
}); });
req.integrationAuth = integrationAuth; req.integrationAuth = integrationAuth;
if (attachAccessToken) { if (attachAccessToken) {
req.accessToken = await IntegrationService.getIntegrationAuthAccess({ req.accessToken = await IntegrationService.getIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString() integrationAuthId: integrationAuth._id.toString()
});
}
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed (authorization) integration authorizationt'
}); });
} }
return next();
}; };
}; };
@@ -1,6 +1,6 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { IOrganization, MembershipOrg } from '../models'; import { IOrganization, MembershipOrg } from '../models';
import { UnauthorizedRequestError, ValidationError } from '../utils/errors';
/** /**
* Validate if user on request is a member with proper roles for organization * Validate if user on request is a member with proper roles for organization
@@ -19,35 +19,28 @@ const requireOrganizationAuth = ({
return async (req: Request, res: Response, next: NextFunction) => { return async (req: Request, res: Response, next: NextFunction) => {
// organization authorization middleware // organization authorization middleware
try { // validate organization membership
// validate organization membership const membershipOrg = await MembershipOrg.findOne({
const membershipOrg = await MembershipOrg.findOne({ user: req.user._id,
user: req.user._id, organization: req.params.organizationId
organization: req.params.organizationId }).populate<{ organization: IOrganization }>('organization');
}).populate<{ organization: IOrganization }>('organization');
if (!membershipOrg) {
throw new Error('Failed to find organization membership');
}
if (!acceptedRoles.includes(membershipOrg.role)) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership role'); return next(UnauthorizedRequestError({message: "You're not a member of this Organization."}))
}
if (!acceptedStatuses.includes(membershipOrg.status)) {
throw new Error('Failed to validate organization membership status');
}
req.membershipOrg = membershipOrg;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed organization authorization'
});
} }
//TODO is this important to validate? I mean is it possible to save wrong role to database or get wrong role from databse? - Zamion101
if (!acceptedRoles.includes(membershipOrg.role)) {
return next(ValidationError({message: 'Failed to validate Organization Membership Role'}))
}
if (!acceptedStatuses.includes(membershipOrg.status)) {
return next(ValidationError({message: 'Failed to validate Organization Membership Status'}))
}
req.membershipOrg = membershipOrg;
return next();
}; };
}; };
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { ServiceToken } from '../models'; import { ServiceToken } from '../models';
import { JWT_SERVICE_SECRET } from '../config'; import { JWT_SERVICE_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -24,33 +24,27 @@ const requireServiceTokenAuth = async (
next: NextFunction next: NextFunction
) => { ) => {
// JWT service token middleware // JWT service token middleware
try {
if (!req.headers?.authorization) const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
throw new Error('Failed to locate authorization header'); if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
//TODO: Determine what is the actual Token Type for Service Token Authentication (ex. Bearer)
//if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(UnauthorizedRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const token = req.headers.authorization.split(' ')[1]; const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET)
);
const decodedToken = <jwt.UserIDJwtPayload>( const serviceToken = await ServiceToken.findOne({
jwt.verify(token, JWT_SERVICE_SECRET) _id: decodedToken.serviceTokenId
); })
.populate('user', '+publicKey')
.select('+encryptedKey +publicKey +nonce');
const serviceToken = await ServiceToken.findOne({ if (!serviceToken) return next(UnauthorizedRequestError({message: 'The service token does not match the record in the database'}))
_id: decodedToken.serviceTokenId
})
.populate('user', '+publicKey')
.select('+encryptedKey +publicKey +nonce');
if (!serviceToken) throw new Error('Failed to find service token'); req.serviceToken = serviceToken;
return next();
req.serviceToken = serviceToken;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed to authenticate service token'
});
}
}; };
export default requireServiceTokenAuth; export default requireServiceTokenAuth;
+16 -24
View File
@@ -1,8 +1,8 @@
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../models'; import { User } from '../models';
import { JWT_SIGNUP_SECRET } from '../config'; import { JWT_SIGNUP_SECRET } from '../config';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
declare module 'jsonwebtoken' { declare module 'jsonwebtoken' {
export interface UserIDJwtPayload extends jwt.JwtPayload { export interface UserIDJwtPayload extends jwt.JwtPayload {
@@ -21,32 +21,24 @@ const requireSignupAuth = async (
) => { ) => {
// JWT (temporary) authentication middleware for complete signup // JWT (temporary) authentication middleware for complete signup
try { const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
if (!req.headers?.authorization) if(AUTH_TOKEN_TYPE === null) return next(BadRequestError({message: `Missing Authorization Header in the request header.`}))
throw new Error('Failed to locate authorization header'); if(AUTH_TOKEN_TYPE.toLowerCase() !== 'bearer') return next(BadRequestError({message: `The provided authentication type '${AUTH_TOKEN_TYPE}' is not supported.`}))
if(AUTH_TOKEN_VALUE === null) return next(BadRequestError({message: 'Missing Authorization Body in the request header'}))
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET)
);
const token = req.headers.authorization.split(' ')[1]; const user = await User.findOne({
const decodedToken = <jwt.UserIDJwtPayload>( _id: decodedToken.userId
jwt.verify(token, JWT_SIGNUP_SECRET) }).select('+publicKey');
);
const user = await User.findOne({ if (!user)
_id: decodedToken.userId return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'}))
}).select('+publicKey');
if (!user) req.user = user;
throw new Error('Failed to temporarily authenticate unfound user'); return next();
req.user = user;
return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error:
'Failed to temporarily authenticate user for complete account. Try logging in'
});
}
}; };
export default requireSignupAuth; export default requireSignupAuth;
@@ -1,6 +1,6 @@
import * as Sentry from '@sentry/node';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { validateMembership } from '../helpers/membership'; import { validateMembership } from '../helpers/membership';
import { UnauthorizedRequestError } from '../utils/errors';
type req = 'params' | 'body' | 'query'; type req = 'params' | 'body' | 'query';
@@ -36,11 +36,7 @@ const requireWorkspaceAuth = ({
return next(); return next();
} catch (err) { } catch (err) {
Sentry.setUser(null); return next(UnauthorizedRequestError({message: 'Unable to authenticate workspace'}))
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed workspace authorization'
});
} }
}; };
}; };
+3 -7
View File
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import * as Sentry from '@sentry/node';
import { validationResult } from 'express-validator'; import { validationResult } from 'express-validator';
import { BadRequestError, UnauthorizedRequestError } from '../utils/errors';
/** /**
* Validate intended inputs on [req] via express-validator * Validate intended inputs on [req] via express-validator
@@ -15,16 +15,12 @@ const validate = (req: Request, res: Response, next: NextFunction) => {
try { try {
const errors = validationResult(req); const errors = validationResult(req);
if (!errors.isEmpty()) { if (!errors.isEmpty()) {
return res.status(400).json({ errors: errors.array() }); return next(BadRequestError({context: {errors: errors.array}}))
} }
return next(); return next();
} catch (err) { } catch (err) {
Sentry.setUser(null); return next(UnauthorizedRequestError({message: 'Unauthenticated requests are not allowed. Try logging in'}))
Sentry.captureException(err);
return res.status(401).send({
error: "Looks like you're unauthenticated . Try logging in"
});
} }
}; };
+72 -66
View File
@@ -1,77 +1,83 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
ENV_DEV, ENV_DEV,
ENV_TESTING, ENV_TESTING,
ENV_STAGING, ENV_STAGING,
ENV_PROD, ENV_PROD,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} from '../variables'; } from '../variables';
export interface IIntegration { export interface IIntegration {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
environment: 'dev' | 'test' | 'staging' | 'prod'; environment: 'dev' | 'test' | 'staging' | 'prod';
isActive: boolean; isActive: boolean;
app: string; app: string;
target: string; target: string;
context: string; context: string;
siteId: string; siteId: string;
integration: 'heroku' | 'vercel' | 'netlify'; integration: 'heroku' | 'vercel' | 'netlify' | 'github';
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
const integrationSchema = new Schema<IIntegration>( const integrationSchema = new Schema<IIntegration>(
{ {
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: 'Workspace',
required: true required: true
}, },
environment: { environment: {
type: String, type: String,
enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD], enum: [ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD],
required: true required: true
}, },
isActive: { isActive: {
type: Boolean, type: Boolean,
required: true required: true
}, },
app: { // name of app in provider app: {
type: String, // name of app in provider
default: null type: String,
}, default: null
target: { // vercel-specific target (environment) },
type: String, target: {
default: null // vercel-specific target (environment)
}, type: String,
context: { // netlify-specific context (deploy) default: null
type: String, },
default: null context: {
}, // netlify-specific context (deploy)
siteId: { // netlify-specific site (app) id type: String,
type: String, default: null
default: null },
}, siteId: {
integration: { // netlify-specific site (app) id
type: String, type: String,
enum: [ default: null
INTEGRATION_HEROKU, },
INTEGRATION_VERCEL, integration: {
INTEGRATION_NETLIFY type: String,
], enum: [
required: true INTEGRATION_HEROKU,
}, INTEGRATION_VERCEL,
integrationAuth: { INTEGRATION_NETLIFY,
type: Schema.Types.ObjectId, INTEGRATION_GITHUB
ref: 'IntegrationAuth', ],
required: true required: true
} },
}, integrationAuth: {
{ type: Schema.Types.ObjectId,
timestamps: true ref: 'IntegrationAuth',
} required: true
}
},
{
timestamps: true
}
); );
const Integration = model<IIntegration>('Integration', integrationSchema); const Integration = model<IIntegration>('Integration', integrationSchema);
+74 -70
View File
@@ -1,83 +1,87 @@
import { Schema, model, Types } from 'mongoose'; import { Schema, model, Types } from 'mongoose';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} from '../variables'; } from '../variables';
export interface IIntegrationAuth { export interface IIntegrationAuth {
_id: Types.ObjectId; _id: Types.ObjectId;
workspace: Types.ObjectId; workspace: Types.ObjectId;
integration: 'heroku' | 'vercel' | 'netlify'; integration: 'heroku' | 'vercel' | 'netlify' | 'github';
teamId: string; teamId: string;
accountId: string; accountId: string;
refreshCiphertext?: string; refreshCiphertext?: string;
refreshIV?: string; refreshIV?: string;
refreshTag?: string; refreshTag?: string;
accessCiphertext?: string; accessCiphertext?: string;
accessIV?: string; accessIV?: string;
accessTag?: string; accessTag?: string;
accessExpiresAt?: Date; accessExpiresAt?: Date;
} }
const integrationAuthSchema = new Schema<IIntegrationAuth>( const integrationAuthSchema = new Schema<IIntegrationAuth>(
{ {
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
required: true required: true
}, },
integration: { integration: {
type: String, type: String,
enum: [ enum: [
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY,
], INTEGRATION_GITHUB
required: true ],
}, required: true
teamId: { // vercel-specific integration param },
type: String teamId: {
}, // vercel-specific integration param
accountId: { // netlify-specific integration param type: String
type: String },
}, accountId: {
refreshCiphertext: { // netlify-specific integration param
type: String, type: String
select: false },
}, refreshCiphertext: {
refreshIV: { type: String,
type: String, select: false
select: false },
}, refreshIV: {
refreshTag: { type: String,
type: String, select: false
select: false },
}, refreshTag: {
accessCiphertext: { type: String,
type: String, select: false
select: false },
}, accessCiphertext: {
accessIV: { type: String,
type: String, select: false
select: false },
}, accessIV: {
accessTag: { type: String,
type: String, select: false
select: false },
}, accessTag: {
accessExpiresAt: { type: String,
type: Date, select: false
select: false },
} accessExpiresAt: {
}, type: Date,
{ select: false
timestamps: true }
} },
{
timestamps: true
}
); );
const IntegrationAuth = model<IIntegrationAuth>( const IntegrationAuth = model<IIntegrationAuth>(
'IntegrationAuth', 'IntegrationAuth',
integrationAuthSchema integrationAuthSchema
); );
export default IntegrationAuth; export default IntegrationAuth;
+25
View File
@@ -10,6 +10,7 @@ import {
export interface ISecret { export interface ISecret {
_id: Types.ObjectId; _id: Types.ObjectId;
version: number;
workspace: Types.ObjectId; workspace: Types.ObjectId;
type: string; type: string;
user: Types.ObjectId; user: Types.ObjectId;
@@ -22,10 +23,18 @@ export interface ISecret {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
secretCommentHash?: string;
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
{ {
version: {
type: Number,
required: true
},
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: 'Workspace', ref: 'Workspace',
@@ -77,6 +86,22 @@ const secretSchema = new Schema<ISecret>(
secretValueHash: { secretValueHash: {
type: String, type: String,
required: true required: true
},
secretCommentCiphertext: {
type: String,
required: false
},
secretCommentIV: {
type: String, // symmetric
required: false
},
secretCommentTag: {
type: String, // symmetric
required: false
},
secretCommentHash: {
type: String,
required: false
} }
}, },
{ {
@@ -1,9 +1,9 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { authController } from '../controllers'; import { authController } from '../../controllers/v1';
import { loginLimiter } from '../helpers/rateLimiter'; import { loginLimiter } from '../../helpers/rateLimiter';
router.post('/token', validateRequest, authController.getNewToken); router.post('/token', validateRequest, authController.getNewToken);
@@ -6,9 +6,9 @@ import {
requireBotAuth, requireBotAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { botController } from '../controllers'; import { botController } from '../../controllers/v1';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
router.get( router.get(
'/:workspaceId', '/:workspaceId',
@@ -4,10 +4,10 @@ import {
requireAuth, requireAuth,
requireIntegrationAuth, requireIntegrationAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../variables'; import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { integrationController } from '../controllers'; import { integrationController } from '../../controllers/v1';
router.patch( router.patch(
'/:integrationId', '/:integrationId',
@@ -6,9 +6,9 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, GRANTED } from '../variables'; import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { integrationAuthController } from '../controllers'; import { integrationAuthController } from '../../controllers/v1';
router.get( router.get(
'/integration-options', '/integration-options',
@@ -1,8 +1,8 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../controllers'; import { membershipOrgController } from '../../controllers/v1';
router.post( router.post(
'/signup', '/signup',
@@ -4,10 +4,10 @@ import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { keyController } from '../controllers'; import { keyController } from '../../controllers/v1';
router.post( router.post(
'/:workspaceId', '/:workspaceId',
@@ -34,6 +34,4 @@ router.get(
keyController.getLatestKey keyController.getLatestKey
); );
router.get('/publicKey/infisical', keyController.getPublicKeyInfisical);
export default router; export default router;
@@ -1,8 +1,8 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body, param } from 'express-validator'; import { body, param } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipController } from '../controllers'; import { membershipController } from '../../controllers/v1';
router.get( // used for CLI (deprecate) router.get( // used for CLI (deprecate)
'/:workspaceId/connect', '/:workspaceId/connect',
@@ -1,8 +1,8 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { param } from 'express-validator'; import { param } from 'express-validator';
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { membershipOrgController } from '../controllers'; import { membershipOrgController } from '../../controllers/v1';
router.post( router.post(
// TODO // TODO
@@ -5,9 +5,9 @@ import {
requireAuth, requireAuth,
requireOrganizationAuth, requireOrganizationAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../variables'; import { OWNER, ADMIN, MEMBER, ACCEPTED } from '../../variables';
import { organizationController } from '../controllers'; import { organizationController } from '../../controllers/v1';
router.get( router.get(
'/', '/',
@@ -1,9 +1,9 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireAuth, requireSignupAuth, validateRequest } from '../middleware'; import { requireAuth, requireSignupAuth, validateRequest } from '../../middleware';
import { passwordController } from '../controllers'; import { passwordController } from '../../controllers/v1';
import { passwordLimiter } from '../helpers/rateLimiter'; import { passwordLimiter } from '../../helpers/rateLimiter';
router.post( router.post(
'/srp1', '/srp1',
@@ -5,10 +5,10 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
requireServiceTokenAuth, requireServiceTokenAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { body, query, param } from 'express-validator'; import { body, query, param } from 'express-validator';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { secretController } from '../../controllers/v1';
import { secretController } from '../controllers'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
router.post( router.post(
'/:workspaceId', '/:workspaceId',
@@ -5,10 +5,10 @@ import {
requireWorkspaceAuth, requireWorkspaceAuth,
requireServiceTokenAuth, requireServiceTokenAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { body } from 'express-validator'; import { body } from 'express-validator';
import { ADMIN, MEMBER, GRANTED } from '../variables'; import { ADMIN, MEMBER, GRANTED } from '../../variables';
import { serviceTokenController } from '../controllers'; import { serviceTokenController } from '../../controllers/v1';
// TODO: revoke service token // TODO: revoke service token
@@ -1,9 +1,9 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body } from 'express-validator'; import { body } from 'express-validator';
import { requireSignupAuth, validateRequest } from '../middleware'; import { requireSignupAuth, validateRequest } from '../../middleware';
import { signupController } from '../controllers'; import { signupController } from '../../controllers/v1';
import { signupLimiter } from '../helpers/rateLimiter'; import { signupLimiter } from '../../helpers/rateLimiter';
router.post( router.post(
'/email/signup', '/email/signup',
@@ -1,6 +1,6 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { stripeController } from '../controllers'; import { stripeController } from '../../controllers/v1';
router.post('/webhook', stripeController.handleWebhook); router.post('/webhook', stripeController.handleWebhook);
@@ -1,7 +1,7 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { requireAuth } from '../middleware'; import { requireAuth } from '../../middleware';
import { userController } from '../controllers'; import { userController } from '../../controllers/v1';
router.get('/', requireAuth, userController.getUser); router.get('/', requireAuth, userController.getUser);
@@ -1,8 +1,8 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { requireAuth, validateRequest } from '../middleware'; import { requireAuth, validateRequest } from '../../middleware';
import { body, query } from 'express-validator'; import { body, query } from 'express-validator';
import { userActionController } from '../controllers'; import { userActionController } from '../../controllers/v1';
router.post( router.post(
'/', '/',
@@ -1,13 +1,13 @@
import express from 'express'; import express from 'express';
const router = express.Router(); const router = express.Router();
import { body, param } from 'express-validator'; import { body, param, query } from 'express-validator';
import { import {
requireAuth, requireAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest validateRequest
} from '../middleware'; } from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables'; import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { workspaceController, membershipController } from '../controllers'; import { workspaceController, membershipController } from '../../controllers/v1';
router.get( router.get(
'/:workspaceId/keys', '/:workspaceId/keys',
+7
View File
@@ -0,0 +1,7 @@
import secret from './secret';
import workspace from './workspace';
export {
secret,
workspace
}
+4
View File
@@ -0,0 +1,4 @@
import express from 'express';
const router = express.Router();
export default router;
+176
View File
@@ -0,0 +1,176 @@
import express from 'express';
const router = express.Router();
import { body, param, query } from 'express-validator';
import {
requireAuth,
requireWorkspaceAuth,
requireServiceTokenAuth,
validateRequest
} from '../../middleware';
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../../variables';
import { membershipController } from '../../controllers/v1';
import { workspaceController } from '../../controllers/v2';
router.get(
'/:workspaceId/keys',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspacePublicKeys
);
router.get(
'/:workspaceId/users',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceMemberships
);
router.get('/', requireAuth, workspaceController.getWorkspaces);
router.get(
'/:workspaceId',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspace
);
router.post(
'/',
requireAuth,
body('workspaceName').exists().trim().notEmpty(),
body('organizationId').exists().trim().notEmpty(),
validateRequest,
workspaceController.createWorkspace
);
router.delete(
'/:workspaceId',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.deleteWorkspace
);
router.post(
'/:workspaceId/name',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
param('workspaceId').exists().trim(),
body('name').exists().trim().notEmpty(),
validateRequest,
workspaceController.changeWorkspaceName
);
router.post(
'/:workspaceId/invite-signup',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
body('email').exists().trim().notEmpty(),
validateRequest,
membershipController.inviteUserToWorkspace
);
router.get(
'/:workspaceId/integrations',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceIntegrations
);
router.get(
'/:workspaceId/authorizations',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceIntegrationAuthorizations
);
router.get( // TODO: modify
'/:workspaceId/service-tokens',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [GRANTED]
}),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.getWorkspaceServiceTokens
);
router.post(
'/:workspaceId/secrets',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
body('secrets').exists(),
body('keys').exists(),
body('environment').exists().trim().notEmpty(),
body('channel'),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.pushWorkspaceSecrets
);
router.get(
'/:workspaceId/secrets',
requireAuth,
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
acceptedStatuses: [COMPLETED, GRANTED]
}),
query('environment').exists().trim(),
query('channel'),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.pullSecrets
);
router.get( // TODO: modify based on upcoming serviceTokenData changes
'/:workspaceId/secrets-service-token',
requireServiceTokenAuth,
query('environment').exists().trim(),
query('channel'),
param('workspaceId').exists().trim(),
validateRequest,
workspaceController.pullSecretsServiceToken
);
export default router;
+9 -1
View File
@@ -5,8 +5,16 @@ import {
POSTHOG_PROJECT_API_KEY, POSTHOG_PROJECT_API_KEY,
TELEMETRY_ENABLED TELEMETRY_ENABLED
} from '../config'; } from '../config';
import { getLogger } from '../utils/logger';
console.log('TELEMETRY_ENABLED: ', TELEMETRY_ENABLED); if(TELEMETRY_ENABLED){
getLogger("backend-main").info([
"",
"Infisical collects telemetry data about general usage.",
"The data helps us understand how the product is doing and guide our product development to create the best possible platform; it also helps us demonstrate growth for investors as we support Infisical as open-source software.",
"To opt out of telemetry, you can set `TELEMETRY_ENABLED=false` within the environment variables",
].join('\n'))
}
let postHogClient: any; let postHogClient: any;
if (NODE_ENV === 'production' && TELEMETRY_ENABLED) { if (NODE_ENV === 'production' && TELEMETRY_ENABLED) {
+10
View File
@@ -0,0 +1,10 @@
import mongoose from 'mongoose';
import { getLogger } from '../utils/logger';
export const initDatabase = (MONGO_URL: string) => {
mongoose
.connect(MONGO_URL)
.then(() => getLogger("database").info("Database connection established"))
.catch((e) => getLogger("database").error(`Unable to establish Database connection due to the error.\n${e}`));
return mongoose.connection;
};
+32
View File
@@ -0,0 +1,32 @@
import mongoose from 'mongoose';
import { createTerminus } from '@godaddy/terminus';
import { getLogger } from '../utils/logger';
export const setUpHealthEndpoint = <T>(server: T) => {
const onSignal = () => {
getLogger('backend-main').info('Server is starting clean-up');
return Promise.all([
new Promise((resolve) => {
if (mongoose.connection && mongoose.connection.readyState == 1) {
mongoose.connection.close()
.then(() => resolve('Database connection closed'));
} else {
resolve('Database connection already closed');
}
})
]);
};
const healthCheck = () => {
// `state.isShuttingDown` (boolean) shows whether the server is shutting down or not
// optionally include a resolve value to be included as info in the health check response
return Promise.resolve();
};
createTerminus(server, {
healthChecks: {
'/healthcheck': healthCheck,
onSignal
}
});
};
+52
View File
@@ -0,0 +1,52 @@
import nodemailer from 'nodemailer';
import { SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_SECURE } from '../config';
import { SMTP_HOST_SENDGRID, SMTP_HOST_MAILGUN } from '../variables';
import SMTPConnection from 'nodemailer/lib/smtp-connection';
import * as Sentry from '@sentry/node';
const mailOpts: SMTPConnection.Options = {
host: SMTP_HOST,
port: SMTP_PORT as number
};
if (SMTP_USERNAME && SMTP_PASSWORD) {
mailOpts.auth = {
user: SMTP_USERNAME,
pass: SMTP_PASSWORD
};
}
if (SMTP_SECURE) {
switch (SMTP_HOST) {
case SMTP_HOST_SENDGRID:
mailOpts.requireTLS = true;
break;
case SMTP_HOST_MAILGUN:
mailOpts.requireTLS = true;
mailOpts.tls = {
ciphers: 'TLSv1.2'
}
break;
default:
mailOpts.secure = true;
break;
}
}
export const initSmtp = () => {
const transporter = nodemailer.createTransport(mailOpts);
transporter
.verify()
.then(() => {
Sentry.setUser(null);
Sentry.captureMessage('SMTP - Successfully connected');
})
.catch((err) => {
Sentry.setUser(null);
Sentry.captureException(
`SMTP - Failed to connect to ${SMTP_HOST}:${SMTP_PORT} \n\t${err}`
);
});
return transporter;
};

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