feat: remove try-catch blocks for handling errors in middleware

This commit is contained in:
Spelchure
2023-05-01 17:14:39 +03:00
parent addac63700
commit 21eb1815c4
22 changed files with 2141 additions and 2580 deletions
+73 -103
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Action } from '../models'; import { Action } from '../models';
import { import {
@@ -36,33 +35,25 @@ const createActionUpdateSecret = async ({
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
let action; const latestSecretVersions = (await getLatestNSecretSecretVersionIds({
try { secretIds,
const latestSecretVersions = (await getLatestNSecretSecretVersionIds({ n: 2
secretIds, }))
n: 2 .map((s) => ({
})) oldSecretVersion: s.versions[0]._id,
.map((s) => ({ newSecretVersion: s.versions[1]._id
oldSecretVersion: s.versions[0]._id, }));
newSecretVersion: s.versions[1]._id
})); const action = await new Action({
name,
action = await new Action({ user: userId,
name, serviceAccount: serviceAccountId,
user: userId, serviceTokenData: serviceTokenDataId,
serviceAccount: serviceAccountId, workspace: workspaceId,
serviceTokenData: serviceTokenDataId, payload: {
workspace: workspaceId, secretVersions: latestSecretVersions
payload: { }
secretVersions: latestSecretVersions }).save();
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create update secret action');
}
return action; return action;
} }
@@ -90,33 +81,25 @@ const createActionSecret = async ({
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
let action; // case: action is adding, deleting, or reading secrets
try { // -> add new secret versions
// case: action is adding, deleting, or reading secrets const latestSecretVersions = (await getLatestSecretVersionIds({
// -> add new secret versions secretIds
const latestSecretVersions = (await getLatestSecretVersionIds({ }))
secretIds .map((s) => ({
})) newSecretVersion: s.versionId
.map((s) => ({ }));
newSecretVersion: s.versionId
})); const action = await new Action({
name,
action = await new Action({ user: userId,
name, serviceAccount: serviceAccountId,
user: userId, serviceTokenData: serviceTokenDataId,
serviceAccount: serviceAccountId, workspace: workspaceId,
serviceTokenData: serviceTokenDataId, payload: {
workspace: workspaceId, secretVersions: latestSecretVersions
payload: { }
secretVersions: latestSecretVersions }).save();
}
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action create/read/delete secret action');
}
return action; return action;
} }
@@ -140,19 +123,12 @@ const createActionClient = ({
serviceAccountId?: Types.ObjectId; serviceAccountId?: Types.ObjectId;
serviceTokenDataId?: Types.ObjectId; serviceTokenDataId?: Types.ObjectId;
}) => { }) => {
let action; const action = new Action({
try { name,
action = new Action({ user: userId,
name, serviceAccount: serviceAccountId,
user: userId, serviceTokenData: serviceTokenDataId
serviceAccount: serviceAccountId, }).save();
serviceTokenData: serviceTokenDataId
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create client action');
}
return action; return action;
} }
@@ -181,40 +157,34 @@ const createActionHelper = async ({
secretIds?: Types.ObjectId[]; secretIds?: Types.ObjectId[];
}) => { }) => {
let action; let action;
try { switch (name) {
switch (name) { case ACTION_LOGIN:
case ACTION_LOGIN: case ACTION_LOGOUT:
case ACTION_LOGOUT: action = await createActionClient({
action = await createActionClient({ name,
name, userId
userId });
}); break;
break; case ACTION_ADD_SECRETS:
case ACTION_ADD_SECRETS: case ACTION_READ_SECRETS:
case ACTION_READ_SECRETS: case ACTION_DELETE_SECRETS:
case ACTION_DELETE_SECRETS: if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); action = await createActionSecret({
action = await createActionSecret({ name,
name, userId,
userId, workspaceId,
workspaceId, secretIds
secretIds });
}); break;
break; case ACTION_UPDATE_SECRETS:
case ACTION_UPDATE_SECRETS: if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret');
if (!workspaceId || !secretIds) throw new Error('Missing required params workspace id or secret ids to create action secret'); action = await createActionUpdateSecret({
action = await createActionUpdateSecret({ name,
name, userId,
userId, workspaceId,
workspaceId, secretIds
secretIds });
}); break;
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create action');
} }
return action; return action;
@@ -222,4 +192,4 @@ const createActionHelper = async ({
export { export {
createActionHelper createActionHelper
}; };
+11 -19
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Log, Log,
@@ -32,27 +31,20 @@ const createLogHelper = async ({
channel: string; channel: string;
ipAddress: string; ipAddress: string;
}) => { }) => {
let log; const log = await new Log({
try { user: userId,
log = await new Log({ serviceAccount: serviceAccountId,
user: userId, serviceTokenData: serviceTokenDataId,
serviceAccount: serviceAccountId, workspace: workspaceId ?? undefined,
serviceTokenData: serviceTokenDataId, actionNames: actions.map((a) => a.name),
workspace: workspaceId ?? undefined, actions,
actionNames: actions.map((a) => a.name), channel,
actions, ipAddress
channel, }).save();
ipAddress
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create log');
}
return log; return log;
} }
export { export {
createLogHelper createLogHelper
} }
+103 -128
View File
@@ -1,14 +1,6 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import { Secret, ISecret } from "../../models";
import { import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
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 * 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 * @returns {SecretSnapshot} secretSnapshot - new secret snapshot
*/ */
const takeSecretSnapshotHelper = async ({ 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; const latestSecretVersions = (
try { await SecretVersion.aggregate([
const secretIds = (await Secret.find({ {
workspace: workspaceId $match: {
}, '_id')).map((s) => s._id); 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([ const latestSecretSnapshot = await SecretSnapshot.findOne({
{ workspace: workspaceId,
$match: { }).sort({ version: -1 });
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({ const secretSnapshot = await new SecretSnapshot({
workspace: workspaceId workspace: workspaceId,
}).sort({ version: -1 }); version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions,
}).save();
secretSnapshot = await new SecretSnapshot({ return secretSnapshot;
workspace: workspaceId, };
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
secretVersions: latestSecretVersions
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to take a secret snapshot');
}
return secretSnapshot;
}
/** /**
* Add secret versions [secretVersions] to the SecretVersion collection. * Add secret versions [secretVersions] to the SecretVersion collection.
@@ -77,93 +66,79 @@ const takeSecretSnapshotHelper = async ({
* @returns {SecretVersion[]} newSecretVersions - new secret versions * @returns {SecretVersion[]} newSecretVersions - new secret versions
*/ */
const addSecretVersionsHelper = async ({ const addSecretVersionsHelper = async ({
secretVersions secretVersions,
}: { }: {
secretVersions: ISecretVersion[] secretVersions: ISecretVersion[];
}) => { }) => {
let newSecretVersions; const newSecretVersions = await SecretVersion.insertMany(secretVersions);
try {
newSecretVersions = await SecretVersion.insertMany(secretVersions);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(`Failed to add secret versions [err=${err}]`);
}
return newSecretVersions; return newSecretVersions;
} };
const markDeletedSecretVersionsHelper = async ({ const markDeletedSecretVersionsHelper = async ({
secretIds secretIds,
}: { }: {
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
try { await SecretVersion.updateMany(
await SecretVersion.updateMany({ {
secret: { $in: secretIds } secret: { $in: secretIds },
}, { },
isDeleted: true {
}, { isDeleted: true,
new: true },
}); {
} catch (err) { new: true,
Sentry.setUser(null); }
Sentry.captureException(err); );
throw new Error('Failed to mark secret versions as deleted'); };
}
}
/** /**
* Initialize secret versioning by setting previously unversioned * Initialize secret versioning by setting previously unversioned
* secrets to version 1 and begin populating secret versions. * secrets to version 1 and begin populating secret versions.
*/ */
const initSecretVersioningHelper = async () => { const initSecretVersioningHelper = async () => {
try { await Secret.updateMany(
{ version: { $exists: false } },
{ $set: { version: 1 } }
);
await Secret.updateMany( const unversionedSecrets: ISecret[] = await Secret.aggregate([
{ version: { $exists: false } }, {
{ $set: { version: 1 } } $lookup: {
); from: "secretversions",
localField: "_id",
foreignField: "secret",
as: "versions",
},
},
{
$match: {
versions: { $size: 0 },
},
},
]);
const unversionedSecrets: ISecret[] = await Secret.aggregate([ if (unversionedSecrets.length > 0) {
{ await addSecretVersionsHelper({
$lookup: { secretVersions: unversionedSecrets.map(
from: 'secretversions', (s, idx) =>
localField: '_id', new SecretVersion({
foreignField: 'secret', ...s,
as: 'versions', secret: s._id,
}, version: s.version ? s.version : 1,
}, isDeleted: false,
{ workspace: s.workspace,
$match: { environment: s.environment,
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');
}
}
export { export {
takeSecretSnapshotHelper, takeSecretSnapshotHelper,
addSecretVersionsHelper, addSecretVersionsHelper,
markDeletedSecretVersionsHelper, markDeletedSecretVersionsHelper,
initSecretVersioningHelper initSecretVersioningHelper,
} };
+41 -59
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { SecretVersion } from '../models'; import { SecretVersion } from '../models';
@@ -13,41 +12,32 @@ const getLatestSecretVersionIds = async ({
}: { }: {
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
}) => { }) => {
interface LatestSecretVersionId { interface LatestSecretVersionId {
_id: Types.ObjectId; _id: Types.ObjectId;
version: number; version: number;
versionId: Types.ObjectId; versionId: Types.ObjectId;
} }
let latestSecretVersionIds: LatestSecretVersionId[]; const latestSecretVersionIds = (await SecretVersion.aggregate([
try { {
latestSecretVersionIds = (await SecretVersion.aggregate([ $match: {
{ secret: {
$match: { $in: secretIds
secret: { }
$in: secretIds
}
}
},
{
$group: {
_id: '$secret',
version: { $max: '$version' },
versionId: { $max: '$_id' } // id of latest secret version
}
},
{
$sort: { version: -1 }
} }
]) },
.exec()); {
$group: {
} catch (err) { _id: '$secret',
Sentry.setUser(null); version: { $max: '$version' },
Sentry.captureException(err); versionId: { $max: '$_id' } // id of latest secret version
throw new Error('Failed to get latest secret versions'); }
} },
{
$sort: { version: -1 }
}
])
.exec());
return latestSecretVersionIds; return latestSecretVersionIds;
} }
@@ -66,40 +56,32 @@ const getLatestNSecretSecretVersionIds = async ({
secretIds: Types.ObjectId[]; secretIds: Types.ObjectId[];
n: number; n: number;
}) => { }) => {
// TODO: optimize query // TODO: optimize query
let latestNSecretVersions; const latestNSecretVersions = (await SecretVersion.aggregate([
try { {
latestNSecretVersions = (await SecretVersion.aggregate([ $match: {
{ secret: {
$match: { $in: secretIds,
secret: {
$in: secretIds,
},
},
}, },
{
$sort: { version: -1 },
}, },
{ },
$group: { {
_id: "$secret", $sort: { version: -1 },
versions: { $push: "$$ROOT" }, },
}, {
$group: {
_id: "$secret",
versions: { $push: "$$ROOT" },
}, },
{ },
$project: { {
_id: 0, $project: {
secret: "$_id", _id: 0,
versions: { $slice: ["$versions", n] }, secret: "$_id",
}, versions: { $slice: ["$versions", n] },
} },
])); }
} catch (err) { ]));
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get latest n secret versions');
}
return latestNSecretVersions; return latestNSecretVersions;
} }
-1
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
+212 -243
View File
@@ -1,41 +1,34 @@
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { Types } from 'mongoose';
import { import {
Bot, Bot,
BotKey, BotKey,
Secret, Secret,
ISecret, ISecret,
IUser, IUser,
User, User,
IServiceAccount, IServiceAccount,
ServiceAccount, ServiceAccount,
IServiceTokenData, IServiceTokenData,
ServiceTokenData ServiceTokenData,
} from '../models'; } from "../models";
import {
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
import { import {
SECRET_SHARED, generateKeyPair,
AUTH_MODE_JWT, encryptSymmetric,
AUTH_MODE_SERVICE_ACCOUNT, decryptSymmetric,
AUTH_MODE_SERVICE_TOKEN, decryptAsymmetric,
AUTH_MODE_API_KEY } from "../utils/crypto";
} from '../variables';
import { getEncryptionKey } from '../config';
import { BotNotFoundError, UnauthorizedRequestError } from '../utils/errors';
import { import {
validateMembership SECRET_SHARED,
} from '../helpers/membership'; AUTH_MODE_JWT,
import { AUTH_MODE_SERVICE_ACCOUNT,
validateUserClientForWorkspace AUTH_MODE_SERVICE_TOKEN,
} from '../helpers/user'; AUTH_MODE_API_KEY,
import { } from "../variables";
validateServiceAccountClientForWorkspace import { getEncryptionKey } from "../config";
} from '../helpers/serviceAccount'; 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 * Validate authenticated clients for bot with id [botId] based
@@ -46,99 +39,104 @@ import {
* @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles * @param {Array<'admin' | 'member'>} obj.acceptedRoles - accepted workspace roles
*/ */
const validateClientForBot = async ({ const validateClientForBot = async ({
authData, authData,
botId, botId,
acceptedRoles acceptedRoles,
}: { }: {
authData: { authData: {
authMode: string; authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData; authPayload: IUser | IServiceAccount | IServiceTokenData;
}; };
botId: Types.ObjectId; botId: Types.ObjectId;
acceptedRoles: Array<'admin' | 'member'>; acceptedRoles: Array<"admin" | "member">;
}) => { }) => {
const bot = await Bot.findById(botId); const bot = await Bot.findById(botId);
if (!bot) throw BotNotFoundError();
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
await validateUserClientForWorkspace({
user: authData.authPayload,
workspaceId: bot.workspace,
acceptedRoles
});
return bot;
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { if (!bot) throw BotNotFoundError();
await validateServiceAccountClientForWorkspace({
serviceAccount: authData.authPayload,
workspaceId: bot.workspace
});
return bot; if (
} authData.authMode === AUTH_MODE_JWT &&
authData.authPayload instanceof User
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { ) {
throw UnauthorizedRequestError({ await validateUserClientForWorkspace({
message: 'Failed service token authorization for bot' user: authData.authPayload,
}); workspaceId: bot.workspace,
} acceptedRoles,
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'
}); });
}
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] * 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.name - name of bot
* @param {String} obj.workspaceId - id of workspace that bot belongs to * @param {String} obj.workspaceId - id of workspace that bot belongs to
*/ */
const createBot = async ({ const createBot = async ({
name, name,
workspaceId, workspaceId,
}: { }: {
name: string; name: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) => { }) => {
let bot; const { publicKey, privateKey } = generateKeyPair();
try { const { ciphertext, iv, tag } = encryptSymmetric({
const { publicKey, privateKey } = generateKeyPair(); plaintext: privateKey,
const { ciphertext, iv, tag } = encryptSymmetric({ key: await getEncryptionKey(),
plaintext: privateKey, });
key: await getEncryptionKey()
});
bot = await new Bot({ const bot = await new Bot({
name, name,
workspace: workspaceId, workspace: workspaceId,
isActive: false, isActive: false,
publicKey, publicKey,
encryptedPrivateKey: ciphertext, encryptedPrivateKey: ciphertext,
iv, iv,
tag tag,
}).save(); }).save();
} catch (err) {
Sentry.setUser(null); return bot;
Sentry.captureException(err); };
throw new Error('Failed to create bot');
}
return bot;
}
/** /**
* Return decrypted secrets for workspace with id [workspaceId] * Return decrypted secrets for workspace with id [workspaceId]
@@ -148,125 +146,105 @@ const createBot = async ({
* @param {String} obj.environment - environment * @param {String} obj.environment - environment
*/ */
const getSecretsHelper = async ({ const getSecretsHelper = async ({
workspaceId, workspaceId,
environment environment,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
}) => { }) => {
const content = {} as any; const content = {} as any;
try { const key = await getKey({ workspaceId: workspaceId.toString() });
const key = await getKey({ workspaceId }); const secrets = await Secret.find({
const secrets = await Secret.find({ workspace: workspaceId,
workspace: workspaceId, environment,
environment, type: SECRET_SHARED,
type: SECRET_SHARED });
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({ secrets.forEach((secret: ISecret) => {
ciphertext: secret.secretValueCiphertext, const secretKey = decryptSymmetric({
iv: secret.secretValueIV, ciphertext: secret.secretKeyCiphertext,
tag: secret.secretValueTag, iv: secret.secretKeyIV,
key tag: secret.secretKeyTag,
}); key,
});
content[secretKey] = secretValue; const secretValue = decryptSymmetric({
}); ciphertext: secret.secretValueCiphertext,
} catch (err) { iv: secret.secretValueIV,
Sentry.setUser(null); tag: secret.secretValueTag,
Sentry.captureException(err); key,
throw new Error('Failed to get secrets'); });
}
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] * with id [workspaceId]
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace * @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key * @returns {String} key - decrypted workspace key
*/ */
const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { const getKey = async ({ workspaceId }: { workspaceId: string }) => {
let key; const botKey = await BotKey.findOne({
try { workspace: workspaceId,
const botKey = await BotKey.findOne({ }).populate<{ sender: IUser }>("sender", "publicKey");
workspace: workspaceId
}).populate<{ sender: IUser }>('sender', 'publicKey'); if (!botKey) throw new Error("Failed to find bot key");
if (!botKey) throw new Error('Failed to find bot key'); const bot = await Bot.findOne({
workspace: workspaceId,
const bot = await Bot.findOne({ }).select("+encryptedPrivateKey +iv +tag");
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");
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,
const privateKeyBot = decryptSymmetric({ iv: bot.iv,
ciphertext: bot.encryptedPrivateKey, tag: bot.tag,
iv: bot.iv, key: await getEncryptionKey(),
tag: bot.tag, });
key: await getEncryptionKey()
}); const key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
key = decryptAsymmetric({ nonce: botKey.nonce,
ciphertext: botKey.encryptedKey, publicKey: botKey.sender.publicKey as string,
nonce: botKey.nonce, privateKey: privateKeyBot,
publicKey: botKey.sender.publicKey as string, });
privateKey: privateKeyBot
}); return key;
} catch (err) { };
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get workspace key');
}
return key;
}
/** /**
* Return symmetrically encrypted [plaintext] using the * Return symmetrically encrypted [plaintext] using the
* key for workspace with id [workspaceId] * key for workspace with id [workspaceId]
* @param {Object} obj1 * @param {Object} obj1
* @param {String} obj1.workspaceId - id of workspace * @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt * @param {String} obj1.plaintext - plaintext to encrypt
*/ */
const encryptSymmetricHelper = async ({ const encryptSymmetricHelper = async ({
workspaceId, workspaceId,
plaintext plaintext,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
plaintext: string; plaintext: string;
}) => { }) => {
const key = await getKey({ workspaceId: workspaceId.toString() });
try { const { ciphertext, iv, tag } = encryptSymmetric({
const key = await getKey({ workspaceId }); plaintext,
const { ciphertext, iv, tag } = encryptSymmetric({ key,
plaintext, });
key
}); return {
ciphertext,
return ({ iv,
ciphertext, tag,
iv, };
tag };
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption with bot');
}
}
/** /**
* Return symmetrically decrypted [ciphertext] using the * Return symmetrically decrypted [ciphertext] using the
* key for workspace with id [workspaceId] * key for workspace with id [workspaceId]
@@ -277,40 +255,31 @@ const encryptSymmetricHelper = async ({
* @param {String} obj.tag - tag * @param {String} obj.tag - tag
*/ */
const decryptSymmetricHelper = async ({ 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, ciphertext,
iv, iv,
tag tag,
}: { key,
workspaceId: Types.ObjectId; });
ciphertext: string;
iv: string; return plaintext;
tag: string; };
}) => {
let plaintext;
try {
const key = await getKey({ workspaceId });
const plaintext = decryptSymmetric({
ciphertext,
iv,
tag,
key
});
return plaintext;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption with bot');
}
return plaintext;
}
export { export {
validateClientForBot, validateClientForBot,
createBot, createBot,
getSecretsHelper, getSecretsHelper,
encryptSymmetricHelper, encryptSymmetricHelper,
decryptSymmetricHelper decryptSymmetricHelper,
} };
+29 -44
View File
@@ -1,14 +1,13 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import * as Sentry from '@sentry/node'; import { Bot, IBot } from "../models";
import { Bot, IBot } from '../models'; import { EVENT_PUSH_SECRETS } from "../variables";
import { EVENT_PUSH_SECRETS } from '../variables'; import { IntegrationService } from "../services";
import { IntegrationService } from '../services';
interface Event { interface Event {
name: string; name: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
payload: any; payload: any;
} }
/** /**
@@ -19,39 +18,25 @@ interface Event {
* @param {String} obj.event.workspaceId - id of workspace that event is part of * @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event) * @param {Object} obj.event.payload - payload of event (depends on event)
*/ */
const handleEventHelper = async ({ const handleEventHelper = async ({ event }: { event: Event }) => {
event const { workspaceId, environment } = 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);
}
}
export { // TODO: moduralize bot check into separate function
handleEventHelper 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 };
+2 -2
View File
@@ -256,7 +256,7 @@ const syncIntegrationsHelper = async ({
integration, integration,
integrationAuth, integrationAuth,
secrets, secrets,
accessId: access.accessId, accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken accessToken: access.accessToken
}); });
} }
@@ -482,4 +482,4 @@ export {
getIntegrationAuthAccessHelper, getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper, setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper setIntegrationAuthAccessHelper
} }
+22 -29
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Key, IKey } from '../models'; import { Key, IKey } from '../models';
interface Key { interface Key {
@@ -27,36 +26,30 @@ const pushKeys = async ({
workspaceId: string; workspaceId: string;
keys: Key[]; keys: Key[];
}): Promise<void> => { }): Promise<void> => {
try { // filter out already-inserted keys
// filter out already-inserted keys const keysSet = new Set(
const keysSet = new Set( (
( await Key.find(
await Key.find( {
{ workspace: workspaceId
workspace: workspaceId },
}, 'receiver'
'receiver' )
) ).map((k: IKey) => k.receiver.toString())
).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 // add new shared keys only
await Key.insertMany( await Key.insertMany(
keys.map((k) => ({ keys.map((k) => ({
encryptedKey: k.encryptedKey, encryptedKey: k.encryptedKey,
nonce: k.nonce, nonce: k.nonce,
sender: userId, sender: userId,
receiver: k.userId, receiver: k.userId,
workspace: workspaceId workspace: workspaceId
})) }))
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to push access keys');
}
}; };
export { pushKeys }; export { pushKeys };
+46 -68
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
MembershipOrg, MembershipOrg,
@@ -144,15 +143,7 @@ const validateMembershipOrg = async ({
* @return {Object} membershipOrg - membership * @return {Object} membershipOrg - membership
*/ */
const findMembershipOrg = (queryObj: any) => { const findMembershipOrg = (queryObj: any) => {
let membershipOrg; const membershipOrg = MembershipOrg.findOne(queryObj);
try {
membershipOrg = MembershipOrg.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find organization membership');
}
return membershipOrg; return membershipOrg;
}; };
@@ -175,33 +166,27 @@ const addMembershipsOrg = async ({
roles: string[]; roles: string[];
statuses: string[]; statuses: string[];
}) => { }) => {
try { const operations = userIds.map((userId, idx) => {
const operations = userIds.map((userId, idx) => { return {
return { updateOne: {
updateOne: { filter: {
filter: { user: userId,
user: userId, organization: organizationId,
organization: organizationId, role: roles[idx],
role: roles[idx], status: statuses[idx]
status: statuses[idx] },
}, update: {
update: { user: userId,
user: userId, organization: organizationId,
organization: organizationId, role: roles[idx],
role: roles[idx], status: statuses[idx]
status: statuses[idx] },
}, upsert: true
upsert: true }
} };
}; });
});
await MembershipOrg.bulkWrite(operations as any); await MembershipOrg.bulkWrite(operations as any);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to add users to organization');
}
}; };
/** /**
@@ -214,43 +199,36 @@ const deleteMembershipOrg = async ({
}: { }: {
membershipOrgId: string; membershipOrgId: string;
}) => { }) => {
let deletedMembershipOrg; const deletedMembershipOrg = await MembershipOrg.findOneAndDelete({
try { _id: membershipOrgId
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 // delete keys associated with organization membership
if (deletedMembershipOrg?.user) { if (deletedMembershipOrg?.user) {
// case: organization membership had a registered user // case: organization membership had a registered user
const workspaces = ( const workspaces = (
await Workspace.find({ await Workspace.find({
organization: deletedMembershipOrg.organization organization: deletedMembershipOrg.organization
}) })
).map((w) => w._id.toString()); ).map((w) => w._id.toString());
await Membership.deleteMany({ await Membership.deleteMany({
user: deletedMembershipOrg.user, user: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces $in: workspaces
} }
}); });
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembershipOrg.user, receiver: deletedMembershipOrg.user,
workspace: { workspace: {
$in: workspaces $in: workspaces
} }
}); });
} }
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete organization membership');
}
return deletedMembershipOrg; return deletedMembershipOrg;
}; };
+197 -207
View File
@@ -1,39 +1,34 @@
import * as Sentry from '@sentry/node'; import Stripe from "stripe";
import Stripe from 'stripe'; import { Types } from "mongoose";
import { Types } from 'mongoose';
import { import {
IUser, IUser,
User, User,
IServiceAccount, IServiceAccount,
ServiceAccount, ServiceAccount,
IServiceTokenData, IServiceTokenData,
ServiceTokenData ServiceTokenData,
} from '../models'; } from "../models";
import { Organization, MembershipOrg } from '../models'; import { Organization, MembershipOrg } from "../models";
import {
ACCEPTED,
AUTH_MODE_JWT,
AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER
} from '../variables';
import {
getStripeSecretKey,
getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter
} from '../config';
import { import {
UnauthorizedRequestError, ACCEPTED,
OrganizationNotFoundError AUTH_MODE_JWT,
} from '../utils/errors'; AUTH_MODE_SERVICE_ACCOUNT,
AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_API_KEY,
OWNER,
} from "../variables";
import { import {
validateUserClientForOrganization getStripeSecretKey,
} from '../helpers/user'; getStripeProductPro,
getStripeProductTeam,
getStripeProductStarter,
} from "../config";
import { import {
validateServiceAccountClientForOrganization UnauthorizedRequestError,
} from '../helpers/serviceAccount'; OrganizationNotFoundError,
} from "../utils/errors";
import { validateUserClientForOrganization } from "../helpers/user";
import { validateServiceAccountClientForOrganization } from "../helpers/serviceAccount";
/** /**
* Validate accepted clients for organization with id [organizationId] * Validate accepted clients for organization with id [organizationId]
@@ -42,69 +37,80 @@ import {
* @param {Types.ObjectId} obj.organizationId - id of organization to validate against * @param {Types.ObjectId} obj.organizationId - id of organization to validate against
*/ */
const validateClientForOrganization = async ({ const validateClientForOrganization = async ({
authData, authData,
organizationId, organizationId,
acceptedRoles, acceptedRoles,
acceptedStatuses acceptedStatuses,
}: { }: {
authData: { authData: {
authMode: string; authMode: string;
authPayload: IUser | IServiceAccount | IServiceTokenData; authPayload: IUser | IServiceAccount | IServiceTokenData;
}, };
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
acceptedRoles: Array<'owner' | 'admin' | 'member'>; acceptedRoles: Array<"owner" | "admin" | "member">;
acceptedStatuses: Array<'invited' | 'accepted'>; acceptedStatuses: Array<"invited" | "accepted">;
}) => { }) => {
const organization = await Organization.findById(organizationId);
const organization = await Organization.findById(organizationId);
if (!organization) {
throw OrganizationNotFoundError({
message: 'Failed to find organization'
});
}
if (authData.authMode === AUTH_MODE_JWT && authData.authPayload instanceof User) {
const membershipOrg = await validateUserClientForOrganization({
user: authData.authPayload,
organization,
acceptedRoles,
acceptedStatuses
});
return ({ organization, membershipOrg });
}
if (authData.authMode === AUTH_MODE_SERVICE_ACCOUNT && authData.authPayload instanceof ServiceAccount) { if (!organization) {
await validateServiceAccountClientForOrganization({ throw OrganizationNotFoundError({
serviceAccount: authData.authPayload, message: "Failed to find organization",
organization });
}); }
return ({ organization });
}
if (authData.authMode === AUTH_MODE_SERVICE_TOKEN && authData.authPayload instanceof ServiceTokenData) { if (
throw UnauthorizedRequestError({ authData.authMode === AUTH_MODE_JWT &&
message: 'Failed service token authorization for organization' 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) { return { organization, membershipOrg };
const membershipOrg = await validateUserClientForOrganization({ }
user: authData.authPayload,
organization, if (
acceptedRoles, authData.authMode === AUTH_MODE_SERVICE_ACCOUNT &&
acceptedStatuses authData.authPayload instanceof ServiceAccount
}); ) {
await validateServiceAccountClientForOrganization({
return ({ organization, membershipOrg }); serviceAccount: authData.authPayload,
} organization,
});
throw UnauthorizedRequestError({
message: 'Failed client authorization for 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] * Create an organization with name [name]
@@ -114,43 +120,37 @@ const validateClientForOrganization = async ({
* @param {Object} organization - new organization * @param {Object} organization - new organization
*/ */
const createOrganization = async ({ const createOrganization = async ({
name, name,
email email,
}: { }: {
name: string; name: string;
email: string; email: string;
}) => { }) => {
let organization; let organization;
try { // register stripe account
// register stripe account const stripe = new Stripe(await getStripeSecretKey(), {
const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: "2022-08-01",
apiVersion: '2022-08-01' });
});
if (await getStripeSecretKey()) { if (await getStripeSecretKey()) {
const customer = await stripe.customers.create({ const customer = await stripe.customers.create({
email, email,
description: name description: name,
}); });
organization = await new Organization({ organization = await new Organization({
name, name,
customerId: customer.id customerId: customer.id,
}).save(); }).save();
} else { } else {
organization = await new Organization({ organization = await new Organization({
name name,
}).save(); }).save();
} }
await initSubscriptionOrg({ organizationId: organization._id }); await initSubscriptionOrg({ organizationId: organization._id });
} catch (err) {
Sentry.setUser({ email });
Sentry.captureException(err);
throw new Error(`Failed to create organization [err=${err}]`);
}
return organization; return organization;
}; };
/** /**
@@ -162,57 +162,52 @@ const createOrganization = async ({
* @return {Subscription} obj.subscription - new subscription * @return {Subscription} obj.subscription - new subscription
*/ */
const initSubscriptionOrg = async ({ const initSubscriptionOrg = async ({
organizationId organizationId,
}: { }: {
organizationId: Types.ObjectId; organizationId: Types.ObjectId;
}) => { }) => {
let stripeSubscription; let stripeSubscription;
let subscription; let subscription;
try {
// find organization
const organization = await Organization.findOne({
_id: organizationId
});
if (organization) { // find organization
if (organization.customerId) { const organization = await Organization.findOne({
// initialize starter subscription with quantity of 0 _id: organizationId,
const stripe = new Stripe(await getStripeSecretKey(), { });
apiVersion: '2022-08-01'
});
const productToPriceMap = { if (organization) {
starter: await getStripeProductStarter(), if (organization.customerId) {
team: await getStripeProductTeam(), // initialize starter subscription with quantity of 0
pro: await getStripeProductPro() const stripe = new Stripe(await getStripeSecretKey(), {
}; apiVersion: "2022-08-01",
});
stripeSubscription = await stripe.subscriptions.create({ const productToPriceMap = {
customer: organization.customerId, starter: await getStripeProductStarter(),
items: [ team: await getStripeProductTeam(),
{ pro: await getStripeProductPro(),
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');
}
return { stripeSubscription = await stripe.subscriptions.create({
stripeSubscription, customer: organization.customerId,
subscription 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 * @param {Number} obj.organizationId - id of subscription's organization
*/ */
const updateSubscriptionOrgQuantity = async ({ const updateSubscriptionOrgQuantity = async ({
organizationId organizationId,
}: { }: {
organizationId: string; organizationId: string;
}) => { }) => {
let stripeSubscription; let stripeSubscription;
try { // find organization
// find organization const organization = await Organization.findOne({
const organization = await Organization.findOne({ _id: organizationId,
_id: organizationId });
});
if (organization && organization.customerId) { if (organization && organization.customerId) {
const quantity = await MembershipOrg.countDocuments({ const quantity = await MembershipOrg.countDocuments({
organization: organizationId, organization: organizationId,
status: ACCEPTED status: ACCEPTED,
}); });
const stripe = new Stripe(await getStripeSecretKey(), { const stripe = new Stripe(await getStripeSecretKey(), {
apiVersion: '2022-08-01' apiVersion: "2022-08-01",
}); });
const subscription = ( const subscription = (
await stripe.subscriptions.list({ await stripe.subscriptions.list({
customer: organization.customerId customer: organization.customerId,
}) })
).data[0]; ).data[0];
stripeSubscription = await stripe.subscriptions.update(subscription.id, { stripeSubscription = await stripe.subscriptions.update(subscription.id, {
items: [ items: [
{ {
id: subscription.items.data[0].id, id: subscription.items.data[0].id,
price: subscription.items.data[0].price.id, price: subscription.items.data[0].price.id,
quantity quantity,
} },
] ],
}); });
} }
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
return stripeSubscription; return stripeSubscription;
}; };
export { export {
validateClientForOrganization, validateClientForOrganization,
createOrganization, createOrganization,
initSubscriptionOrg, initSubscriptionOrg,
updateSubscriptionOrgQuantity updateSubscriptionOrgQuantity,
}; };
File diff suppressed because it is too large Load Diff
+183 -181
View File
@@ -1,16 +1,15 @@
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { Types } from 'mongoose'; import { TokenData } from "../models";
import { TokenData } from '../models'; import crypto from "crypto";
import crypto from 'crypto'; import bcrypt from "bcrypt";
import bcrypt from 'bcrypt';
import { import {
TOKEN_EMAIL_CONFIRMATION, TOKEN_EMAIL_CONFIRMATION,
TOKEN_EMAIL_MFA, TOKEN_EMAIL_MFA,
TOKEN_EMAIL_ORG_INVITATION, TOKEN_EMAIL_ORG_INVITATION,
TOKEN_EMAIL_PASSWORD_RESET TOKEN_EMAIL_PASSWORD_RESET,
} from '../variables'; } from "../variables";
import { UnauthorizedRequestError } from '../utils/errors'; import { UnauthorizedRequestError } from "../utils/errors";
import { getSaltRounds } from '../config'; import { getSaltRounds } from "../config";
/** /**
* Create and store a token in the database for purpose [type] * 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 * @returns {String} token - the created token
*/ */
const createTokenHelper = async ({ const createTokenHelper = async ({
type, type,
email, email,
phoneNumber, phoneNumber,
organizationId 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; email?: string;
phoneNumber?: string; phoneNumber?: string;
organizationId?: Types.ObjectId organization?: 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;
}
const query: TokenDataQuery = { type }; interface TokenDataUpdate {
const update: TokenDataUpdate = { type: string;
type, email?: string;
tokenHash: await bcrypt.hash(token, await getSaltRounds()), phoneNumber?: string;
expiresAt organization?: Types.ObjectId;
} tokenHash: string;
triesLeft?: number;
expiresAt: Date;
}
if (email) { const query: TokenDataQuery = { type };
query.email = email; const update: TokenDataUpdate = {
update.email = email; type,
} tokenHash: await bcrypt.hash(token, await getSaltRounds()),
if (phoneNumber) { expiresAt,
query.phoneNumber = phoneNumber; };
update.phoneNumber = phoneNumber;
} if (email) {
if (organizationId) { query.email = email;
query.organization = organizationId update.email = email;
update.organization = organizationId }
} if (phoneNumber) {
query.phoneNumber = phoneNumber;
if (triesLeft) { update.phoneNumber = phoneNumber;
update.triesLeft = triesLeft; }
} if (organizationId) {
query.organization = organizationId;
await TokenData.findOneAndUpdate( update.organization = organizationId;
query, }
update,
{ if (triesLeft) {
new: true, update.triesLeft = triesLeft;
upsert: true }
}
); await TokenData.findOneAndUpdate(query, update, {
} catch (err) { new: true,
Sentry.setUser(null); upsert: true,
Sentry.captureException(err); });
throw new Error(
"Failed to create token" return token;
); };
}
return token;
}
/** /**
* *
* @param {Object} obj * @param {Object} obj
* @param {String} obj.email - email associated with the token * @param {String} obj.email - email associated with the token
* @param {String} obj.token - value of the token * @param {String} obj.token - value of the token
*/ */
const validateTokenHelper = async ({ const validateTokenHelper = async ({
type, type,
email, email,
phoneNumber, phoneNumber,
organizationId, organizationId,
token 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; email?: string;
phoneNumber?: string; phoneNumber?: string;
organizationId?: Types.ObjectId; organization?: Types.ObjectId;
token: string; }
}) => {
interface Query {
type: string;
email?: string;
phoneNumber?: string;
organization?: Types.ObjectId;
}
const query: Query = { type }; const query: Query = { type };
if (email) { query.email = email; } if (email) {
if (phoneNumber) { query.phoneNumber = phoneNumber; } query.email = email;
if (organizationId) { query.organization = organizationId; } }
if (phoneNumber) {
query.phoneNumber = phoneNumber;
}
if (organizationId) {
query.organization = organizationId;
}
const tokenData = await TokenData.findOne(query).select('+tokenHash'); const tokenData = await TokenData.findOne(query).select("+tokenHash");
if (!tokenData) throw new Error('Failed to find token to validate');
if (tokenData.expiresAt < new Date()) {
// case: token expired
await TokenData.findByIdAndDelete(tokenData._id);
throw UnauthorizedRequestError({
message: 'MFA session expired. Please log in again',
context: {
code: 'mfa_expired'
}
});
}
const isValid = await bcrypt.compare(token, tokenData.tokenHash); if (!tokenData) throw new Error("Failed to find token to validate");
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({ if (tokenData.expiresAt < new Date()) {
message: 'MFA code is invalid', // case: token expired
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); await TokenData.findByIdAndDelete(tokenData._id);
} throw UnauthorizedRequestError({
message: "MFA session expired. Please log in again",
context: {
code: "mfa_expired",
},
});
}
export { const isValid = await bcrypt.compare(token, tokenData.tokenHash);
createTokenHelper, if (!isValid) {
validateTokenHelper // 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 };
+24 -39
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
IUser, IUser,
@@ -28,16 +27,9 @@ import {
* @returns {Object} user - the initialized user * @returns {Object} user - the initialized user
*/ */
const setupAccount = async ({ email }: { email: string }) => { const setupAccount = async ({ email }: { email: string }) => {
let user; const user = await new User({
try { email
user = await new User({ }).save();
email
}).save();
} catch (err) {
Sentry.setUser({ email });
Sentry.captureException(err);
throw new Error('Failed to set up account');
}
return user; return user;
}; };
@@ -89,34 +81,27 @@ const completeAccount = async ({
salt: string; salt: string;
verifier: string; verifier: string;
}) => { }) => {
let user; const options = {
try { new: true
const options = { };
new: true const user = await User.findByIdAndUpdate(
}; userId,
user = await User.findByIdAndUpdate( {
userId, firstName,
{ lastName,
firstName, encryptionVersion,
lastName, protectedKey,
encryptionVersion, protectedKeyIV,
protectedKey, protectedKeyTag,
protectedKeyIV, publicKey,
protectedKeyTag, encryptedPrivateKey,
publicKey, iv: encryptedPrivateKeyIV,
encryptedPrivateKey, tag: encryptedPrivateKeyTag,
iv: encryptedPrivateKeyIV, salt,
tag: encryptedPrivateKeyTag, verifier
salt, },
verifier options
}, );
options
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to complete account set up');
}
return user; return user;
}; };
+371 -453
View File
@@ -1,7 +1,6 @@
import * as Sentry from "@sentry/node";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import { IIntegrationAuth } from "../models"; import { IIntegrationAuth } from "../models";
import request from '../config/request'; import request from "../config/request";
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_AWS_PARAMETER_STORE, INTEGRATION_AWS_PARAMETER_STORE,
@@ -26,7 +25,7 @@ import {
INTEGRATION_FLYIO_API_URL, INTEGRATION_FLYIO_API_URL,
INTEGRATION_CIRCLECI_API_URL, INTEGRATION_CIRCLECI_API_URL,
INTEGRATION_TRAVISCI_API_URL, INTEGRATION_TRAVISCI_API_URL,
INTEGRATION_SUPABASE_API_URL INTEGRATION_SUPABASE_API_URL,
} from "../variables"; } from "../variables";
interface App { interface App {
@@ -47,87 +46,80 @@ interface App {
const getApps = async ({ const getApps = async ({
integrationAuth, integrationAuth,
accessToken, accessToken,
teamId teamId,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
teamId?: string; teamId?: string;
}) => { }) => {
let apps: App[] = []; let apps: App[] = [];
try { switch (integrationAuth.integration) {
switch (integrationAuth.integration) { case INTEGRATION_AZURE_KEY_VAULT:
case INTEGRATION_AZURE_KEY_VAULT: apps = [];
apps = []; break;
break; case INTEGRATION_AWS_PARAMETER_STORE:
case INTEGRATION_AWS_PARAMETER_STORE: apps = [];
apps = []; break;
break; case INTEGRATION_AWS_SECRET_MANAGER:
case INTEGRATION_AWS_SECRET_MANAGER: apps = [];
apps = []; break;
break; case INTEGRATION_HEROKU:
case INTEGRATION_HEROKU: apps = await getAppsHeroku({
apps = await getAppsHeroku({ accessToken,
accessToken, });
}); break;
break; case INTEGRATION_VERCEL:
case INTEGRATION_VERCEL: apps = await getAppsVercel({
apps = await getAppsVercel({ integrationAuth,
integrationAuth, accessToken,
accessToken, });
}); break;
break; case INTEGRATION_NETLIFY:
case INTEGRATION_NETLIFY: apps = await getAppsNetlify({
apps = await getAppsNetlify({ accessToken,
accessToken, });
}); break;
break; case INTEGRATION_GITHUB:
case INTEGRATION_GITHUB: apps = await getAppsGithub({
apps = await getAppsGithub({ accessToken,
accessToken, });
}); break;
break; case INTEGRATION_GITLAB:
case INTEGRATION_GITLAB: apps = await getAppsGitlab({
apps = await getAppsGitlab({ accessToken,
accessToken, teamId,
teamId });
}); break;
break; case INTEGRATION_RENDER:
case INTEGRATION_RENDER: apps = await getAppsRender({
apps = await getAppsRender({ accessToken,
accessToken, });
}); break;
break; case INTEGRATION_RAILWAY:
case INTEGRATION_RAILWAY: apps = await getAppsRailway({
apps = await getAppsRailway({ accessToken,
accessToken });
}); break;
break; case INTEGRATION_FLYIO:
case INTEGRATION_FLYIO: apps = await getAppsFlyio({
apps = await getAppsFlyio({ accessToken,
accessToken, });
}); break;
break; case INTEGRATION_CIRCLECI:
case INTEGRATION_CIRCLECI: apps = await getAppsCircleCI({
apps = await getAppsCircleCI({ accessToken,
accessToken, });
}); break;
break; case INTEGRATION_TRAVISCI:
case INTEGRATION_TRAVISCI: apps = await getAppsTravisCI({
apps = await getAppsTravisCI({ accessToken,
accessToken, });
}) break;
break; case INTEGRATION_SUPABASE:
case INTEGRATION_SUPABASE: apps = await getAppsSupabase({
apps = await getAppsSupabase({ accessToken,
accessToken });
}); break;
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get integration apps");
} }
return apps; return apps;
@@ -141,25 +133,18 @@ const getApps = async ({
* @returns {String} apps.name - name of Heroku app * @returns {String} apps.name - name of Heroku app
*/ */
const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => { const getAppsHeroku = async ({ accessToken }: { accessToken: string }) => {
let apps; const res = (
try { await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, {
const res = ( headers: {
await request.get(`${INTEGRATION_HEROKU_API_URL}/apps`, { Accept: "application/vnd.heroku+json; version=3",
headers: { Authorization: `Bearer ${accessToken}`,
Accept: "application/vnd.heroku+json; version=3", },
Authorization: `Bearer ${accessToken}`, })
}, ).data;
})
).data;
apps = res.map((a: any) => ({ const apps = res.map((a: any) => ({
name: a.name, name: a.name,
})); }));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Heroku integration apps");
}
return apps; return apps;
}; };
@@ -178,33 +163,26 @@ const getAppsVercel = async ({
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
accessToken: string; accessToken: string;
}) => { }) => {
let apps; const res = (
try { await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, {
const res = ( headers: {
await request.get(`${INTEGRATION_VERCEL_API_URL}/v9/projects`, { Authorization: `Bearer ${accessToken}`,
headers: { "Accept-Encoding": "application/json",
Authorization: `Bearer ${accessToken}`, },
'Accept-Encoding': 'application/json' ...(integrationAuth?.teamId
}, ? {
...(integrationAuth?.teamId params: {
? { teamId: integrationAuth.teamId,
params: { },
teamId: integrationAuth.teamId, }
}, : {}),
} })
: {}), ).data;
})
).data;
apps = res.projects.map((a: any) => ({ const apps = res.projects.map((a: any) => ({
name: a.name, name: a.name,
appId: a.id appId: a.id,
})); }));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Vercel integration apps");
}
return apps; return apps;
}; };
@@ -218,43 +196,40 @@ const getAppsVercel = async ({
*/ */
const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => { const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
const apps: any = []; const apps: any = [];
try { let page = 1;
let page = 1; const perPage = 10;
const perPage = 10; let hasMorePages = true;
let hasMorePages = true;
// 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`, { // 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, params,
headers: { headers: {
Authorization: `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
'Accept-Encoding': 'application/json' "Accept-Encoding": "application/json",
} },
});
data.map((a: any) => {
apps.push({
name: a.name,
appId: a.site_id
});
});
if (data.length < perPage) {
hasMorePages = false;
} }
);
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); page++;
Sentry.captureException(err);
throw new Error("Failed to get Netlify integration apps");
} }
return apps; return apps;
@@ -268,67 +243,59 @@ const getAppsNetlify = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of Github site * @returns {String} apps.name - name of Github site
*/ */
const getAppsGithub = async ({ accessToken }: { accessToken: string }) => { const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
let apps; interface GitHubApp {
try { id: string;
interface GitHubApp { name: string;
id: string; permissions: {
name: string; admin: boolean;
permissions: { };
admin: boolean; owner: {
}; login: string;
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({ return repos;
auth: accessToken, };
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; return apps;
}; };
@@ -341,29 +308,20 @@ const getAppsGithub = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.appId - id of Render service * @returns {String} apps.appId - id of Render service
*/ */
const getAppsRender = async ({ accessToken }: { accessToken: string }) => { const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
let apps: any; const res = (
try { await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, {
const res = ( headers: {
await request.get(`${INTEGRATION_RENDER_API_URL}/v1/services`, { Authorization: `Bearer ${accessToken}`,
headers: { Accept: "application/json",
Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json",
Accept: 'application/json', },
'Accept-Encoding': 'application/json', })
}, ).data;
})
).data; const apps = res.map((a: any) => ({
name: a.service.name,
apps = res appId: a.service.id,
.map((a: any) => ({ }));
name: a.service.name,
appId: a.service.id
}));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Render services");
}
return apps; return apps;
}; };
@@ -376,49 +334,51 @@ const getAppsRender = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of Railway project * @returns {String} apps.name - name of Railway project
* @returns {String} apps.appId - id of Railway project * @returns {String} apps.appId - id of Railway project
* *
*/ */
const getAppsRailway = async ({ accessToken }: { accessToken: string }) => { const getAppsRailway = async ({ accessToken }: { accessToken: string }) => {
let apps: any[] = []; const query = `
try { query GetProjects($userId: String, $teamId: String) {
const query = ` projects(userId: $userId, teamId: $teamId) {
query GetProjects($userId: String, $teamId: String) { edges {
projects(userId: $userId, teamId: $teamId) { node {
edges { id
node { name
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, query,
variables, variables,
}, { },
{
headers: { headers: {
'Authorization': `Bearer ${accessToken}`, Authorization: `Bearer ${accessToken}`,
'Content-Type': 'application/json', "Content-Type": "application/json",
'Accept-Encoding': 'application/json' "Accept-Encoding": "application/json",
}, },
}); }
);
apps = edges.map((e: any) => ({
name: e.node.name, const apps = edges.map((e: any) => ({
appId: e.node.id 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 apps;
} };
/** /**
* Return list of apps for Fly.io integration * 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 * @returns {String} apps.name - name of Fly.io apps
*/ */
const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => { const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
let apps; const query = `
try { query($role: String) {
const query = ` apps(type: "container", first: 400, role: $role) {
query($role: String) { nodes {
apps(type: "container", first: 400, role: $role) { id
nodes { name
id hostname
name
hostname
}
} }
} }
`; }
`;
const res = (await request.post(INTEGRATION_FLYIO_API_URL, { const res = (
query, await request.post(
variables: { INTEGRATION_FLYIO_API_URL,
role: null, {
query,
variables: {
role: null,
},
}, },
}, { {
headers: { headers: {
Authorization: "Bearer " + accessToken, Authorization: "Bearer " + accessToken,
'Accept': 'application/json', Accept: "application/json",
'Accept-Encoding': 'application/json', "Accept-Encoding": "application/json",
}, },
})).data.data.apps.nodes; }
)
).data.data.apps.nodes;
apps = res.map((a: any) => ({ const apps = res.map((a: any) => ({
name: a.name, name: a.name,
})); }));
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get Fly.io apps");
}
return apps; return apps;
}; };
@@ -475,63 +434,43 @@ const getAppsFlyio = async ({ accessToken }: { accessToken: string }) => {
* @returns {String} apps.name - name of CircleCI apps * @returns {String} apps.name - name of CircleCI apps
*/ */
const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => { const getAppsCircleCI = async ({ accessToken }: { accessToken: string }) => {
let apps: any; const res = (
try { await request.get(`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, {
const res = ( headers: {
await request.get( "Circle-Token": accessToken,
`${INTEGRATION_CIRCLECI_API_URL}/v1.1/projects`, "Accept-Encoding": "application/json",
{ },
headers: { })
"Circle-Token": accessToken, ).data;
"Accept-Encoding": "application/json",
}, const apps = res?.map((a: any) => {
} return {
) name: a?.reponame,
).data };
});
apps = res?.map((a: any) => {
return {
name: a?.reponame
}
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get CircleCI projects");
}
return apps; return apps;
}; };
const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => { const getAppsTravisCI = async ({ accessToken }: { accessToken: string }) => {
let apps: any; const res = (
try { await request.get(`${INTEGRATION_TRAVISCI_API_URL}/repos`, {
const res = ( headers: {
await request.get( Authorization: `token ${accessToken}`,
`${INTEGRATION_TRAVISCI_API_URL}/repos`, "Accept-Encoding": "application/json",
{ },
headers: { })
"Authorization": `token ${accessToken}`, ).data;
"Accept-Encoding": "application/json",
}, const apps = res?.map((a: any) => {
} return {
) name: a?.slug?.split("/")[1],
).data; 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 apps;
} };
/** /**
* Return list of repositories for GitLab integration * 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 {Object[]} apps - names of GitLab sites
* @returns {String} apps.name - name of GitLab site * @returns {String} apps.name - name of GitLab site
*/ */
const getAppsGitlab = async ({ const getAppsGitlab = async ({
accessToken, accessToken,
teamId teamId,
}: { }: {
accessToken: string; accessToken: string;
teamId?: string; teamId?: string;
}) => { }) => {
const apps: App[] = []; const apps: App[] = [];
let page = 1; let page = 1;
const perPage = 10; const perPage = 10;
let hasMorePages = true; let hasMorePages = true;
try {
if (teamId) { if (teamId) {
// case: fetch projects for group with id [teamId] in GitLab // case: fetch projects for group with id [teamId] in GitLab
while (hasMorePages) {
const params = new URLSearchParams({
page: String(page),
per_page: String(perPage)
});
const { data } = ( while (hasMorePages) {
await request.get( const params = new URLSearchParams({
`${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`, page: String(page),
{ per_page: String(perPage),
params, });
headers: {
"Authorization": `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
},
}
)
);
data.map((a: any) => { const { data } = await request.get(
apps.push({ `${INTEGRATION_GITLAB_API_URL}/v4/groups/${teamId}/projects`,
name: a.name, {
appId: a.id params,
}); headers: {
}); Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json",
if (data.length < perPage) { },
hasMorePages = false;
} }
);
page++;
} data.map((a: any) => {
} else { apps.push({
// case: fetch projects for individual in GitLab name: a.name,
appId: a.id,
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 } = ( if (data.length < perPage) {
await request.get( hasMorePages = false;
`${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++;
} }
page++;
} }
} catch (err) { } else {
Sentry.setUser(null); // case: fetch projects for individual in GitLab
Sentry.captureException(err);
throw new Error("Failed to get GitLab projects");
}
return apps;
}
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 * Return list of projects for Supabase integration
@@ -655,30 +580,23 @@ const getAppsGitlab = async ({
* @returns {String} apps.name - name of Supabase app * @returns {String} apps.name - name of Supabase app
*/ */
const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => { const getAppsSupabase = async ({ accessToken }: { accessToken: string }) => {
let apps: any; const { data } = await request.get(
try { `${INTEGRATION_SUPABASE_API_URL}/v1/projects`,
const { data } = await request.get( {
`${INTEGRATION_SUPABASE_API_URL}/v1/projects`, headers: {
{ Authorization: `Bearer ${accessToken}`,
headers: { "Accept-Encoding": "application/json",
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; return apps;
}; };
+138 -198
View File
@@ -1,5 +1,4 @@
import * as Sentry from '@sentry/node'; import request from "../config/request";
import request from '../config/request';
import { import {
INTEGRATION_AZURE_KEY_VAULT, INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
@@ -12,8 +11,8 @@ import {
INTEGRATION_VERCEL_TOKEN_URL, INTEGRATION_VERCEL_TOKEN_URL,
INTEGRATION_NETLIFY_TOKEN_URL, INTEGRATION_NETLIFY_TOKEN_URL,
INTEGRATION_GITHUB_TOKEN_URL, INTEGRATION_GITHUB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL INTEGRATION_GITLAB_TOKEN_URL,
} from '../variables'; } from "../variables";
import { import {
getSiteURL, getSiteURL,
getClientIdAzure, getClientIdAzure,
@@ -26,8 +25,8 @@ import {
getClientIdGitHub, getClientIdGitHub,
getClientSecretGitHub, getClientSecretGitHub,
getClientIdGitLab, getClientIdGitLab,
getClientSecretGitLab getClientSecretGitLab,
} from '../config'; } from "../config";
interface ExchangeCodeAzureResponse { interface ExchangeCodeAzureResponse {
token_type: string; token_type: string;
@@ -93,49 +92,43 @@ interface ExchangeCodeGitlabResponse {
*/ */
const exchangeCode = async ({ const exchangeCode = async ({
integration, integration,
code code,
}: { }: {
integration: string; integration: string;
code: string; code: string;
}) => { }) => {
let obj = {} as any; let obj = {} as any;
try { switch (integration) {
switch (integration) { case INTEGRATION_AZURE_KEY_VAULT:
case INTEGRATION_AZURE_KEY_VAULT: obj = await exchangeCodeAzure({
obj = await exchangeCodeAzure({ code,
code });
}); break;
break; case INTEGRATION_HEROKU:
case INTEGRATION_HEROKU: obj = await exchangeCodeHeroku({
obj = await exchangeCodeHeroku({ code,
code });
}); break;
break; case INTEGRATION_VERCEL:
case INTEGRATION_VERCEL: obj = await exchangeCodeVercel({
obj = await exchangeCodeVercel({ code,
code });
}); break;
break; case INTEGRATION_NETLIFY:
case INTEGRATION_NETLIFY: obj = await exchangeCodeNetlify({
obj = await exchangeCodeNetlify({ code,
code });
}); break;
break; case INTEGRATION_GITHUB:
case INTEGRATION_GITHUB: obj = await exchangeCodeGithub({
obj = await exchangeCodeGithub({ code,
code });
}); break;
break; case INTEGRATION_GITLAB:
case INTEGRATION_GITLAB: obj = await exchangeCodeGitlab({
obj = await exchangeCodeGitlab({ code,
code });
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange');
} }
return obj; return obj;
@@ -143,43 +136,33 @@ const exchangeCode = async ({
/** /**
* Return [accessToken] for Azure OAuth2 code-token exchange * Return [accessToken] for Azure OAuth2 code-token exchange
* @param param0 * @param param0
*/ */
const exchangeCodeAzure = async ({ const exchangeCodeAzure = async ({ code }: { code: string }) => {
code
}: {
code: string;
}) => {
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
let res: ExchangeCodeAzureResponse;
try { const res: ExchangeCodeAzureResponse = (
res = (await request.post( await request.post(
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: "authorization_code",
code: 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_id: await getClientIdAzure(),
client_secret: await getClientSecretAzure(), 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) } as any)
)).data; )
).data;
accessExpiresAt.setSeconds( accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Azure');
}
return ({ return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: res.refresh_token, refreshToken: res.refresh_token,
accessExpiresAt accessExpiresAt,
}); };
} };
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku * Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
@@ -191,38 +174,28 @@ const exchangeCodeAzure = async ({
* @returns {String} obj2.refreshToken - refresh token for Heroku API * @returns {String} obj2.refreshToken - refresh token for Heroku API
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeHeroku = async ({ const exchangeCodeHeroku = async ({ code }: { code: string }) => {
code
}: {
code: string;
}) => {
let res: ExchangeCodeHerokuResponse;
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
try {
res = (await request.post( const res: ExchangeCodeHerokuResponse = (
await request.post(
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
new URLSearchParams({ new URLSearchParams({
grant_type: 'authorization_code', grant_type: "authorization_code",
code: code, code: code,
client_secret: await getClientSecretHeroku() client_secret: await getClientSecretHeroku(),
} as any) } as any)
)).data; )
).data;
accessExpiresAt.setSeconds( accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Heroku');
}
return ({ return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: res.refresh_token, refreshToken: res.refresh_token,
accessExpiresAt accessExpiresAt,
}); };
} };
/** /**
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel * Return [accessToken], [accessExpiresAt], and [refreshToken] for Vercel
@@ -235,30 +208,23 @@ const exchangeCodeHeroku = async ({
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeVercel = async ({ code }: { code: string }) => { const exchangeCodeVercel = async ({ code }: { code: string }) => {
let res: ExchangeCodeVercelResponse; const res: ExchangeCodeVercelResponse = (
try { await request.post(
res = ( INTEGRATION_VERCEL_TOKEN_URL,
await request.post( new URLSearchParams({
INTEGRATION_VERCEL_TOKEN_URL, code: code,
new URLSearchParams({ client_id: await getClientIdVercel(),
code: code, client_secret: await getClientSecretVercel(),
client_id: await getClientIdVercel(), redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback`,
client_secret: await getClientSecretVercel(), } as any)
redirect_uri: `${await getSiteURL()}/integrations/vercel/oauth2/callback` )
} as any) ).data;
)
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(`Failed OAuth2 code-token exchange with Vercel [err=${err}]`);
}
return { return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: null, refreshToken: null,
accessExpiresAt: null, accessExpiresAt: null,
teamId: res.team_id teamId: res.team_id,
}; };
}; };
@@ -273,47 +239,39 @@ const exchangeCodeVercel = async ({ code }: { code: string }) => {
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeNetlify = async ({ code }: { code: string }) => { const exchangeCodeNetlify = async ({ code }: { code: string }) => {
let res: ExchangeCodeNetlifyResponse; const res: ExchangeCodeNetlifyResponse = (
let accountId; await request.post(
try { INTEGRATION_NETLIFY_TOKEN_URL,
res = ( new URLSearchParams({
await request.post( grant_type: "authorization_code",
INTEGRATION_NETLIFY_TOKEN_URL, code: code,
new URLSearchParams({ client_id: await getClientIdNetlify(),
grant_type: 'authorization_code', client_secret: await getClientSecretNetlify(),
code: code, redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback`,
client_id: await getClientIdNetlify(), } as any)
client_secret: await getClientSecretNetlify(), )
redirect_uri: `${await getSiteURL()}/integrations/netlify/oauth2/callback` ).data;
} 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: { headers: {
Authorization: `Bearer ${res.access_token}` Authorization: `Bearer ${res.access_token}`,
} },
}); })
).data;
const res3 = ( const accountId = res3[0].id;
await request.get('https://api.netlify.com/api/v1/accounts', {
headers: {
Authorization: `Bearer ${res.access_token}`
}
})
).data;
accountId = res3[0].id;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Netlify');
}
return { return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: res.refresh_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 * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeGithub = async ({ code }: { code: string }) => { const exchangeCodeGithub = async ({ code }: { code: string }) => {
let res: ExchangeCodeGithubResponse; const res: ExchangeCodeGithubResponse = (
try { await request.get(INTEGRATION_GITHUB_TOKEN_URL, {
res = ( params: {
await request.get(INTEGRATION_GITHUB_TOKEN_URL, { client_id: await getClientIdGitHub(),
params: { client_secret: await getClientSecretGitHub(),
client_id: await getClientIdGitHub(), code: code,
client_secret: await getClientSecretGitHub(), redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback`,
code: code, },
redirect_uri: `${await getSiteURL()}/integrations/github/oauth2/callback` headers: {
}, Accept: "application/json",
headers: { "Accept-Encoding": "application/json",
'Accept': 'application/json', },
'Accept-Encoding': 'application/json' })
} ).data;
})
).data;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Github');
}
return { return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: null, 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 * @returns {Date} obj2.accessExpiresAt - date of expiration for access token
*/ */
const exchangeCodeGitlab = async ({ code }: { code: string }) => { const exchangeCodeGitlab = async ({ code }: { code: string }) => {
let res: ExchangeCodeGitlabResponse;
const accessExpiresAt = new Date(); const accessExpiresAt = new Date();
const res: ExchangeCodeGitlabResponse = (
try { await request.post(
res = ( INTEGRATION_GITLAB_TOKEN_URL,
await request.post( new URLSearchParams({
INTEGRATION_GITLAB_TOKEN_URL, grant_type: "authorization_code",
new URLSearchParams({ code: code,
grant_type: 'authorization_code', client_id: await getClientIdGitLab(),
code: code, client_secret: await getClientSecretGitLab(),
client_id: await getClientIdGitLab(), redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
client_secret: await getClientSecretGitLab(), } as any),
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback` {
} as any), headers: {
{ "Accept-Encoding": "application/json",
headers: { },
"Accept-Encoding": "application/json", }
} )
} ).data;
)
).data; accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + res.expires_in);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.expires_in
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed OAuth2 code-token exchange with Gitlab');
}
return { return {
accessToken: res.access_token, accessToken: res.access_token,
refreshToken: res.refresh_token, refreshToken: res.refresh_token,
accessExpiresAt accessExpiresAt,
}; };
} };
export { exchangeCode }; export { exchangeCode };
+114 -145
View File
@@ -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 { import {
IIntegrationAuth INTEGRATION_AZURE_KEY_VAULT,
} from '../models';
import {
INTEGRATION_AZURE_KEY_VAULT,
INTEGRATION_HEROKU, INTEGRATION_HEROKU,
INTEGRATION_GITLAB, INTEGRATION_GITLAB,
} from '../variables'; } from "../variables";
import { import {
INTEGRATION_AZURE_TOKEN_URL, INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL INTEGRATION_GITLAB_TOKEN_URL,
} from '../variables'; } from "../variables";
import { import { IntegrationService } from "../services";
IntegrationService
} from '../services';
import { import {
getSiteURL, getSiteURL,
getClientIdAzure, getClientIdAzure,
getClientSecretAzure, getClientSecretAzure,
getClientSecretHeroku, getClientSecretHeroku,
getClientIdGitLab, getClientIdGitLab,
getClientSecretGitLab getClientSecretGitLab,
} from '../config'; } from "../config";
interface RefreshTokenAzureResponse { interface RefreshTokenAzureResponse {
token_type: string; token_type: string;
@@ -60,60 +55,57 @@ interface RefreshTokenGitLabResponse {
*/ */
const exchangeRefresh = async ({ const exchangeRefresh = async ({
integrationAuth, integrationAuth,
refreshToken refreshToken,
}: { }: {
integrationAuth: IIntegrationAuth; integrationAuth: IIntegrationAuth;
refreshToken: string; refreshToken: string;
}) => { }) => {
interface TokenDetails { interface TokenDetails {
accessToken: string; accessToken: string;
refreshToken: string; refreshToken: string;
accessExpiresAt: Date; accessExpiresAt: Date;
} }
let tokenDetails: TokenDetails; let tokenDetails: TokenDetails;
try { switch (integrationAuth.integration) {
switch (integrationAuth.integration) { case INTEGRATION_AZURE_KEY_VAULT:
case INTEGRATION_AZURE_KEY_VAULT: tokenDetails = await exchangeRefreshAzure({
tokenDetails = await exchangeRefreshAzure({ refreshToken,
refreshToken
});
break;
case INTEGRATION_HEROKU:
tokenDetails = await exchangeRefreshHeroku({
refreshToken
});
break;
case INTEGRATION_GITLAB:
tokenDetails = await exchangeRefreshGitLab({
refreshToken
});
break;
default:
throw new Error('Failed to exchange token for incompatible integration');
}
if (tokenDetails?.accessToken && tokenDetails?.refreshToken && tokenDetails?.accessExpiresAt) {
await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: tokenDetails.accessToken,
accessExpiresAt: tokenDetails.accessExpiresAt
}); });
break;
await IntegrationService.setIntegrationAuthRefresh({ case INTEGRATION_HEROKU:
integrationAuthId: integrationAuth._id.toString(), tokenDetails = await exchangeRefreshHeroku({
refreshToken: tokenDetails.refreshToken refreshToken,
}); });
} break;
case INTEGRATION_GITLAB:
return tokenDetails.accessToken; tokenDetails = await exchangeRefreshGitLab({
} catch (err) { refreshToken,
Sentry.setUser(null); });
Sentry.captureException(err); break;
throw new Error('Failed to get new OAuth2 access token'); 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 * @returns
*/ */
const exchangeRefreshAzure = async ({ const exchangeRefreshAzure = async ({
refreshToken refreshToken,
}: { }: {
refreshToken: string; refreshToken: string;
}) => { }) => {
try { const accessExpiresAt = new Date();
const accessExpiresAt = new Date(); const { data }: { data: RefreshTokenAzureResponse } = await request.post(
const { data }: { data: RefreshTokenAzureResponse } = await request.post( INTEGRATION_AZURE_TOKEN_URL,
INTEGRATION_AZURE_TOKEN_URL, new URLSearchParams({
new URLSearchParams({ client_id: await getClientIdAzure(),
client_id: await getClientIdAzure(), scope: "openid offline_access",
scope: 'openid offline_access', refresh_token: refreshToken,
refresh_token: refreshToken, grant_type: "refresh_token",
grant_type: 'refresh_token', client_secret: await getClientSecretAzure(),
client_secret: await getClientSecretAzure() } as any)
} as any) );
);
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + data.expires_in
);
return ({ accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
accessToken: data.access_token,
refreshToken: data.refresh_token, return {
accessExpiresAt accessToken: data.access_token,
}); refreshToken: data.refresh_token,
} catch (err) { accessExpiresAt,
Sentry.setUser(null); };
Sentry.captureException(err); };
throw new Error('Failed to get refresh OAuth2 access token for Azure');
}
}
/** /**
* Return new access token by exchanging refresh token [refreshToken] for the * Return new access token by exchanging refresh token [refreshToken] for the
@@ -165,39 +149,31 @@ const exchangeRefreshAzure = async ({
* @returns * @returns
*/ */
const exchangeRefreshHeroku = async ({ const exchangeRefreshHeroku = async ({
refreshToken refreshToken,
}: { }: {
refreshToken: string; refreshToken: string;
}) => { }) => {
try { const accessExpiresAt = new Date();
const accessExpiresAt = new Date(); const {
const { data,
data }: {
}: { data: RefreshTokenHerokuResponse;
data: RefreshTokenHerokuResponse } = await request.post(
} = await request.post( INTEGRATION_HEROKU_TOKEN_URL,
INTEGRATION_HEROKU_TOKEN_URL, new URLSearchParams({
new URLSearchParams({ grant_type: "refresh_token",
grant_type: 'refresh_token', refresh_token: refreshToken,
refresh_token: refreshToken, client_secret: await getClientSecretHeroku(),
client_secret: await getClientSecretHeroku() } as any)
} as any) );
);
accessExpiresAt.setSeconds( accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
accessExpiresAt.getSeconds() + data.expires_in
);
return ({ return {
accessToken: data.access_token, accessToken: data.access_token,
refreshToken: data.refresh_token, refreshToken: data.refresh_token,
accessExpiresAt accessExpiresAt,
}); };
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for Heroku');
}
}; };
/** /**
@@ -208,45 +184,38 @@ const exchangeRefreshHeroku = async ({
* @returns * @returns
*/ */
const exchangeRefreshGitLab = async ({ const exchangeRefreshGitLab = async ({
refreshToken refreshToken,
}: { }: {
refreshToken: string; refreshToken: string;
}) => { }) => {
try { const accessExpiresAt = new Date();
const accessExpiresAt = new Date(); const {
const { data,
data }: {
}: { data: RefreshTokenGitLabResponse;
data: RefreshTokenGitLabResponse } = await request.post(
} = await request.post( INTEGRATION_GITLAB_TOKEN_URL,
INTEGRATION_GITLAB_TOKEN_URL, new URLSearchParams({
new URLSearchParams({ grant_type: "refresh_token",
grant_type: 'refresh_token', refresh_token: refreshToken,
refresh_token: refreshToken, client_id: await getClientIdGitLab,
client_id: await getClientIdGitLab, client_secret: await getClientSecretGitLab(),
client_secret: await getClientSecretGitLab(), redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback`,
redirect_uri: `${await getSiteURL()}/integrations/gitlab/oauth2/callback` } as any),
} as any), {
{ headers: {
headers: { "Accept-Encoding": "application/json",
"Accept-Encoding": "application/json", },
} }
}); );
accessExpiresAt.setSeconds( accessExpiresAt.setSeconds(accessExpiresAt.getSeconds() + data.expires_in);
accessExpiresAt.getSeconds() + data.expires_in
);
return ({ return {
accessToken: data.access_token, accessToken: data.access_token,
refreshToken: data.refresh_token, refreshToken: data.refresh_token,
accessExpiresAt accessExpiresAt,
}); };
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to refresh OAuth2 access token for GitLab');
}
}; };
export { exchangeRefresh }; export { exchangeRefresh };
+20 -27
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { import {
IIntegrationAuth, IIntegrationAuth,
IntegrationAuth, IntegrationAuth,
@@ -22,34 +21,28 @@ const revokeAccess = async ({
accessToken: string; accessToken: string;
}) => { }) => {
let deletedIntegrationAuth; let deletedIntegrationAuth;
try { // add any integration-specific revocation logic
// add any integration-specific revocation logic switch (integrationAuth.integration) {
switch (integrationAuth.integration) { case INTEGRATION_HEROKU:
case INTEGRATION_HEROKU: break;
break; case INTEGRATION_VERCEL:
case INTEGRATION_VERCEL: break;
break; case INTEGRATION_NETLIFY:
case INTEGRATION_NETLIFY: break;
break; case INTEGRATION_GITHUB:
case INTEGRATION_GITHUB: break;
break; case INTEGRATION_GITLAB:
case INTEGRATION_GITLAB: break;
break; }
}
deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({ deletedIntegrationAuth = await IntegrationAuth.findOneAndDelete({
_id: integrationAuth._id _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; return deletedIntegrationAuth;
+21 -34
View File
@@ -1,4 +1,3 @@
import * as Sentry from "@sentry/node";
import { import {
IIntegrationAuth IIntegrationAuth
} from '../models'; } from '../models';
@@ -31,21 +30,15 @@ const getTeams = async ({
}) => { }) => {
let teams: Team[] = []; let teams: Team[] = [];
try {
switch (integrationAuth.integration) { switch (integrationAuth.integration) {
case INTEGRATION_GITLAB: case INTEGRATION_GITLAB:
teams = await getTeamsGitLab({ teams = await getTeamsGitLab({
accessToken accessToken
}); });
break; break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration teams');
} }
return teams; return teams;
} }
@@ -63,30 +56,24 @@ const getTeamsGitLab = async ({
accessToken: string; accessToken: string;
}) => { }) => {
let teams: Team[] = []; let teams: Team[] = [];
try { const res = (await request.get(
const res = (await request.get( `${INTEGRATION_GITLAB_API_URL}/v4/groups`,
`${INTEGRATION_GITLAB_API_URL}/v4/groups`, {
{ headers: {
headers: { Authorization: `Bearer ${accessToken}`,
Authorization: `Bearer ${accessToken}`, "Accept-Encoding": "application/json"
"Accept-Encoding": "application/json"
}
} }
)).data; }
)).data;
teams = res.map((t: any) => ({
name: t.name, teams = res.map((t: any) => ({
teamId: t.id name: t.name,
})); teamId: t.id
} catch (err) { }));
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Failed to get GitLab integration teams");
}
return teams; return teams;
} }
export { export {
getTeams getTeams
} }
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response, NextFunction } from 'express'; import { Request, Response, NextFunction } from 'express';
import { IntegrationAuth, IWorkspace } from '../models'; import { IntegrationAuth, IWorkspace } from '../models';
+16 -48
View File
@@ -1,7 +1,6 @@
import nacl from 'tweetnacl'; import nacl from 'tweetnacl';
import util from 'tweetnacl-util'; import util from 'tweetnacl-util';
import AesGCM from './aes-gcm'; import AesGCM from './aes-gcm';
import * as Sentry from '@sentry/node';
/** /**
* Return new base64, NaCl, public-private key pair. * Return new base64, NaCl, public-private key pair.
@@ -38,20 +37,13 @@ const encryptAsymmetric = ({
publicKey: string; publicKey: string;
privateKey: string; privateKey: string;
}) => { }) => {
let nonce, ciphertext; const nonce = nacl.randomBytes(24);
try { const ciphertext = nacl.box(
nonce = nacl.randomBytes(24); util.decodeUTF8(plaintext),
ciphertext = nacl.box( nonce,
util.decodeUTF8(plaintext), util.decodeBase64(publicKey),
nonce, util.decodeBase64(privateKey)
util.decodeBase64(publicKey), );
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric encryption');
}
return { return {
ciphertext: util.encodeBase64(ciphertext), ciphertext: util.encodeBase64(ciphertext),
@@ -80,19 +72,12 @@ const decryptAsymmetric = ({
publicKey: string; publicKey: string;
privateKey: string; privateKey: string;
}): string => { }): string => {
let plaintext: any; const plaintext: any = nacl.box.open(
try { util.decodeBase64(ciphertext),
plaintext = nacl.box.open( util.decodeBase64(nonce),
util.decodeBase64(ciphertext), util.decodeBase64(publicKey),
util.decodeBase64(nonce), util.decodeBase64(privateKey)
util.decodeBase64(publicKey), );
util.decodeBase64(privateKey)
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform asymmetric decryption');
}
return util.encodeUTF8(plaintext); return util.encodeUTF8(plaintext);
}; };
@@ -110,17 +95,8 @@ const encryptSymmetric = ({
plaintext: string; plaintext: string;
key: string; key: string;
}) => { }) => {
let ciphertext, iv, tag; const obj = AesGCM.encrypt(plaintext, key);
try { const { ciphertext, iv, tag } = obj;
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');
}
return { return {
ciphertext, ciphertext,
@@ -150,15 +126,7 @@ const decryptSymmetric = ({
tag: string; tag: string;
key: string; key: string;
}): string => { }): string => {
let plaintext; const plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
try {
plaintext = AesGCM.decrypt(ciphertext, iv, tag, key);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption');
}
return plaintext; return plaintext;
}; };
+13 -13
View File
@@ -28,14 +28,14 @@ describe('Crypto', () => {
test('should throw error if publicKey is undefined', () => { test('should throw error if publicKey is undefined', () => {
expect(() => { expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey }); encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption'); }).toThrowError('invalid encoding');
}); });
test('should throw error if publicKey is empty string', () => { test('should throw error if publicKey is empty string', () => {
publicKey = ''; publicKey = '';
expect(() => { expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey }); 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', () => { test('should throw error if privateKey is undefined', () => {
expect(() => { expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey }); encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption'); }).toThrowError('invalid encoding');
}); });
test('should throw error if privateKey is empty string', () => { test('should throw error if privateKey is empty string', () => {
privateKey = ''; privateKey = '';
expect(() => { expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey }); 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', () => { test('should throw error if plaintext is undefined', () => {
expect(() => { expect(() => {
encryptAsymmetric({ plaintext, publicKey, privateKey }); encryptAsymmetric({ plaintext, publicKey, privateKey });
}).toThrowError('Failed to perform asymmetric encryption'); }).toThrowError('expected string');
}); });
test('should encrypt plaintext containing special characters', () => { test('should encrypt plaintext containing special characters', () => {
@@ -130,7 +130,7 @@ describe('Crypto', () => {
publicKey, publicKey,
privateKey privateKey
}); });
}).toThrowError('Failed to perform asymmetric decryption'); }).toThrowError('invalid encoding');
}); });
test('should throw error if nonce is modified', () => { test('should throw error if nonce is modified', () => {
@@ -149,7 +149,7 @@ describe('Crypto', () => {
publicKey, publicKey,
privateKey privateKey
}); });
}).toThrowError('Failed to perform asymmetric decryption'); }).toThrowError('invalid encoding');
}); });
}); });
}); });
@@ -170,7 +170,7 @@ describe('Crypto', () => {
const invalidKey = 'invalid-key'; const invalidKey = 'invalid-key';
expect(() => { expect(() => {
encryptSymmetric({ plaintext, key: invalidKey }); encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Failed to perform symmetric encryption'); }).toThrowError('Invalid key length');
}); });
test('should throw an error when invalid key is provided', () => { test('should throw an error when invalid key is provided', () => {
@@ -179,7 +179,7 @@ describe('Crypto', () => {
expect(() => { expect(() => {
encryptSymmetric({ plaintext, key: invalidKey }); encryptSymmetric({ plaintext, key: invalidKey });
}).toThrowError('Failed to perform symmetric encryption'); }).toThrowError('Invalid key length');
}); });
}); });
@@ -209,7 +209,7 @@ describe('Crypto', () => {
tag, tag,
key key
}); });
}).toThrowError('Failed to perform symmetric decryption'); }).toThrowError('Unsupported state or unable to authenticate data');
}); });
test('should fail if iv is modified', () => { test('should fail if iv is modified', () => {
@@ -221,7 +221,7 @@ describe('Crypto', () => {
tag, tag,
key key
}); });
}).toThrowError('Failed to perform symmetric decryption'); }).toThrowError('Unsupported state or unable to authenticate data');
}); });
test('should fail if tag is modified', () => { test('should fail if tag is modified', () => {
@@ -233,7 +233,7 @@ describe('Crypto', () => {
tag: modifiedTag, tag: modifiedTag,
key key
}); });
}).toThrowError('Failed to perform symmetric decryption'); }).toThrowError(/Invalid authentication tag length: \d+/);
}); });
test('should throw an error when decryption fails', () => { test('should throw an error when decryption fails', () => {
@@ -245,7 +245,7 @@ describe('Crypto', () => {
tag, tag,
key: invalidKey key: invalidKey
}); });
}).toThrowError('Failed to perform symmetric decryption'); }).toThrowError('Invalid key length');
}); });
}); });
}); });