feat(folder-scoped-integration): implemented api changes for integrations to support folders

This commit is contained in:
akhilmhdh
2023-06-15 22:46:39 +05:30
parent c77ebd4d0e
commit 2471418591
8 changed files with 599 additions and 485 deletions
@@ -1,10 +1,11 @@
import { Request, Response } from 'express'; import { Request, Response } from "express";
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import { Integration } from "../../models";
Integration import { EventService } from "../../services";
} from '../../models'; import { eventPushSecrets } from "../../events";
import { EventService } from '../../services'; import Folder from "../../models/folder";
import { eventPushSecrets } from '../../events'; import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/** /**
* Create/initialize an (empty) integration for integration authorization * Create/initialize an (empty) integration for integration authorization
@@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => {
targetServiceId, targetServiceId,
owner, owner,
path, path,
region region,
secretPath,
} = req.body; } = req.body;
const folders = await Folder.findOne({
workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist",
});
}
}
// TODO: validate [sourceEnvironment] and [targetEnvironment] // TODO: validate [sourceEnvironment] and [targetEnvironment]
// initialize new integration after saving integration access token // initialize new integration after saving integration access token
@@ -44,8 +60,9 @@ export const createIntegration = async (req: Request, res: Response) => {
owner, owner,
path, path,
region, region,
secretPath,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId) integrationAuth: new Types.ObjectId(integrationAuthId),
}).save(); }).save();
if (integration) { if (integration) {
@@ -53,8 +70,8 @@ export const createIntegration = async (req: Request, res: Response) => {
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment: sourceEnvironment environment: sourceEnvironment,
}) }),
}); });
} }
@@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
@@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
secretPath,
} = req.body; } = req.body;
const folders = await Folder.findOne({
workspace: req.integration.workspace,
environment,
});
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw BadRequestError({
message: "Path for service token does not exist",
});
}
}
const integration = await Integration.findOneAndUpdate( const integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id, _id: req.integration._id,
@@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
secretPath,
}, },
{ {
new: true, new: true,
@@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment environment,
}), }),
}); });
} }
+53 -33
View File
@@ -1,29 +1,21 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { Bot, BotKey, Secret, ISecret, IUser } from "../models";
Bot,
BotKey,
Secret,
ISecret,
IUser
} from "../models";
import { import {
generateKeyPair, generateKeyPair,
encryptSymmetric128BitHexKeyUTF8, encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8, decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric decryptAsymmetric,
} from '../utils/crypto'; } from "../utils/crypto";
import { import {
SECRET_SHARED, SECRET_SHARED,
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64 ENCODING_SCHEME_BASE64,
} from "../variables"; } from "../variables";
import { import { getEncryptionKey, getRootEncryptionKey, client } from "../config";
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { InternalServerError } from "../utils/errors"; import { InternalServerError } from "../utils/errors";
import Folder from "../models/folder";
import { getFolderByPath } from "../services/FolderService";
/** /**
* Create an inactive bot with name [name] for workspace with id [workspaceId] * Create an inactive bot with name [name] for workspace with id [workspaceId]
@@ -44,11 +36,10 @@ export const createBot = async ({
const { publicKey, privateKey } = generateKeyPair(); const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) { if (rootEncryptionKey) {
const { const { ciphertext, iv, tag } = client.encryptSymmetric(
ciphertext, privateKey,
iv, rootEncryptionKey
tag );
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
return await new Bot({ return await new Bot({
name, name,
@@ -59,9 +50,8 @@ export const createBot = async ({
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64 keyEncoding: ENCODING_SCHEME_BASE64,
}).save(); }).save();
} else if (encryptionKey) { } else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({ const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey, plaintext: privateKey,
@@ -77,12 +67,12 @@ export const createBot = async ({
iv, iv,
tag, tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
}).save(); }).save();
} }
throw InternalServerError({ throw InternalServerError({
message: 'Failed to create new bot due to missing encryption key' message: "Failed to create new bot due to missing encryption key",
}); });
}; };
@@ -92,11 +82,11 @@ export const createBot = async ({
*/ */
export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => { export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
const botKey = await BotKey.exists({ const botKey = await BotKey.exists({
workspace: workspaceId workspace: workspaceId,
}); });
return botKey ? false : true; return botKey ? false : true;
} };
/** /**
* Return decrypted secrets for workspace with id [workspaceId] * Return decrypted secrets for workspace with id [workspaceId]
@@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
export const getSecretsBotHelper = async ({ export const getSecretsBotHelper = async ({
workspaceId, workspaceId,
environment, environment,
secretPath,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
secretPath: string;
}) => { }) => {
const content = {} as any; const content = {} as any;
const key = await getKey({ workspaceId: workspaceId }); const key = await getKey({ workspaceId: workspaceId });
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment,
});
if (!folders && secretPath !== "/") {
throw InternalServerError({ message: "Folder not found" });
}
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) {
throw InternalServerError({ message: "Folder not found" });
}
folderId = folder.id;
}
const secrets = await Secret.find({ const secrets = await Secret.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
type: SECRET_SHARED, type: SECRET_SHARED,
folder: folderId,
}); });
secrets.forEach((secret: ISecret) => { secrets.forEach((secret: ISecret) => {
@@ -148,14 +160,17 @@ export const getSecretsBotHelper = async ({
* @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
*/ */
export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) => { export const getKey = async ({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) => {
const encryptionKey = await getEncryptionKey(); const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey(); const rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({ const botKey = await BotKey.findOne({
workspace: workspaceId, workspace: workspaceId,
}) }).populate<{ sender: IUser }>("sender", "publicKey");
.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");
@@ -168,7 +183,12 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) { if (rootEncryptionKey && bot.keyEncoding === ENCODING_SCHEME_BASE64) {
// case: encoding scheme is base64 // case: encoding scheme is base64
const privateKeyBot = client.decryptSymmetric(bot.encryptedPrivateKey, rootEncryptionKey, bot.iv, bot.tag); const privateKeyBot = client.decryptSymmetric(
bot.encryptedPrivateKey,
rootEncryptionKey,
bot.iv,
bot.tag
);
return decryptAsymmetric({ return decryptAsymmetric({
ciphertext: botKey.encryptedKey, ciphertext: botKey.encryptedKey,
@@ -177,13 +197,12 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
privateKey: privateKeyBot, privateKey: privateKeyBot,
}); });
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) { } else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8 // case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({ const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey, ciphertext: bot.encryptedPrivateKey,
iv: bot.iv, iv: bot.iv,
tag: bot.tag, tag: bot.tag,
key: encryptionKey key: encryptionKey,
}); });
return decryptAsymmetric({ return decryptAsymmetric({
@@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
} }
throw InternalServerError({ throw InternalServerError({
message: "Failed to obtain bot's copy of workspace key needed for bot operations" message:
"Failed to obtain bot's copy of workspace key needed for bot operations",
}); });
}; };
+120 -87
View File
@@ -1,20 +1,14 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import { Bot, Integration, IntegrationAuth } from "../models";
Bot, import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations";
Integration, import { BotService } from "../services";
IntegrationAuth
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import { import {
INTEGRATION_VERCEL, INTEGRATION_VERCEL,
INTEGRATION_NETLIFY, INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM, ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8 ENCODING_SCHEME_UTF8,
} from '../variables'; } from "../variables";
import { import { UnauthorizedRequestError } from "../utils/errors";
UnauthorizedRequestError,
} from '../utils/errors';
interface Update { interface Update {
workspace: string; workspace: string;
@@ -34,12 +28,12 @@ interface Update {
* @param {String} obj.integration - name of integration * @param {String} obj.integration - name of integration
* @param {String} obj.code - code * @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange * @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/ */
export const handleOAuthExchangeHelper = async ({ export const handleOAuthExchangeHelper = async ({
workspaceId, workspaceId,
integration, integration,
code, code,
environment environment,
}: { }: {
workspaceId: string; workspaceId: string;
integration: string; integration: string;
@@ -48,21 +42,22 @@ export const handleOAuthExchangeHelper = async ({
}) => { }) => {
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: workspaceId, workspace: workspaceId,
isActive: true isActive: true,
}); });
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); if (!bot)
throw new Error("Bot must be enabled for OAuth2 code-token exchange");
// exchange code for access and refresh tokens // exchange code for access and refresh tokens
const res = await exchangeCode({ const res = await exchangeCode({
integration, integration,
code code,
}); });
const update: Update = { const update: Update = {
workspace: workspaceId, workspace: workspaceId,
integration integration,
} };
switch (integration) { switch (integration) {
case INTEGRATION_VERCEL: case INTEGRATION_VERCEL:
@@ -73,20 +68,24 @@ export const handleOAuthExchangeHelper = async ({
break; break;
} }
const integrationAuth = await IntegrationAuth.findOneAndUpdate({ const integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
workspace: workspaceId, workspace: workspaceId,
integration integration,
}, update, { },
update,
{
new: true, new: true,
upsert: true upsert: true,
}); }
);
if (res.refreshToken) { if (res.refreshToken) {
// case: refresh token returned from exchange // case: refresh token returned from exchange
// set integration auth refresh token // set integration auth refresh token
await setIntegrationAuthRefreshHelper({ await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken refreshToken: res.refreshToken,
}); });
} }
@@ -97,12 +96,12 @@ export const handleOAuthExchangeHelper = async ({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId: null, accessId: null,
accessToken: res.accessToken, accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt accessExpiresAt: res.accessExpiresAt,
}); });
} }
return integrationAuth; return integrationAuth;
} };
/** /**
* Sync/push environment variables in workspace with id [workspaceId] to * Sync/push environment variables in workspace with id [workspaceId] to
* all active integrations for that workspace * all active integrations for that workspace
@@ -111,35 +110,41 @@ export const handleOAuthExchangeHelper = async ({
*/ */
export const syncIntegrationsHelper = async ({ export const syncIntegrationsHelper = async ({
workspaceId, workspaceId,
environment environment,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
}) => { }) => {
const integrations = await Integration.find({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId,
...(environment ? { ...(environment
environment ? {
} : {}), environment,
}
: {}),
isActive: true, isActive: true,
app: { $ne: null } app: { $ne: null },
}); });
// for each workspace integration, sync/push secrets // for each workspace integration, sync/push secrets
// to that integration // to that integration
for await (const integration of integrations) { for await (const integration of integrations) {
// get workspace, environment (shared) secrets // get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({ // issue here? const secrets = await BotService.getSecrets({
// issue here?
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment: integration.environment environment: integration.environment,
secretPath: integration.secretPath,
}); });
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); const integrationAuth = await IntegrationAuth.findById(
if (!integrationAuth) throw new Error('Failed to find integration auth'); integration.integrationAuth
);
if (!integrationAuth) throw new Error("Failed to find integration auth");
// get integration auth access token // get integration auth access token
const access = await getIntegrationAuthAccessHelper({ const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth integrationAuthId: integration.integrationAuth,
}); });
// sync secrets to integration // sync secrets to integration
@@ -148,10 +153,10 @@ export const syncIntegrationsHelper = async ({
integrationAuth, integrationAuth,
secrets, secrets,
accessId: access.accessId === undefined ? null : access.accessId, accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken accessToken: access.accessToken,
}); });
} }
} };
/** /**
* Return decrypted refresh token using the bot's copy * Return decrypted refresh token using the bot's copy
@@ -161,22 +166,29 @@ export const syncIntegrationsHelper = async ({
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token * @param {String} refreshToken - decrypted refresh token
*/ */
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthRefreshHelper = async ({
const integrationAuth = await IntegrationAuth integrationAuthId,
.findById(integrationAuthId) }: {
.select('+refreshCiphertext +refreshIV +refreshTag'); integrationAuthId: Types.ObjectId;
}) => {
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select("+refreshCiphertext +refreshIV +refreshTag");
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
});
const refreshToken = await BotService.decryptSymmetric({ const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string, ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string, iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string tag: integrationAuth.refreshTag as string,
}); });
return refreshToken; return refreshToken;
} };
/** /**
* Return decrypted access token using the bot's copy * Return decrypted access token using the bot's copy
@@ -186,20 +198,29 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i
* @param {String} obj.integrationAuthId - id of integration auth * @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token * @returns {String} accessToken - decrypted access token
*/ */
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { export const getIntegrationAuthAccessHelper = async ({
integrationAuthId,
}: {
integrationAuthId: Types.ObjectId;
}) => {
let accessId; let accessId;
let accessToken; let accessToken;
const integrationAuth = await IntegrationAuth const integrationAuth = await IntegrationAuth.findById(
.findById(integrationAuthId) integrationAuthId
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag'); ).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
);
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
});
accessToken = await BotService.decryptSymmetric({ accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string, ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string, iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string tag: integrationAuth.accessTag as string,
}); });
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
@@ -208,28 +229,34 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in
if (integrationAuth.accessExpiresAt < new Date()) { if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired // access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId,
});
accessToken = await exchangeRefresh({ accessToken = await exchangeRefresh({
integrationAuth, integrationAuth,
refreshToken refreshToken,
}); });
} }
} }
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) { if (
integrationAuth?.accessIdCiphertext &&
integrationAuth?.accessIdIV &&
integrationAuth?.accessIdTag
) {
accessId = await BotService.decryptSymmetric({ accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string, ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string, iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string tag: integrationAuth.accessIdTag as string,
}); });
} }
return ({ return {
accessId, accessId,
accessToken accessToken,
}); };
} };
/** /**
* Encrypt refresh token [refreshToken] using the bot's copy * Encrypt refresh token [refreshToken] using the bot's copy
@@ -241,36 +268,38 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in
*/ */
export const setIntegrationAuthRefreshHelper = async ({ export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId, integrationAuthId,
refreshToken refreshToken,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
refreshToken: string; refreshToken: string;
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
let integrationAuth = await IntegrationAuth if (!integrationAuth) throw new Error("Failed to find integration auth");
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({ const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
plaintext: refreshToken plaintext: refreshToken,
}); });
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate(
_id: integrationAuthId {
}, { _id: integrationAuthId,
},
{
refreshCiphertext: obj.ciphertext, refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv, refreshIV: obj.iv,
refreshTag: obj.tag, refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
}, { },
new: true {
}); new: true,
}
);
return integrationAuth; return integrationAuth;
} };
/** /**
* Encrypt access token [accessToken] and (optionally) access id [accessId] * Encrypt access token [accessToken] and (optionally) access id [accessId]
@@ -285,7 +314,7 @@ export const setIntegrationAuthAccessHelper = async ({
integrationAuthId, integrationAuthId,
accessId, accessId,
accessToken, accessToken,
accessExpiresAt accessExpiresAt,
}: { }: {
integrationAuthId: string; integrationAuthId: string;
accessId: string | null; accessId: string | null;
@@ -294,24 +323,26 @@ export const setIntegrationAuthAccessHelper = async ({
}) => { }) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId); let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({ const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
plaintext: accessToken plaintext: accessToken,
}); });
let encryptedAccessIdObj; let encryptedAccessIdObj;
if (accessId) { if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({ encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
plaintext: accessId plaintext: accessId,
}); });
} }
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate(
_id: integrationAuthId {
}, { _id: integrationAuthId,
},
{
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined, accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined, accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined, accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
@@ -320,10 +351,12 @@ export const setIntegrationAuthAccessHelper = async ({
accessTag: encryptedAccessTokenObj.tag, accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt, accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8,
}, { },
new: true {
}); new: true,
}
);
return integrationAuth; return integrationAuth;
} };
+30 -24
View File
@@ -15,7 +15,7 @@ import {
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT INTEGRATION_HASHICORP_VAULT,
} from "../variables"; } from "../variables";
export interface IIntegration { export interface IIntegration {
@@ -33,23 +33,24 @@ export interface IIntegration {
targetServiceId: string; targetServiceId: string;
path: string; path: string;
region: string; region: string;
secretPath: string;
integration: integration:
| 'azure-key-vault' | "azure-key-vault"
| 'aws-parameter-store' | "aws-parameter-store"
| 'aws-secret-manager' | "aws-secret-manager"
| 'heroku' | "heroku"
| 'vercel' | "vercel"
| 'netlify' | "netlify"
| 'github' | "github"
| 'gitlab' | "gitlab"
| 'render' | "render"
| 'railway' | "railway"
| 'flyio' | "flyio"
| 'circleci' | "circleci"
| 'travisci' | "travisci"
| 'supabase' | "supabase"
| 'checkly' | "checkly"
| 'hashicorp-vault'; | "hashicorp-vault";
integrationAuth: Types.ObjectId; integrationAuth: Types.ObjectId;
} }
@@ -71,7 +72,7 @@ const integrationSchema = new Schema<IIntegration>(
url: { url: {
// for custom self-hosted integrations (e.g. self-hosted GitHub enterprise) // for custom self-hosted integrations (e.g. self-hosted GitHub enterprise)
type: String, type: String,
default: null default: null,
}, },
app: { app: {
// name of app in provider // name of app in provider
@@ -90,17 +91,17 @@ const integrationSchema = new Schema<IIntegration>(
}, },
targetEnvironmentId: { targetEnvironmentId: {
type: String, type: String,
default: null default: null,
}, },
targetService: { targetService: {
// railway-specific service // railway-specific service
type: String, type: String,
default: null default: null,
}, },
targetServiceId: { targetServiceId: {
// railway-specific service // railway-specific service
type: String, type: String,
default: null default: null,
}, },
owner: { owner: {
// github-specific repo owner-login // github-specific repo owner-login
@@ -111,12 +112,12 @@ const integrationSchema = new Schema<IIntegration>(
// aws-parameter-store-specific path // aws-parameter-store-specific path
// (also) vercel preview-branch // (also) vercel preview-branch
type: String, type: String,
default: null default: null,
}, },
region: { region: {
// aws-parameter-store-specific path // aws-parameter-store-specific path
type: String, type: String,
default: null default: null,
}, },
integration: { integration: {
type: String, type: String,
@@ -136,7 +137,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI, INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE, INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY, INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT INTEGRATION_HASHICORP_VAULT,
], ],
required: true, required: true,
}, },
@@ -145,6 +146,11 @@ const integrationSchema = new Schema<IIntegration>(
ref: "IntegrationAuth", ref: "IntegrationAuth",
required: true, required: true,
}, },
secretPath: {
type: String,
required: true,
default: "/",
},
}, },
{ {
timestamps: true, timestamps: true,
+38 -36
View File
@@ -1,73 +1,75 @@
import express from 'express'; import express from "express";
const router = express.Router(); const router = express.Router();
import { import {
requireAuth, requireAuth,
requireIntegrationAuth, requireIntegrationAuth,
requireIntegrationAuthorizationAuth, requireIntegrationAuthorizationAuth,
validateRequest validateRequest,
} from '../../middleware'; } from "../../middleware";
import { import {
ADMIN, ADMIN,
MEMBER, MEMBER,
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY AUTH_MODE_API_KEY,
} from '../../variables'; } from "../../variables";
import { body, param } from 'express-validator'; import { body, param } from "express-validator";
import { integrationController } from '../../controllers/v1'; import { integrationController } from "../../controllers/v1";
router.post( router.post(
'/', "/",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY] acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}), }),
requireIntegrationAuthorizationAuth({ requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
location: 'body' location: "body",
}), }),
body('integrationAuthId').exists().isString().trim(), body("integrationAuthId").exists().isString().trim(),
body('app').trim(), body("app").trim(),
body('isActive').exists().isBoolean(), body("isActive").exists().isBoolean(),
body('appId').trim(), body("appId").trim(),
body('sourceEnvironment').trim(), body("secretPath").default("/").isString().trim(),
body('targetEnvironment').trim(), body("sourceEnvironment").trim(),
body('targetEnvironmentId').trim(), body("targetEnvironment").trim(),
body('targetService').trim(), body("targetEnvironmentId").trim(),
body('targetServiceId').trim(), body("targetService").trim(),
body('owner').trim(), body("targetServiceId").trim(),
body('path').trim(), body("owner").trim(),
body('region').trim(), body("path").trim(),
body("region").trim(),
validateRequest, validateRequest,
integrationController.createIntegration integrationController.createIntegration
); );
router.patch( router.patch(
'/:integrationId', "/:integrationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('integrationId').exists().trim(), param("integrationId").exists().trim(),
body('isActive').exists().isBoolean(), body("isActive").exists().isBoolean(),
body('app').exists().trim(), body("app").exists().trim(),
body('environment').exists().trim(), body("secretPath").default("/").isString().trim(),
body('appId').exists(), body("environment").exists().trim(),
body('targetEnvironment').exists(), body("appId").exists(),
body('owner').exists(), body("targetEnvironment").exists(),
body("owner").exists(),
validateRequest, validateRequest,
integrationController.updateIntegration integrationController.updateIntegration
); );
router.delete( router.delete(
'/:integrationId', "/:integrationId",
requireAuth({ requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT] acceptedAuthModes: [AUTH_MODE_JWT],
}), }),
requireIntegrationAuth({ requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER] acceptedRoles: [ADMIN, MEMBER],
}), }),
param('integrationId').exists().trim(), param("integrationId").exists().trim(),
validateRequest, validateRequest,
integrationController.deleteIntegration integrationController.deleteIntegration
); );
+14 -12
View File
@@ -1,17 +1,16 @@
import { Types } from 'mongoose'; import { Types } from "mongoose";
import { import {
getSecretsBotHelper, getSecretsBotHelper,
encryptSymmetricHelper, encryptSymmetricHelper,
decryptSymmetricHelper, decryptSymmetricHelper,
getKey, getKey,
getIsWorkspaceE2EEHelper getIsWorkspaceE2EEHelper,
} from '../helpers/bot'; } from "../helpers/bot";
/** /**
* Class to handle bot actions * Class to handle bot actions
*/ */
class BotService { class BotService {
/** /**
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted * Return whether or not workspace with id [workspaceId] is end-to-end encrypted
* @param workspaceId - id of workspace * @param workspaceId - id of workspace
@@ -28,12 +27,12 @@ class BotService {
* @returns * @returns
*/ */
static async getWorkspaceKeyWithBot({ static async getWorkspaceKeyWithBot({
workspaceId workspaceId,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) { }) {
return await getKey({ return await getKey({
workspaceId workspaceId,
}); });
} }
@@ -47,14 +46,17 @@ class BotService {
*/ */
static async getSecrets({ static async getSecrets({
workspaceId, workspaceId,
environment environment,
secretPath,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
secretPath: string;
}) { }) {
return await getSecretsBotHelper({ return await getSecretsBotHelper({
workspaceId, workspaceId,
environment environment,
secretPath,
}); });
} }
@@ -67,14 +69,14 @@ class BotService {
*/ */
static async encryptSymmetric({ static async encryptSymmetric({
workspaceId, workspaceId,
plaintext plaintext,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
plaintext: string; plaintext: string;
}) { }) {
return await encryptSymmetricHelper({ return await encryptSymmetricHelper({
workspaceId, workspaceId,
plaintext plaintext,
}); });
} }
@@ -91,7 +93,7 @@ class BotService {
workspaceId, workspaceId,
ciphertext, ciphertext,
iv, iv,
tag tag,
}: { }: {
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
ciphertext: string; ciphertext: string;
@@ -102,7 +104,7 @@ class BotService {
workspaceId, workspaceId,
ciphertext, ciphertext,
iv, iv,
tag tag,
}); });
} }
} }
+17
View File
@@ -13,6 +13,7 @@ import {
BackupPrivateKey, BackupPrivateKey,
IntegrationAuth, IntegrationAuth,
ServiceTokenData, ServiceTokenData,
Integration,
} from "../../models"; } from "../../models";
import { generateKeyPair } from "../../utils/crypto"; import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
@@ -424,3 +425,19 @@ export const backfillServiceToken = async () => {
); );
console.log("Migration: Service token migration v1 complete"); console.log("Migration: Service token migration v1 complete");
}; };
export const backfillIntegration = async () => {
await Integration.updateMany(
{
secretPath: {
$exists: false,
},
},
{
$set: {
secretPath: "/",
},
}
);
console.log("Migration: Integration migration v1 complete");
};
+2
View File
@@ -13,6 +13,7 @@ import {
backfillEncryptionMetadata, backfillEncryptionMetadata,
backfillSecretFolders, backfillSecretFolders,
backfillServiceToken, backfillServiceToken,
backfillIntegration,
} from "./backfillData"; } from "./backfillData";
import { import {
reencryptBotPrivateKeys, reencryptBotPrivateKeys,
@@ -77,6 +78,7 @@ export const setup = async () => {
await backfillEncryptionMetadata(); await backfillEncryptionMetadata();
await backfillSecretFolders(); await backfillSecretFolders();
await backfillServiceToken(); await backfillServiceToken();
await backfillIntegration();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY