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
@@ -85,15 +84,3 @@ 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 {
@@ -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';
+13 -5
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,6 +194,9 @@ const syncIntegrationsHelper = async ({
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token'); throw new Error('Failed to get integration refresh token');
} }
@@ -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,6 +245,9 @@ const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrati
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration access token'); throw new Error('Failed to get integration access token');
} }
+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 };
+415 -102
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
}; };
+14 -120
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);
setTransporter(initSmtp());
if (NODE_ENV !== 'test') {
Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
tracesSampleRate: 1.0, tracesSampleRate: 1.0,
debug: NODE_ENV === 'production' ? false : true, debug: NODE_ENV === 'production' ? false : true,
environment: NODE_ENV environment: NODE_ENV
});
import {
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 = () => {
mongoose
.connect(MONGO_URL)
.then(() => console.log('Successfully connected to DB'))
.catch((e) => {
console.log('Failed to connect to DB ', e);
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);
});
+74 -30
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 { import { Octokit } from '@octokit/rest';
IIntegrationAuth import { IIntegrationAuth } from '../models';
} from '../models';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_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
@@ -27,7 +32,6 @@ const getApps = async ({
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
interface App { interface App {
name: string; name: string;
siteId?: string; siteId?: string;
@@ -52,8 +56,13 @@ const getApps = async ({
accessToken accessToken
}); });
break; break;
case INTEGRATION_GITHUB:
apps = await getAppsGithub({
integrationAuth,
accessToken
});
break;
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -61,7 +70,7 @@ const getApps = async ({
} }
return apps; return apps;
} };
/** /**
* Return list of names of apps for Heroku integration * Return list of names of apps for Heroku integration
@@ -70,19 +79,17 @@ 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
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { const res = (
await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
headers: { headers: {
Accept: 'application/vnd.heroku+json; version=3', Accept: 'application/vnd.heroku+json; version=3',
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
} }
})).data; })
).data;
apps = res.map((a: any) => ({ apps = res.map((a: any) => ({
name: a.name name: a.name
@@ -94,7 +101,7 @@ const getAppsHeroku = async ({
} }
return apps; return apps;
} };
/** /**
* Return list of names of apps for Vercel integration * Return list of names of apps for Vercel integration
@@ -103,18 +110,16 @@ 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
}: {
accessToken: string;
}) => {
let apps; let apps;
try { try {
const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { const res = (
await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
} }
})).data; })
).data;
apps = res.projects.map((a: any) => ({ apps = res.projects.map((a: any) => ({
name: a.name name: a.name
@@ -126,7 +131,7 @@ const getAppsVercel = async ({
} }
return apps; return apps;
} };
/** /**
* Return list of names of sites for Netlify integration * Return list of names of sites for Netlify integration
@@ -144,17 +149,18 @@ const getAppsNetlify = async ({
}) => { }) => {
let apps; let apps;
try { try {
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { const res = (
await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
headers: { headers: {
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
} }
})).data; })
).data;
apps = res.map((a: any) => ({ apps = res.map((a: any) => ({
name: a.name, name: a.name,
siteId: a.site_id siteId: a.site_id
})); }));
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
@@ -162,8 +168,46 @@ const getAppsNetlify = async ({
} }
return apps; return apps;
} };
export { /**
getApps * 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 };
+82 -38
View File
@@ -4,18 +4,22 @@ import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
ACTION_PUSH_TO_HEROKU 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_HEROKU,
CLIENT_SECRET_VERCEL, CLIENT_SECRET_VERCEL,
CLIENT_SECRET_NETLIFY CLIENT_SECRET_NETLIFY,
CLIENT_SECRET_GITHUB
} from '../config'; } from '../config';
interface ExchangeCodeHerokuResponse { interface ExchangeCodeHerokuResponse {
@@ -43,6 +47,12 @@ interface ExchangeCodeNetlifyResponse {
created_at: number; created_at: number;
} }
interface ExchangeCodeGithubResponse {
access_token: string;
scope: string;
token_type: string;
}
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2 * Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
* code-token exchange for integration named [integration] * code-token exchange for integration named [integration]
@@ -81,6 +91,11 @@ const exchangeCode = async ({
code code
}); });
break; break;
case INTEGRATION_GITHUB:
obj = await exchangeCodeGithub({
code
});
break;
} }
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
@@ -89,7 +104,7 @@ const exchangeCode = async ({
} }
return obj; return obj;
} };
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
@@ -144,14 +159,11 @@ 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
}: {
code: string;
}) => {
let res: ExchangeCodeVercelResponse; let res: ExchangeCodeVercelResponse;
try { try {
res = (await axios.post( res = (
await axios.post(
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
code: code, code: code,
@@ -159,20 +171,21 @@ const exchangeCodeVercel = async ({
client_secret: CLIENT_SECRET_VERCEL, client_secret: CLIENT_SECRET_VERCEL,
redirect_uri: `${SITE_URL}/vercel` redirect_uri: `${SITE_URL}/vercel`
} as any) } as any)
)).data; )
).data;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Vercel'); throw new Error('Failed OAuth2 code-token exchange with Vercel');
} }
return ({ return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: null, refreshToken: null,
accessExpiresAt: null, accessExpiresAt: null,
teamId: res.team_id teamId: res.team_id
}); };
} };
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@@ -184,15 +197,12 @@ 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
}: {
code: string;
}) => {
let res: ExchangeCodeNetlifyResponse; let res: ExchangeCodeNetlifyResponse;
let accountId; let accountId;
try { try {
res = (await axios.post( res = (
await axios.post(
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: 'authorization_code',
@@ -201,41 +211,75 @@ const exchangeCodeNetlify = async ({
client_secret: CLIENT_SECRET_NETLIFY, client_secret: CLIENT_SECRET_NETLIFY,
redirect_uri: `${SITE_URL}/netlify` redirect_uri: `${SITE_URL}/netlify`
} as any) } as any)
)).data; )
).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: { headers: {
Authorization: `Bearer ${res.access_token}` Authorization: `Bearer ${res.access_token}`
} }
} });
);
const res3 = (await axios.get( const res3 = (
'https://api.netlify.com/api/v1/accounts', await axios.get('https://api.netlify.com/api/v1/accounts', {
{
headers: { headers: {
Authorization: `Bearer ${res.access_token}` Authorization: `Bearer ${res.access_token}`
} }
} })
)).data; ).data;
accountId = res3[0].id; accountId = res3[0].id;
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify'); throw new Error('Failed OAuth2 code-token exchange with Netlify');
} }
return ({ return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: res.refresh_token, refreshToken: res.refresh_token,
accountId accountId
}); };
} };
export { /**
exchangeCode * 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 };
+4 -5
View File
@@ -38,7 +38,7 @@ const exchangeRefresh = async ({
} }
return accessToken; return accessToken;
} };
/** /**
* Return new access token by exchanging refresh token [refreshToken] for the * Return new access token by exchanging refresh token [refreshToken] for the
@@ -53,6 +53,7 @@ const exchangeRefreshHeroku = async ({
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,
@@ -71,8 +72,6 @@ const exchangeRefreshHeroku = async ({
} }
return accessToken; return accessToken;
} };
export { export { exchangeRefresh };
exchangeRefresh
}
+9 -12
View File
@@ -1,22 +1,19 @@
import axios from 'axios'; import axios from 'axios';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { import { IIntegrationAuth, IntegrationAuth, Integration } from '../models';
IIntegrationAuth,
IntegrationAuth,
Integration
} from '../models';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
} 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
@@ -27,6 +24,8 @@ const revokeAccess = async ({
break; break;
case INTEGRATION_NETLIFY: case INTEGRATION_NETLIFY:
break; break;
case INTEGRATION_GITHUB:
break;
} }
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
@@ -43,8 +42,6 @@ const revokeAccess = async ({
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to delete integration authorization'); throw new Error('Failed to delete integration authorization');
} }
} };
export { export { revokeAccess };
revokeAccess
}
+140 -16
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 { import { Octokit } from '@octokit/rest';
IIntegration, IIntegrationAuth // import * as sodium from 'libsodium-wrappers';
} from '../models'; import sodium from 'libsodium-wrappers';
// const sodium = require('libsodium-wrappers');
import { IIntegration, IIntegrationAuth } from '../models';
import { import {
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
INTEGRATION_GITHUB,
INTEGRATION_HEROKU_API_URL, INTEGRATION_HEROKU_API_URL,
INTEGRATION_VERCEL_API_URL, INTEGRATION_VERCEL_API_URL,
INTEGRATION_NETLIFY_API_URL INTEGRATION_NETLIFY_API_URL,
INTEGRATION_GITHUB_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)
@@ -29,7 +34,7 @@ const syncSecrets = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessToken, accessToken
}: { }: {
integration: IIntegration; integration: IIntegration;
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
@@ -60,15 +65,20 @@ const syncSecrets = async ({
accessToken accessToken
}); });
break; break;
case INTEGRATION_GITHUB:
await syncSecretsGitHub({
integration,
secrets,
accessToken
});
break;
} }
// TODO: set integration to inactive if it was not synced correctly (send alert?)
} catch (err) { } catch (err) {
Sentry.setUser(null); Sentry.setUser(null);
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to sync secrets to integration'); throw new Error('Failed to sync secrets to integration');
} }
} };
/** /**
* Sync/push [secrets] to Heroku [app] * Sync/push [secrets] to Heroku [app]
@@ -81,12 +91,13 @@ const syncSecretsHeroku = async ({
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 = (
await axios.get(
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`, `${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
{ {
headers: { headers: {
@@ -94,9 +105,10 @@ const syncSecretsHeroku = async ({
Authorization: `Bearer ${accessToken}` Authorization: `Bearer ${accessToken}`
} }
} }
)).data; )
).data;
Object.keys(herokuSecrets).forEach(key => { Object.keys(herokuSecrets).forEach((key) => {
if (!(key in secrets)) { if (!(key in secrets)) {
secrets[key] = null; secrets[key] = null;
} }
@@ -117,7 +129,7 @@ const syncSecretsHeroku = async ({
Sentry.captureException(err); Sentry.captureException(err);
throw new Error('Failed to sync secrets to Heroku'); throw new Error('Failed to sync secrets to Heroku');
} }
} };
/** /**
* Sync/push [secrets] to Heroku [app] * Sync/push [secrets] to Heroku [app]
@@ -475,7 +487,119 @@ const syncSecretsNetlify = async ({
} }
} }
/**
* 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 {
export { interface GitHubRepoKey {
syncSecrets 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()
}
+8 -15
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(token, JWT_AUTH_SECRET) jwt.verify(AUTH_TOKEN_VALUE, 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;
+2 -10
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,11 +15,10 @@ 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) {
throw new Error('Failed to find bot'); return next(AccountNotFoundError({message: 'Failed to locate Bot account'}))
} }
await validateMembership({ await validateMembership({
@@ -32,13 +31,6 @@ const requireBotAuth = ({
req.bot = bot; req.bot = bot;
next(); next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed bot authorization'
});
}
} }
} }
@@ -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,7 +21,6 @@ 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
@@ -30,7 +29,7 @@ const requireIntegrationAuth = ({
}); });
if (!integration) { if (!integration) {
throw new Error('Failed to find integration'); return next(IntegrationNotFoundError({message: 'Failed to locate Integration'}))
} }
await validateMembership({ await validateMembership({
@@ -47,7 +46,7 @@ const requireIntegrationAuth = ({
); );
if (!integrationAuth) { if (!integrationAuth) {
throw new Error('Failed to find integration authorization'); return next(UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}))
} }
req.integration = integration; req.integration = integration;
@@ -56,13 +55,6 @@ const requireIntegrationAuth = ({
}); });
return next(); return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed integration authorization'
});
}
}; };
}; };
@@ -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,7 +23,6 @@ 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({
@@ -32,7 +32,7 @@ const requireIntegrationAuthorizationAuth = ({
); );
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({
@@ -50,13 +50,6 @@ const requireIntegrationAuthorizationAuth = ({
} }
return next(); return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed (authorization) integration authorizationt'
});
}
}; };
}; };
@@ -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 (!membershipOrg) {
return next(UnauthorizedRequestError({message: "You're not a member of this Organization."}))
}
//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)) { if (!acceptedRoles.includes(membershipOrg.role)) {
throw new Error('Failed to validate organization membership role'); return next(ValidationError({message: 'Failed to validate Organization Membership Role'}))
} }
if (!acceptedStatuses.includes(membershipOrg.status)) { if (!acceptedStatuses.includes(membershipOrg.status)) {
throw new Error('Failed to validate organization membership status'); return next(ValidationError({message: 'Failed to validate Organization Membership Status'}))
} }
req.membershipOrg = membershipOrg; req.membershipOrg = membershipOrg;
return next(); return next();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(401).send({
error: 'Failed organization authorization'
});
}
}; };
}; };
@@ -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,14 +24,15 @@ const requireServiceTokenAuth = async (
next: NextFunction next: NextFunction
) => { ) => {
// JWT service token middleware // JWT service token middleware
try {
if (!req.headers?.authorization)
throw new Error('Failed to locate authorization header');
const token = req.headers.authorization.split(' ')[1]; const [ AUTH_TOKEN_TYPE, AUTH_TOKEN_VALUE ] = <[string, string]>req.headers['authorization']?.split(' ', 2) ?? [null, null]
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 decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(token, JWT_SERVICE_SECRET) jwt.verify(AUTH_TOKEN_VALUE, JWT_SERVICE_SECRET)
); );
const serviceToken = await ServiceToken.findOne({ const serviceToken = await ServiceToken.findOne({
@@ -40,17 +41,10 @@ const requireServiceTokenAuth = async (
.populate('user', '+publicKey') .populate('user', '+publicKey')
.select('+encryptedKey +publicKey +nonce'); .select('+encryptedKey +publicKey +nonce');
if (!serviceToken) throw new Error('Failed to find service token'); if (!serviceToken) return next(UnauthorizedRequestError({message: 'The service token does not match the record in the database'}))
req.serviceToken = serviceToken; req.serviceToken = serviceToken;
return next(); 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;
+7 -15
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,13 +21,13 @@ 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 token = req.headers.authorization.split(' ')[1];
const decodedToken = <jwt.UserIDJwtPayload>( const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(token, JWT_SIGNUP_SECRET) jwt.verify(AUTH_TOKEN_VALUE, JWT_SIGNUP_SECRET)
); );
const user = await User.findOne({ const user = await User.findOne({
@@ -35,18 +35,10 @@ const requireSignupAuth = async (
}).select('+publicKey'); }).select('+publicKey');
if (!user) if (!user)
throw new Error('Failed to temporarily authenticate unfound user'); return next(UnauthorizedRequestError({message: 'Unable to authenticate for User account completion. Try logging in again'}))
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 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"
});
} }
}; };
+13 -7
View File
@@ -6,7 +6,8 @@ import {
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 {
@@ -18,7 +19,7 @@ export interface IIntegration {
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;
} }
@@ -38,19 +39,23 @@ const integrationSchema = new Schema<IIntegration>(
type: Boolean, type: Boolean,
required: true required: true
}, },
app: { // name of app in provider app: {
// name of app in provider
type: String, type: String,
default: null default: null
}, },
target: { // vercel-specific target (environment) target: {
// vercel-specific target (environment)
type: String, type: String,
default: null default: null
}, },
context: { // netlify-specific context (deploy) context: {
// netlify-specific context (deploy)
type: String, type: String,
default: null default: null
}, },
siteId: { // netlify-specific site (app) id siteId: {
// netlify-specific site (app) id
type: String, type: String,
default: null default: null
}, },
@@ -59,7 +64,8 @@ const integrationSchema = new Schema<IIntegration>(
enum: [ enum: [
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY INTEGRATION_NETLIFY,
INTEGRATION_GITHUB
], ],
required: true required: true
}, },
+9 -5
View File
@@ -2,13 +2,14 @@ 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;
@@ -31,14 +32,17 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
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 teamId: {
// vercel-specific integration param
type: String type: String
}, },
accountId: { // netlify-specific integration param accountId: {
// netlify-specific integration param
type: String type: String
}, },
refreshCiphertext: { refreshCiphertext: {
+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