mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 18:29:07 +00:00
Fix merge conflicts
This commit is contained in:
+2
-3
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"plugins": ["@typescript-eslint", "prettier"],
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/eslint-recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"prettier"
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"rules": {
|
||||
"no-console": 2
|
||||
|
||||
+2
-2
@@ -4,12 +4,12 @@ WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json ./
|
||||
|
||||
RUN npm ci --only-production
|
||||
RUN npm ci --only-production --ignore-scripts
|
||||
|
||||
COPY . .
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=3s --start-period=10s \
|
||||
CMD node healthcheck.js
|
||||
CMD node healthcheck.js
|
||||
|
||||
|
||||
CMD ["npm", "run", "start"]
|
||||
|
||||
Vendored
+6
-2
@@ -14,8 +14,12 @@ declare global {
|
||||
JWT_SIGNUP_SECRET: string;
|
||||
MONGO_URL: string;
|
||||
NODE_ENV: 'development' | 'staging' | 'testing' | 'production';
|
||||
OAUTH_CLIENT_SECRET_HEROKU: string;
|
||||
OAUTH_TOKEN_URL_HEROKU: string;
|
||||
CLIENT_ID_HEROKU: string;
|
||||
CLIENT_ID_VERCEL: string;
|
||||
CLIENT_ID_NETLIFY: string;
|
||||
CLIENT_SECRET_HEROKU: string;
|
||||
CLIENT_SECRET_VERCEL: string;
|
||||
CLIENT_SECRET_NETLIFY: string;
|
||||
POSTHOG_HOST: string;
|
||||
POSTHOG_PROJECT_API_KEY: string;
|
||||
PRIVATE_KEY: string;
|
||||
|
||||
Generated
+7
-123
@@ -49,13 +49,10 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.3.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm": "^8.19.3",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
},
|
||||
@@ -2918,12 +2915,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@@ -3770,6 +3761,7 @@
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
@@ -4266,39 +4258,6 @@
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-config-prettier": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
|
||||
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"eslint-config-prettier": "bin/cli.js"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-prettier": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
||||
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=7.28.0",
|
||||
"prettier": ">=2.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint-config-prettier": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
@@ -4616,12 +4575,6 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-diff": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
@@ -5015,6 +4968,7 @@
|
||||
"minimist": "^1.2.5",
|
||||
"neo-async": "^2.6.0",
|
||||
"source-map": "^0.6.1",
|
||||
"uglify-js": "^3.1.4",
|
||||
"wordwrap": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
@@ -5664,6 +5618,7 @@
|
||||
"@types/node": "*",
|
||||
"anymatch": "^3.0.3",
|
||||
"fb-watchman": "^2.0.0",
|
||||
"fsevents": "^2.3.2",
|
||||
"graceful-fs": "^4.2.9",
|
||||
"jest-regex-util": "^29.2.0",
|
||||
"jest-util": "^29.3.1",
|
||||
@@ -5880,7 +5835,6 @@
|
||||
"@jest/transform": "^29.3.1",
|
||||
"@jest/types": "^29.3.1",
|
||||
"@types/babel__traverse": "^7.0.6",
|
||||
"@types/prettier": "^2.1.5",
|
||||
"babel-preset-current-node-syntax": "^1.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"expect": "^29.3.1",
|
||||
@@ -6455,9 +6409,11 @@
|
||||
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-4.11.0.tgz",
|
||||
"integrity": "sha512-9l9n4Nk2BYZzljW3vHah3Z0rfS5npKw6ktnkmFgTcnzaXH1DRm3pDl6VMHu84EVb1lzmSaJC4OzWZqTkB5i2wg==",
|
||||
"dependencies": {
|
||||
"@aws-sdk/credential-providers": "^3.186.0",
|
||||
"bson": "^4.7.0",
|
||||
"denque": "^2.1.0",
|
||||
"mongodb-connection-string-url": "^2.5.4",
|
||||
"saslprep": "^1.0.3",
|
||||
"socks": "^2.7.1"
|
||||
},
|
||||
"engines": {
|
||||
@@ -6649,9 +6605,6 @@
|
||||
},
|
||||
"bin": {
|
||||
"nopt": "bin/nopt.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
@@ -7505,6 +7458,7 @@
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@colors/colors": "1.5.0",
|
||||
"string-width": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -8300,6 +8254,7 @@
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"encoding": "^0.1.13",
|
||||
"minipass": "^3.1.6",
|
||||
"minipass-sized": "^1.0.3",
|
||||
"minizlib": "^2.1.2"
|
||||
@@ -9673,33 +9628,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"prettier": "bin-prettier.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.13.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier-linter-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fast-diff": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pretty-format": {
|
||||
"version": "29.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
||||
@@ -13508,12 +13436,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/qs": {
|
||||
"version": "6.9.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz",
|
||||
@@ -14504,22 +14426,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-config-prettier": {
|
||||
"version": "8.5.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz",
|
||||
"integrity": "sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==",
|
||||
"dev": true,
|
||||
"requires": {}
|
||||
},
|
||||
"eslint-plugin-prettier": {
|
||||
"version": "4.2.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz",
|
||||
"integrity": "sha512-f/0rXLXUt0oFYs8ra4w49wYZBG5GKZpAYsJSm6rnYL5uVDjd+zowwMwVZHnAjf4edNrKpCDYfXDgmRE/Ak7QyQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"prettier-linter-helpers": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz",
|
||||
@@ -14742,12 +14648,6 @@
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-diff": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz",
|
||||
"integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==",
|
||||
"dev": true
|
||||
},
|
||||
"fast-glob": {
|
||||
"version": "3.2.12",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz",
|
||||
@@ -15672,7 +15572,6 @@
|
||||
"@jest/transform": "^29.3.1",
|
||||
"@jest/types": "^29.3.1",
|
||||
"@types/babel__traverse": "^7.0.6",
|
||||
"@types/prettier": "^2.1.5",
|
||||
"babel-preset-current-node-syntax": "^1.0.0",
|
||||
"chalk": "^4.0.0",
|
||||
"expect": "^29.3.1",
|
||||
@@ -18315,21 +18214,6 @@
|
||||
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier": {
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.7.1.tgz",
|
||||
"integrity": "sha512-ujppO+MkdPqoVINuDFDRLClm7D78qbDt0/NR+wp5FqEZOoTNAjPHWj17QRhu7geIHJfcNhRk1XVQmF8Bp3ye+g==",
|
||||
"dev": true
|
||||
},
|
||||
"prettier-linter-helpers": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz",
|
||||
"integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fast-diff": "^1.1.2"
|
||||
}
|
||||
},
|
||||
"pretty-format": {
|
||||
"version": "29.3.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz",
|
||||
|
||||
@@ -31,12 +31,12 @@
|
||||
"version": "1.0.0",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"prepare": "cd .. && npm install",
|
||||
"start": "npm run build && node build/index.js",
|
||||
"dev": "nodemon",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./src/json ./build",
|
||||
"build": "rimraf ./build && tsc && cp -R ./src/templates ./build",
|
||||
"lint": "eslint . --ext .ts",
|
||||
"lint-and-fix": "eslint . --ext .ts --fix",
|
||||
"prettier-format": "prettier --config .prettierrc 'src/**/*.ts' --write",
|
||||
"lint-staged": "lint-staged"
|
||||
},
|
||||
"repository": {
|
||||
@@ -63,13 +63,10 @@
|
||||
"@typescript-eslint/eslint-plugin": "^5.40.1",
|
||||
"@typescript-eslint/parser": "^5.40.1",
|
||||
"eslint": "^8.26.0",
|
||||
"eslint-config-prettier": "^8.5.0",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"install": "^0.13.0",
|
||||
"jest": "^29.3.1",
|
||||
"nodemon": "^2.0.19",
|
||||
"npm": "^8.19.3",
|
||||
"prettier": "^2.7.1",
|
||||
"ts-node": "^10.9.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,8 +10,13 @@ const JWT_SIGNUP_LIFETIME = process.env.JWT_SIGNUP_LIFETIME! || '15m';
|
||||
const JWT_SIGNUP_SECRET = process.env.JWT_SIGNUP_SECRET!;
|
||||
const MONGO_URL = process.env.MONGO_URL!;
|
||||
const NODE_ENV = process.env.NODE_ENV! || 'production';
|
||||
const OAUTH_CLIENT_SECRET_HEROKU = process.env.OAUTH_CLIENT_SECRET_HEROKU!;
|
||||
const OAUTH_TOKEN_URL_HEROKU = process.env.OAUTH_TOKEN_URL_HEROKU!;
|
||||
const CLIENT_SECRET_HEROKU = process.env.CLIENT_SECRET_HEROKU!;
|
||||
const CLIENT_ID_HEROKU = process.env.CLIENT_ID_HEROKU!;
|
||||
const CLIENT_ID_VERCEL = process.env.CLIENT_ID_VERCEL!;
|
||||
const CLIENT_ID_NETLIFY = process.env.CLIENT_ID_NETLIFY!;
|
||||
const CLIENT_SECRET_VERCEL = process.env.CLIENT_SECRET_VERCEL!;
|
||||
const CLIENT_SECRET_NETLIFY = process.env.CLIENT_SECRET_NETLIFY!;
|
||||
const CLIENT_SLUG_VERCEL= process.env.CLIENT_SLUG_VERCEL!;
|
||||
const POSTHOG_HOST = process.env.POSTHOG_HOST! || 'https://app.posthog.com';
|
||||
const POSTHOG_PROJECT_API_KEY =
|
||||
process.env.POSTHOG_PROJECT_API_KEY! ||
|
||||
@@ -46,8 +51,13 @@ export {
|
||||
JWT_SIGNUP_SECRET,
|
||||
MONGO_URL,
|
||||
NODE_ENV,
|
||||
OAUTH_CLIENT_SECRET_HEROKU,
|
||||
OAUTH_TOKEN_URL_HEROKU,
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY,
|
||||
CLIENT_SLUG_VERCEL,
|
||||
POSTHOG_HOST,
|
||||
POSTHOG_PROJECT_API_KEY,
|
||||
PRIVATE_KEY,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Bot, BotKey } from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
|
||||
interface BotKey {
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||
* then create and return a new bot.
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||
let bot;
|
||||
try {
|
||||
const { workspaceId } = req.params;
|
||||
|
||||
bot = await Bot.findOne({
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!bot) {
|
||||
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||
// -> create a new bot and return it
|
||||
bot = await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get bot for workspace'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Return bot with id [req.bot._id] with active state set to [isActive].
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||
let bot;
|
||||
try {
|
||||
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||
|
||||
if (isActive) {
|
||||
// bot state set to active -> share workspace key with bot
|
||||
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||
return res.status(400).send({
|
||||
message: 'Failed to set bot state to active - missing bot key'
|
||||
});
|
||||
}
|
||||
|
||||
await BotKey.findOneAndUpdate({
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
encryptedKey: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
sender: req.user._id,
|
||||
bot: req.bot._id,
|
||||
workspace: req.bot.workspace
|
||||
}, {
|
||||
upsert: true,
|
||||
new: true
|
||||
});
|
||||
} else {
|
||||
// case: bot state set to inactive -> delete bot's workspace key
|
||||
await BotKey.deleteOne({
|
||||
bot: req.bot._id
|
||||
});
|
||||
}
|
||||
|
||||
bot = await Bot.findOneAndUpdate({
|
||||
_id: req.bot._id
|
||||
}, {
|
||||
isActive
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Failed to update bot active state');
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to update bot active state'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
bot
|
||||
});
|
||||
};
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as authController from './authController';
|
||||
import * as botController from './botController';
|
||||
import * as integrationAuthController from './integrationAuthController';
|
||||
import * as integrationController from './integrationController';
|
||||
import * as keyController from './keyController';
|
||||
@@ -17,6 +18,7 @@ import * as logController from './logController';
|
||||
|
||||
export {
|
||||
authController,
|
||||
botController,
|
||||
integrationAuthController,
|
||||
integrationController,
|
||||
keyController,
|
||||
|
||||
@@ -3,69 +3,45 @@ import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { readFileSync } from 'fs';
|
||||
import { IntegrationAuth, Integration } from '../models';
|
||||
import { processOAuthTokenRes } from '../helpers/integrationAuth';
|
||||
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
||||
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
||||
import { INTEGRATION_SET, INTEGRATION_OPTIONS, ENV_DEV } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
import { getApps, revokeAccess } from '../integrations';
|
||||
|
||||
export const getIntegrationOptions = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
return res.status(200).send({
|
||||
integrationOptions: INTEGRATION_OPTIONS
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const integrationAuthOauthExchange = async (
|
||||
export const oAuthExchange = async (
|
||||
req: Request,
|
||||
res: Response
|
||||
) => {
|
||||
try {
|
||||
let clientSecret;
|
||||
|
||||
const { workspaceId, code, integration } = req.body;
|
||||
|
||||
if (!INTEGRATION_SET.has(integration))
|
||||
throw new Error('Failed to validate integration');
|
||||
|
||||
// use correct client secret
|
||||
switch (integration) {
|
||||
case 'heroku':
|
||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
||||
}
|
||||
|
||||
// TODO: unfinished - make compatible with other integration types
|
||||
const res = await axios.post(
|
||||
OAUTH_TOKEN_URL_HEROKU!,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_secret: clientSecret
|
||||
} as any)
|
||||
);
|
||||
|
||||
const integrationAuth = await processOAuthTokenRes({
|
||||
|
||||
await IntegrationService.handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
res
|
||||
code
|
||||
});
|
||||
|
||||
// create or replace integration
|
||||
const integrationObj = await Integration.findOneAndUpdate(
|
||||
{ workspace: workspaceId, integration },
|
||||
{
|
||||
workspace: workspaceId,
|
||||
environment: ENV_DEV,
|
||||
isActive: false,
|
||||
app: null,
|
||||
integration,
|
||||
integrationAuth: integrationAuth._id
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get OAuth2 token'
|
||||
message: 'Failed to get OAuth2 code-token exchange'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,26 +51,25 @@ export const integrationAuthOauthExchange = async (
|
||||
};
|
||||
|
||||
/**
|
||||
* Return list of applications allowed for integration with id [integrationAuthId]
|
||||
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
// TODO: unfinished - make compatible with other integration types
|
||||
let apps;
|
||||
try {
|
||||
const res = await axios.get('https://api.heroku.com/apps', {
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: 'Bearer ' + req.accessToken
|
||||
}
|
||||
apps = await getApps({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
|
||||
apps = res.data.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
} catch (err) {}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get integration authorization applications'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
apps
|
||||
@@ -108,46 +83,22 @@ export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||
* @returns
|
||||
*/
|
||||
export const deleteIntegrationAuth = async (req: Request, res: Response) => {
|
||||
// TODO: unfinished - disable application via Heroku API and make compatible with other integration types
|
||||
try {
|
||||
const { integrationAuthId } = req.params;
|
||||
|
||||
// TODO: disable application via Heroku API; figure out what authorization id is
|
||||
|
||||
const integrations = JSON.parse(
|
||||
readFileSync('./src/json/integrations.json').toString()
|
||||
);
|
||||
|
||||
let authorizationId;
|
||||
switch (req.integrationAuth.integration) {
|
||||
case 'heroku':
|
||||
authorizationId = integrations.heroku.clientId;
|
||||
}
|
||||
|
||||
// not sure what authorizationId is?
|
||||
// // revoke authorization
|
||||
// const res2 = await axios.delete(
|
||||
// `https://api.heroku.com/oauth/authorizations/${authorizationId}`,
|
||||
// {
|
||||
// headers: {
|
||||
// 'Accept': 'application/vnd.heroku+json; version=3',
|
||||
// 'Authorization': 'Bearer ' + req.accessToken
|
||||
// }
|
||||
// }
|
||||
// );
|
||||
|
||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuthId
|
||||
await revokeAccess({
|
||||
integrationAuth: req.integrationAuth,
|
||||
accessToken: req.accessToken
|
||||
});
|
||||
|
||||
if (deletedIntegrationAuth) {
|
||||
await Integration.deleteMany({
|
||||
integrationAuth: deletedIntegrationAuth._id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to delete integration authorization'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully deleted integration authorization'
|
||||
});
|
||||
}
|
||||
@@ -1,11 +1,9 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { Integration } from '../models';
|
||||
import { decryptAsymmetric } from '../utils/crypto';
|
||||
import { decryptSecrets } from '../helpers/secret';
|
||||
import { PRIVATE_KEY } from '../config';
|
||||
import { Integration, Bot, BotKey } from '../models';
|
||||
import { EventService } from '../services';
|
||||
import { eventPushSecrets } from '../events';
|
||||
|
||||
interface Key {
|
||||
encryptedKey: string;
|
||||
@@ -24,104 +22,58 @@ interface PushSecret {
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of all available integrations on Infisical
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const getIntegrations = async (req: Request, res: Response) => {
|
||||
let integrations;
|
||||
try {
|
||||
integrations = JSON.parse(
|
||||
readFileSync('./src/json/integrations.json').toString()
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to get integrations'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
integrations
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync secrets [secrets] to integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const syncIntegration = async (req: Request, res: Response) => {
|
||||
// TODO: unfinished - make more versatile to accomodate for other integrations
|
||||
try {
|
||||
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
||||
const symmetricKey = decryptAsymmetric({
|
||||
ciphertext: key.encryptedKey,
|
||||
nonce: key.nonce,
|
||||
publicKey: req.user.publicKey,
|
||||
privateKey: PRIVATE_KEY
|
||||
});
|
||||
|
||||
// decrypt secrets with symmetric key
|
||||
const content = decryptSecrets({
|
||||
secrets,
|
||||
key: symmetricKey,
|
||||
format: 'object'
|
||||
});
|
||||
|
||||
// TODO: make integration work for other integrations as well
|
||||
const res = await axios.patch(
|
||||
`https://api.heroku.com/apps/${req.integration.app}/config-vars`,
|
||||
content,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: 'Bearer ' + req.accessToken
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to sync secrets with integration'
|
||||
});
|
||||
}
|
||||
|
||||
return res.status(200).send({
|
||||
message: 'Successfully synced secrets with integration'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Change environment or name of integration with id [integrationId]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
export const modifyIntegration = async (req: Request, res: Response) => {
|
||||
export const updateIntegration = async (req: Request, res: Response) => {
|
||||
let integration;
|
||||
|
||||
// TODO: add integration-specific validation to ensure that each
|
||||
// integration has the correct fields populated in [Integration]
|
||||
|
||||
try {
|
||||
const { update } = req.body;
|
||||
|
||||
const {
|
||||
app,
|
||||
environment,
|
||||
isActive,
|
||||
target, // vercel-specific integration param
|
||||
context, // netlify-specific integration param
|
||||
siteId // netlify-specific integration param
|
||||
} = req.body;
|
||||
|
||||
integration = await Integration.findOneAndUpdate(
|
||||
{
|
||||
_id: req.integration._id
|
||||
},
|
||||
update,
|
||||
{
|
||||
environment,
|
||||
isActive,
|
||||
app,
|
||||
target,
|
||||
context,
|
||||
siteId
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
|
||||
if (integration) {
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId: integration.workspace.toString()
|
||||
})
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
return res.status(400).send({
|
||||
message: 'Failed to modify integration'
|
||||
message: 'Failed to update integration'
|
||||
});
|
||||
}
|
||||
|
||||
@@ -131,7 +83,8 @@ export const modifyIntegration = async (req: Request, res: Response) => {
|
||||
};
|
||||
|
||||
/**
|
||||
* Delete integration with id [integrationId]
|
||||
* Delete integration with id [integrationId] and deactivate bot if there are
|
||||
* no integrations left
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
@@ -144,6 +97,29 @@ export const deleteIntegration = async (req: Request, res: Response) => {
|
||||
deletedIntegration = await Integration.findOneAndDelete({
|
||||
_id: integrationId
|
||||
});
|
||||
|
||||
if (!deletedIntegration) throw new Error('Failed to find integration');
|
||||
|
||||
const integrations = await Integration.find({
|
||||
workspace: deletedIntegration.workspace
|
||||
});
|
||||
|
||||
if (integrations.length === 0) {
|
||||
// case: no integrations left, deactivate bot
|
||||
const bot = await Bot.findOneAndUpdate({
|
||||
workspace: deletedIntegration.workspace
|
||||
}, {
|
||||
isActive: false
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
|
||||
if (bot) {
|
||||
await BotKey.deleteOne({
|
||||
bot: bot._id
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
|
||||
@@ -17,16 +17,6 @@ export const uploadKey = async (req: Request, res: Response) => {
|
||||
const { workspaceId } = req.params;
|
||||
const { key } = req.body;
|
||||
|
||||
// validate membership of sender
|
||||
const senderMembership = await findMembership({
|
||||
user: req.user._id,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!senderMembership) {
|
||||
throw new Error('Failed sender membership validation for workspace');
|
||||
}
|
||||
|
||||
// validate membership of receiver
|
||||
const receiverMembership = await findMembership({
|
||||
user: key.userId,
|
||||
|
||||
@@ -7,8 +7,9 @@ import {
|
||||
reformatPullSecrets
|
||||
} from '../helpers/secret';
|
||||
import { pushKeys } from '../helpers/key';
|
||||
import { eventPushSecrets } from '../events';
|
||||
import { EventService } from '../services';
|
||||
import { ENV_SET } from '../variables';
|
||||
|
||||
import { postHogClient } from '../services';
|
||||
|
||||
interface PushSecret {
|
||||
@@ -60,7 +61,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
workspaceId,
|
||||
keys
|
||||
});
|
||||
|
||||
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: 'secrets pushed',
|
||||
@@ -74,6 +76,13 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
// trigger event - push secrets
|
||||
EventService.handleEvent({
|
||||
event: eventPushSecrets({
|
||||
workspaceId
|
||||
})
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser({ email: req.user.email });
|
||||
Sentry.captureException(err);
|
||||
@@ -192,7 +201,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
||||
};
|
||||
|
||||
if (postHogClient) {
|
||||
// capture secrets pushed event in production
|
||||
// capture secrets pulled event in production
|
||||
postHogClient.capture({
|
||||
distinctId: req.serviceToken.user.email,
|
||||
event: 'secrets pulled',
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
import { eventPushSecrets } from "./secret"
|
||||
|
||||
export {
|
||||
eventPushSecrets
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||
|
||||
interface PushSecret {
|
||||
ciphertextKey: string;
|
||||
ivKey: string;
|
||||
tagKey: string;
|
||||
hashKey: string;
|
||||
ciphertextValue: string;
|
||||
ivValue: string;
|
||||
tagValue: string;
|
||||
hashValue: string;
|
||||
type: 'shared' | 'personal';
|
||||
}
|
||||
|
||||
/**
|
||||
* Return event for pushing secrets
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace to push secrets to
|
||||
* @returns
|
||||
*/
|
||||
const eventPushSecrets = ({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
return ({
|
||||
name: EVENT_PUSH_SECRETS,
|
||||
workspaceId,
|
||||
payload: {
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
eventPushSecrets
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Bot,
|
||||
BotKey,
|
||||
Secret,
|
||||
ISecret,
|
||||
IUser
|
||||
} from '../models';
|
||||
import {
|
||||
generateKeyPair,
|
||||
encryptSymmetric,
|
||||
decryptSymmetric,
|
||||
decryptAsymmetric
|
||||
} from '../utils/crypto';
|
||||
import { decryptSecrets } from '../helpers/secret';
|
||||
import { ENCRYPTION_KEY } from '../config';
|
||||
import { SECRET_SHARED } from '../variables';
|
||||
|
||||
/**
|
||||
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.name - name of bot
|
||||
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||
*/
|
||||
const createBot = async ({
|
||||
name,
|
||||
workspaceId,
|
||||
}: {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
let bot;
|
||||
try {
|
||||
const { publicKey, privateKey } = generateKeyPair();
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext: privateKey,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
bot = await new Bot({
|
||||
name,
|
||||
workspace: workspaceId,
|
||||
isActive: false,
|
||||
publicKey,
|
||||
encryptedPrivateKey: ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to create bot');
|
||||
}
|
||||
|
||||
return bot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId]
|
||||
* and [environment] using bot
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.environment - environment
|
||||
*/
|
||||
const getSecretsHelper = async ({
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
}) => {
|
||||
const content = {} as any;
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const secrets = await Secret.find({
|
||||
workspaceId,
|
||||
environment,
|
||||
type: SECRET_SHARED
|
||||
});
|
||||
|
||||
secrets.forEach((secret: ISecret) => {
|
||||
const secretKey = decryptSymmetric({
|
||||
ciphertext: secret.secretKeyCiphertext,
|
||||
iv: secret.secretKeyIV,
|
||||
tag: secret.secretKeyTag,
|
||||
key
|
||||
});
|
||||
|
||||
const secretValue = decryptSymmetric({
|
||||
ciphertext: secret.secretValueCiphertext,
|
||||
iv: secret.secretValueIV,
|
||||
tag: secret.secretValueTag,
|
||||
key
|
||||
});
|
||||
|
||||
content[secretKey] = secretValue;
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get secrets');
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return bot's copy of the workspace key for workspace
|
||||
* with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @returns {String} key - decrypted workspace key
|
||||
*/
|
||||
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||
let key;
|
||||
try {
|
||||
const botKey = await BotKey.findOne({
|
||||
workspace: workspaceId
|
||||
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
||||
|
||||
if (!botKey) throw new Error('Failed to find bot key');
|
||||
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId
|
||||
}).select('+encryptedPrivateKey +iv +tag');
|
||||
|
||||
if (!bot) throw new Error('Failed to find bot');
|
||||
if (!bot.isActive) throw new Error('Bot is not active');
|
||||
|
||||
const privateKeyBot = decryptSymmetric({
|
||||
ciphertext: bot.encryptedPrivateKey,
|
||||
iv: bot.iv,
|
||||
tag: bot.tag,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
key = decryptAsymmetric({
|
||||
ciphertext: botKey.encryptedKey,
|
||||
nonce: botKey.nonce,
|
||||
publicKey: botKey.sender.publicKey as string,
|
||||
privateKey: privateKeyBot
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get workspace key');
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return symmetrically encrypted [plaintext] using the
|
||||
* key for workspace with id [workspaceId]
|
||||
* @param {Object} obj1
|
||||
* @param {String} obj1.workspaceId - id of workspace
|
||||
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||
*/
|
||||
const encryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
plaintext
|
||||
}: {
|
||||
workspaceId: string;
|
||||
plaintext: string;
|
||||
}) => {
|
||||
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||
plaintext,
|
||||
key
|
||||
});
|
||||
|
||||
return ({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric encryption with bot');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Return symmetrically decrypted [ciphertext] using the
|
||||
* key for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
const decryptSymmetricHelper = async ({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) => {
|
||||
let plaintext;
|
||||
try {
|
||||
const key = await getKey({ workspaceId });
|
||||
const plaintext = decryptSymmetric({
|
||||
ciphertext,
|
||||
iv,
|
||||
tag,
|
||||
key
|
||||
});
|
||||
|
||||
return plaintext;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric decryption with bot');
|
||||
}
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
export {
|
||||
createBot,
|
||||
getSecretsHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Bot, IBot } from '../models';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
|
||||
interface Event {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event [event]
|
||||
* @param {Object} obj
|
||||
* @param {Event} obj.event - an event
|
||||
* @param {String} obj.event.name - name of event
|
||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||
*/
|
||||
const handleEventHelper = async ({
|
||||
event
|
||||
}: {
|
||||
event: Event;
|
||||
}) => {
|
||||
const { workspaceId } = event;
|
||||
|
||||
// TODO: moduralize bot check into separate function
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) return;
|
||||
|
||||
try {
|
||||
switch (event.name) {
|
||||
case EVENT_PUSH_SECRETS:
|
||||
IntegrationService.syncIntegrations({
|
||||
workspaceId
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
handleEventHelper
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Bot,
|
||||
Integration,
|
||||
IIntegration,
|
||||
IntegrationAuth,
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||
import { BotService, IntegrationService } from '../services';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
interface Update {
|
||||
workspace: string;
|
||||
integration: string;
|
||||
teamId?: string;
|
||||
accountId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||
* named [integration]
|
||||
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||
* - Add placeholder inactive integration
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
*/
|
||||
const handleOAuthExchangeHelper = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
}) => {
|
||||
let action;
|
||||
let integrationAuth;
|
||||
try {
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
||||
|
||||
// exchange code for access and refresh tokens
|
||||
const res = await exchangeCode({
|
||||
integration,
|
||||
code
|
||||
});
|
||||
|
||||
const update: Update = {
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}
|
||||
|
||||
switch (integration) {
|
||||
case INTEGRATION_VERCEL:
|
||||
update.teamId = res.teamId;
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
update.accountId = res.accountId;
|
||||
break;
|
||||
}
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
workspace: workspaceId,
|
||||
integration
|
||||
}, update, {
|
||||
new: true,
|
||||
upsert: true
|
||||
});
|
||||
|
||||
if (res.refreshToken) {
|
||||
// case: refresh token returned from exchange
|
||||
// set integration auth refresh token
|
||||
await setIntegrationAuthRefreshHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
refreshToken: res.refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
if (res.accessToken) {
|
||||
// case: access token returned from exchange
|
||||
// set integration auth access token
|
||||
await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integrationAuth._id.toString(),
|
||||
accessToken: res.accessToken,
|
||||
accessExpiresAt: res.accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
// initialize new integration after exchange
|
||||
await new Integration({
|
||||
workspace: workspaceId,
|
||||
environment: ENV_DEV,
|
||||
isActive: false,
|
||||
app: null,
|
||||
integration,
|
||||
integrationAuth: integrationAuth._id
|
||||
}).save();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||
* all active integrations for that workspace
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.workspaceId - id of workspace
|
||||
*/
|
||||
const syncIntegrationsHelper = async ({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) => {
|
||||
let integrations;
|
||||
try {
|
||||
|
||||
integrations = await Integration.find({
|
||||
workspace: workspaceId,
|
||||
isActive: true,
|
||||
app: { $ne: null }
|
||||
});
|
||||
|
||||
// for each workspace integration, sync/push secrets
|
||||
// to that integration
|
||||
for await (const integration of integrations) {
|
||||
// get workspace, environment (shared) secrets
|
||||
const secrets = await BotService.getSecrets({ // issue here?
|
||||
workspaceId: integration.workspace.toString(),
|
||||
environment: integration.environment
|
||||
});
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
// get integration auth access token
|
||||
const accessToken = await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId: integration.integrationAuth.toString()
|
||||
});
|
||||
|
||||
// sync secrets to integration
|
||||
await syncSecrets({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to integrations');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted refresh token using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} refreshToken - decrypted refresh token
|
||||
*/
|
||||
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||
let refreshToken;
|
||||
try {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
refreshToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||
iv: integrationAuth.refreshIV as string,
|
||||
tag: integrationAuth.refreshTag as string
|
||||
});
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration refresh token');
|
||||
}
|
||||
|
||||
return refreshToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted access token using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @returns {String} accessToken - decrypted access token
|
||||
*/
|
||||
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||
let accessToken;
|
||||
try {
|
||||
const integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId)
|
||||
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
accessToken = await BotService.decryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
ciphertext: integrationAuth.accessCiphertext as string,
|
||||
iv: integrationAuth.accessIV as string,
|
||||
tag: integrationAuth.accessTag as string
|
||||
});
|
||||
|
||||
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
|
||||
// there is a access token expiration date
|
||||
// and refresh token to exchange with the OAuth2 server
|
||||
|
||||
if (integrationAuth.accessExpiresAt < new Date()) {
|
||||
// access token is expired
|
||||
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
|
||||
accessToken = await exchangeRefresh({
|
||||
integration: integrationAuth.integration,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId] and store it
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.refreshToken - refresh token
|
||||
*/
|
||||
const setIntegrationAuthRefreshHelper = async ({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
|
||||
let integrationAuth;
|
||||
try {
|
||||
integrationAuth = await IntegrationAuth
|
||||
.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: refreshToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
refreshCiphertext: obj.ciphertext,
|
||||
refreshIV: obj.iv,
|
||||
refreshTag: obj.tag
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to set integration auth refresh token');
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt access token [accessToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId] and store it along with [accessExpiresAt]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.accessToken - access token
|
||||
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||
*/
|
||||
const setIntegrationAuthAccessHelper = async ({
|
||||
integrationAuthId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}) => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||
|
||||
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||
|
||||
const obj = await BotService.encryptSymmetric({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
plaintext: accessToken
|
||||
});
|
||||
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||
_id: integrationAuthId
|
||||
}, {
|
||||
accessCiphertext: obj.ciphertext,
|
||||
accessIV: obj.iv,
|
||||
accessTag: obj.tag,
|
||||
accessExpiresAt
|
||||
}, {
|
||||
new: true
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to save integration auth access token');
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
}
|
||||
|
||||
export {
|
||||
handleOAuthExchangeHelper,
|
||||
syncIntegrationsHelper,
|
||||
getIntegrationAuthRefreshHelper,
|
||||
getIntegrationAuthAccessHelper,
|
||||
setIntegrationAuthRefreshHelper,
|
||||
setIntegrationAuthAccessHelper
|
||||
}
|
||||
@@ -1,174 +0,0 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import axios from 'axios';
|
||||
import { IntegrationAuth } from '../models';
|
||||
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
|
||||
import { IIntegrationAuth } from '../models';
|
||||
import {
|
||||
ENCRYPTION_KEY,
|
||||
OAUTH_CLIENT_SECRET_HEROKU,
|
||||
OAUTH_TOKEN_URL_HEROKU
|
||||
} from '../config';
|
||||
|
||||
/**
|
||||
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
||||
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
||||
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.integration - name of integration (e.g. heroku)
|
||||
* @param {Object} obj.res - response from OAuth2 authorization server
|
||||
*/
|
||||
const processOAuthTokenRes = async ({
|
||||
workspaceId,
|
||||
integration,
|
||||
res
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
res: any;
|
||||
}): Promise<IIntegrationAuth> => {
|
||||
let integrationAuth;
|
||||
try {
|
||||
// encrypt refresh + access tokens
|
||||
const {
|
||||
ciphertext: refreshCiphertext,
|
||||
iv: refreshIV,
|
||||
tag: refreshTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: res.data.refresh_token,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
const {
|
||||
ciphertext: accessCiphertext,
|
||||
iv: accessIV,
|
||||
tag: accessTag
|
||||
} = encryptSymmetric({
|
||||
plaintext: res.data.access_token,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
// compute access token expiration date
|
||||
const accessExpiresAt = new Date();
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
||||
);
|
||||
|
||||
// create or replace integration authorization with encrypted tokens
|
||||
// and access token expiration date
|
||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
||||
{ workspace: workspaceId, integration },
|
||||
{
|
||||
workspace: workspaceId,
|
||||
integration,
|
||||
refreshCiphertext,
|
||||
refreshIV,
|
||||
refreshTag,
|
||||
accessCiphertext,
|
||||
accessIV,
|
||||
accessTag,
|
||||
accessExpiresAt
|
||||
},
|
||||
{ upsert: true, new: true }
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error(
|
||||
'Failed to process OAuth2 authorization server token response'
|
||||
);
|
||||
}
|
||||
|
||||
return integrationAuth;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
||||
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
||||
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
|
||||
* @param {Object} obj
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
|
||||
* @returns {String} access token - new access token
|
||||
*/
|
||||
const getOAuthAccessToken = async ({
|
||||
integrationAuth
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
}) => {
|
||||
let accessToken;
|
||||
try {
|
||||
const {
|
||||
refreshCiphertext,
|
||||
refreshIV,
|
||||
refreshTag,
|
||||
accessCiphertext,
|
||||
accessIV,
|
||||
accessTag,
|
||||
accessExpiresAt
|
||||
} = integrationAuth;
|
||||
|
||||
if (
|
||||
refreshCiphertext &&
|
||||
refreshIV &&
|
||||
refreshTag &&
|
||||
accessCiphertext &&
|
||||
accessIV &&
|
||||
accessTag &&
|
||||
accessExpiresAt
|
||||
) {
|
||||
if (accessExpiresAt < new Date()) {
|
||||
// case: access token expired
|
||||
// TODO: fetch another access token
|
||||
|
||||
let clientSecret;
|
||||
switch (integrationAuth.integration) {
|
||||
case 'heroku':
|
||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
||||
}
|
||||
|
||||
// record new access token and refresh token
|
||||
// encrypt refresh + access tokens
|
||||
const refreshToken = decryptSymmetric({
|
||||
ciphertext: refreshCiphertext,
|
||||
iv: refreshIV,
|
||||
tag: refreshTag,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
|
||||
// TODO: make route compatible with other integration types
|
||||
const res = await axios.post(
|
||||
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: clientSecret
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessToken = res.data.access_token;
|
||||
|
||||
await processOAuthTokenRes({
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
integration: integrationAuth.integration,
|
||||
res
|
||||
});
|
||||
} else {
|
||||
// case: access token still works
|
||||
accessToken = decryptSymmetric({
|
||||
ciphertext: accessCiphertext,
|
||||
iv: accessIV,
|
||||
tag: accessTag,
|
||||
key: ENCRYPTION_KEY
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get OAuth2 access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
};
|
||||
|
||||
export { processOAuthTokenRes, getOAuthAccessToken };
|
||||
|
||||
@@ -1,6 +1,51 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Membership, Key } from '../models';
|
||||
|
||||
/**
|
||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||
* and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.userId - id of user to validate
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
*/
|
||||
const validateMembership = async ({
|
||||
userId,
|
||||
workspaceId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
}: {
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
}) => {
|
||||
|
||||
let membership;
|
||||
try {
|
||||
membership = await Membership.findOne({
|
||||
user: userId,
|
||||
workspace: workspaceId
|
||||
});
|
||||
|
||||
if (!membership) throw new Error('Failed to find membership');
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate membership status');
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to validate membership');
|
||||
}
|
||||
|
||||
return membership;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return membership matching criteria specified in query [queryObj]
|
||||
* @param {Object} queryObj - query object
|
||||
@@ -97,4 +142,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
||||
return deletedMembership;
|
||||
};
|
||||
|
||||
export { addMemberships, findMembership, deleteMembership };
|
||||
export {
|
||||
validateMembership,
|
||||
addMemberships,
|
||||
findMembership,
|
||||
deleteMembership
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Workspace,
|
||||
Bot,
|
||||
Membership,
|
||||
Key,
|
||||
Secret
|
||||
} from '../models';
|
||||
import { createBot } from '../helpers/bot';
|
||||
|
||||
/**
|
||||
* Create a workspace with name [name] in organization with id [organizationId]
|
||||
* and a bot for it.
|
||||
* @param {String} name - name of workspace to create.
|
||||
* @param {String} organizationId - id of organization to create workspace in
|
||||
* @param {Object} workspace - new workspace
|
||||
@@ -21,10 +24,16 @@ const createWorkspace = async ({
|
||||
}) => {
|
||||
let workspace;
|
||||
try {
|
||||
// create workspace
|
||||
workspace = await new Workspace({
|
||||
name,
|
||||
organization: organizationId
|
||||
}).save();
|
||||
|
||||
const bot = await createBot({
|
||||
name: 'Infisical Bot',
|
||||
workspaceId: workspace._id.toString()
|
||||
});
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
@@ -43,6 +52,9 @@ const createWorkspace = async ({
|
||||
const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||
try {
|
||||
await Workspace.deleteOne({ _id: id });
|
||||
await Bot.deleteOne({
|
||||
workspace: id
|
||||
});
|
||||
await Membership.deleteMany({
|
||||
workspace: id
|
||||
});
|
||||
|
||||
+19
-17
@@ -23,23 +23,24 @@ Sentry.init({
|
||||
});
|
||||
|
||||
import {
|
||||
signup as signupRouter,
|
||||
auth as authRouter,
|
||||
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
|
||||
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 = () => {
|
||||
@@ -79,6 +80,7 @@ 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);
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Return list of names of apps for integration named [integration]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.accessToken - access token for integration
|
||||
* @returns {Object[]} apps - names of integration apps
|
||||
* @returns {String} apps.name - name of integration app
|
||||
*/
|
||||
const getApps = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
|
||||
interface App {
|
||||
name: string;
|
||||
siteId?: string;
|
||||
}
|
||||
|
||||
let apps: App[]; // TODO: add type and define payloads for apps
|
||||
try {
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
apps = await getAppsHeroku({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
apps = await getAppsVercel({
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
apps = await getAppsNetlify({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of apps for Heroku integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Heroku API
|
||||
* @returns {Object[]} apps - names of Heroku apps
|
||||
* @returns {String} apps.name - name of Heroku app
|
||||
*/
|
||||
const getAppsHeroku = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (await axios.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).data;
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Heroku integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of apps for Vercel integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.accessToken - access token for Vercel API
|
||||
* @returns {Object[]} apps - names of Vercel apps
|
||||
* @returns {String} apps.name - name of Vercel app
|
||||
*/
|
||||
const getAppsVercel = async ({
|
||||
accessToken
|
||||
}: {
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (await axios.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).data;
|
||||
|
||||
apps = res.projects.map((a: any) => ({
|
||||
name: a.name
|
||||
}));
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Vercel integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return list of names of sites for Netlify 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 getAppsNetlify = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
let apps;
|
||||
try {
|
||||
const res = (await axios.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
})).data;
|
||||
|
||||
apps = res.map((a: any) => ({
|
||||
name: a.name,
|
||||
siteId: a.site_id
|
||||
}));
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get Netlify integration apps');
|
||||
}
|
||||
|
||||
return apps;
|
||||
}
|
||||
|
||||
export {
|
||||
getApps
|
||||
}
|
||||
@@ -0,0 +1,241 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
ACTION_PUSH_TO_HEROKU
|
||||
} from '../variables';
|
||||
import {
|
||||
SITE_URL,
|
||||
CLIENT_SECRET_HEROKU,
|
||||
CLIENT_ID_VERCEL,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_SECRET_VERCEL,
|
||||
CLIENT_SECRET_NETLIFY
|
||||
} from '../config';
|
||||
|
||||
interface ExchangeCodeHerokuResponse {
|
||||
token_type: string;
|
||||
access_token: string;
|
||||
expires_in: number;
|
||||
refresh_token: string;
|
||||
user_id: string;
|
||||
session_nonce?: string;
|
||||
}
|
||||
|
||||
interface ExchangeCodeVercelResponse {
|
||||
token_type: string;
|
||||
access_token: string;
|
||||
installation_id: string;
|
||||
user_id: string;
|
||||
team_id?: string;
|
||||
}
|
||||
|
||||
interface ExchangeCodeNetlifyResponse {
|
||||
access_token: string;
|
||||
token_type: string;
|
||||
refresh_token: string;
|
||||
scope: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||
* code-token exchange for integration named [integration]
|
||||
* @param {Object} obj1
|
||||
* @param {String} obj1.integration - name of integration
|
||||
* @param {String} obj1.code - code for code-token exchange
|
||||
* @returns {Object} obj
|
||||
* @returns {String} obj.accessToken - access token for integration
|
||||
* @returns {String} obj.refreshToken - refresh token for integration
|
||||
* @returns {Date} obj.accessExpiresAt - date of expiration for access token
|
||||
* @returns {String} obj.action - integration action for bot sequence
|
||||
*/
|
||||
const exchangeCode = async ({
|
||||
integration,
|
||||
code
|
||||
}: {
|
||||
integration: string;
|
||||
code: string;
|
||||
}) => {
|
||||
let obj = {} as any;
|
||||
|
||||
try {
|
||||
switch (integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
obj = await exchangeCodeHeroku({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
obj = await exchangeCodeVercel({
|
||||
code
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
obj = await exchangeCodeNetlify({
|
||||
code
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange');
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||
* OAuth2 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 Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeHeroku = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeHerokuResponse;
|
||||
const accessExpiresAt = new Date();
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
accessExpiresAt.setSeconds(
|
||||
accessExpiresAt.getSeconds() + res.expires_in
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||
* 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 Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeVercel = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeVercelResponse;
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
code: code,
|
||||
client_id: CLIENT_ID_VERCEL,
|
||||
client_secret: CLIENT_SECRET_VERCEL,
|
||||
redirect_uri: `${SITE_URL}/vercel`
|
||||
} as any)
|
||||
)).data;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Vercel');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: null,
|
||||
accessExpiresAt: null,
|
||||
teamId: res.team_id
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
|
||||
* 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 Heroku API
|
||||
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||
*/
|
||||
const exchangeCodeNetlify = async ({
|
||||
code
|
||||
}: {
|
||||
code: string;
|
||||
}) => {
|
||||
let res: ExchangeCodeNetlifyResponse;
|
||||
let accountId;
|
||||
try {
|
||||
res = (await axios.post(
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
code: code,
|
||||
client_id: CLIENT_ID_NETLIFY,
|
||||
client_secret: CLIENT_SECRET_NETLIFY,
|
||||
redirect_uri: `${SITE_URL}/netlify`
|
||||
} as any)
|
||||
)).data;
|
||||
|
||||
const res2 = await axios.get(
|
||||
'https://api.netlify.com/api/v1/sites',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const res3 = (await axios.get(
|
||||
'https://api.netlify.com/api/v1/accounts',
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${res.access_token}`
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
accountId = res3[0].id;
|
||||
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed OAuth2 code-token exchange with Netlify');
|
||||
}
|
||||
|
||||
return ({
|
||||
accessToken: res.access_token,
|
||||
refreshToken: res.refresh_token,
|
||||
accountId
|
||||
});
|
||||
}
|
||||
|
||||
export {
|
||||
exchangeCode
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { exchangeCode } from './exchange';
|
||||
import { exchangeRefresh } from './refresh';
|
||||
import { getApps } from './apps';
|
||||
import { syncSecrets } from './sync';
|
||||
import { revokeAccess } from './revoke';
|
||||
|
||||
export {
|
||||
exchangeCode,
|
||||
exchangeRefresh,
|
||||
getApps,
|
||||
syncSecrets,
|
||||
revokeAccess
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { INTEGRATION_HEROKU } from '../variables';
|
||||
import {
|
||||
CLIENT_SECRET_HEROKU
|
||||
} from '../config';
|
||||
import {
|
||||
INTEGRATION_HEROKU_TOKEN_URL
|
||||
} from '../variables';
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||
* named [integration]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||
*/
|
||||
const exchangeRefresh = async ({
|
||||
integration,
|
||||
refreshToken
|
||||
}: {
|
||||
integration: string;
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
let accessToken;
|
||||
try {
|
||||
switch (integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
accessToken = await exchangeRefreshHeroku({
|
||||
refreshToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get new OAuth2 access token');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||
* Heroku integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||
* @returns
|
||||
*/
|
||||
const exchangeRefreshHeroku = async ({
|
||||
refreshToken
|
||||
}: {
|
||||
refreshToken: string;
|
||||
}) => {
|
||||
let accessToken;
|
||||
try {
|
||||
const res = await axios.post(
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
new URLSearchParams({
|
||||
grant_type: 'refresh_token',
|
||||
refresh_token: refreshToken,
|
||||
client_secret: CLIENT_SECRET_HEROKU
|
||||
} as any)
|
||||
);
|
||||
|
||||
accessToken = res.data.access_token;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to get new OAuth2 access token for Heroku');
|
||||
}
|
||||
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
export {
|
||||
exchangeRefresh
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
IIntegrationAuth,
|
||||
IntegrationAuth,
|
||||
Integration
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
const revokeAccess = async ({
|
||||
integrationAuth,
|
||||
accessToken
|
||||
}: {
|
||||
integrationAuth: IIntegrationAuth,
|
||||
accessToken: string
|
||||
}) => {
|
||||
try {
|
||||
// add any integration-specific revocation logic
|
||||
switch (integrationAuth.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
break;
|
||||
}
|
||||
|
||||
const deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
|
||||
_id: integrationAuth._id
|
||||
});
|
||||
|
||||
if (deletedIntegrationAuth) {
|
||||
await Integration.deleteMany({
|
||||
integrationAuth: deletedIntegrationAuth._id
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to delete integration authorization');
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
revokeAccess
|
||||
}
|
||||
@@ -0,0 +1,481 @@
|
||||
import axios from 'axios';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
IIntegration, IIntegrationAuth
|
||||
} from '../models';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL
|
||||
} from '../variables';
|
||||
|
||||
// TODO: need a helper function in the future to handle integration
|
||||
// envar priorities (i.e. prioritize secrets within integration or those on Infisical)
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to [app] in integration named [integration]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||
* @param {Object} obj.app - app in integration
|
||||
* @param {Object} obj.target - (optional) target (environment) in integration
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
* @param {String} obj.accessToken - access token for integration
|
||||
*/
|
||||
const syncSecrets = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken,
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
switch (integration.integration) {
|
||||
case INTEGRATION_HEROKU:
|
||||
await syncSecretsHeroku({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_VERCEL:
|
||||
await syncSecretsVercel({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
case INTEGRATION_NETLIFY:
|
||||
await syncSecretsNetlify({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: set integration to inactive if it was not synced correctly (send alert?)
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to integration');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku [app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
*/
|
||||
const syncSecretsHeroku = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration,
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
const herokuSecrets = (await axios.get(
|
||||
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
Object.keys(herokuSecrets).forEach(key => {
|
||||
if (!(key in secrets)) {
|
||||
secrets[key] = null;
|
||||
}
|
||||
});
|
||||
|
||||
await axios.patch(
|
||||
`${INTEGRATION_HEROKU_API_URL}/apps/${integration.app}/config-vars`,
|
||||
secrets,
|
||||
{
|
||||
headers: {
|
||||
Accept: 'application/vnd.heroku+json; version=3',
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Heroku');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Heroku [app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
*/
|
||||
const syncSecretsVercel = async ({
|
||||
integration,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration,
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
|
||||
interface VercelSecret {
|
||||
id?: string;
|
||||
type: string;
|
||||
key: string;
|
||||
value: string;
|
||||
target: string[];
|
||||
}
|
||||
|
||||
try {
|
||||
// Get all (decrypted) secrets back from Vercel in
|
||||
// decrypted format
|
||||
const params = new URLSearchParams({
|
||||
decrypt: "true"
|
||||
});
|
||||
|
||||
const res = (await Promise.all((await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.envs
|
||||
.filter((secret: VercelSecret) => secret.target.includes(integration.target))
|
||||
.map(async (secret: VercelSecret) => (await axios.get(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
|
||||
}
|
||||
)).data)
|
||||
)).reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const updateSecrets: VercelSecret[] = [];
|
||||
const deleteSecrets: VercelSecret[] = [];
|
||||
const newSecrets: VercelSecret[] = [];
|
||||
|
||||
// Identify secrets to create
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: secret has been created
|
||||
newSecrets.push({
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Identify secrets to update and delete
|
||||
Object.keys(res).map((key) => {
|
||||
if (key in secrets) {
|
||||
if (res[key].value !== secrets[key]) {
|
||||
// case: secret value has changed
|
||||
updateSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: secrets[key],
|
||||
type: 'encrypted',
|
||||
target: [integration.target]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: secret has been deleted
|
||||
deleteSecrets.push({
|
||||
id: res[key].id,
|
||||
key: key,
|
||||
value: res[key].value,
|
||||
type: 'encrypted',
|
||||
target: [integration.target],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Sync/push new secrets
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v10/projects/${integration.app}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Sync/push updated secrets
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: VercelSecret) => {
|
||||
const {
|
||||
id,
|
||||
...updatedSecret
|
||||
} = secret;
|
||||
await axios.patch(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
updatedSecret,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Delete secrets
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (secret: VercelSecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_VERCEL_API_URL}/v9/projects/${integration.app}/env/${secret.id}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Vercel');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Netlify site [app]
|
||||
* @param {Object} obj
|
||||
* @param {IIntegration} obj.integration - integration details
|
||||
* @param {IIntegrationAuth} obj.integrationAuth - integration auth details
|
||||
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||
*/
|
||||
const syncSecretsNetlify = async ({
|
||||
integration,
|
||||
integrationAuth,
|
||||
secrets,
|
||||
accessToken
|
||||
}: {
|
||||
integration: IIntegration;
|
||||
integrationAuth: IIntegrationAuth;
|
||||
secrets: any;
|
||||
accessToken: string;
|
||||
}) => {
|
||||
try {
|
||||
|
||||
interface NetlifyValue {
|
||||
id?: string;
|
||||
context: string; // 'dev' | 'branch-deploy' | 'deploy-preview' | 'production',
|
||||
value: string;
|
||||
}
|
||||
|
||||
interface NetlifySecret {
|
||||
key: string;
|
||||
values: NetlifyValue[];
|
||||
}
|
||||
|
||||
interface NetlifySecretsRes {
|
||||
[index: string]: NetlifySecret;
|
||||
}
|
||||
|
||||
const getParams = new URLSearchParams({
|
||||
context_name: 'all', // integration.context or all
|
||||
site_id: integration.siteId
|
||||
});
|
||||
|
||||
const res = (await axios.get(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
{
|
||||
params: getParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
))
|
||||
.data
|
||||
.reduce((obj: any, secret: any) => ({
|
||||
...obj,
|
||||
[secret.key]: secret
|
||||
}), {});
|
||||
|
||||
const newSecrets: NetlifySecret[] = []; // createEnvVars
|
||||
const deleteSecrets: string[] = []; // deleteEnvVar
|
||||
const deleteSecretValues: NetlifySecret[] = []; // deleteEnvVarValue
|
||||
const updateSecrets: NetlifySecret[] = []; // setEnvVarValue
|
||||
|
||||
// identify secrets to create and update
|
||||
Object.keys(secrets).map((key) => {
|
||||
if (!(key in res)) {
|
||||
// case: Infisical secret does not exist in Netlify -> create secret
|
||||
newSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
value: secrets[key],
|
||||
context: integration.context
|
||||
}]
|
||||
});
|
||||
} else {
|
||||
// case: Infisical secret exists in Netlify
|
||||
const contexts = res[key].values
|
||||
.reduce((obj: any, value: NetlifyValue) => ({
|
||||
...obj,
|
||||
[value.context]: value
|
||||
}), {});
|
||||
|
||||
if (integration.context in contexts) {
|
||||
// case: Netlify secret value exists in integration context
|
||||
if (secrets[key] !== contexts[integration.context].value) {
|
||||
// case: Infisical and Netlify secret values are different
|
||||
// -> update Netlify secret context and value
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// case: Netlify secret value does not exist in integration context
|
||||
// -> add the new Netlify secret context and value
|
||||
updateSecrets.push({
|
||||
key,
|
||||
values: [{
|
||||
context: integration.context,
|
||||
value: secrets[key]
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// identify secrets to delete
|
||||
// TODO: revise (patch case where 1 context was deleted but others still there
|
||||
Object.keys(res).map((key) => {
|
||||
// loop through each key's context
|
||||
if (!(key in secrets)) {
|
||||
// case: Netlify secret does not exist in Infisical
|
||||
|
||||
const numberOfValues = res[key].values.length;
|
||||
|
||||
res[key].values.forEach((value: NetlifyValue) => {
|
||||
if (value.context === integration.context) {
|
||||
if (numberOfValues <= 1) {
|
||||
// case: Netlify secret value has less than 1 context -> delete secret
|
||||
deleteSecrets.push(key);
|
||||
} else {
|
||||
// case: Netlify secret value has more than 1 context -> delete secret value context
|
||||
deleteSecretValues.push({
|
||||
key,
|
||||
values: [{
|
||||
id: value.id,
|
||||
context: integration.context,
|
||||
value: value.value
|
||||
}]
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const syncParams = new URLSearchParams({
|
||||
site_id: integration.siteId
|
||||
});
|
||||
|
||||
if (newSecrets.length > 0) {
|
||||
await axios.post(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env`,
|
||||
newSecrets,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (updateSecrets.length > 0) {
|
||||
updateSecrets.forEach(async (secret: NetlifySecret) => {
|
||||
await axios.patch(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}`,
|
||||
{
|
||||
context: secret.values[0].context,
|
||||
value: secret.values[0].value
|
||||
},
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSecrets.length > 0) {
|
||||
deleteSecrets.forEach(async (key: string) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${key}`,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (deleteSecretValues.length > 0) {
|
||||
deleteSecretValues.forEach(async (secret: NetlifySecret) => {
|
||||
await axios.delete(
|
||||
`${INTEGRATION_NETLIFY_API_URL}/api/v1/accounts/${integrationAuth.accountId}/env/${secret.key}/value/${secret.values[0].id}`,
|
||||
{
|
||||
params: syncParams,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to sync secrets to Heroku');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export {
|
||||
syncSecrets
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
{
|
||||
"heroku": {
|
||||
"name": "Heroku",
|
||||
"type": "oauth2",
|
||||
"clientId": "bc132901-935a-4590-b010-f1857efc380d",
|
||||
"docsLink": ""
|
||||
},
|
||||
"netlify": {
|
||||
"name": "Netlify",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"digitalocean": {
|
||||
"name": "Digital Ocean",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"gcp": {
|
||||
"name": "Google Cloud Platform",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"aws": {
|
||||
"name": "Amazon Web Services",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"azure": {
|
||||
"name": "Microsoft Azure",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"travisci": {
|
||||
"name": "Travis CI",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
},
|
||||
"circleci": {
|
||||
"name": "Circle CI",
|
||||
"type": "oauth2",
|
||||
"clientId": "",
|
||||
"docsLink": ""
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import requireAuth from './requireAuth';
|
||||
import requireBotAuth from './requireBotAuth';
|
||||
import requireSignupAuth from './requireSignupAuth';
|
||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||
import requireOrganizationAuth from './requireOrganizationAuth';
|
||||
@@ -9,6 +10,7 @@ import validateRequest from './validateRequest';
|
||||
|
||||
export {
|
||||
requireAuth,
|
||||
requireBotAuth,
|
||||
requireSignupAuth,
|
||||
requireWorkspaceAuth,
|
||||
requireOrganizationAuth,
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Bot } from '../models';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
const requireBotAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses,
|
||||
location = 'params'
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
location?: req;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
try {
|
||||
const bot = await Bot.findOne({ _id: req[location].botId });
|
||||
|
||||
if (!bot) {
|
||||
throw new Error('Failed to find bot');
|
||||
}
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: bot.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
req.bot = bot;
|
||||
|
||||
next();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
return res.status(401).send({
|
||||
error: 'Failed bot authorization'
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default requireBotAuth;
|
||||
@@ -1,7 +1,8 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Integration, IntegrationAuth, Membership } from '../models';
|
||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||
import { Bot, Integration, IntegrationAuth, Membership } from '../models';
|
||||
import { IntegrationService } from '../services';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
/**
|
||||
* Validate if user on request is a member of workspace with proper roles associated
|
||||
@@ -31,24 +32,14 @@ const requireIntegrationAuth = ({
|
||||
if (!integration) {
|
||||
throw new Error('Failed to find integration');
|
||||
}
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: integration.workspace
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: integration.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error('Failed to find integration workspace membership');
|
||||
}
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate workspace membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate workspace membership status');
|
||||
}
|
||||
|
||||
const integrationAuth = await IntegrationAuth.findOne({
|
||||
_id: integration.integrationAuth
|
||||
}).select(
|
||||
@@ -60,7 +51,9 @@ const requireIntegrationAuth = ({
|
||||
}
|
||||
|
||||
req.integration = integration;
|
||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
||||
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString()
|
||||
});
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { IntegrationAuth, Membership } from '../models';
|
||||
import { decryptSymmetric } from '../utils/crypto';
|
||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||
import { IntegrationAuth } from '../models';
|
||||
import { IntegrationService } from '../services';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
/**
|
||||
* Validate if user on request is a member of workspace with proper roles associated
|
||||
@@ -10,18 +10,18 @@ import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||
* @param {Object} obj
|
||||
* @param {String[]} obj.acceptedRoles - accepted workspace roles
|
||||
* @param {String[]} obj.acceptedStatuses - accepted workspace statuses
|
||||
* @param {Boolean} obj.attachRefresh - whether or not to decrypt and attach integration authorization refresh token onto request
|
||||
* @param {Boolean} obj.attachAccessToken - whether or not to decrypt and attach integration authorization access token onto request
|
||||
*/
|
||||
const requireIntegrationAuthorizationAuth = ({
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
acceptedStatuses,
|
||||
attachAccessToken = true
|
||||
}: {
|
||||
acceptedRoles: string[];
|
||||
acceptedStatuses: string[];
|
||||
attachAccessToken?: boolean;
|
||||
}) => {
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
// (authorization) integration authorization middleware
|
||||
|
||||
try {
|
||||
const { integrationAuthId } = req.params;
|
||||
|
||||
@@ -34,30 +34,21 @@ const requireIntegrationAuthorizationAuth = ({
|
||||
if (!integrationAuth) {
|
||||
throw new Error('Failed to find integration authorization');
|
||||
}
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: integrationAuth.workspace
|
||||
|
||||
await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: integrationAuth.workspace.toString(),
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
if (!membership) {
|
||||
throw new Error(
|
||||
'Failed to find integration authorization workspace membership'
|
||||
);
|
||||
}
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate workspace membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate workspace membership status');
|
||||
}
|
||||
|
||||
req.integrationAuth = integrationAuth;
|
||||
|
||||
// TODO: make compatible with other integration types since they won't necessarily have access tokens
|
||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
||||
if (attachAccessToken) {
|
||||
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||
integrationAuthId: integrationAuth._id.toString()
|
||||
});
|
||||
}
|
||||
|
||||
return next();
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { Membership, IWorkspace } from '../models';
|
||||
import { validateMembership } from '../helpers/membership';
|
||||
|
||||
type req = 'params' | 'body' | 'query';
|
||||
|
||||
@@ -25,24 +25,12 @@ const requireWorkspaceAuth = ({
|
||||
// workspace authorization middleware
|
||||
|
||||
try {
|
||||
// validate workspace membership
|
||||
|
||||
const membership = await Membership.findOne({
|
||||
user: req.user._id,
|
||||
workspace: req[location].workspaceId
|
||||
}).populate<{ workspace: IWorkspace }>('workspace');
|
||||
|
||||
if (!membership) {
|
||||
throw new Error('Failed to find workspace membership');
|
||||
}
|
||||
|
||||
if (!acceptedRoles.includes(membership.role)) {
|
||||
throw new Error('Failed to validate workspace membership role');
|
||||
}
|
||||
|
||||
if (!acceptedStatuses.includes(membership.status)) {
|
||||
throw new Error('Failed to validate workspace membership status');
|
||||
}
|
||||
const membership = await validateMembership({
|
||||
userId: req.user._id.toString(),
|
||||
workspaceId: req[location].workspaceId,
|
||||
acceptedRoles,
|
||||
acceptedStatuses
|
||||
});
|
||||
|
||||
req.membership = membership;
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IBot {
|
||||
_id: Types.ObjectId;
|
||||
name: string;
|
||||
workspace: Types.ObjectId;
|
||||
isActive: boolean;
|
||||
publicKey: string;
|
||||
encryptedPrivateKey: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}
|
||||
|
||||
const botSchema = new Schema<IBot>(
|
||||
{
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
},
|
||||
isActive: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
default: false
|
||||
},
|
||||
publicKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
encryptedPrivateKey: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
},
|
||||
iv: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
},
|
||||
tag: {
|
||||
type: String,
|
||||
required: true,
|
||||
select: false
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const Bot = model<IBot>('Bot', botSchema);
|
||||
|
||||
export default Bot;
|
||||
@@ -0,0 +1,45 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
|
||||
export interface IBotKey {
|
||||
_id: Types.ObjectId;
|
||||
encryptedKey: string;
|
||||
nonce: string;
|
||||
sender: Types.ObjectId;
|
||||
bot: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
}
|
||||
|
||||
const botKeySchema = new Schema<IBotKey>(
|
||||
{
|
||||
encryptedKey: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
nonce: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
sender: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'User',
|
||||
required: true
|
||||
},
|
||||
bot: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Bot',
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: 'Workspace',
|
||||
required: true
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
const BotKey = model<IBotKey>('BotKey', botKeySchema);
|
||||
|
||||
export default BotKey;
|
||||
@@ -1,4 +1,6 @@
|
||||
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
||||
import Bot, { IBot } from './bot';
|
||||
import BotKey, { IBotKey } from './botKey';
|
||||
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
||||
import Integration, { IIntegration } from './integration';
|
||||
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
||||
@@ -17,6 +19,10 @@ import Log, { ILog } from './log';
|
||||
export {
|
||||
BackupPrivateKey,
|
||||
IBackupPrivateKey,
|
||||
Bot,
|
||||
IBot,
|
||||
BotKey,
|
||||
IBotKey,
|
||||
IncidentContactOrg,
|
||||
IIncidentContactOrg,
|
||||
Integration,
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
@@ -14,7 +15,10 @@ export interface IIntegration {
|
||||
environment: 'dev' | 'test' | 'staging' | 'prod';
|
||||
isActive: boolean;
|
||||
app: string;
|
||||
integration: 'heroku' | 'netlify';
|
||||
target: string;
|
||||
context: string;
|
||||
siteId: string;
|
||||
integration: 'heroku' | 'vercel' | 'netlify';
|
||||
integrationAuth: Types.ObjectId;
|
||||
}
|
||||
|
||||
@@ -34,15 +38,29 @@ const integrationSchema = new Schema<IIntegration>(
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
app: {
|
||||
// name of app in provider
|
||||
app: { // name of app in provider
|
||||
type: String,
|
||||
default: null,
|
||||
required: true
|
||||
default: null
|
||||
},
|
||||
target: { // vercel-specific target (environment)
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
context: { // netlify-specific context (deploy)
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
siteId: { // netlify-specific site (app) id
|
||||
type: String,
|
||||
default: null
|
||||
},
|
||||
integration: {
|
||||
type: String,
|
||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
||||
enum: [
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
],
|
||||
required: true
|
||||
},
|
||||
integrationAuth: {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { Schema, model, Types } from 'mongoose';
|
||||
import { INTEGRATION_HEROKU, INTEGRATION_NETLIFY } from '../variables';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
} from '../variables';
|
||||
|
||||
export interface IIntegrationAuth {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
integration: 'heroku' | 'netlify';
|
||||
integration: 'heroku' | 'vercel' | 'netlify';
|
||||
teamId: string;
|
||||
accountId: string;
|
||||
refreshCiphertext?: string;
|
||||
refreshIV?: string;
|
||||
refreshTag?: string;
|
||||
@@ -22,9 +28,19 @@ const integrationAuthSchema = new Schema<IIntegrationAuth>(
|
||||
},
|
||||
integration: {
|
||||
type: String,
|
||||
enum: [INTEGRATION_HEROKU, INTEGRATION_NETLIFY],
|
||||
enum: [
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
],
|
||||
required: true
|
||||
},
|
||||
teamId: { // vercel-specific integration param
|
||||
type: String
|
||||
},
|
||||
accountId: { // netlify-specific integration param
|
||||
type: String
|
||||
},
|
||||
refreshCiphertext: {
|
||||
type: String,
|
||||
select: false
|
||||
|
||||
@@ -5,17 +5,14 @@ export interface ILog {
|
||||
user?: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
event: string;
|
||||
source: string;
|
||||
payload: {
|
||||
numberofSecrets?: number;
|
||||
environment?: string;
|
||||
},
|
||||
channel: string;
|
||||
ipAddress?: string;
|
||||
}
|
||||
|
||||
// TODO: need a way to store payload info for each
|
||||
// log
|
||||
|
||||
// which secret is being ref etc.
|
||||
|
||||
// user logged in
|
||||
|
||||
const logSchema = new Schema<ILog>(
|
||||
{
|
||||
user: {
|
||||
@@ -30,7 +27,7 @@ const logSchema = new Schema<ILog>(
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
payload: { // should this just be a payload attr?
|
||||
payload: {
|
||||
numberOfSecrets: {
|
||||
type: Number
|
||||
},
|
||||
|
||||
@@ -11,7 +11,7 @@ export interface IUser {
|
||||
tag?: string;
|
||||
salt?: string;
|
||||
verifier?: string;
|
||||
refreshVersion?: Number;
|
||||
refreshVersion?: number;
|
||||
}
|
||||
|
||||
const userSchema = new Schema<IUser>(
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import express from 'express';
|
||||
const router = express.Router();
|
||||
import { body, param } from 'express-validator';
|
||||
import {
|
||||
requireAuth,
|
||||
requireBotAuth,
|
||||
requireWorkspaceAuth,
|
||||
validateRequest
|
||||
} from '../middleware';
|
||||
import { botController } from '../controllers';
|
||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||
|
||||
router.get(
|
||||
'/:workspaceId',
|
||||
requireAuth,
|
||||
requireWorkspaceAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
param('workspaceId').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
botController.getBotByWorkspaceId
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:botId/active',
|
||||
requireAuth,
|
||||
requireBotAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [COMPLETED, GRANTED]
|
||||
}),
|
||||
body('isActive').isBoolean(),
|
||||
body('botKey'),
|
||||
validateRequest,
|
||||
botController.setBotActiveState
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -1,4 +1,5 @@
|
||||
import signup from './signup';
|
||||
import bot from './bot';
|
||||
import auth from './auth';
|
||||
import user from './user';
|
||||
import userAction from './userAction';
|
||||
@@ -19,6 +20,7 @@ import log from './log';
|
||||
export {
|
||||
signup,
|
||||
auth,
|
||||
bot,
|
||||
user,
|
||||
userAction,
|
||||
organization,
|
||||
|
||||
@@ -9,22 +9,6 @@ import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { body, param } from 'express-validator';
|
||||
import { integrationController } from '../controllers';
|
||||
|
||||
router.get('/integrations', requireAuth, integrationController.getIntegrations);
|
||||
|
||||
router.post(
|
||||
'/:integrationId/sync',
|
||||
requireAuth,
|
||||
requireIntegrationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('integrationId').exists().trim(),
|
||||
body('key').exists(),
|
||||
body('secrets').exists(),
|
||||
validateRequest,
|
||||
integrationController.syncIntegration
|
||||
);
|
||||
|
||||
router.patch(
|
||||
'/:integrationId',
|
||||
requireAuth,
|
||||
@@ -32,10 +16,15 @@ router.patch(
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('integrationId'),
|
||||
body('update'),
|
||||
param('integrationId').exists().trim(),
|
||||
body('app').exists().trim(),
|
||||
body('environment').exists().trim(),
|
||||
body('isActive').exists().isBoolean(),
|
||||
body('target').exists(),
|
||||
body('context').exists(),
|
||||
body('siteId').exists(),
|
||||
validateRequest,
|
||||
integrationController.modifyIntegration
|
||||
integrationController.updateIntegration
|
||||
);
|
||||
|
||||
router.delete(
|
||||
@@ -45,7 +34,7 @@ router.delete(
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
}),
|
||||
param('integrationId'),
|
||||
param('integrationId').exists().trim(),
|
||||
validateRequest,
|
||||
integrationController.deleteIntegration
|
||||
);
|
||||
|
||||
@@ -10,6 +10,12 @@ import {
|
||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||
import { integrationAuthController } from '../controllers';
|
||||
|
||||
router.get(
|
||||
'/integration-options',
|
||||
requireAuth,
|
||||
integrationAuthController.getIntegrationOptions
|
||||
);
|
||||
|
||||
router.post(
|
||||
'/oauth-token',
|
||||
requireAuth,
|
||||
@@ -22,7 +28,7 @@ router.post(
|
||||
body('code').exists().trim().notEmpty(),
|
||||
body('integration').exists().trim().notEmpty(),
|
||||
validateRequest,
|
||||
integrationAuthController.integrationAuthOauthExchange
|
||||
integrationAuthController.oAuthExchange
|
||||
);
|
||||
|
||||
router.get(
|
||||
@@ -42,7 +48,8 @@ router.delete(
|
||||
requireAuth,
|
||||
requireIntegrationAuthorizationAuth({
|
||||
acceptedRoles: [ADMIN, MEMBER],
|
||||
acceptedStatuses: [GRANTED]
|
||||
acceptedStatuses: [GRANTED],
|
||||
attachAccessToken: false
|
||||
}),
|
||||
param('integrationAuthId'),
|
||||
validateRequest,
|
||||
|
||||
@@ -27,7 +27,6 @@ router.post(
|
||||
passwordController.changePassword
|
||||
);
|
||||
|
||||
// NEW
|
||||
router.post(
|
||||
'/email/password-reset',
|
||||
passwordLimiter,
|
||||
@@ -36,7 +35,6 @@ router.post(
|
||||
passwordController.emailPasswordReset
|
||||
);
|
||||
|
||||
// NEW
|
||||
router.post(
|
||||
'/email/password-reset-verify',
|
||||
passwordLimiter,
|
||||
@@ -46,7 +44,6 @@ router.post(
|
||||
passwordController.emailPasswordResetVerify
|
||||
);
|
||||
|
||||
// NEW
|
||||
router.get(
|
||||
'/backup-private-key',
|
||||
passwordLimiter,
|
||||
@@ -68,7 +65,6 @@ router.post(
|
||||
passwordController.createBackupPrivateKey
|
||||
);
|
||||
|
||||
// NEW
|
||||
router.post(
|
||||
'/password-reset',
|
||||
requireSignupAuth,
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
getSecretsHelper,
|
||||
encryptSymmetricHelper,
|
||||
decryptSymmetricHelper
|
||||
} from '../helpers/bot';
|
||||
|
||||
/**
|
||||
* Class to handle bot actions
|
||||
*/
|
||||
class BotService {
|
||||
|
||||
/**
|
||||
* Return decrypted secrets for workspace with id [workspaceId] and
|
||||
* environment [environmen] shared to bot.
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||
* @param {String} obj.environment - environment for secrets
|
||||
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
|
||||
*/
|
||||
static async getSecrets({
|
||||
workspaceId,
|
||||
environment
|
||||
}: {
|
||||
workspaceId: string;
|
||||
environment: string;
|
||||
}) {
|
||||
return await getSecretsHelper({
|
||||
workspaceId,
|
||||
environment
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return symmetrically encrypted [plaintext] using the
|
||||
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
*/
|
||||
static async encryptSymmetric({
|
||||
workspaceId,
|
||||
plaintext
|
||||
}: {
|
||||
workspaceId: string;
|
||||
plaintext: string;
|
||||
}) {
|
||||
return await encryptSymmetricHelper({
|
||||
workspaceId,
|
||||
plaintext
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return symmetrically decrypted [ciphertext] using the
|
||||
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
*/
|
||||
static async decryptSymmetric({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
}: {
|
||||
workspaceId: string;
|
||||
ciphertext: string;
|
||||
iv: string;
|
||||
tag: string;
|
||||
}) {
|
||||
return await decryptSymmetricHelper({
|
||||
workspaceId,
|
||||
ciphertext,
|
||||
iv,
|
||||
tag
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default BotService;
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Bot, IBot } from '../models';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { handleEventHelper } from '../helpers/event';
|
||||
|
||||
interface Event {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Class to handle events.
|
||||
*/
|
||||
class EventService {
|
||||
/**
|
||||
* Handle event [event]
|
||||
* @param {Object} obj
|
||||
* @param {Event} obj.event - an event
|
||||
* @param {String} obj.event.name - name of event
|
||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||
*/
|
||||
static async handleEvent({ event }: { event: Event }): Promise<void> {
|
||||
await handleEventHelper({
|
||||
event
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default EventService;
|
||||
@@ -0,0 +1,145 @@
|
||||
import * as Sentry from '@sentry/node';
|
||||
import {
|
||||
Integration
|
||||
} from '../models';
|
||||
import {
|
||||
handleOAuthExchangeHelper,
|
||||
syncIntegrationsHelper,
|
||||
getIntegrationAuthRefreshHelper,
|
||||
getIntegrationAuthAccessHelper,
|
||||
setIntegrationAuthRefreshHelper,
|
||||
setIntegrationAuthAccessHelper,
|
||||
} from '../helpers/integration';
|
||||
import { exchangeCode } from '../integrations';
|
||||
import {
|
||||
ENV_DEV,
|
||||
EVENT_PUSH_SECRETS
|
||||
} from '../variables';
|
||||
|
||||
// should sync stuff be here too? Probably.
|
||||
// TODO: move bot functions to IntegrationService.
|
||||
|
||||
/**
|
||||
* Class to handle integrations
|
||||
*/
|
||||
class IntegrationService {
|
||||
|
||||
/**
|
||||
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||
* named [integration]
|
||||
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||
* - Add placeholder inactive integration
|
||||
* - Create bot sequence for integration
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.workspaceId - id of workspace
|
||||
* @param {String} obj.integration - name of integration
|
||||
* @param {String} obj.code - code
|
||||
*/
|
||||
static async handleOAuthExchange({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
}: {
|
||||
workspaceId: string;
|
||||
integration: string;
|
||||
code: string;
|
||||
}) {
|
||||
await handleOAuthExchangeHelper({
|
||||
workspaceId,
|
||||
integration,
|
||||
code
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||
* all associated integrations
|
||||
* @param {Object} obj
|
||||
* @param {Object} obj.workspaceId - id of workspace
|
||||
*/
|
||||
static async syncIntegrations({
|
||||
workspaceId
|
||||
}: {
|
||||
workspaceId: string;
|
||||
}) {
|
||||
return await syncIntegrationsHelper({
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted refresh token for integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} refreshToken - decrypted refresh token
|
||||
*/
|
||||
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) {
|
||||
return await getIntegrationAuthRefreshHelper({
|
||||
integrationAuthId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return decrypted access token for integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} accessToken - decrypted access token
|
||||
*/
|
||||
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) {
|
||||
return await getIntegrationAuthAccessHelper({
|
||||
integrationAuthId
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.refreshToken - refresh token
|
||||
* @returns {IntegrationAuth} integrationAuth - updated integration auth
|
||||
*/
|
||||
static async setIntegrationAuthRefresh({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
refreshToken: string;
|
||||
}) {
|
||||
return await setIntegrationAuthRefreshHelper({
|
||||
integrationAuthId,
|
||||
refreshToken
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt access token [accessToken] using the bot's copy
|
||||
* of the workspace key for workspace belonging to integration auth
|
||||
* with id [integrationAuthId]
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.integrationAuthId - id of integration auth
|
||||
* @param {String} obj.accessToken - access token
|
||||
* @param {String} obj.accessExpiresAt - expiration date of access token
|
||||
* @returns {IntegrationAuth} - updated integration auth
|
||||
*/
|
||||
static async setIntegrationAuthAccess({
|
||||
integrationAuthId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
}: {
|
||||
integrationAuthId: string;
|
||||
accessToken: string;
|
||||
accessExpiresAt: Date;
|
||||
}) {
|
||||
return await setIntegrationAuthAccessHelper({
|
||||
integrationAuthId,
|
||||
accessToken,
|
||||
accessExpiresAt
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default IntegrationService;
|
||||
@@ -1,5 +1,11 @@
|
||||
import postHogClient from './PostHogClient';
|
||||
import BotService from './BotService';
|
||||
import EventService from './EventService';
|
||||
import IntegrationService from './IntegrationService';
|
||||
|
||||
export {
|
||||
postHogClient
|
||||
postHogClient,
|
||||
BotService,
|
||||
EventService,
|
||||
IntegrationService
|
||||
}
|
||||
Vendored
+1
@@ -11,6 +11,7 @@ declare global {
|
||||
membershipOrg: any;
|
||||
integration: any;
|
||||
integrationAuth: any;
|
||||
bot: any;
|
||||
serviceToken: any;
|
||||
accessToken: any;
|
||||
query?: any;
|
||||
|
||||
@@ -1,6 +1,22 @@
|
||||
import nacl from 'tweetnacl';
|
||||
import util from 'tweetnacl-util';
|
||||
import AesGCM from './aes-gcm';
|
||||
import * as Sentry from '@sentry/node';
|
||||
|
||||
/**
|
||||
* Return new base64, NaCl, public-private key pair.
|
||||
* @returns {Object} obj
|
||||
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||
*/
|
||||
const generateKeyPair = () => {
|
||||
const pair = nacl.box.keyPair();
|
||||
|
||||
return ({
|
||||
publicKey: util.encodeBase64(pair.publicKey),
|
||||
privateKey: util.encodeBase64(pair.secretKey)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||
@@ -32,6 +48,8 @@ const encryptAsymmetric = ({
|
||||
util.decodeBase64(privateKey)
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform asymmetric encryption');
|
||||
}
|
||||
|
||||
@@ -71,6 +89,8 @@ const decryptAsymmetric = ({
|
||||
util.decodeBase64(privateKey)
|
||||
);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform asymmetric decryption');
|
||||
}
|
||||
|
||||
@@ -81,7 +101,7 @@ const decryptAsymmetric = ({
|
||||
* Return symmetrically encrypted [plaintext] using [key].
|
||||
* @param {Object} obj
|
||||
* @param {String} obj.plaintext - plaintext to encrypt
|
||||
* @param {String} obj.key - 16-byte hex key
|
||||
* @param {String} obj.key - hex key
|
||||
*/
|
||||
const encryptSymmetric = ({
|
||||
plaintext,
|
||||
@@ -97,6 +117,8 @@ const encryptSymmetric = ({
|
||||
iv = obj.iv;
|
||||
tag = obj.tag;
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric encryption');
|
||||
}
|
||||
|
||||
@@ -114,7 +136,7 @@ const encryptSymmetric = ({
|
||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||
* @param {String} obj.iv - iv
|
||||
* @param {String} obj.tag - tag
|
||||
* @param {String} obj.key - 32-byte hex key
|
||||
* @param {String} obj.key - hex key
|
||||
*
|
||||
*/
|
||||
const decryptSymmetric = ({
|
||||
@@ -132,6 +154,8 @@ const decryptSymmetric = ({
|
||||
try {
|
||||
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
throw new Error('Failed to perform symmetric decryption');
|
||||
}
|
||||
|
||||
@@ -139,6 +163,7 @@ const decryptSymmetric = ({
|
||||
};
|
||||
|
||||
export {
|
||||
generateKeyPair,
|
||||
encryptAsymmetric,
|
||||
decryptAsymmetric,
|
||||
encryptSymmetric,
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
// membership roles
|
||||
const OWNER = 'owner';
|
||||
const ADMIN = 'admin';
|
||||
const MEMBER = 'member';
|
||||
|
||||
// membership statuses
|
||||
const INVITED = 'invited';
|
||||
|
||||
// -- organization
|
||||
const ACCEPTED = 'accepted';
|
||||
|
||||
// -- workspace
|
||||
const COMPLETED = 'completed';
|
||||
const GRANTED = 'granted';
|
||||
|
||||
// subscriptions
|
||||
const PLAN_STARTER = 'starter';
|
||||
const PLAN_PRO = 'pro';
|
||||
|
||||
// secrets
|
||||
const SECRET_SHARED = 'shared';
|
||||
const SECRET_PERSONAL = 'personal';
|
||||
|
||||
// environments
|
||||
const ENV_DEV = 'dev';
|
||||
const ENV_TESTING = 'test';
|
||||
const ENV_STAGING = 'staging';
|
||||
const ENV_PROD = 'prod';
|
||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||
|
||||
// integrations
|
||||
const INTEGRATION_HEROKU = 'heroku';
|
||||
const INTEGRATION_NETLIFY = 'netlify';
|
||||
const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]);
|
||||
|
||||
// integration types
|
||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED,
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO,
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2
|
||||
};
|
||||
@@ -0,0 +1,5 @@
|
||||
const ACTION_PUSH_TO_HEROKU = 'pushToHeroku';
|
||||
|
||||
export {
|
||||
ACTION_PUSH_TO_HEROKU
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
// environments
|
||||
const ENV_DEV = 'dev';
|
||||
const ENV_TESTING = 'test';
|
||||
const ENV_STAGING = 'staging';
|
||||
const ENV_PROD = 'prod';
|
||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||
|
||||
export {
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const EVENT_PUSH_SECRETS = 'pushSecrets';
|
||||
const EVENT_PULL_SECRETS = 'pullSecrets';
|
||||
|
||||
export {
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
import {
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET
|
||||
} from './environment';
|
||||
import {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
} from './integration';
|
||||
import {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED
|
||||
} from './organization';
|
||||
import {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL
|
||||
} from './secret';
|
||||
import {
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO
|
||||
} from './stripe';
|
||||
import {
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS
|
||||
} from './event';
|
||||
import {
|
||||
ACTION_PUSH_TO_HEROKU
|
||||
} from './action';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED,
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO,
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL,
|
||||
ENV_DEV,
|
||||
ENV_TESTING,
|
||||
ENV_STAGING,
|
||||
ENV_PROD,
|
||||
ENV_SET,
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
EVENT_PUSH_SECRETS,
|
||||
EVENT_PULL_SECRETS,
|
||||
ACTION_PUSH_TO_HEROKU,
|
||||
INTEGRATION_OPTIONS
|
||||
};
|
||||
@@ -0,0 +1,119 @@
|
||||
import {
|
||||
CLIENT_ID_HEROKU,
|
||||
CLIENT_ID_NETLIFY,
|
||||
CLIENT_SLUG_VERCEL
|
||||
} from '../config';
|
||||
|
||||
// integrations
|
||||
const INTEGRATION_HEROKU = 'heroku';
|
||||
const INTEGRATION_VERCEL = 'vercel';
|
||||
const INTEGRATION_NETLIFY = 'netlify';
|
||||
const INTEGRATION_SET = new Set([
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY
|
||||
]);
|
||||
|
||||
// integration types
|
||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||
|
||||
// integration oauth endpoints
|
||||
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||
const INTEGRATION_VERCEL_TOKEN_URL = 'https://api.vercel.com/v2/oauth/access_token';
|
||||
const INTEGRATION_NETLIFY_TOKEN_URL = 'https://api.netlify.com/oauth/token';
|
||||
|
||||
// integration apps endpoints
|
||||
const INTEGRATION_HEROKU_API_URL = 'https://api.heroku.com';
|
||||
const INTEGRATION_VERCEL_API_URL = 'https://api.vercel.com';
|
||||
const INTEGRATION_NETLIFY_API_URL = 'https://api.netlify.com';
|
||||
|
||||
const INTEGRATION_OPTIONS = [
|
||||
{
|
||||
name: 'Heroku',
|
||||
slug: 'heroku',
|
||||
image: 'Heroku',
|
||||
isAvailable: true,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_HEROKU,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Vercel',
|
||||
slug: 'vercel',
|
||||
image: 'Vercel',
|
||||
isAvailable: true,
|
||||
type: 'vercel',
|
||||
clientId: '',
|
||||
clientSlug: CLIENT_SLUG_VERCEL,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Netlify',
|
||||
slug: 'netlify',
|
||||
image: 'Netlify',
|
||||
isAvailable: true,
|
||||
type: 'oauth2',
|
||||
clientId: CLIENT_ID_NETLIFY,
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Google Cloud Platform',
|
||||
slug: 'gcp',
|
||||
image: 'Google Cloud Platform',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Amazon Web Services',
|
||||
slug: 'aws',
|
||||
image: 'Amazon Web Services',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Microsoft Azure',
|
||||
slug: 'azure',
|
||||
image: 'Microsoft Azure',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Travis CI',
|
||||
slug: 'travisci',
|
||||
image: 'Travis CI',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
},
|
||||
{
|
||||
name: 'Circle CI',
|
||||
slug: 'circleci',
|
||||
image: 'Circle CI',
|
||||
isAvailable: false,
|
||||
type: '',
|
||||
clientId: '',
|
||||
docsLink: ''
|
||||
}
|
||||
]
|
||||
|
||||
export {
|
||||
INTEGRATION_HEROKU,
|
||||
INTEGRATION_VERCEL,
|
||||
INTEGRATION_NETLIFY,
|
||||
INTEGRATION_SET,
|
||||
INTEGRATION_OAUTH2,
|
||||
INTEGRATION_HEROKU_TOKEN_URL,
|
||||
INTEGRATION_VERCEL_TOKEN_URL,
|
||||
INTEGRATION_NETLIFY_TOKEN_URL,
|
||||
INTEGRATION_HEROKU_API_URL,
|
||||
INTEGRATION_VERCEL_API_URL,
|
||||
INTEGRATION_NETLIFY_API_URL,
|
||||
INTEGRATION_OPTIONS
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// membership roles
|
||||
const OWNER = 'owner';
|
||||
const ADMIN = 'admin';
|
||||
const MEMBER = 'member';
|
||||
|
||||
// membership statuses
|
||||
const INVITED = 'invited';
|
||||
|
||||
// -- organization
|
||||
const ACCEPTED = 'accepted';
|
||||
|
||||
// -- workspace
|
||||
const COMPLETED = 'completed';
|
||||
const GRANTED = 'granted';
|
||||
|
||||
export {
|
||||
OWNER,
|
||||
ADMIN,
|
||||
MEMBER,
|
||||
INVITED,
|
||||
ACCEPTED,
|
||||
COMPLETED,
|
||||
GRANTED
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// secrets
|
||||
const SECRET_SHARED = 'shared';
|
||||
const SECRET_PERSONAL = 'personal';
|
||||
|
||||
export {
|
||||
SECRET_SHARED,
|
||||
SECRET_PERSONAL
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const PLAN_STARTER = 'starter';
|
||||
const PLAN_PRO = 'pro';
|
||||
|
||||
export {
|
||||
PLAN_STARTER,
|
||||
PLAN_PRO
|
||||
}
|
||||
Reference in New Issue
Block a user