From 21eb1815c49a87ea83135b4ab52aef836895a86c Mon Sep 17 00:00:00 2001 From: Spelchure Date: Mon, 1 May 2023 10:56:18 +0300 Subject: [PATCH] feat: remove try-catch blocks for handling errors in middleware --- backend/src/ee/helpers/action.ts | 176 ++- backend/src/ee/helpers/log.ts | 30 +- backend/src/ee/helpers/secret.ts | 231 ++-- backend/src/ee/helpers/secretVersion.ts | 100 +- backend/src/helpers/auth.ts | 1 - backend/src/helpers/bot.ts | 455 ++++--- backend/src/helpers/event.ts | 73 +- backend/src/helpers/integration.ts | 4 +- backend/src/helpers/key.ts | 51 +- backend/src/helpers/membershipOrg.ts | 114 +- backend/src/helpers/organization.ts | 404 ++++--- backend/src/helpers/secret.ts | 1043 ++++++++--------- backend/src/helpers/token.ts | 364 +++--- backend/src/helpers/user.ts | 63 +- backend/src/integrations/apps.ts | 824 ++++++------- backend/src/integrations/exchange.ts | 336 +++--- backend/src/integrations/refresh.ts | 259 ++-- backend/src/integrations/revoke.ts | 47 +- backend/src/integrations/teams.ts | 55 +- .../requireIntegrationAuthorizationAuth.ts | 1 - backend/src/utils/crypto.ts | 64 +- backend/tests/unit-tests/utils/crypto.test.ts | 26 +- 22 files changed, 2141 insertions(+), 2580 deletions(-) diff --git a/backend/src/ee/helpers/action.ts b/backend/src/ee/helpers/action.ts index 33111d4ba..94e6bd24d 100644 --- a/backend/src/ee/helpers/action.ts +++ b/backend/src/ee/helpers/action.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Action } from '../models'; import { @@ -36,33 +35,25 @@ const createActionUpdateSecret = async ({ workspaceId: Types.ObjectId; secretIds: Types.ObjectId[]; }) => { - let action; - try { - const latestSecretVersions = (await getLatestNSecretSecretVersionIds({ - secretIds, - n: 2 - })) - .map((s) => ({ - oldSecretVersion: s.versions[0]._id, - newSecretVersion: s.versions[1]._id - })); - - action = await new Action({ - name, - user: userId, - serviceAccount: serviceAccountId, - serviceTokenData: serviceTokenDataId, - workspace: workspaceId, - payload: { - secretVersions: latestSecretVersions - } - }).save(); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create update secret action'); - } + const latestSecretVersions = (await getLatestNSecretSecretVersionIds({ + secretIds, + n: 2 + })) + .map((s) => ({ + oldSecretVersion: s.versions[0]._id, + newSecretVersion: s.versions[1]._id + })); + + const action = await new Action({ + name, + user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, + workspace: workspaceId, + payload: { + secretVersions: latestSecretVersions + } + }).save(); return action; } @@ -90,33 +81,25 @@ const createActionSecret = async ({ workspaceId: Types.ObjectId; secretIds: Types.ObjectId[]; }) => { - let action; - try { - // case: action is adding, deleting, or reading secrets - // -> add new secret versions - const latestSecretVersions = (await getLatestSecretVersionIds({ - secretIds - })) - .map((s) => ({ - newSecretVersion: s.versionId - })); - - action = await new Action({ - name, - user: userId, - serviceAccount: serviceAccountId, - serviceTokenData: serviceTokenDataId, - workspace: workspaceId, - payload: { - secretVersions: latestSecretVersions - } - }).save(); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create action create/read/delete secret action'); - } + // case: action is adding, deleting, or reading secrets + // -> add new secret versions + const latestSecretVersions = (await getLatestSecretVersionIds({ + secretIds + })) + .map((s) => ({ + newSecretVersion: s.versionId + })); + + const action = await new Action({ + name, + user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, + workspace: workspaceId, + payload: { + secretVersions: latestSecretVersions + } + }).save(); return action; } @@ -140,19 +123,12 @@ const createActionClient = ({ serviceAccountId?: Types.ObjectId; serviceTokenDataId?: Types.ObjectId; }) => { - let action; - try { - action = new Action({ - name, - user: userId, - serviceAccount: serviceAccountId, - serviceTokenData: serviceTokenDataId - }).save(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create client action'); - } + const action = new Action({ + name, + user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId + }).save(); return action; } @@ -181,40 +157,34 @@ const createActionHelper = async ({ secretIds?: Types.ObjectId[]; }) => { let action; - try { - switch (name) { - case ACTION_LOGIN: - case ACTION_LOGOUT: - action = await createActionClient({ - name, - userId - }); - break; - case ACTION_ADD_SECRETS: - case ACTION_READ_SECRETS: - case ACTION_DELETE_SECRETS: - if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); - action = await createActionSecret({ - name, - userId, - workspaceId, - secretIds - }); - break; - case ACTION_UPDATE_SECRETS: - if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); - action = await createActionUpdateSecret({ - name, - userId, - workspaceId, - secretIds - }); - break; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create action'); + switch (name) { + case ACTION_LOGIN: + case ACTION_LOGOUT: + action = await createActionClient({ + name, + userId + }); + break; + case ACTION_ADD_SECRETS: + case ACTION_READ_SECRETS: + case ACTION_DELETE_SECRETS: + if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); + action = await createActionSecret({ + name, + userId, + workspaceId, + secretIds + }); + break; + case ACTION_UPDATE_SECRETS: + if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); + action = await createActionUpdateSecret({ + name, + userId, + workspaceId, + secretIds + }); + break; } return action; @@ -222,4 +192,4 @@ const createActionHelper = async ({ export { createActionHelper -}; \ No newline at end of file +}; diff --git a/backend/src/ee/helpers/log.ts b/backend/src/ee/helpers/log.ts index 077d5fe5a..5b6d78f31 100644 --- a/backend/src/ee/helpers/log.ts +++ b/backend/src/ee/helpers/log.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Log, @@ -32,27 +31,20 @@ const createLogHelper = async ({ channel: string; ipAddress: string; }) => { - let log; - try { - log = await new Log({ - user: userId, - serviceAccount: serviceAccountId, - serviceTokenData: serviceTokenDataId, - workspace: workspaceId ?? undefined, - actionNames: actions.map((a) => a.name), - actions, - channel, - ipAddress - }).save(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to create log'); - } + const log = await new Log({ + user: userId, + serviceAccount: serviceAccountId, + serviceTokenData: serviceTokenDataId, + workspace: workspaceId ?? undefined, + actionNames: actions.map((a) => a.name), + actions, + channel, + ipAddress + }).save(); return log; } export { createLogHelper -} \ No newline at end of file +} diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index a642859ef..0bf172581 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -1,14 +1,6 @@ -import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; -import { - Secret, - ISecret, -} from '../../models'; -import { - SecretSnapshot, - SecretVersion, - ISecretVersion -} from '../models'; +import { Types } from "mongoose"; +import { Secret, ISecret } from "../../models"; +import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models"; /** * Save a secret snapshot that is a copy of the current state of secrets in workspace with id @@ -19,56 +11,53 @@ import { * @returns {SecretSnapshot} secretSnapshot - new secret snapshot */ const takeSecretSnapshotHelper = async ({ - workspaceId + workspaceId, }: { - workspaceId: Types.ObjectId; + workspaceId: Types.ObjectId; }) => { + const secretIds = ( + await Secret.find( + { + workspace: workspaceId, + }, + "_id" + ) + ).map((s) => s._id); - let secretSnapshot; - try { - const secretIds = (await Secret.find({ - workspace: workspaceId - }, '_id')).map((s) => s._id); + const latestSecretVersions = ( + await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds, + }, + }, + }, + { + $group: { + _id: "$secret", + version: { $max: "$version" }, + versionId: { $max: "$_id" }, // secret version id + }, + }, + { + $sort: { version: -1 }, + }, + ]).exec() + ).map((s) => s.versionId); - const latestSecretVersions = (await SecretVersion.aggregate([ - { - $match: { - secret: { - $in: secretIds - } - } - }, - { - $group: { - _id: '$secret', - version: { $max: '$version' }, - versionId: { $max: '$_id' } // secret version id - } - }, - { - $sort: { version: -1 } - } - ]) - .exec()) - .map((s) => s.versionId); + const latestSecretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId, + }).sort({ version: -1 }); - const latestSecretSnapshot = await SecretSnapshot.findOne({ - workspace: workspaceId - }).sort({ version: -1 }); + const secretSnapshot = await new SecretSnapshot({ + workspace: workspaceId, + version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, + secretVersions: latestSecretVersions, + }).save(); - secretSnapshot = await new SecretSnapshot({ - workspace: workspaceId, - version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, - secretVersions: latestSecretVersions - }).save(); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to take a secret snapshot'); - } - - return secretSnapshot; -} + return secretSnapshot; +}; /** * Add secret versions [secretVersions] to the SecretVersion collection. @@ -77,93 +66,79 @@ const takeSecretSnapshotHelper = async ({ * @returns {SecretVersion[]} newSecretVersions - new secret versions */ const addSecretVersionsHelper = async ({ - secretVersions + secretVersions, }: { - secretVersions: ISecretVersion[] + secretVersions: ISecretVersion[]; }) => { - let newSecretVersions; - try { - newSecretVersions = await SecretVersion.insertMany(secretVersions); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error(`Failed to add secret versions [err=${err}]`); - } + const newSecretVersions = await SecretVersion.insertMany(secretVersions); - return newSecretVersions; -} + return newSecretVersions; +}; const markDeletedSecretVersionsHelper = async ({ - secretIds + secretIds, }: { - secretIds: Types.ObjectId[]; + secretIds: Types.ObjectId[]; }) => { - try { - await SecretVersion.updateMany({ - secret: { $in: secretIds } - }, { - isDeleted: true - }, { - new: true - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to mark secret versions as deleted'); - } -} + await SecretVersion.updateMany( + { + secret: { $in: secretIds }, + }, + { + isDeleted: true, + }, + { + new: true, + } + ); +}; /** * Initialize secret versioning by setting previously unversioned * secrets to version 1 and begin populating secret versions. */ const initSecretVersioningHelper = async () => { - try { + await Secret.updateMany( + { version: { $exists: false } }, + { $set: { version: 1 } } + ); - await Secret.updateMany( - { version: { $exists: false } }, - { $set: { version: 1 } } - ); + const unversionedSecrets: ISecret[] = await Secret.aggregate([ + { + $lookup: { + from: "secretversions", + localField: "_id", + foreignField: "secret", + as: "versions", + }, + }, + { + $match: { + versions: { $size: 0 }, + }, + }, + ]); - const unversionedSecrets: ISecret[] = await Secret.aggregate([ - { - $lookup: { - from: 'secretversions', - localField: '_id', - foreignField: 'secret', - as: 'versions', - }, - }, - { - $match: { - versions: { $size: 0 }, - }, - }, - ]); - - if (unversionedSecrets.length > 0) { - await addSecretVersionsHelper({ - secretVersions: unversionedSecrets.map((s, idx) => new SecretVersion({ - ...s, - secret: s._id, - version: s.version ? s.version : 1, - isDeleted: false, - workspace: s.workspace, - environment: s.environment - })) - }); - } - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to ensure that secrets are versioned'); - } -} + if (unversionedSecrets.length > 0) { + await addSecretVersionsHelper({ + secretVersions: unversionedSecrets.map( + (s, idx) => + new SecretVersion({ + ...s, + secret: s._id, + version: s.version ? s.version : 1, + isDeleted: false, + workspace: s.workspace, + environment: s.environment, + }) + ), + }); + } +}; export { - takeSecretSnapshotHelper, - addSecretVersionsHelper, - markDeletedSecretVersionsHelper, - initSecretVersioningHelper -} \ No newline at end of file + takeSecretSnapshotHelper, + addSecretVersionsHelper, + markDeletedSecretVersionsHelper, + initSecretVersioningHelper, +}; diff --git a/backend/src/ee/helpers/secretVersion.ts b/backend/src/ee/helpers/secretVersion.ts index d5859e183..c7190c3bc 100644 --- a/backend/src/ee/helpers/secretVersion.ts +++ b/backend/src/ee/helpers/secretVersion.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { SecretVersion } from '../models'; @@ -13,41 +12,32 @@ const getLatestSecretVersionIds = async ({ }: { secretIds: Types.ObjectId[]; }) => { - interface LatestSecretVersionId { _id: Types.ObjectId; version: number; versionId: Types.ObjectId; } - let latestSecretVersionIds: LatestSecretVersionId[]; - try { - latestSecretVersionIds = (await SecretVersion.aggregate([ - { - $match: { - secret: { - $in: secretIds - } - } - }, - { - $group: { - _id: '$secret', - version: { $max: '$version' }, - versionId: { $max: '$_id' } // id of latest secret version - } - }, - { - $sort: { version: -1 } + const latestSecretVersionIds = (await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds + } } - ]) - .exec()); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get latest secret versions'); - } + }, + { + $group: { + _id: '$secret', + version: { $max: '$version' }, + versionId: { $max: '$_id' } // id of latest secret version + } + }, + { + $sort: { version: -1 } + } + ]) + .exec()); return latestSecretVersionIds; } @@ -66,40 +56,32 @@ const getLatestNSecretSecretVersionIds = async ({ secretIds: Types.ObjectId[]; n: number; }) => { - // TODO: optimize query - let latestNSecretVersions; - try { - latestNSecretVersions = (await SecretVersion.aggregate([ - { - $match: { - secret: { - $in: secretIds, - }, - }, + const latestNSecretVersions = (await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds, }, - { - $sort: { version: -1 }, }, - { - $group: { - _id: "$secret", - versions: { $push: "$$ROOT" }, - }, + }, + { + $sort: { version: -1 }, + }, + { + $group: { + _id: "$secret", + versions: { $push: "$$ROOT" }, }, - { - $project: { - _id: 0, - secret: "$_id", - versions: { $slice: ["$versions", n] }, - }, - } - ])); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get latest n secret versions'); - } + }, + { + $project: { + _id: 0, + secret: "$_id", + versions: { $slice: ["$versions", n] }, + }, + } + ])); return latestNSecretVersions; } diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index fcc3ba8ff..a52abb709 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcrypt'; diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts index db022c42f..96ad452d9 100644 --- a/backend/src/helpers/bot.ts +++ b/backend/src/helpers/bot.ts @@ -1,41 +1,34 @@ -import * as Sentry from '@sentry/node'; -import { Types } from 'mongoose'; +import { Types } from "mongoose"; import { - Bot, - BotKey, - Secret, - ISecret, - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData -} from '../models'; -import { - generateKeyPair, - encryptSymmetric, - decryptSymmetric, - decryptAsymmetric -} from '../utils/crypto'; + Bot, + BotKey, + Secret, + ISecret, + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData, +} from "../models"; import { - SECRET_SHARED, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY -} from '../variables'; -import { getEncryptionKey } from '../config'; -import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors'; + generateKeyPair, + encryptSymmetric, + decryptSymmetric, + decryptAsymmetric, +} from "../utils/crypto"; import { - validateMembership -} from '../helpers/membership'; -import { - validateUserClientForWorkspace -} from '../helpers/user'; -import { - validateServiceAccountClientForWorkspace -} from '../helpers/serviceAccount'; + SECRET_SHARED, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY, +} from "../variables"; +import { getEncryptionKey } from "../config"; +import { BotNotFoundError, UnauthorizedRequestError } from "../utils/errors"; +import { validateMembership } from "../helpers/membership"; +import { validateUserClientForWorkspace } from "../helpers/user"; +import { validateServiceAccountClientForWorkspace } from "../helpers/serviceAccount"; /** * Validate authenticated clients for bot with id [botId] based @@ -46,99 +39,104 @@ import { * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles */ const validateClientForBot = async ({ - authData, - botId, - acceptedRoles + authData, + botId, + acceptedRoles, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }; - botId: Types.ObjectId; - acceptedRoles: Array<'admin' | 'member'>; + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + botId: Types.ObjectId; + acceptedRoles: Array<"admin" | "member">; }) => { - const bot = await Bot.findById(botId); - - if (!bot) throw BotNotFoundError(); - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: bot.workspace, - acceptedRoles - }); - - return bot; - } + const bot = await Bot.findById(botId); - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForWorkspace({ - serviceAccount: authData.authPayload, - workspaceId: bot.workspace - }); + if (!bot) throw BotNotFoundError(); - return bot; - } - - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: 'Failed service token authorization for bot' - }); - } - - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - await validateUserClientForWorkspace({ - user: authData.authPayload, - workspaceId: bot.workspace, - acceptedRoles - }); - - return bot; - } - - throw BotNotFoundError({ - message: 'Failed client authorization for bot' + if ( + authData.authMode === AUTH_MODE_JWT && + authData.authPayload instanceof User + ) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: bot.workspace, + acceptedRoles, }); -} + + return bot; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && + authData.authPayload instanceof ServiceAccount + ) { + await validateServiceAccountClientForWorkspace({ + serviceAccount: authData.authPayload, + workspaceId: bot.workspace, + }); + + return bot; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_TOKEN && + authData.authPayload instanceof ServiceTokenData + ) { + throw UnauthorizedRequestError({ + message: "Failed service token authorization for bot", + }); + } + + if ( + authData.authMode === AUTH_MODE_API_KEY && + authData.authPayload instanceof User + ) { + await validateUserClientForWorkspace({ + user: authData.authPayload, + workspaceId: bot.workspace, + acceptedRoles, + }); + + return bot; + } + + throw BotNotFoundError({ + message: "Failed client authorization for bot", + }); +}; /** * Create an inactive bot with name [name] for workspace with id [workspaceId] - * @param {Object} obj + * @param {Object} obj * @param {String} obj.name - name of bot * @param {String} obj.workspaceId - id of workspace that bot belongs to */ const createBot = async ({ - name, - workspaceId, + name, + workspaceId, }: { - name: string; - workspaceId: Types.ObjectId; + name: string; + workspaceId: Types.ObjectId; }) => { - let bot; - try { - const { publicKey, privateKey } = generateKeyPair(); - const { ciphertext, iv, tag } = encryptSymmetric({ - plaintext: privateKey, - key: await getEncryptionKey() - }); + const { publicKey, privateKey } = generateKeyPair(); + const { ciphertext, iv, tag } = encryptSymmetric({ + plaintext: privateKey, + key: await getEncryptionKey(), + }); - 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; -} + const bot = await new Bot({ + name, + workspace: workspaceId, + isActive: false, + publicKey, + encryptedPrivateKey: ciphertext, + iv, + tag, + }).save(); + + return bot; +}; /** * Return decrypted secrets for workspace with id [workspaceId] @@ -148,125 +146,105 @@ const createBot = async ({ * @param {String} obj.environment - environment */ const getSecretsHelper = async ({ - workspaceId, - environment + workspaceId, + environment, }: { - workspaceId: Types.ObjectId; - environment: string; + workspaceId: Types.ObjectId; + environment: string; }) => { - const content = {} as any; - try { - const key = await getKey({ workspaceId }); - const secrets = await Secret.find({ - workspace: workspaceId, - environment, - type: SECRET_SHARED - }); - - secrets.forEach((secret: ISecret) => { - const secretKey = decryptSymmetric({ - ciphertext: secret.secretKeyCiphertext, - iv: secret.secretKeyIV, - tag: secret.secretKeyTag, - key - }); + const content = {} as any; + const key = await getKey({ workspaceId: workspaceId.toString() }); + const secrets = await Secret.find({ + workspace: workspaceId, + environment, + type: SECRET_SHARED, + }); - const secretValue = decryptSymmetric({ - ciphertext: secret.secretValueCiphertext, - iv: secret.secretValueIV, - tag: secret.secretValueTag, - key - }); + secrets.forEach((secret: ISecret) => { + const secretKey = decryptSymmetric({ + ciphertext: secret.secretKeyCiphertext, + iv: secret.secretKeyIV, + tag: secret.secretKeyTag, + key, + }); - content[secretKey] = secretValue; - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get secrets'); - } + const secretValue = decryptSymmetric({ + ciphertext: secret.secretValueCiphertext, + iv: secret.secretValueIV, + tag: secret.secretValueTag, + key, + }); - return content; -} + content[secretKey] = secretValue; + }); + + return content; +}; /** - * Return bot's copy of the workspace key for workspace + * Return bot's copy of the workspace key for workspace * with id [workspaceId] * @param {Object} obj * @param {String} obj.workspaceId - id of workspace * @returns {String} key - decrypted workspace key */ -const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { - 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: await getEncryptionKey() - }); - - key = decryptAsymmetric({ - ciphertext: botKey.encryptedKey, - nonce: botKey.nonce, - publicKey: botKey.sender.publicKey as string, - privateKey: privateKeyBot - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get workspace key'); - } - - return key; -} +const getKey = async ({ workspaceId }: { workspaceId: string }) => { + const botKey = await BotKey.findOne({ + workspace: workspaceId, + }).populate<{ sender: IUser }>("sender", "publicKey"); + + if (!botKey) throw new Error("Failed to find bot key"); + + const bot = await Bot.findOne({ + workspace: workspaceId, + }).select("+encryptedPrivateKey +iv +tag"); + + if (!bot) throw new Error("Failed to find bot"); + if (!bot.isActive) throw new Error("Bot is not active"); + + const privateKeyBot = decryptSymmetric({ + ciphertext: bot.encryptedPrivateKey, + iv: bot.iv, + tag: bot.tag, + key: await getEncryptionKey(), + }); + + const key = decryptAsymmetric({ + ciphertext: botKey.encryptedKey, + nonce: botKey.nonce, + publicKey: botKey.sender.publicKey as string, + privateKey: privateKeyBot, + }); + + return key; +}; /** * Return symmetrically encrypted [plaintext] using the - * key for workspace with id [workspaceId] + * key for workspace with id [workspaceId] * @param {Object} obj1 * @param {String} obj1.workspaceId - id of workspace * @param {String} obj1.plaintext - plaintext to encrypt */ const encryptSymmetricHelper = async ({ - workspaceId, - plaintext + workspaceId, + plaintext, }: { - workspaceId: Types.ObjectId; - plaintext: string; + workspaceId: Types.ObjectId; + 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'); - } -} + const key = await getKey({ workspaceId: workspaceId.toString() }); + const { ciphertext, iv, tag } = encryptSymmetric({ + plaintext, + key, + }); + + return { + ciphertext, + iv, + tag, + }; +}; /** * Return symmetrically decrypted [ciphertext] using the * key for workspace with id [workspaceId] @@ -277,40 +255,31 @@ const encryptSymmetricHelper = async ({ * @param {String} obj.tag - tag */ const decryptSymmetricHelper = async ({ - workspaceId, + workspaceId, + ciphertext, + iv, + tag, +}: { + workspaceId: Types.ObjectId; + ciphertext: string; + iv: string; + tag: string; +}) => { + const key = await getKey({ workspaceId: workspaceId.toString() }); + const plaintext = decryptSymmetric({ ciphertext, iv, - tag -}: { - workspaceId: Types.ObjectId; - 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; -} + tag, + key, + }); + + return plaintext; +}; export { - validateClientForBot, - createBot, - getSecretsHelper, - encryptSymmetricHelper, - decryptSymmetricHelper -} \ No newline at end of file + validateClientForBot, + createBot, + getSecretsHelper, + encryptSymmetricHelper, + decryptSymmetricHelper, +}; diff --git a/backend/src/helpers/event.ts b/backend/src/helpers/event.ts index 43375814d..c58da5b70 100644 --- a/backend/src/helpers/event.ts +++ b/backend/src/helpers/event.ts @@ -1,14 +1,13 @@ -import { Types } from 'mongoose'; -import * as Sentry from '@sentry/node'; -import { Bot, IBot } from '../models'; -import { EVENT_PUSH_SECRETS } from '../variables'; -import { IntegrationService } from '../services'; +import { Types } from "mongoose"; +import { Bot, IBot } from "../models"; +import { EVENT_PUSH_SECRETS } from "../variables"; +import { IntegrationService } from "../services"; interface Event { - name: string; - workspaceId: Types.ObjectId; - environment?: string; - payload: any; + name: string; + workspaceId: Types.ObjectId; + environment?: string; + payload: any; } /** @@ -19,39 +18,25 @@ interface Event { * @param {String} obj.event.workspaceId - id of workspace that event is part of * @param {Object} obj.event.payload - payload of event (depends on event) */ -const handleEventHelper = async ({ - event -}: { - event: Event; -}) => { - const { - workspaceId, - environment - } = event; - - // 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, - environment - }); - break; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - } -} +const handleEventHelper = async ({ event }: { event: Event }) => { + const { workspaceId, environment } = event; -export { - handleEventHelper -} \ No newline at end of file + // TODO: moduralize bot check into separate function + const bot = await Bot.findOne({ + workspace: workspaceId, + isActive: true, + }); + + if (!bot) return; + + switch (event.name) { + case EVENT_PUSH_SECRETS: + IntegrationService.syncIntegrations({ + workspaceId, + environment, + }); + break; + } +}; + +export { handleEventHelper }; diff --git a/backend/src/helpers/integration.ts b/backend/src/helpers/integration.ts index 4d2666de4..46eb22766 100644 --- a/backend/src/helpers/integration.ts +++ b/backend/src/helpers/integration.ts @@ -256,7 +256,7 @@ const syncIntegrationsHelper = async ({ integration, integrationAuth, secrets, - accessId: access.accessId, + accessId: access.accessId === undefined ? null : access.accessId, accessToken: access.accessToken }); } @@ -482,4 +482,4 @@ export { getIntegrationAuthAccessHelper, setIntegrationAuthRefreshHelper, setIntegrationAuthAccessHelper -} \ No newline at end of file +} diff --git a/backend/src/helpers/key.ts b/backend/src/helpers/key.ts index 76fdcac3b..bbd15ba58 100644 --- a/backend/src/helpers/key.ts +++ b/backend/src/helpers/key.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Key, IKey } from '../models'; interface Key { @@ -27,36 +26,30 @@ const pushKeys = async ({ workspaceId: string; keys: Key[]; }): Promise => { - try { - // filter out already-inserted keys - const keysSet = new Set( - ( - await Key.find( - { - workspace: workspaceId - }, - 'receiver' - ) - ).map((k: IKey) => k.receiver.toString()) - ); + // filter out already-inserted keys + const keysSet = new Set( + ( + await Key.find( + { + workspace: workspaceId + }, + 'receiver' + ) + ).map((k: IKey) => k.receiver.toString()) + ); - keys = keys.filter((key) => !keysSet.has(key.userId)); + keys = keys.filter((key) => !keysSet.has(key.userId)); - // add new shared keys only - await Key.insertMany( - keys.map((k) => ({ - encryptedKey: k.encryptedKey, - nonce: k.nonce, - sender: userId, - receiver: k.userId, - workspace: workspaceId - })) - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to push access keys'); - } + // add new shared keys only + await Key.insertMany( + keys.map((k) => ({ + encryptedKey: k.encryptedKey, + nonce: k.nonce, + sender: userId, + receiver: k.userId, + workspace: workspaceId + })) + ); }; export { pushKeys }; diff --git a/backend/src/helpers/membershipOrg.ts b/backend/src/helpers/membershipOrg.ts index b34e5dd2f..d8b944145 100644 --- a/backend/src/helpers/membershipOrg.ts +++ b/backend/src/helpers/membershipOrg.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { MembershipOrg, @@ -144,15 +143,7 @@ const validateMembershipOrg = async ({ * @return {Object} membershipOrg - membership */ const findMembershipOrg = (queryObj: any) => { - let membershipOrg; - try { - membershipOrg = MembershipOrg.findOne(queryObj); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to find organization membership'); - } - + const membershipOrg = MembershipOrg.findOne(queryObj); return membershipOrg; }; @@ -175,33 +166,27 @@ const addMembershipsOrg = async ({ roles: string[]; statuses: string[]; }) => { - try { - const operations = userIds.map((userId, idx) => { - return { - updateOne: { - filter: { - user: userId, - organization: organizationId, - role: roles[idx], - status: statuses[idx] - }, - update: { - user: userId, - organization: organizationId, - role: roles[idx], - status: statuses[idx] - }, - upsert: true - } - }; - }); + const operations = userIds.map((userId, idx) => { + return { + updateOne: { + filter: { + user: userId, + organization: organizationId, + role: roles[idx], + status: statuses[idx] + }, + update: { + user: userId, + organization: organizationId, + role: roles[idx], + status: statuses[idx] + }, + upsert: true + } + }; + }); - await MembershipOrg.bulkWrite(operations as any); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to add users to organization'); - } + await MembershipOrg.bulkWrite(operations as any); }; /** @@ -214,43 +199,36 @@ const deleteMembershipOrg = async ({ }: { membershipOrgId: string; }) => { - let deletedMembershipOrg; - try { - deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ - _id: membershipOrgId - }); + const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({ + _id: membershipOrgId + }); - if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership'); + if (!deletedMembershipOrg) throw new Error('Failed to delete organization membership'); - // delete keys associated with organization membership - if (deletedMembershipOrg?.user) { - // case: organization membership had a registered user + // delete keys associated with organization membership + if (deletedMembershipOrg?.user) { + // case: organization membership had a registered user - const workspaces = ( - await Workspace.find({ - organization: deletedMembershipOrg.organization - }) - ).map((w) => w._id.toString()); + const workspaces = ( + await Workspace.find({ + organization: deletedMembershipOrg.organization + }) + ).map((w) => w._id.toString()); - await Membership.deleteMany({ - user: deletedMembershipOrg.user, - workspace: { - $in: workspaces - } - }); + await Membership.deleteMany({ + user: deletedMembershipOrg.user, + workspace: { + $in: workspaces + } + }); - await Key.deleteMany({ - receiver: deletedMembershipOrg.user, - workspace: { - $in: workspaces - } - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to delete organization membership'); - } + await Key.deleteMany({ + receiver: deletedMembershipOrg.user, + workspace: { + $in: workspaces + } + }); + } return deletedMembershipOrg; }; diff --git a/backend/src/helpers/organization.ts b/backend/src/helpers/organization.ts index ee52bac8e..9e67ebb00 100644 --- a/backend/src/helpers/organization.ts +++ b/backend/src/helpers/organization.ts @@ -1,39 +1,34 @@ -import * as Sentry from '@sentry/node'; -import Stripe from 'stripe'; -import { Types } from 'mongoose'; +import Stripe from "stripe"; +import { Types } from "mongoose"; import { - IUser, - User, - IServiceAccount, - ServiceAccount, - IServiceTokenData, - ServiceTokenData -} from '../models'; -import { Organization, MembershipOrg } from '../models'; -import { - ACCEPTED, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY, - OWNER -} from '../variables'; -import { - getStripeSecretKey, - getStripeProductPro, - getStripeProductTeam, - getStripeProductStarter -} from '../config'; + IUser, + User, + IServiceAccount, + ServiceAccount, + IServiceTokenData, + ServiceTokenData, +} from "../models"; +import { Organization, MembershipOrg } from "../models"; import { - UnauthorizedRequestError, - OrganizationNotFoundError -} from '../utils/errors'; + ACCEPTED, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY, + OWNER, +} from "../variables"; import { - validateUserClientForOrganization -} from '../helpers/user'; + getStripeSecretKey, + getStripeProductPro, + getStripeProductTeam, + getStripeProductStarter, +} from "../config"; import { - validateServiceAccountClientForOrganization -} from '../helpers/serviceAccount'; + UnauthorizedRequestError, + OrganizationNotFoundError, +} from "../utils/errors"; +import { validateUserClientForOrganization } from "../helpers/user"; +import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount"; /** * Validate accepted clients for organization with id [organizationId] @@ -42,69 +37,80 @@ import { * @param {Types.ObjectId} obj.organizationId - id of organization to validate against */ const validateClientForOrganization = async ({ - authData, - organizationId, - acceptedRoles, - acceptedStatuses + authData, + organizationId, + acceptedRoles, + acceptedStatuses, }: { - authData: { - authMode: string; - authPayload: IUser | IServiceAccount | IServiceTokenData; - }, - organizationId: Types.ObjectId; - acceptedRoles: Array<'owner' | 'admin' | 'member'>; - acceptedStatuses: Array<'invited' | 'accepted'>; + authData: { + authMode: string; + authPayload: IUser | IServiceAccount | IServiceTokenData; + }; + organizationId: Types.ObjectId; + acceptedRoles: Array<"owner" | "admin" | "member">; + acceptedStatuses: Array<"invited" | "accepted">; }) => { - - const organization = await Organization.findById(organizationId); - - if (!organization) { - throw OrganizationNotFoundError({ - message: 'Failed to find organization' - }); - } - - if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) { - const membershipOrg = await validateUserClientForOrganization({ - user: authData.authPayload, - organization, - acceptedRoles, - acceptedStatuses - }); - - return ({ organization, membershipOrg }); - } + const organization = await Organization.findById(organizationId); - if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { - await validateServiceAccountClientForOrganization({ - serviceAccount: authData.authPayload, - organization - }); - - return ({ organization }); - } + if (!organization) { + throw OrganizationNotFoundError({ + message: "Failed to find organization", + }); + } - if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { - throw UnauthorizedRequestError({ - message: 'Failed service token authorization for organization' - }); - } + if ( + authData.authMode === AUTH_MODE_JWT && + authData.authPayload instanceof User + ) { + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses, + }); - if (authData.authMode === AUTH_MODE_API_KEY && authData.authPayload instanceof User) { - const membershipOrg = await validateUserClientForOrganization({ - user: authData.authPayload, - organization, - acceptedRoles, - acceptedStatuses - }); - - return ({ organization, membershipOrg }); - } - - throw UnauthorizedRequestError({ - message: 'Failed client authorization for organization' - }); -} + return { organization, membershipOrg }; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && + authData.authPayload instanceof ServiceAccount + ) { + await validateServiceAccountClientForOrganization({ + serviceAccount: authData.authPayload, + organization, + }); + + return { organization }; + } + + if ( + authData.authMode === AUTH_MODE_SERVICE_TOKEN && + authData.authPayload instanceof ServiceTokenData + ) { + throw UnauthorizedRequestError({ + message: "Failed service token authorization for organization", + }); + } + + if ( + authData.authMode === AUTH_MODE_API_KEY && + authData.authPayload instanceof User + ) { + const membershipOrg = await validateUserClientForOrganization({ + user: authData.authPayload, + organization, + acceptedRoles, + acceptedStatuses, + }); + + return { organization, membershipOrg }; + } + + throw UnauthorizedRequestError({ + message: "Failed client authorization for organization", + }); +}; /** * Create an organization with name [name] @@ -114,43 +120,37 @@ const validateClientForOrganization = async ({ * @param {Object} organization - new organization */ const createOrganization = async ({ - name, - email + name, + email, }: { - name: string; - email: string; + name: string; + email: string; }) => { - let organization; - try { - // register stripe account - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' - }); + let organization; + // register stripe account + const stripe = new Stripe(await getStripeSecretKey(), { + apiVersion: "2022-08-01", + }); - if (await getStripeSecretKey()) { - const customer = await stripe.customers.create({ - email, - description: name - }); + if (await getStripeSecretKey()) { + const customer = await stripe.customers.create({ + email, + description: name, + }); - organization = await new Organization({ - name, - customerId: customer.id - }).save(); - } else { - organization = await new Organization({ - name - }).save(); - } + organization = await new Organization({ + name, + customerId: customer.id, + }).save(); + } else { + organization = await new Organization({ + name, + }).save(); + } - await initSubscriptionOrg({ organizationId: organization._id }); - } catch (err) { - Sentry.setUser({ email }); - Sentry.captureException(err); - throw new Error(`Failed to create organization [err=${err}]`); - } + await initSubscriptionOrg({ organizationId: organization._id }); - return organization; + return organization; }; /** @@ -162,57 +162,52 @@ const createOrganization = async ({ * @return {Subscription} obj.subscription - new subscription */ const initSubscriptionOrg = async ({ - organizationId + organizationId, }: { - organizationId: Types.ObjectId; + organizationId: Types.ObjectId; }) => { - let stripeSubscription; - let subscription; - try { - // find organization - const organization = await Organization.findOne({ - _id: organizationId - }); + let stripeSubscription; + let subscription; - if (organization) { - if (organization.customerId) { - // initialize starter subscription with quantity of 0 - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' - }); + // find organization + const organization = await Organization.findOne({ + _id: organizationId, + }); - const productToPriceMap = { - starter: await getStripeProductStarter(), - team: await getStripeProductTeam(), - pro: await getStripeProductPro() - }; + if (organization) { + if (organization.customerId) { + // initialize starter subscription with quantity of 0 + const stripe = new Stripe(await getStripeSecretKey(), { + apiVersion: "2022-08-01", + }); - stripeSubscription = await stripe.subscriptions.create({ - customer: organization.customerId, - items: [ - { - price: productToPriceMap['starter'], - quantity: 1 - } - ], - payment_behavior: 'default_incomplete', - proration_behavior: 'none', - expand: ['latest_invoice.payment_intent'] - }); - } - } else { - throw new Error('Failed to initialize free organization subscription'); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to initialize free organization subscription'); - } + const productToPriceMap = { + starter: await getStripeProductStarter(), + team: await getStripeProductTeam(), + pro: await getStripeProductPro(), + }; - return { - stripeSubscription, - subscription - }; + stripeSubscription = await stripe.subscriptions.create({ + customer: organization.customerId, + items: [ + { + price: productToPriceMap["starter"], + quantity: 1, + }, + ], + payment_behavior: "default_incomplete", + proration_behavior: "none", + expand: ["latest_invoice.payment_intent"], + }); + } + } else { + throw new Error("Failed to initialize free organization subscription"); + } + + return { + stripeSubscription, + subscription, + }; }; /** @@ -222,54 +217,49 @@ const initSubscriptionOrg = async ({ * @param {Number} obj.organizationId - id of subscription's organization */ const updateSubscriptionOrgQuantity = async ({ - organizationId + organizationId, }: { - organizationId: string; + organizationId: string; }) => { - let stripeSubscription; - try { - // find organization - const organization = await Organization.findOne({ - _id: organizationId - }); + let stripeSubscription; + // find organization + const organization = await Organization.findOne({ + _id: organizationId, + }); - if (organization && organization.customerId) { - const quantity = await MembershipOrg.countDocuments({ - organization: organizationId, - status: ACCEPTED - }); + if (organization && organization.customerId) { + const quantity = await MembershipOrg.countDocuments({ + organization: organizationId, + status: ACCEPTED, + }); - const stripe = new Stripe(await getStripeSecretKey(), { - apiVersion: '2022-08-01' - }); + const stripe = new Stripe(await getStripeSecretKey(), { + apiVersion: "2022-08-01", + }); - const subscription = ( - await stripe.subscriptions.list({ - customer: organization.customerId - }) - ).data[0]; + const subscription = ( + await stripe.subscriptions.list({ + customer: organization.customerId, + }) + ).data[0]; - stripeSubscription = await stripe.subscriptions.update(subscription.id, { - items: [ - { - id: subscription.items.data[0].id, - price: subscription.items.data[0].price.id, - quantity - } - ] - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - } + stripeSubscription = await stripe.subscriptions.update(subscription.id, { + items: [ + { + id: subscription.items.data[0].id, + price: subscription.items.data[0].price.id, + quantity, + }, + ], + }); + } - return stripeSubscription; + return stripeSubscription; }; export { - validateClientForOrganization, - createOrganization, - initSubscriptionOrg, - updateSubscriptionOrgQuantity -}; \ No newline at end of file + validateClientForOrganization, + createOrganization, + initSubscriptionOrg, + updateSubscriptionOrgQuantity, +}; diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index d9e9f807b..e75f46e0f 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,63 +1,52 @@ -import * as Sentry from '@sentry/node'; -import { Types } from 'mongoose'; +import { Types } from "mongoose"; +import { Secret, ISecret, Membership } from "../models"; +import { EESecretService, EELogService } from "../ee/services"; +import { IAction, SecretVersion } from "../ee/models"; import { - Secret, - ISecret, - Membership -} from '../models'; -import { - EESecretService, - EELogService -} from '../ee/services'; -import { - IAction, - SecretVersion -} from '../ee/models'; -import { - SECRET_SHARED, - SECRET_PERSONAL, - ACTION_ADD_SECRETS, - ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS, - ACTION_READ_SECRETS -} from '../variables'; -import _ from 'lodash'; -import { BadRequestError, UnauthorizedRequestError } from '../utils/errors'; + SECRET_SHARED, + SECRET_PERSONAL, + ACTION_ADD_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_DELETE_SECRETS, + ACTION_READ_SECRETS, +} from "../variables"; +import _ from "lodash"; +import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; interface V1PushSecret { - ciphertextKey: string; - ivKey: string; - tagKey: string; - hashKey: string; - ciphertextValue: string; - ivValue: string; - tagValue: string; - hashValue: string; - ciphertextComment: string; - ivComment: string; - tagComment: string; - hashComment: string; - type: 'shared' | 'personal'; + ciphertextKey: string; + ivKey: string; + tagKey: string; + hashKey: string; + ciphertextValue: string; + ivValue: string; + tagValue: string; + hashValue: string; + ciphertextComment: string; + ivComment: string; + tagComment: string; + hashComment: string; + type: "shared" | "personal"; } interface V2PushSecret { - type: string; // personal or shared - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretKeyHash: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretValueHash: string; - secretCommentCiphertext?: string; - secretCommentIV?: string; - secretCommentTag?: string; - secretCommentHash?: string; + type: string; // personal or shared + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; } interface Update { - [index: string]: any; + [index: string]: any; } /** @@ -72,224 +61,225 @@ interface Update { * @param {Object[]} obj.secrets - secrets to push */ const v1PushSecrets = async ({ - userId, - workspaceId, - environment, - secrets, + userId, + workspaceId, + environment, + secrets, }: { - userId: string; - workspaceId: string; - environment: string; - secrets: V1PushSecret[]; + userId: string; + workspaceId: string; + environment: string; + secrets: V1PushSecret[]; }): Promise => { - // TODO: clean up function and fix up types - try { - // construct useful data structures - const oldSecrets = await getSecrets({ - userId, - workspaceId, - environment - }); + // TODO: clean up function and fix up types + // construct useful data structures + const oldSecrets = await getSecrets({ + userId, + workspaceId, + environment, + }); - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => - ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) - , {}); - const newSecretsObj: any = secrets.reduce((accumulator, s) => - ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }) - , {}); + const oldSecretsObj: any = oldSecrets.reduce( + (accumulator, s: any) => ({ + ...accumulator, + [`${s.type}-${s.secretKeyHash}`]: s, + }), + {} + ); + const newSecretsObj: any = secrets.reduce( + (accumulator, s) => ({ ...accumulator, [`${s.type}-${s.hashKey}`]: s }), + {} + ); - // handle deleting secrets - const toDelete = oldSecrets - .filter( - (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) - ) - .map((s) => s._id); - if (toDelete.length > 0) { - await Secret.deleteMany({ - _id: { $in: toDelete } - }); + // handle deleting secrets + const toDelete = oldSecrets + .filter((s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)) + .map((s) => s._id); + if (toDelete.length > 0) { + await Secret.deleteMany({ + _id: { $in: toDelete }, + }); - await EESecretService.markDeletedSecretVersions({ - secretIds: toDelete - }); - } + await EESecretService.markDeletedSecretVersions({ + secretIds: toDelete, + }); + } - const toUpdate = oldSecrets - .filter((s) => { - if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { - if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue - || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment) { - // case: filter secrets where value or comment changed - return true; - } + const toUpdate = oldSecrets.filter((s) => { + if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { + if ( + s.secretValueHash !== + newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashValue || + s.secretCommentHash !== + newSecretsObj[`${s.type}-${s.secretKeyHash}`].hashComment + ) { + // case: filter secrets where value or comment changed + return true; + } - if (!s.version) { - // case: filter (legacy) secrets that were not versioned - return true; - } - } + if (!s.version) { + // case: filter (legacy) secrets that were not versioned + return true; + } + } - return false; - }); + return false; + }); - const operations = toUpdate - .map((s) => { - const { - ciphertextValue, - ivValue, - tagValue, - hashValue, - ciphertextComment, - ivComment, - tagComment, - hashComment - } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + const operations = toUpdate.map((s) => { + const { + ciphertextValue, + ivValue, + tagValue, + hashValue, + ciphertextComment, + ivComment, + tagComment, + hashComment, + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; - const update: Update = { - secretValueCiphertext: ciphertextValue, - secretValueIV: ivValue, - secretValueTag: tagValue, - secretValueHash: hashValue, - secretCommentCiphertext: ciphertextComment, - secretCommentIV: ivComment, - secretCommentTag: tagComment, - secretCommentHash: hashComment, - } + const update: Update = { + secretValueCiphertext: ciphertextValue, + secretValueIV: ivValue, + secretValueTag: tagValue, + secretValueHash: hashValue, + secretCommentCiphertext: ciphertextComment, + secretCommentIV: ivComment, + secretCommentTag: tagComment, + secretCommentHash: hashComment, + }; - if (!s.version) { - // case: (legacy) secret was not versioned - update.version = 1; - } else { - update['$inc'] = { - version: 1 - } - } + if (!s.version) { + // case: (legacy) secret was not versioned + update.version = 1; + } else { + update["$inc"] = { + version: 1, + }; + } - if (s.type === SECRET_PERSONAL) { - // attach user associated with the personal secret - update['user'] = userId; - } + if (s.type === SECRET_PERSONAL) { + // attach user associated with the personal secret + update["user"] = userId; + } - return { - updateOne: { - filter: { - _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id - }, - update - } - }; - }); - await Secret.bulkWrite(operations as any); + return { + updateOne: { + filter: { + _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id, + }, + update, + }, + }; + }); + await Secret.bulkWrite(operations as any); - // (EE) add secret versions for updated secrets - await EESecretService.addSecretVersions({ - secretVersions: toUpdate.map(({ - _id, - version, - type, - secretKeyHash, - }) => { - const newSecret = newSecretsObj[`${type}-${secretKeyHash}`]; - return new SecretVersion({ - secret: _id, - version: version ? version + 1 : 1, - workspace: new Types.ObjectId(workspaceId), - type: newSecret.type, - user: new Types.ObjectId(userId), - environment, - isDeleted: false, - secretKeyCiphertext: newSecret.ciphertextKey, - secretKeyIV: newSecret.ivKey, - secretKeyTag: newSecret.tagKey, - secretKeyHash: newSecret.hashKey, - secretValueCiphertext: newSecret.ciphertextValue, - secretValueIV: newSecret.ivValue, - secretValueTag: newSecret.tagValue, - secretValueHash: newSecret.hashValue - }) - }) - }); + // (EE) add secret versions for updated secrets + await EESecretService.addSecretVersions({ + secretVersions: toUpdate.map(({ _id, version, type, secretKeyHash }) => { + const newSecret = newSecretsObj[`${type}-${secretKeyHash}`]; + return new SecretVersion({ + secret: _id, + version: version ? version + 1 : 1, + workspace: new Types.ObjectId(workspaceId), + type: newSecret.type, + user: new Types.ObjectId(userId), + environment, + isDeleted: false, + secretKeyCiphertext: newSecret.ciphertextKey, + secretKeyIV: newSecret.ivKey, + secretKeyTag: newSecret.tagKey, + secretKeyHash: newSecret.hashKey, + secretValueCiphertext: newSecret.ciphertextValue, + secretValueIV: newSecret.ivValue, + secretValueTag: newSecret.tagValue, + secretValueHash: newSecret.hashValue, + }); + }), + }); - // handle adding new secrets - const toAdd = secrets.filter((s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj)); + // handle adding new secrets + const toAdd = secrets.filter( + (s) => !(`${s.type}-${s.hashKey}` in oldSecretsObj) + ); - if (toAdd.length > 0) { - // add secrets - const newSecrets: ISecret[] = (await Secret.insertMany( - toAdd.map((s, idx) => { - const obj: any = { - version: 1, - workspace: workspaceId, - type: toAdd[idx].type, - environment, - secretKeyCiphertext: s.ciphertextKey, - secretKeyIV: s.ivKey, - secretKeyTag: s.tagKey, - secretKeyHash: s.hashKey, - secretValueCiphertext: s.ciphertextValue, - secretValueIV: s.ivValue, - secretValueTag: s.tagValue, - secretValueHash: s.hashValue, - secretCommentCiphertext: s.ciphertextComment, - secretCommentIV: s.ivComment, - secretCommentTag: s.tagComment, - secretCommentHash: s.hashComment - }; + if (toAdd.length > 0) { + // add secrets + const newSecrets: ISecret[] = ( + await Secret.insertMany( + toAdd.map((s, idx) => { + const obj: any = { + version: 1, + workspace: workspaceId, + type: toAdd[idx].type, + environment, + secretKeyCiphertext: s.ciphertextKey, + secretKeyIV: s.ivKey, + secretKeyTag: s.tagKey, + secretKeyHash: s.hashKey, + secretValueCiphertext: s.ciphertextValue, + secretValueIV: s.ivValue, + secretValueTag: s.tagValue, + secretValueHash: s.hashValue, + secretCommentCiphertext: s.ciphertextComment, + secretCommentIV: s.ivComment, + secretCommentTag: s.tagComment, + secretCommentHash: s.hashComment, + }; - if (toAdd[idx].type === 'personal') { - obj['user' as keyof typeof obj] = userId; - } + if (toAdd[idx].type === "personal") { + obj["user" as keyof typeof obj] = userId; + } - return obj; - }) - )).map((insertedSecret) => insertedSecret.toObject()); + return obj; + }) + ) + ).map((insertedSecret) => insertedSecret.toObject()); - // (EE) add secret versions for new secrets - EESecretService.addSecretVersions({ - secretVersions: newSecrets.map(({ - _id, - version, - workspace, - type, - user, - environment, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash - }) => new SecretVersion({ - secret: _id, - version, - workspace, - type, - user, - environment, - isDeleted: false, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash - })) - }); - } + // (EE) add secret versions for new secrets + EESecretService.addSecretVersions({ + secretVersions: newSecrets.map( + ({ + _id, + version, + workspace, + type, + user, + environment, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + }) => + new SecretVersion({ + secret: _id, + version, + workspace, + type, + user, + environment, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + }) + ), + }); + } - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(workspaceId) - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to push shared and personal secrets'); - } + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: new Types.ObjectId(workspaceId), + }); }; /** @@ -306,212 +296,216 @@ const v1PushSecrets = async ({ * @param {String} obj.ipAddress - ip address of request to push secrets */ const v2PushSecrets = async ({ - userId, - workspaceId, - environment, - secrets, - channel, - ipAddress + userId, + workspaceId, + environment, + secrets, + channel, + ipAddress, }: { - userId: string; - workspaceId: string; - environment: string; - secrets: V2PushSecret[]; - channel: string; - ipAddress: string; + userId: string; + workspaceId: string; + environment: string; + secrets: V2PushSecret[]; + channel: string; + ipAddress: string; }): Promise => { - // TODO: clean up function and fix up types - try { - const actions: IAction[] = []; + // TODO: clean up function and fix up types + const actions: IAction[] = []; - // construct useful data structures - const oldSecrets = await getSecrets({ - userId, - workspaceId, - environment - }); + // construct useful data structures + const oldSecrets = await getSecrets({ + userId, + workspaceId, + environment, + }); - const oldSecretsObj: any = oldSecrets.reduce((accumulator, s: any) => - ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) - , {}); - const newSecretsObj: any = secrets.reduce((accumulator, s) => - ({ ...accumulator, [`${s.type}-${s.secretKeyHash}`]: s }) - , {}); + const oldSecretsObj: any = oldSecrets.reduce( + (accumulator, s: any) => ({ + ...accumulator, + [`${s.type}-${s.secretKeyHash}`]: s, + }), + {} + ); + const newSecretsObj: any = secrets.reduce( + (accumulator, s) => ({ + ...accumulator, + [`${s.type}-${s.secretKeyHash}`]: s, + }), + {} + ); - // handle deleting secrets - const toDelete = oldSecrets - .filter( - (s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj) - ) - .map((s) => s._id); - if (toDelete.length > 0) { - await Secret.deleteMany({ - _id: { $in: toDelete } - }); + // handle deleting secrets + const toDelete = oldSecrets + .filter((s: ISecret) => !(`${s.type}-${s.secretKeyHash}` in newSecretsObj)) + .map((s) => s._id); + if (toDelete.length > 0) { + await Secret.deleteMany({ + _id: { $in: toDelete }, + }); - await EESecretService.markDeletedSecretVersions({ - secretIds: toDelete - }); + await EESecretService.markDeletedSecretVersions({ + secretIds: toDelete, + }); - const deleteAction = await EELogService.createAction({ - name: ACTION_DELETE_SECRETS, - userId: new Types.ObjectId(userId), - workspaceId: new Types.ObjectId(userId), - secretIds: toDelete - }); + const deleteAction = await EELogService.createAction({ + name: ACTION_DELETE_SECRETS, + userId: new Types.ObjectId(userId), + workspaceId: new Types.ObjectId(userId), + secretIds: toDelete, + }); - deleteAction && actions.push(deleteAction); - } + deleteAction && actions.push(deleteAction); + } - const toUpdate = oldSecrets - .filter((s) => { - if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { - if (s.secretValueHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash - || s.secretCommentHash !== newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash) { - // case: filter secrets where value or comment changed - return true; - } + const toUpdate = oldSecrets.filter((s) => { + if (`${s.type}-${s.secretKeyHash}` in newSecretsObj) { + if ( + s.secretValueHash !== + newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretValueHash || + s.secretCommentHash !== + newSecretsObj[`${s.type}-${s.secretKeyHash}`].secretCommentHash + ) { + // case: filter secrets where value or comment changed + return true; + } - if (!s.version) { - // case: filter (legacy) secrets that were not versioned - return true; - } - } + if (!s.version) { + // case: filter (legacy) secrets that were not versioned + return true; + } + } - return false; - }); + return false; + }); - if (toUpdate.length > 0) { - const operations = toUpdate - .map((s) => { - const { - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentHash, - } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; + if (toUpdate.length > 0) { + const operations = toUpdate.map((s) => { + const { + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + } = newSecretsObj[`${s.type}-${s.secretKeyHash}`]; - const update: Update = { - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - secretCommentHash, - } + const update: Update = { + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + secretCommentHash, + }; - if (!s.version) { - // case: (legacy) secret was not versioned - update.version = 1; - } else { - update['$inc'] = { - version: 1 - } - } + if (!s.version) { + // case: (legacy) secret was not versioned + update.version = 1; + } else { + update["$inc"] = { + version: 1, + }; + } - if (s.type === SECRET_PERSONAL) { - // attach user associated with the personal secret - update['user'] = userId; - } + if (s.type === SECRET_PERSONAL) { + // attach user associated with the personal secret + update["user"] = userId; + } - return { - updateOne: { - filter: { - _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id - }, - update - } - }; - }); - await Secret.bulkWrite(operations as any); + return { + updateOne: { + filter: { + _id: oldSecretsObj[`${s.type}-${s.secretKeyHash}`]._id, + }, + update, + }, + }; + }); + await Secret.bulkWrite(operations as any); - // (EE) add secret versions for updated secrets - await EESecretService.addSecretVersions({ - secretVersions: toUpdate.map((s) => { - return ({ - ...newSecretsObj[`${s.type}-${s.secretKeyHash}`], - secret: s._id, - version: s.version ? s.version + 1 : 1, - workspace: new Types.ObjectId(workspaceId), - user: s.user, - environment: s.environment, - isDeleted: false - }) - }) - }); + // (EE) add secret versions for updated secrets + await EESecretService.addSecretVersions({ + secretVersions: toUpdate.map((s) => { + return { + ...newSecretsObj[`${s.type}-${s.secretKeyHash}`], + secret: s._id, + version: s.version ? s.version + 1 : 1, + workspace: new Types.ObjectId(workspaceId), + user: s.user, + environment: s.environment, + isDeleted: false, + }; + }), + }); - const updateAction = await EELogService.createAction({ - name: ACTION_UPDATE_SECRETS, - userId: new Types.ObjectId(userId), - workspaceId: new Types.ObjectId(workspaceId), - secretIds: toUpdate.map((u) => u._id) - }); + const updateAction = await EELogService.createAction({ + name: ACTION_UPDATE_SECRETS, + userId: new Types.ObjectId(userId), + workspaceId: new Types.ObjectId(workspaceId), + secretIds: toUpdate.map((u) => u._id), + }); - updateAction && actions.push(updateAction); - } + updateAction && actions.push(updateAction); + } - // handle adding new secrets - const toAdd = secrets.filter((s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj)); + // handle adding new secrets + const toAdd = secrets.filter( + (s) => !(`${s.type}-${s.secretKeyHash}` in oldSecretsObj) + ); - if (toAdd.length > 0) { - // add secrets - const newSecrets = await Secret.insertMany( - toAdd.map((s, idx) => ({ - ...s, - version: 1, - workspace: workspaceId, - type: toAdd[idx].type, - environment, - ...(toAdd[idx].type === 'personal' ? { user: userId } : {}) - })) - ); + if (toAdd.length > 0) { + // add secrets + const newSecrets = await Secret.insertMany( + toAdd.map((s, idx) => ({ + ...s, + version: 1, + workspace: workspaceId, + type: toAdd[idx].type, + environment, + ...(toAdd[idx].type === "personal" ? { user: userId } : {}), + })) + ); - // (EE) add secret versions for new secrets - EESecretService.addSecretVersions({ - secretVersions: newSecrets.map((secretDocument: ISecret) => { - return new SecretVersion({ - ...secretDocument, - secret: secretDocument._id, - isDeleted: false - }) - }) - }); + // (EE) add secret versions for new secrets + EESecretService.addSecretVersions({ + secretVersions: newSecrets.map((secretDocument: ISecret) => { + return new SecretVersion({ + ...secretDocument, + secret: secretDocument._id, + isDeleted: false, + }); + }), + }); - const addAction = await EELogService.createAction({ - name: ACTION_ADD_SECRETS, - userId: new Types.ObjectId(userId), - workspaceId: new Types.ObjectId(workspaceId), - secretIds: newSecrets.map((n) => n._id) - }); - addAction && actions.push(addAction); - } + const addAction = await EELogService.createAction({ + name: ACTION_ADD_SECRETS, + userId: new Types.ObjectId(userId), + workspaceId: new Types.ObjectId(workspaceId), + secretIds: newSecrets.map((n) => n._id), + }); + addAction && actions.push(addAction); + } - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(workspaceId) - }) + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: new Types.ObjectId(workspaceId), + }); - // (EE) create (audit) log - if (actions.length > 0) { - await EELogService.createLog({ - userId: new Types.ObjectId(userId), - workspaceId: new Types.ObjectId(workspaceId), - actions, - channel, - ipAddress - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to push shared and personal secrets'); - } + // (EE) create (audit) log + if (actions.length > 0) { + await EELogService.createLog({ + userId: new Types.ObjectId(userId), + workspaceId: new Types.ObjectId(workspaceId), + actions, + channel, + ipAddress, + }); + } }; /** @@ -523,41 +517,33 @@ const v2PushSecrets = async ({ * @param {String} obj.environment - environment for secrets */ const getSecrets = async ({ - userId, - workspaceId, - environment + userId, + workspaceId, + environment, }: { - userId: string; - workspaceId: string; - environment: string; + userId: string; + workspaceId: string; + environment: string; }): Promise => { - let secrets: any; // TODO: FIX any + // get shared workspace secrets + const sharedSecrets = await Secret.find({ + workspace: workspaceId, + environment, + type: SECRET_SHARED, + }); - try { - // get shared workspace secrets - const sharedSecrets = await Secret.find({ - workspace: workspaceId, - environment, - type: SECRET_SHARED - }); + // get personal workspace secrets + const personalSecrets = await Secret.find({ + workspace: workspaceId, + environment, + type: SECRET_PERSONAL, + user: userId, + }); - // get personal workspace secrets - const personalSecrets = await Secret.find({ - workspace: workspaceId, - environment, - type: SECRET_PERSONAL, - user: userId - }); + // concat shared and personal workspace secrets + const secrets = personalSecrets.concat(sharedSecrets); - // concat shared and personal workspace secrets - secrets = personalSecrets.concat(sharedSecrets); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to pull shared and personal secrets'); - } - - return secrets; + return secrets; }; /** @@ -571,48 +557,41 @@ const getSecrets = async ({ * @param {String} obj.ipAddress - ip address of request to push secrets */ const pullSecrets = async ({ - userId, - workspaceId, - environment, - channel, - ipAddress + userId, + workspaceId, + environment, + channel, + ipAddress, }: { - userId: string; - workspaceId: string; - environment: string; - channel: string; - ipAddress: string; + userId: string; + workspaceId: string; + environment: string; + channel: string; + ipAddress: string; }): Promise => { - let secrets: any; + const secrets = await getSecrets({ + userId, + workspaceId, + environment, + }); - try { - secrets = await getSecrets({ - userId, - workspaceId, - environment - }) + const readAction = await EELogService.createAction({ + name: ACTION_READ_SECRETS, + userId: new Types.ObjectId(userId), + workspaceId: new Types.ObjectId(workspaceId), + secretIds: secrets.map((n: any) => n._id), + }); - const readAction = await EELogService.createAction({ - name: ACTION_READ_SECRETS, - userId: new Types.ObjectId(userId), - workspaceId: new Types.ObjectId(workspaceId), - secretIds: secrets.map((n: any) => n._id) - }); + readAction && + (await EELogService.createLog({ + userId: new Types.ObjectId(userId), + workspaceId: new Types.ObjectId(workspaceId), + actions: [readAction], + channel, + ipAddress, + })); - readAction && await EELogService.createLog({ - userId: new Types.ObjectId(userId), - workspaceId: new Types.ObjectId(workspaceId), - actions: [readAction], - channel, - ipAddress - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to pull shared and personal secrets'); - } - - return secrets; + return secrets; }; /** @@ -622,47 +601,35 @@ const pullSecrets = async ({ * @param {Object} obj.secrets */ const reformatPullSecrets = ({ secrets }: { secrets: ISecret[] }) => { - let reformatedSecrets; - try { - reformatedSecrets = secrets.map((s) => ({ - _id: s._id, - workspace: s.workspace, - type: s.type, - environment: s.environment, - secretKey: { - workspace: s.workspace, - ciphertext: s.secretKeyCiphertext, - iv: s.secretKeyIV, - tag: s.secretKeyTag, - hash: s.secretKeyHash - }, - secretValue: { - workspace: s.workspace, - ciphertext: s.secretValueCiphertext, - iv: s.secretValueIV, - tag: s.secretValueTag, - hash: s.secretValueHash - }, - secretComment: { - workspace: s.workspace, - ciphertext: s.secretCommentCiphertext, - iv: s.secretCommentIV, - tag: s.secretCommentTag, - hash: s.secretCommentHash - } - })); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to reformat pulled secrets'); - } + const reformatedSecrets = secrets.map((s) => ({ + _id: s._id, + workspace: s.workspace, + type: s.type, + environment: s.environment, + secretKey: { + workspace: s.workspace, + ciphertext: s.secretKeyCiphertext, + iv: s.secretKeyIV, + tag: s.secretKeyTag, + hash: s.secretKeyHash, + }, + secretValue: { + workspace: s.workspace, + ciphertext: s.secretValueCiphertext, + iv: s.secretValueIV, + tag: s.secretValueTag, + hash: s.secretValueHash, + }, + secretComment: { + workspace: s.workspace, + ciphertext: s.secretCommentCiphertext, + iv: s.secretCommentIV, + tag: s.secretCommentTag, + hash: s.secretCommentHash, + }, + })); - return reformatedSecrets; + return reformatedSecrets; }; -export { - v1PushSecrets, - v2PushSecrets, - pullSecrets, - reformatPullSecrets -}; +export { v1PushSecrets, v2PushSecrets, pullSecrets, reformatPullSecrets }; diff --git a/backend/src/helpers/token.ts b/backend/src/helpers/token.ts index 8fcbb1dd6..b44641dae 100644 --- a/backend/src/helpers/token.ts +++ b/backend/src/helpers/token.ts @@ -1,16 +1,15 @@ -import * as Sentry from '@sentry/node'; -import { Types } from 'mongoose'; -import { TokenData } from '../models'; -import crypto from 'crypto'; -import bcrypt from 'bcrypt'; +import { Types } from "mongoose"; +import { TokenData } from "../models"; +import crypto from "crypto"; +import bcrypt from "bcrypt"; import { - TOKEN_EMAIL_CONFIRMATION, - TOKEN_EMAIL_MFA, - TOKEN_EMAIL_ORG_INVITATION, - TOKEN_EMAIL_PASSWORD_RESET -} from '../variables'; -import { UnauthorizedRequestError } from '../utils/errors'; -import { getSaltRounds } from '../config'; + TOKEN_EMAIL_CONFIRMATION, + TOKEN_EMAIL_MFA, + TOKEN_EMAIL_ORG_INVITATION, + TOKEN_EMAIL_PASSWORD_RESET, +} from "../variables"; +import { UnauthorizedRequestError } from "../utils/errors"; +import { getSaltRounds } from "../config"; /** * Create and store a token in the database for purpose [type] @@ -22,194 +21,197 @@ import { getSaltRounds } from '../config'; * @returns {String} token - the created token */ const createTokenHelper = async ({ - type, - email, - phoneNumber, - organizationId + type, + email, + phoneNumber, + organizationId, }: { - type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset'; + type: + | "emailConfirmation" + | "emailMfa" + | "organizationInvitation" + | "passwordReset"; + email?: string; + phoneNumber?: string; + organizationId?: Types.ObjectId; +}) => { + let token, expiresAt, triesLeft; + // generate random token based on specified token use-case + // type [type] + switch (type) { + case TOKEN_EMAIL_CONFIRMATION: + // generate random 6-digit code + token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1)); + expiresAt = new Date(new Date().getTime() + 86400000); + break; + case TOKEN_EMAIL_MFA: + // generate random 6-digit code + token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1)); + triesLeft = 5; + expiresAt = new Date(new Date().getTime() + 300000); + break; + case TOKEN_EMAIL_ORG_INVITATION: + // generate random hex + token = crypto.randomBytes(16).toString("hex"); + expiresAt = new Date(new Date().getTime() + 259200000); + break; + case TOKEN_EMAIL_PASSWORD_RESET: + // generate random hex + token = crypto.randomBytes(16).toString("hex"); + expiresAt = new Date(new Date().getTime() + 86400000); + break; + default: + token = crypto.randomBytes(16).toString("hex"); + expiresAt = new Date(); + break; + } + + interface TokenDataQuery { + type: string; email?: string; phoneNumber?: string; - organizationId?: Types.ObjectId -}) => { - let token, expiresAt, triesLeft; - try { - // generate random token based on specified token use-case - // type [type] - switch (type) { - case TOKEN_EMAIL_CONFIRMATION: - // generate random 6-digit code - token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1)); - expiresAt = new Date((new Date()).getTime() + 86400000); - break; - case TOKEN_EMAIL_MFA: - // generate random 6-digit code - token = String(crypto.randomInt(Math.pow(10, 5), Math.pow(10, 6) - 1)); - triesLeft = 5; - expiresAt = new Date((new Date()).getTime() + 300000); - break; - case TOKEN_EMAIL_ORG_INVITATION: - // generate random hex - token = crypto.randomBytes(16).toString('hex'); - expiresAt = new Date((new Date()).getTime() + 259200000); - break; - case TOKEN_EMAIL_PASSWORD_RESET: - // generate random hex - token = crypto.randomBytes(16).toString('hex'); - expiresAt = new Date((new Date()).getTime() + 86400000); - break; - default: - token = crypto.randomBytes(16).toString('hex'); - expiresAt = new Date(); - break; - } - - interface TokenDataQuery { - type: string; - email?: string; - phoneNumber?: string; - organization?: Types.ObjectId; - } - - interface TokenDataUpdate { - type: string; - email?: string; - phoneNumber?: string; - organization?: Types.ObjectId; - tokenHash: string; - triesLeft?: number; - expiresAt: Date; - } + organization?: Types.ObjectId; + } - const query: TokenDataQuery = { type }; - const update: TokenDataUpdate = { - type, - tokenHash: await bcrypt.hash(token, await getSaltRounds()), - expiresAt - } + interface TokenDataUpdate { + type: string; + email?: string; + phoneNumber?: string; + organization?: Types.ObjectId; + tokenHash: string; + triesLeft?: number; + expiresAt: Date; + } - if (email) { - query.email = email; - update.email = email; - } - if (phoneNumber) { - query.phoneNumber = phoneNumber; - update.phoneNumber = phoneNumber; - } - if (organizationId) { - query.organization = organizationId - update.organization = organizationId - } - - if (triesLeft) { - update.triesLeft = triesLeft; - } - - await TokenData.findOneAndUpdate( - query, - update, - { - new: true, - upsert: true - } - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error( - "Failed to create token" - ); - } - - return token; -} + const query: TokenDataQuery = { type }; + const update: TokenDataUpdate = { + type, + tokenHash: await bcrypt.hash(token, await getSaltRounds()), + expiresAt, + }; + + if (email) { + query.email = email; + update.email = email; + } + if (phoneNumber) { + query.phoneNumber = phoneNumber; + update.phoneNumber = phoneNumber; + } + if (organizationId) { + query.organization = organizationId; + update.organization = organizationId; + } + + if (triesLeft) { + update.triesLeft = triesLeft; + } + + await TokenData.findOneAndUpdate(query, update, { + new: true, + upsert: true, + }); + + return token; +}; /** - * + * * @param {Object} obj * @param {String} obj.email - email associated with the token * @param {String} obj.token - value of the token */ const validateTokenHelper = async ({ - type, - email, - phoneNumber, - organizationId, - token + type, + email, + phoneNumber, + organizationId, + token, }: { - type: 'emailConfirmation' | 'emailMfa' | 'organizationInvitation' | 'passwordReset'; + type: + | "emailConfirmation" + | "emailMfa" + | "organizationInvitation" + | "passwordReset"; + email?: string; + phoneNumber?: string; + organizationId?: Types.ObjectId; + token: string; +}) => { + interface Query { + type: string; email?: string; phoneNumber?: string; - organizationId?: Types.ObjectId; - token: string; -}) => { - interface Query { - type: string; - email?: string; - phoneNumber?: string; - organization?: Types.ObjectId; - } + organization?: Types.ObjectId; + } - const query: Query = { type }; + const query: Query = { type }; - if (email) { query.email = email; } - if (phoneNumber) { query.phoneNumber = phoneNumber; } - if (organizationId) { query.organization = organizationId; } + if (email) { + query.email = email; + } + if (phoneNumber) { + query.phoneNumber = phoneNumber; + } + if (organizationId) { + query.organization = organizationId; + } - const tokenData = await TokenData.findOne(query).select('+tokenHash'); - - if (!tokenData) throw new Error('Failed to find token to validate'); - - if (tokenData.expiresAt < new Date()) { - // case: token expired - await TokenData.findByIdAndDelete(tokenData._id); - throw UnauthorizedRequestError({ - message: 'MFA session expired. Please log in again', - context: { - code: 'mfa_expired' - } - }); - } + const tokenData = await TokenData.findOne(query).select("+tokenHash"); - const isValid = await bcrypt.compare(token, tokenData.tokenHash); - if (!isValid) { - // case: token is not valid - if (tokenData?.triesLeft !== undefined) { - // case: token has a try-limit - if (tokenData.triesLeft === 1) { - // case: token is out of tries - await TokenData.findByIdAndDelete(tokenData._id); - } else { - // case: token has more than 1 try left - await TokenData.findByIdAndUpdate(tokenData._id, { - triesLeft: tokenData.triesLeft - 1 - }, { - new: true - }); - } + if (!tokenData) throw new Error("Failed to find token to validate"); - throw UnauthorizedRequestError({ - message: 'MFA code is invalid', - context: { - code: 'mfa_invalid', - triesLeft: tokenData.triesLeft - 1 - } - }); - } - - throw UnauthorizedRequestError({ - message: 'MFA code is invalid', - context: { - code: 'mfa_invalid' - } - }); - } - - // case: token is valid + if (tokenData.expiresAt < new Date()) { + // case: token expired await TokenData.findByIdAndDelete(tokenData._id); -} + throw UnauthorizedRequestError({ + message: "MFA session expired. Please log in again", + context: { + code: "mfa_expired", + }, + }); + } -export { - createTokenHelper, - validateTokenHelper -} \ No newline at end of file + const isValid = await bcrypt.compare(token, tokenData.tokenHash); + if (!isValid) { + // case: token is not valid + if (tokenData?.triesLeft !== undefined) { + // case: token has a try-limit + if (tokenData.triesLeft === 1) { + // case: token is out of tries + await TokenData.findByIdAndDelete(tokenData._id); + } else { + // case: token has more than 1 try left + await TokenData.findByIdAndUpdate( + tokenData._id, + { + triesLeft: tokenData.triesLeft - 1, + }, + { + new: true, + } + ); + } + + throw UnauthorizedRequestError({ + message: "MFA code is invalid", + context: { + code: "mfa_invalid", + triesLeft: tokenData.triesLeft - 1, + }, + }); + } + + throw UnauthorizedRequestError({ + message: "MFA code is invalid", + context: { + code: "mfa_invalid", + }, + }); + } + + // case: token is valid + await TokenData.findByIdAndDelete(tokenData._id); +}; + +export { createTokenHelper, validateTokenHelper }; diff --git a/backend/src/helpers/user.ts b/backend/src/helpers/user.ts index 5a33f3d2e..73b87f5d6 100644 --- a/backend/src/helpers/user.ts +++ b/backend/src/helpers/user.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { IUser, @@ -28,16 +27,9 @@ import { * @returns {Object} user - the initialized user */ const setupAccount = async ({ email }: { email: string }) => { - let user; - try { - user = await new User({ - email - }).save(); - } catch (err) { - Sentry.setUser({ email }); - Sentry.captureException(err); - throw new Error('Failed to set up account'); - } + const user = await new User({ + email + }).save(); return user; }; @@ -89,34 +81,27 @@ const completeAccount = async ({ salt: string; verifier: string; }) => { - let user; - try { - const options = { - new: true - }; - user = await User.findByIdAndUpdate( - userId, - { - firstName, - lastName, - encryptionVersion, - protectedKey, - protectedKeyIV, - protectedKeyTag, - publicKey, - encryptedPrivateKey, - iv: encryptedPrivateKeyIV, - tag: encryptedPrivateKeyTag, - salt, - verifier - }, - options - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to complete account set up'); - } + const options = { + new: true + }; + const user = await User.findByIdAndUpdate( + userId, + { + firstName, + lastName, + encryptionVersion, + protectedKey, + protectedKeyIV, + protectedKeyTag, + publicKey, + encryptedPrivateKey, + iv: encryptedPrivateKeyIV, + tag: encryptedPrivateKeyTag, + salt, + verifier + }, + options + ); return user; }; diff --git a/backend/src/integrations/apps.ts b/backend/src/integrations/apps.ts index b5b17aa4c..5feb5e6d8 100644 --- a/backend/src/integrations/apps.ts +++ b/backend/src/integrations/apps.ts @@ -1,7 +1,6 @@ -import * as Sentry from "@sentry/node"; import { Octokit } from "@octokit/rest"; import { IIntegrationAuth } from "../models"; -import request from '../config/request'; +import request from "../config/request"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AWS_PARAMETER_STORE, @@ -26,7 +25,7 @@ import { INTEGRATION_FLYIO_API_URL, INTEGRATION_CIRCLECI_API_URL, INTEGRATION_TRAVISCI_API_URL, - INTEGRATION_SUPABASE_API_URL + INTEGRATION_SUPABASE_API_URL, } from "../variables"; interface App { @@ -47,87 +46,80 @@ interface App { const getApps = async ({ integrationAuth, accessToken, - teamId + teamId, }: { integrationAuth: IIntegrationAuth; accessToken: string; teamId?: string; }) => { - let apps: App[] = []; - try { - switch (integrationAuth.integration) { - case INTEGRATION_AZURE_KEY_VAULT: - apps = []; - break; - case INTEGRATION_AWS_PARAMETER_STORE: - apps = []; - break; - case INTEGRATION_AWS_SECRET_MANAGER: - apps = []; - break; - case INTEGRATION_HEROKU: - apps = await getAppsHeroku({ - accessToken, - }); - break; - case INTEGRATION_VERCEL: - apps = await getAppsVercel({ - integrationAuth, - accessToken, - }); - break; - case INTEGRATION_NETLIFY: - apps = await getAppsNetlify({ - accessToken, - }); - break; - case INTEGRATION_GITHUB: - apps = await getAppsGithub({ - accessToken, - }); - break; - case INTEGRATION_GITLAB: - apps = await getAppsGitlab({ - accessToken, - teamId - }); - break; - case INTEGRATION_RENDER: - apps = await getAppsRender({ - accessToken, - }); - break; - case INTEGRATION_RAILWAY: - apps = await getAppsRailway({ - accessToken - }); - break; - case INTEGRATION_FLYIO: - apps = await getAppsFlyio({ - accessToken, - }); - break; - case INTEGRATION_CIRCLECI: - apps = await getAppsCircleCI({ - accessToken, - }); - break; - case INTEGRATION_TRAVISCI: - apps = await getAppsTravisCI({ - accessToken, - }) - break; - case INTEGRATION_SUPABASE: - apps = await getAppsSupabase({ - accessToken - }); - break; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get integration apps"); + switch (integrationAuth.integration) { + case INTEGRATION_AZURE_KEY_VAULT: + apps = []; + break; + case INTEGRATION_AWS_PARAMETER_STORE: + apps = []; + break; + case INTEGRATION_AWS_SECRET_MANAGER: + apps = []; + break; + case INTEGRATION_HEROKU: + apps = await getAppsHeroku({ + accessToken, + }); + break; + case INTEGRATION_VERCEL: + apps = await getAppsVercel({ + integrationAuth, + accessToken, + }); + break; + case INTEGRATION_NETLIFY: + apps = await getAppsNetlify({ + accessToken, + }); + break; + case INTEGRATION_GITHUB: + apps = await getAppsGithub({ + accessToken, + }); + break; + case INTEGRATION_GITLAB: + apps = await getAppsGitlab({ + accessToken, + teamId, + }); + break; + case INTEGRATION_RENDER: + apps = await getAppsRender({ + accessToken, + }); + break; + case INTEGRATION_RAILWAY: + apps = await getAppsRailway({ + accessToken, + }); + break; + case INTEGRATION_FLYIO: + apps = await getAppsFlyio({ + accessToken, + }); + break; + case INTEGRATION_CIRCLECI: + apps = await getAppsCircleCI({ + accessToken, + }); + break; + case INTEGRATION_TRAVISCI: + apps = await getAppsTravisCI({ + accessToken, + }); + break; + case INTEGRATION_SUPABASE: + apps = await getAppsSupabase({ + accessToken, + }); + break; } return apps; @@ -141,25 +133,18 @@ const getApps = async ({ * @returns {String} apps.name - name of Heroku app */ const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { - let apps; - try { - const res = ( - await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { - headers: { - Accept: "application/vnd.heroku+json; version=3", - Authorization: `Bearer ${accessToken}`, - }, - }) - ).data; + const res = ( + await request.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"); - } + const apps = res.map((a: any) => ({ + name: a.name, + })); return apps; }; @@ -178,33 +163,26 @@ const getAppsVercel = async ({ integrationAuth: IIntegrationAuth; accessToken: string; }) => { - let apps; - try { - const res = ( - await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - }, - ...(integrationAuth?.teamId - ? { - params: { - teamId: integrationAuth.teamId, - }, - } - : {}), - }) - ).data; + const res = ( + await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + ...(integrationAuth?.teamId + ? { + params: { + teamId: integrationAuth.teamId, + }, + } + : {}), + }) + ).data; - apps = res.projects.map((a: any) => ({ - name: a.name, - appId: a.id - })); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get Vercel integration apps"); - } + const apps = res.projects.map((a: any) => ({ + name: a.name, + appId: a.id, + })); return apps; }; @@ -218,43 +196,40 @@ const getAppsVercel = async ({ */ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { const apps: any = []; - try { - let page = 1; - const perPage = 10; - let hasMorePages = true; - - // paginate through all sites - while (hasMorePages) { - const params = new URLSearchParams({ - page: String(page), - per_page: String(perPage) - }); + let page = 1; + const perPage = 10; + let hasMorePages = true; - const { data } = await request.get(`${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, { + // paginate through all sites + while (hasMorePages) { + const params = new URLSearchParams({ + page: String(page), + per_page: String(perPage), + }); + + const { data } = await request.get( + `${INTEGRATION_NETLIFY_API_URL}/api/v1/sites`, + { params, headers: { Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } - }); - - data.map((a: any) => { - apps.push({ - name: a.name, - appId: a.site_id - }); - }); - - if (data.length < perPage) { - hasMorePages = false; + "Accept-Encoding": "application/json", + }, } + ); - page++; + data.map((a: any) => { + apps.push({ + name: a.name, + appId: a.site_id, + }); + }); + + if (data.length < perPage) { + hasMorePages = false; } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get Netlify integration apps"); + + page++; } return apps; @@ -268,67 +243,59 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of Github site */ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { - let apps; - try { - interface GitHubApp { - id: string; - name: string; - permissions: { - admin: boolean; - }; - owner: { - login: string; + interface GitHubApp { + id: string; + name: string; + permissions: { + admin: boolean; + }; + owner: { + login: string; + }; + } + + const octokit = new Octokit({ + auth: accessToken, + }); + + const getAllRepos = async () => { + let repos: GitHubApp[] = []; + let page = 1; + const per_page = 100; + let hasMore = true; + + while (hasMore) { + const response = await octokit.request( + "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", + { + per_page, + page, + } + ); + + if (response.data.length > 0) { + repos = repos.concat(response.data); + page++; + } else { + hasMore = false; } } - const octokit = new Octokit({ - auth: accessToken, + return repos; + }; + + const repos = await getAllRepos(); + + const apps = repos + .filter((a: GitHubApp) => a.permissions.admin === true) + .map((a: GitHubApp) => { + return { + appId: a.id, + name: a.name, + owner: a.owner.login, + }; }); - const getAllRepos = async () => { - let repos: GitHubApp[] = []; - let page = 1; - const per_page = 100; - let hasMore = true; - - while (hasMore) { - const response = await octokit.request( - "GET /user/repos{?visibility,affiliation,type,sort,direction,per_page,page,since,before}", - { - per_page, - page, - } - ); - - if (response.data.length > 0) { - repos = repos.concat(response.data); - page++; - } else { - hasMore = false; - } - } - - return repos; - }; - - const repos = await getAllRepos(); - - apps = repos - .filter((a: GitHubApp) => a.permissions.admin === true) - .map((a: GitHubApp) => { - return { - appId: a.id, - name: a.name, - owner: a.owner.login, - }; - }); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get Github repos"); - } - return apps; }; @@ -341,29 +308,20 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.appId - id of Render service */ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { - let apps: any; - try { - const res = ( - await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { - headers: { - Authorization: `Bearer ${accessToken}`, - Accept: 'application/json', - 'Accept-Encoding': 'application/json', - }, - }) - ).data; - - apps = res - .map((a: any) => ({ - name: a.service.name, - appId: a.service.id - })); - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get Render services"); - } + const res = ( + await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { + headers: { + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + "Accept-Encoding": "application/json", + }, + }) + ).data; + + const apps = res.map((a: any) => ({ + name: a.service.name, + appId: a.service.id, + })); return apps; }; @@ -376,49 +334,51 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of Railway project * @returns {String} apps.appId - id of Railway project * -*/ + */ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { - let apps: any[] = []; - try { - const query = ` - query GetProjects($userId: String, $teamId: String) { - projects(userId: $userId, teamId: $teamId) { - edges { - node { - id - name - } + const query = ` + query GetProjects($userId: String, $teamId: String) { + projects(userId: $userId, teamId: $teamId) { + edges { + node { + id + name } } } - `; + } + `; - const variables = {}; + const variables = {}; - const { data: { data: { projects: { edges }}} } = await request.post(INTEGRATION_RAILWAY_API_URL, { + const { + data: { + data: { + projects: { edges }, + }, + }, + } = await request.post( + INTEGRATION_RAILWAY_API_URL, + { query, variables, - }, { + }, + { headers: { - 'Authorization': `Bearer ${accessToken}`, - 'Content-Type': 'application/json', - 'Accept-Encoding': 'application/json' + Authorization: `Bearer ${accessToken}`, + "Content-Type": "application/json", + "Accept-Encoding": "application/json", }, - }); - - apps = edges.map((e: any) => ({ - name: e.node.name, - appId: e.node.id - })); + } + ); + + const apps = edges.map((e: any) => ({ + name: e.node.name, + appId: e.node.id, + })); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get Railway services"); - } - return apps; -} +}; /** * Return list of apps for Fly.io integration @@ -428,41 +388,40 @@ const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of Fly.io apps */ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { - let apps; - try { - const query = ` - query($role: String) { - apps(type: "container", first: 400, role: $role) { - nodes { - id - name - hostname - } + const query = ` + query($role: String) { + apps(type: "container", first: 400, role: $role) { + nodes { + id + name + hostname } } - `; + } + `; - const res = (await request.post(INTEGRATION_FLYIO_API_URL, { - query, - variables: { - role: null, + const res = ( + await request.post( + INTEGRATION_FLYIO_API_URL, + { + query, + variables: { + role: null, + }, }, - }, { - headers: { - Authorization: "Bearer " + accessToken, - 'Accept': 'application/json', - 'Accept-Encoding': 'application/json', - }, - })).data.data.apps.nodes; + { + headers: { + Authorization: "Bearer " + accessToken, + Accept: "application/json", + "Accept-Encoding": "application/json", + }, + } + ) + ).data.data.apps.nodes; - apps = res.map((a: any) => ({ - name: a.name, - })); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get Fly.io apps"); - } + const apps = res.map((a: any) => ({ + name: a.name, + })); return apps; }; @@ -475,63 +434,43 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { * @returns {String} apps.name - name of CircleCI apps */ const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { - let apps: any; - try { - const res = ( - await request.get( - `${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, - { - headers: { - "Circle-Token": accessToken, - "Accept-Encoding": "application/json", - }, - } - ) - ).data + const res = ( + await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, { + headers: { + "Circle-Token": accessToken, + "Accept-Encoding": "application/json", + }, + }) + ).data; + + const apps = res?.map((a: any) => { + return { + name: a?.reponame, + }; + }); - apps = res?.map((a: any) => { - return { - name: a?.reponame - } - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get CircleCI projects"); - } - return apps; }; const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { - let apps: any; - try { - const res = ( - await request.get( - `${INTEGRATION_TRAVISCI_API_URL}/repos`, - { - headers: { - "Authorization": `token ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - ) - ).data; + const res = ( + await request.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, { + headers: { + Authorization: `token ${accessToken}`, + "Accept-Encoding": "application/json", + }, + }) + ).data; + + const apps = res?.map((a: any) => { + return { + name: a?.slug?.split("/")[1], + appId: a?.id, + }; + }); - apps = res?.map((a: any) => { - return { - name: a?.slug?.split("/")[1], - appId: a?.id, - } - }); - }catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get TravisCI projects"); - } - return apps; -} +}; /** * Return list of repositories for GitLab integration @@ -540,112 +479,98 @@ const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { * @returns {Object[]} apps - names of GitLab sites * @returns {String} apps.name - name of GitLab site */ -const getAppsGitlab = async ({ +const getAppsGitlab = async ({ accessToken, - teamId + teamId, }: { accessToken: string; teamId?: string; }) => { const apps: App[] = []; - + let page = 1; const perPage = 10; let hasMorePages = true; - try { - if (teamId) { - // case: fetch projects for group with id [teamId] in GitLab - - while (hasMorePages) { - const params = new URLSearchParams({ - page: String(page), - per_page: String(perPage) - }); + if (teamId) { + // case: fetch projects for group with id [teamId] in GitLab - const { data } = ( - await request.get( - `${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`, - { - params, - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - ) - ); + while (hasMorePages) { + const params = new URLSearchParams({ + page: String(page), + per_page: String(perPage), + }); - data.map((a: any) => { - apps.push({ - name: a.name, - appId: a.id - }); - }); - - if (data.length < perPage) { - hasMorePages = false; + const { data } = await request.get( + `${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, } - - page++; - } - } else { - // case: fetch projects for individual in GitLab - - const { id } = ( - await request.get( - `${INTEGRATION_GITLAB_API_URL}/v4/user`, - { - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - ) - ).data; - - while (hasMorePages) { - const params = new URLSearchParams({ - page: String(page), - per_page: String(perPage) + ); + + data.map((a: any) => { + apps.push({ + name: a.name, + appId: a.id, }); + }); - const { data } = ( - await request.get( - `${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`, - { - params, - headers: { - "Authorization": `Bearer ${accessToken}`, - "Accept-Encoding": "application/json", - }, - } - ) - ); - - data.map((a: any) => { - apps.push({ - name: a.name, - appId: a.id - }); - }); - - if (data.length < perPage) { - hasMorePages = false; - } - - page++; + if (data.length < perPage) { + hasMorePages = false; } + + page++; } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get GitLab projects"); - } - - return apps; -} + } else { + // case: fetch projects for individual in GitLab + const { id } = ( + await request.get(`${INTEGRATION_GITLAB_API_URL}/v4/user`, { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + }) + ).data; + + while (hasMorePages) { + const params = new URLSearchParams({ + page: String(page), + per_page: String(perPage), + }); + + const { data } = await request.get( + `${INTEGRATION_GITLAB_API_URL}/v4/users/${id}/projects`, + { + params, + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + data.map((a: any) => { + apps.push({ + name: a.name, + appId: a.id, + }); + }); + + if (data.length < perPage) { + hasMorePages = false; + } + + page++; + } + } + + return apps; +}; /** * Return list of projects for Supabase integration @@ -655,30 +580,23 @@ const getAppsGitlab = async ({ * @returns {String} apps.name - name of Supabase app */ const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { - let apps: any; - try { - const { data } = await request.get( - `${INTEGRATION_SUPABASE_API_URL}/v1/projects`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - 'Accept-Encoding': 'application/json' - } - } - ); + const { data } = await request.get( + `${INTEGRATION_SUPABASE_API_URL}/v1/projects`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json", + }, + } + ); + + const apps = data.map((a: any) => { + return { + name: a.name, + appId: a.id, + }; + }); - apps = data.map((a: any) => { - return { - name: a.name, - appId: a.id - }; - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get Supabase projects'); - } - return apps; }; diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts index 04dc96ca0..f7bb0222b 100644 --- a/backend/src/integrations/exchange.ts +++ b/backend/src/integrations/exchange.ts @@ -1,5 +1,4 @@ -import * as Sentry from '@sentry/node'; -import request from '../config/request'; +import request from "../config/request"; import { INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, @@ -12,8 +11,8 @@ import { INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL, - INTEGRATION_GITLAB_TOKEN_URL -} from '../variables'; + INTEGRATION_GITLAB_TOKEN_URL, +} from "../variables"; import { getSiteURL, getClientIdAzure, @@ -26,8 +25,8 @@ import { getClientIdGitHub, getClientSecretGitHub, getClientIdGitLab, - getClientSecretGitLab -} from '../config'; + getClientSecretGitLab, +} from "../config"; interface ExchangeCodeAzureResponse { token_type: string; @@ -93,49 +92,43 @@ interface ExchangeCodeGitlabResponse { */ const exchangeCode = async ({ integration, - code + code, }: { integration: string; code: string; }) => { let obj = {} as any; - try { - switch (integration) { - case INTEGRATION_AZURE_KEY_VAULT: - obj = await exchangeCodeAzure({ - code - }); - break; - case INTEGRATION_HEROKU: - obj = await exchangeCodeHeroku({ - code - }); - break; - case INTEGRATION_VERCEL: - obj = await exchangeCodeVercel({ - code - }); - break; - case INTEGRATION_NETLIFY: - obj = await exchangeCodeNetlify({ - code - }); - break; - case INTEGRATION_GITHUB: - obj = await exchangeCodeGithub({ - code - }); - break; - case INTEGRATION_GITLAB: - obj = await exchangeCodeGitlab({ - code - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange'); + switch (integration) { + case INTEGRATION_AZURE_KEY_VAULT: + obj = await exchangeCodeAzure({ + code, + }); + break; + case INTEGRATION_HEROKU: + obj = await exchangeCodeHeroku({ + code, + }); + break; + case INTEGRATION_VERCEL: + obj = await exchangeCodeVercel({ + code, + }); + break; + case INTEGRATION_NETLIFY: + obj = await exchangeCodeNetlify({ + code, + }); + break; + case INTEGRATION_GITHUB: + obj = await exchangeCodeGithub({ + code, + }); + break; + case INTEGRATION_GITLAB: + obj = await exchangeCodeGitlab({ + code, + }); } return obj; @@ -143,43 +136,33 @@ const exchangeCode = async ({ /** * Return [accessToken] for Azure OAuth2 code-token exchange - * @param param0 + * @param param0 */ -const exchangeCodeAzure = async ({ - code -}: { - code: string; -}) => { +const exchangeCodeAzure = async ({ code }: { code: string }) => { const accessExpiresAt = new Date(); - let res: ExchangeCodeAzureResponse; - try { - res = (await request.post( + + const res: ExchangeCodeAzureResponse = ( + await request.post( INTEGRATION_AZURE_TOKEN_URL, new URLSearchParams({ - grant_type: 'authorization_code', + grant_type: "authorization_code", code: code, - scope: 'https://vault.azure.net/.default openid offline_access', + scope: "https://vault.azure.net/.default openid offline_access", client_id: await getClientIdAzure(), client_secret: await getClientSecretAzure(), - redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback` + redirect_uri: `${await getSiteURL()}/integrations/azure-key-vault/oauth2/callback`, } as any) - )).data; + ) + ).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 Azure'); - } + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in); - return ({ + return { accessToken: res.access_token, refreshToken: res.refresh_token, - accessExpiresAt - }); -} + accessExpiresAt, + }; +}; /** * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku @@ -191,38 +174,28 @@ const exchangeCodeAzure = async ({ * @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 exchangeCodeHeroku = async ({ code }: { code: string }) => { const accessExpiresAt = new Date(); - try { - res = (await request.post( + + const res: ExchangeCodeHerokuResponse = ( + await request.post( INTEGRATION_HEROKU_TOKEN_URL, new URLSearchParams({ - grant_type: 'authorization_code', + grant_type: "authorization_code", code: code, - client_secret: await getClientSecretHeroku() + client_secret: await getClientSecretHeroku(), } as any) - )).data; + ) + ).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'); - } + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in); - return ({ + return { accessToken: res.access_token, refreshToken: res.refresh_token, - accessExpiresAt - }); -} + accessExpiresAt, + }; +}; /** * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel @@ -235,30 +208,23 @@ const exchangeCodeHeroku = async ({ * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ const exchangeCodeVercel = async ({ code }: { code: string }) => { - let res: ExchangeCodeVercelResponse; - try { - res = ( - await request.post( - INTEGRATION_VERCEL_TOKEN_URL, - new URLSearchParams({ - code: code, - client_id: await getClientIdVercel(), - client_secret: await getClientSecretVercel(), - redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback` - } as any) - ) - ).data; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`); - } + const res: ExchangeCodeVercelResponse = ( + await request.post( + INTEGRATION_VERCEL_TOKEN_URL, + new URLSearchParams({ + code: code, + client_id: await getClientIdVercel(), + client_secret: await getClientSecretVercel(), + redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`, + } as any) + ) + ).data; return { accessToken: res.access_token, refreshToken: null, accessExpiresAt: null, - teamId: res.team_id + teamId: res.team_id, }; }; @@ -273,47 +239,39 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => { * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ const exchangeCodeNetlify = async ({ code }: { code: string }) => { - let res: ExchangeCodeNetlifyResponse; - let accountId; - try { - res = ( - await request.post( - INTEGRATION_NETLIFY_TOKEN_URL, - new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - client_id: await getClientIdNetlify(), - client_secret: await getClientSecretNetlify(), - redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback` - } as any) - ) - ).data; + const res: ExchangeCodeNetlifyResponse = ( + await request.post( + INTEGRATION_NETLIFY_TOKEN_URL, + new URLSearchParams({ + grant_type: "authorization_code", + code: code, + client_id: await getClientIdNetlify(), + client_secret: await getClientSecretNetlify(), + redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`, + } as any) + ) + ).data; - const res2 = await request.get('https://api.netlify.com/api/v1/sites', { + const res2 = await request.get("https://api.netlify.com/api/v1/sites", { + headers: { + Authorization: `Bearer ${res.access_token}`, + }, + }); + + const res3 = ( + await request.get("https://api.netlify.com/api/v1/accounts", { headers: { - Authorization: `Bearer ${res.access_token}` - } - }); + Authorization: `Bearer ${res.access_token}`, + }, + }) + ).data; - const res3 = ( - await request.get('https://api.netlify.com/api/v1/accounts', { - headers: { - Authorization: `Bearer ${res.access_token}` - } - }) - ).data; - - accountId = res3[0].id; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange with Netlify'); - } + const accountId = res3[0].id; return { accessToken: res.access_token, refreshToken: res.refresh_token, - accountId + accountId, }; }; @@ -328,33 +286,25 @@ const exchangeCodeNetlify = async ({ code }: { code: string }) => { * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ const exchangeCodeGithub = async ({ code }: { code: string }) => { - let res: ExchangeCodeGithubResponse; - try { - res = ( - await request.get(INTEGRATION_GITHUB_TOKEN_URL, { - params: { - client_id: await getClientIdGitHub(), - client_secret: await getClientSecretGitHub(), - code: code, - redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback` - }, - headers: { - 'Accept': 'application/json', - 'Accept-Encoding': 'application/json' - } - }) - ).data; - - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange with Github'); - } + const res: ExchangeCodeGithubResponse = ( + await request.get(INTEGRATION_GITHUB_TOKEN_URL, { + params: { + client_id: await getClientIdGitHub(), + client_secret: await getClientSecretGitHub(), + code: code, + redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`, + }, + headers: { + Accept: "application/json", + "Accept-Encoding": "application/json", + }, + }) + ).data; return { accessToken: res.access_token, refreshToken: null, - accessExpiresAt: null + accessExpiresAt: null, }; }; @@ -369,42 +319,32 @@ const exchangeCodeGithub = async ({ code }: { code: string }) => { * @returns {Date} obj2.accessExpiresAt - date of expiration for access token */ const exchangeCodeGitlab = async ({ code }: { code: string }) => { - let res: ExchangeCodeGitlabResponse; const accessExpiresAt = new Date(); - - try { - res = ( - await request.post( - INTEGRATION_GITLAB_TOKEN_URL, - new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - client_id: await getClientIdGitLab(), - client_secret: await getClientSecretGitLab(), - redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback` - } as any), - { - headers: { - "Accept-Encoding": "application/json", - } - } - ) - ).data; - - accessExpiresAt.setSeconds( - accessExpiresAt.getSeconds() + res.expires_in - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed OAuth2 code-token exchange with Gitlab'); - } + const res: ExchangeCodeGitlabResponse = ( + await request.post( + INTEGRATION_GITLAB_TOKEN_URL, + new URLSearchParams({ + grant_type: "authorization_code", + code: code, + client_id: await getClientIdGitLab(), + client_secret: await getClientSecretGitLab(), + redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`, + } as any), + { + headers: { + "Accept-Encoding": "application/json", + }, + } + ) + ).data; + + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in); return { accessToken: res.access_token, refreshToken: res.refresh_token, - accessExpiresAt + accessExpiresAt, }; -} +}; export { exchangeCode }; diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts index 79b89ecfb..85d1a7f06 100644 --- a/backend/src/integrations/refresh.ts +++ b/backend/src/integrations/refresh.ts @@ -1,29 +1,24 @@ -import * as Sentry from '@sentry/node'; -import request from '../config/request'; +import request from "../config/request"; +import { IIntegrationAuth } from "../models"; import { - IIntegrationAuth -} from '../models'; -import { - INTEGRATION_AZURE_KEY_VAULT, + INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_HEROKU, INTEGRATION_GITLAB, -} from '../variables'; +} from "../variables"; import { INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL, - INTEGRATION_GITLAB_TOKEN_URL -} from '../variables'; -import { - IntegrationService -} from '../services'; + INTEGRATION_GITLAB_TOKEN_URL, +} from "../variables"; +import { IntegrationService } from "../services"; import { getSiteURL, getClientIdAzure, getClientSecretAzure, getClientSecretHeroku, getClientIdGitLab, - getClientSecretGitLab -} from '../config'; + getClientSecretGitLab, +} from "../config"; interface RefreshTokenAzureResponse { token_type: string; @@ -60,60 +55,57 @@ interface RefreshTokenGitLabResponse { */ const exchangeRefresh = async ({ integrationAuth, - refreshToken + refreshToken, }: { integrationAuth: IIntegrationAuth; refreshToken: string; }) => { - interface TokenDetails { accessToken: string; refreshToken: string; accessExpiresAt: Date; } - + let tokenDetails: TokenDetails; - try { - switch (integrationAuth.integration) { - case INTEGRATION_AZURE_KEY_VAULT: - tokenDetails = await exchangeRefreshAzure({ - refreshToken - }); - break; - case INTEGRATION_HEROKU: - tokenDetails = await exchangeRefreshHeroku({ - refreshToken - }); - break; - case INTEGRATION_GITLAB: - tokenDetails = await exchangeRefreshGitLab({ - refreshToken - }); - break; - default: - throw new Error('Failed to exchange token for incompatible integration'); - } - - if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) { - await IntegrationService.setIntegrationAuthAccess({ - integrationAuthId: integrationAuth._id.toString(), - accessId: null, - accessToken: tokenDetails.accessToken, - accessExpiresAt: tokenDetails.accessExpiresAt + switch (integrationAuth.integration) { + case INTEGRATION_AZURE_KEY_VAULT: + tokenDetails = await exchangeRefreshAzure({ + refreshToken, }); - - await IntegrationService.setIntegrationAuthRefresh({ - integrationAuthId: integrationAuth._id.toString(), - refreshToken: tokenDetails.refreshToken + break; + case INTEGRATION_HEROKU: + tokenDetails = await exchangeRefreshHeroku({ + refreshToken, }); - } - - return tokenDetails.accessToken; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get new OAuth2 access token'); + break; + case INTEGRATION_GITLAB: + tokenDetails = await exchangeRefreshGitLab({ + refreshToken, + }); + break; + default: + throw new Error("Failed to exchange token for incompatible integration"); } + + if ( + tokenDetails?.accessToken && + tokenDetails?.refreshToken && + tokenDetails?.accessExpiresAt + ) { + await IntegrationService.setIntegrationAuthAccess({ + integrationAuthId: integrationAuth._id.toString(), + accessId: null, + accessToken: tokenDetails.accessToken, + accessExpiresAt: tokenDetails.accessExpiresAt, + }); + + await IntegrationService.setIntegrationAuthRefresh({ + integrationAuthId: integrationAuth._id.toString(), + refreshToken: tokenDetails.refreshToken, + }); + } + + return tokenDetails.accessToken; }; /** @@ -124,38 +116,30 @@ const exchangeRefresh = async ({ * @returns */ const exchangeRefreshAzure = async ({ - refreshToken + refreshToken, }: { refreshToken: string; }) => { - try { - const accessExpiresAt = new Date(); - const { data }: { data: RefreshTokenAzureResponse } = await request.post( - INTEGRATION_AZURE_TOKEN_URL, - new URLSearchParams({ - client_id: await getClientIdAzure(), - scope: 'openid offline_access', - refresh_token: refreshToken, - grant_type: 'refresh_token', - client_secret: await getClientSecretAzure() - } as any) - ); - - accessExpiresAt.setSeconds( - accessExpiresAt.getSeconds() + data.expires_in - ); + const accessExpiresAt = new Date(); + const { data }: { data: RefreshTokenAzureResponse } = await request.post( + INTEGRATION_AZURE_TOKEN_URL, + new URLSearchParams({ + client_id: await getClientIdAzure(), + scope: "openid offline_access", + refresh_token: refreshToken, + grant_type: "refresh_token", + client_secret: await getClientSecretAzure(), + } as any) + ); - return ({ - accessToken: data.access_token, - refreshToken: data.refresh_token, - accessExpiresAt - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get refresh OAuth2 access token for Azure'); - } -} + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in); + + return { + accessToken: data.access_token, + refreshToken: data.refresh_token, + accessExpiresAt, + }; +}; /** * Return new access token by exchanging refresh token [refreshToken] for the @@ -165,39 +149,31 @@ const exchangeRefreshAzure = async ({ * @returns */ const exchangeRefreshHeroku = async ({ - refreshToken + refreshToken, }: { refreshToken: string; }) => { - try { - const accessExpiresAt = new Date(); - const { - data - }: { - data: RefreshTokenHerokuResponse - } = await request.post( - INTEGRATION_HEROKU_TOKEN_URL, - new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_secret: await getClientSecretHeroku() - } as any) - ); + const accessExpiresAt = new Date(); + const { + data, + }: { + data: RefreshTokenHerokuResponse; + } = await request.post( + INTEGRATION_HEROKU_TOKEN_URL, + new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_secret: await getClientSecretHeroku(), + } as any) + ); - accessExpiresAt.setSeconds( - accessExpiresAt.getSeconds() + data.expires_in - ); + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in); - return ({ - accessToken: data.access_token, - refreshToken: data.refresh_token, - accessExpiresAt - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to refresh OAuth2 access token for Heroku'); - } + return { + accessToken: data.access_token, + refreshToken: data.refresh_token, + accessExpiresAt, + }; }; /** @@ -208,45 +184,38 @@ const exchangeRefreshHeroku = async ({ * @returns */ const exchangeRefreshGitLab = async ({ - refreshToken + refreshToken, }: { refreshToken: string; }) => { - try { - const accessExpiresAt = new Date(); - const { - data - }: { - data: RefreshTokenGitLabResponse - } = await request.post( - INTEGRATION_GITLAB_TOKEN_URL, - new URLSearchParams({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - client_id: await getClientIdGitLab, - client_secret: await getClientSecretGitLab(), - redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback` - } as any), - { - headers: { - "Accept-Encoding": "application/json", - } - }); + const accessExpiresAt = new Date(); + const { + data, + }: { + data: RefreshTokenGitLabResponse; + } = await request.post( + INTEGRATION_GITLAB_TOKEN_URL, + new URLSearchParams({ + grant_type: "refresh_token", + refresh_token: refreshToken, + client_id: await getClientIdGitLab, + client_secret: await getClientSecretGitLab(), + redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`, + } as any), + { + headers: { + "Accept-Encoding": "application/json", + }, + } + ); - accessExpiresAt.setSeconds( - accessExpiresAt.getSeconds() + data.expires_in - ); + accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in); - return ({ - accessToken: data.access_token, - refreshToken: data.refresh_token, - accessExpiresAt - }); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to refresh OAuth2 access token for GitLab'); - } + return { + accessToken: data.access_token, + refreshToken: data.refresh_token, + accessExpiresAt, + }; }; export { exchangeRefresh }; diff --git a/backend/src/integrations/revoke.ts b/backend/src/integrations/revoke.ts index 188b166a7..46c93017d 100644 --- a/backend/src/integrations/revoke.ts +++ b/backend/src/integrations/revoke.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { IIntegrationAuth, IntegrationAuth, @@ -22,34 +21,28 @@ const revokeAccess = async ({ accessToken: string; }) => { let deletedIntegrationAuth; - try { - // add any integration-specific revocation logic - switch (integrationAuth.integration) { - case INTEGRATION_HEROKU: - break; - case INTEGRATION_VERCEL: - break; - case INTEGRATION_NETLIFY: - break; - case INTEGRATION_GITHUB: - break; - case INTEGRATION_GITLAB: - break; - } + // add any integration-specific revocation logic + switch (integrationAuth.integration) { + case INTEGRATION_HEROKU: + break; + case INTEGRATION_VERCEL: + break; + case INTEGRATION_NETLIFY: + break; + case INTEGRATION_GITHUB: + break; + case INTEGRATION_GITLAB: + break; + } - deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ - _id: integrationAuth._id + deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ + _id: integrationAuth._id + }); + + if (deletedIntegrationAuth) { + await Integration.deleteMany({ + integrationAuth: deletedIntegrationAuth._id }); - - if (deletedIntegrationAuth) { - await Integration.deleteMany({ - integrationAuth: deletedIntegrationAuth._id - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to delete integration authorization'); } return deletedIntegrationAuth; diff --git a/backend/src/integrations/teams.ts b/backend/src/integrations/teams.ts index 9943c531e..3bed93c9a 100644 --- a/backend/src/integrations/teams.ts +++ b/backend/src/integrations/teams.ts @@ -1,4 +1,3 @@ -import * as Sentry from "@sentry/node"; import { IIntegrationAuth } from '../models'; @@ -31,21 +30,15 @@ const getTeams = async ({ }) => { let teams: Team[] = []; - try { - switch (integrationAuth.integration) { - case INTEGRATION_GITLAB: - teams = await getTeamsGitLab({ - accessToken - }); - break; - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to get integration teams'); + + switch (integrationAuth.integration) { + case INTEGRATION_GITLAB: + teams = await getTeamsGitLab({ + accessToken + }); + break; } - return teams; } @@ -63,30 +56,24 @@ const getTeamsGitLab = async ({ accessToken: string; }) => { let teams: Team[] = []; - try { - const res = (await request.get( - `${INTEGRATION_GITLAB_API_URL}/v4/groups`, - { - headers: { - Authorization: `Bearer ${accessToken}`, - "Accept-Encoding": "application/json" - } + const res = (await request.get( + `${INTEGRATION_GITLAB_API_URL}/v4/groups`, + { + headers: { + Authorization: `Bearer ${accessToken}`, + "Accept-Encoding": "application/json" } - )).data; - - teams = res.map((t: any) => ({ - name: t.name, - teamId: t.id - })); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error("Failed to get GitLab integration teams"); - } + } + )).data; + + teams = res.map((t: any) => ({ + name: t.name, + teamId: t.id + })); return teams; } export { getTeams -} \ No newline at end of file +} diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index af1eb1a53..8619fe084 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -1,4 +1,3 @@ -import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; import { Request, Response, NextFunction } from 'express'; import { IntegrationAuth, IWorkspace } from '../models'; diff --git a/backend/src/utils/crypto.ts b/backend/src/utils/crypto.ts index 28f96b0cf..ec61b496e 100644 --- a/backend/src/utils/crypto.ts +++ b/backend/src/utils/crypto.ts @@ -1,7 +1,6 @@ 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. @@ -38,20 +37,13 @@ const encryptAsymmetric = ({ publicKey: string; privateKey: string; }) => { - let nonce, ciphertext; - try { - nonce = nacl.randomBytes(24); - ciphertext = nacl.box( - util.decodeUTF8(plaintext), - nonce, - util.decodeBase64(publicKey), - util.decodeBase64(privateKey) - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to perform asymmetric encryption'); - } + const nonce = nacl.randomBytes(24); + const ciphertext = nacl.box( + util.decodeUTF8(plaintext), + nonce, + util.decodeBase64(publicKey), + util.decodeBase64(privateKey) + ); return { ciphertext: util.encodeBase64(ciphertext), @@ -80,19 +72,12 @@ const decryptAsymmetric = ({ publicKey: string; privateKey: string; }): string => { - let plaintext: any; - try { - plaintext = nacl.box.open( - util.decodeBase64(ciphertext), - util.decodeBase64(nonce), - util.decodeBase64(publicKey), - util.decodeBase64(privateKey) - ); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to perform asymmetric decryption'); - } + const plaintext: any = nacl.box.open( + util.decodeBase64(ciphertext), + util.decodeBase64(nonce), + util.decodeBase64(publicKey), + util.decodeBase64(privateKey) + ); return util.encodeUTF8(plaintext); }; @@ -110,17 +95,8 @@ const encryptSymmetric = ({ plaintext: string; key: string; }) => { - let ciphertext, iv, tag; - try { - const obj = AesGCM.encrypt(plaintext, key); - ciphertext = obj.ciphertext; - iv = obj.iv; - tag = obj.tag; - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to perform symmetric encryption'); - } + const obj = AesGCM.encrypt(plaintext, key); + const { ciphertext, iv, tag } = obj; return { ciphertext, @@ -150,15 +126,7 @@ const decryptSymmetric = ({ tag: string; key: string; }): string => { - let plaintext; - try { - plaintext = AesGCM.decrypt(ciphertext, iv, tag, key); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to perform symmetric decryption'); - } - + const plaintext = AesGCM.decrypt(ciphertext, iv, tag, key); return plaintext; }; diff --git a/backend/tests/unit-tests/utils/crypto.test.ts b/backend/tests/unit-tests/utils/crypto.test.ts index bbbfd0297..7509e53b9 100644 --- a/backend/tests/unit-tests/utils/crypto.test.ts +++ b/backend/tests/unit-tests/utils/crypto.test.ts @@ -28,14 +28,14 @@ describe('Crypto', () => { test('should throw error if publicKey is undefined', () => { expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('Failed to perform asymmetric encryption'); + }).toThrowError('invalid encoding'); }); test('should throw error if publicKey is empty string', () => { publicKey = ''; expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('Failed to perform asymmetric encryption'); + }).toThrowError('bad public key size'); }); }); @@ -47,14 +47,14 @@ describe('Crypto', () => { test('should throw error if privateKey is undefined', () => { expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('Failed to perform asymmetric encryption'); + }).toThrowError('invalid encoding'); }); test('should throw error if privateKey is empty string', () => { privateKey = ''; expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('Failed to perform asymmetric encryption'); + }).toThrowError('bad secret key size'); }); }); @@ -66,7 +66,7 @@ describe('Crypto', () => { test('should throw error if plaintext is undefined', () => { expect(() => { encryptAsymmetric({ plaintext, publicKey, privateKey }); - }).toThrowError('Failed to perform asymmetric encryption'); + }).toThrowError('expected string'); }); test('should encrypt plaintext containing special characters', () => { @@ -130,7 +130,7 @@ describe('Crypto', () => { publicKey, privateKey }); - }).toThrowError('Failed to perform asymmetric decryption'); + }).toThrowError('invalid encoding'); }); test('should throw error if nonce is modified', () => { @@ -149,7 +149,7 @@ describe('Crypto', () => { publicKey, privateKey }); - }).toThrowError('Failed to perform asymmetric decryption'); + }).toThrowError('invalid encoding'); }); }); }); @@ -170,7 +170,7 @@ describe('Crypto', () => { const invalidKey = 'invalid-key'; expect(() => { encryptSymmetric({ plaintext, key: invalidKey }); - }).toThrowError('Failed to perform symmetric encryption'); + }).toThrowError('Invalid key length'); }); test('should throw an error when invalid key is provided', () => { @@ -179,7 +179,7 @@ describe('Crypto', () => { expect(() => { encryptSymmetric({ plaintext, key: invalidKey }); - }).toThrowError('Failed to perform symmetric encryption'); + }).toThrowError('Invalid key length'); }); }); @@ -209,7 +209,7 @@ describe('Crypto', () => { tag, key }); - }).toThrowError('Failed to perform symmetric decryption'); + }).toThrowError('Unsupported state or unable to authenticate data'); }); test('should fail if iv is modified', () => { @@ -221,7 +221,7 @@ describe('Crypto', () => { tag, key }); - }).toThrowError('Failed to perform symmetric decryption'); + }).toThrowError('Unsupported state or unable to authenticate data'); }); test('should fail if tag is modified', () => { @@ -233,7 +233,7 @@ describe('Crypto', () => { tag: modifiedTag, key }); - }).toThrowError('Failed to perform symmetric decryption'); + }).toThrowError(/Invalid authentication tag length: \d+/); }); test('should throw an error when decryption fails', () => { @@ -245,7 +245,7 @@ describe('Crypto', () => { tag, key: invalidKey }); - }).toThrowError('Failed to perform symmetric decryption'); + }).toThrowError('Invalid key length'); }); }); });