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

This commit is contained in:
akhilmhdh
2023-06-15 22:38:26 +05:30
parent c77ebd4d0e
commit 2471418591
8 changed files with 599 additions and 485 deletions

View File

@@ -1,10 +1,11 @@
import { Request, Response } from 'express';
import { Types } from 'mongoose';
import {
Integration
} from '../../models';
import { EventService } from '../../services';
import { eventPushSecrets } from '../../events';
import { Request, Response } from "express";
import { Types } from "mongoose";
import { Integration } from "../../models";
import { EventService } from "../../services";
import { eventPushSecrets } from "../../events";
import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors";
/**
* Create/initialize an (empty) integration for integration authorization
@@ -25,9 +26,24 @@ export const createIntegration = async (req: Request, res: Response) => {
targetServiceId,
owner,
path,
region
region,
secretPath,
} = 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]
// initialize new integration after saving integration access token
@@ -44,17 +60,18 @@ export const createIntegration = async (req: Request, res: Response) => {
owner,
path,
region,
secretPath,
integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId)
integrationAuth: new Types.ObjectId(integrationAuthId),
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
environment: sourceEnvironment,
}),
});
}
@@ -70,7 +87,6 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns
*/
export const updateIntegration = async (req: Request, res: Response) => {
// TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration]
@@ -81,8 +97,23 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId,
targetEnvironment,
owner, // github-specific integration param
secretPath,
} = 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(
{
_id: req.integration._id,
@@ -94,6 +125,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId,
targetEnvironment,
owner,
secretPath,
},
{
new: true,
@@ -105,7 +137,7 @@ export const updateIntegration = async (req: Request, res: Response) => {
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
environment,
}),
});
}

View File

@@ -1,29 +1,21 @@
import { Types } from "mongoose";
import {
Bot,
BotKey,
Secret,
ISecret,
IUser
} from "../models";
import { Bot, BotKey, Secret, ISecret, IUser } from "../models";
import {
generateKeyPair,
encryptSymmetric128BitHexKeyUTF8,
decryptSymmetric128BitHexKeyUTF8,
decryptAsymmetric
} from '../utils/crypto';
decryptAsymmetric,
} from "../utils/crypto";
import {
SECRET_SHARED,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
ENCODING_SCHEME_BASE64
ENCODING_SCHEME_BASE64,
} from "../variables";
import {
getEncryptionKey,
getRootEncryptionKey,
client
} from "../config";
import { getEncryptionKey, getRootEncryptionKey, client } from "../config";
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]
@@ -40,15 +32,14 @@ export const createBot = async ({
}) => {
const encryptionKey = await getEncryptionKey();
const rootEncryptionKey = await getRootEncryptionKey();
const { publicKey, privateKey } = generateKeyPair();
if (rootEncryptionKey) {
const {
ciphertext,
iv,
tag
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
const { ciphertext, iv, tag } = client.encryptSymmetric(
privateKey,
rootEncryptionKey
);
return await new Bot({
name,
@@ -59,9 +50,8 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
keyEncoding: ENCODING_SCHEME_BASE64,
}).save();
} else if (encryptionKey) {
const { ciphertext, iv, tag } = encryptSymmetric128BitHexKeyUTF8({
plaintext: privateKey,
@@ -77,12 +67,12 @@ export const createBot = async ({
iv,
tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
keyEncoding: ENCODING_SCHEME_UTF8,
}).save();
}
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) => {
const botKey = await BotKey.exists({
workspace: workspaceId
});
workspace: workspaceId,
});
return botKey ? false : true;
}
};
/**
* Return decrypted secrets for workspace with id [workspaceId]
@@ -108,16 +98,38 @@ export const getIsWorkspaceE2EEHelper = async (workspaceId: Types.ObjectId) => {
export const getSecretsBotHelper = async ({
workspaceId,
environment,
secretPath,
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
const content = {} as any;
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({
workspace: workspaceId,
environment,
type: SECRET_SHARED,
folder: folderId,
});
secrets.forEach((secret: ISecret) => {
@@ -148,14 +160,17 @@ export const getSecretsBotHelper = async ({
* @param {String} obj.workspaceId - id of workspace
* @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 rootEncryptionKey = await getRootEncryptionKey();
const botKey = await BotKey.findOne({
workspace: workspaceId,
})
.populate<{ sender: IUser }>("sender", "publicKey");
}).populate<{ sender: IUser }>("sender", "publicKey");
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) {
// 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({
ciphertext: botKey.encryptedKey,
@@ -177,15 +197,14 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
privateKey: privateKeyBot,
});
} else if (encryptionKey && bot.keyEncoding === ENCODING_SCHEME_UTF8) {
// case: encoding scheme is utf8
const privateKeyBot = decryptSymmetric128BitHexKeyUTF8({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: encryptionKey
key: encryptionKey,
});
return decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
@@ -195,7 +214,8 @@ export const getKey = async ({ workspaceId }: { workspaceId: Types.ObjectId }) =
}
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",
});
};
@@ -254,4 +274,4 @@ export const decryptSymmetricHelper = async ({
});
return plaintext;
};
};

View File

@@ -1,26 +1,20 @@
import { Types } from 'mongoose';
import { Types } from "mongoose";
import { Bot, Integration, IntegrationAuth } from "../models";
import { exchangeCode, exchangeRefresh, syncSecrets } from "../integrations";
import { BotService } from "../services";
import {
Bot,
Integration,
IntegrationAuth
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService } from '../services';
import {
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8
} from '../variables';
import {
UnauthorizedRequestError,
} from '../utils/errors';
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY,
ALGORITHM_AES_256_GCM,
ENCODING_SCHEME_UTF8,
} from "../variables";
import { UnauthorizedRequestError } from "../utils/errors";
interface Update {
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
}
/**
@@ -31,78 +25,83 @@ interface Update {
* - Create bot sequence for integration
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
* @returns {IntegrationAuth} integrationAuth - integration auth after OAuth2 code-token exchange
*/
*/
export const handleOAuthExchangeHelper = async ({
workspaceId,
workspaceId,
integration,
code,
environment,
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true,
});
if (!bot)
throw new Error("Bot must be enabled for OAuth2 code-token exchange");
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code,
environment
}: {
workspaceId: string;
integration: string;
code: string;
environment: string;
}) => {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
const update: Update = {
workspace: workspaceId,
integration,
};
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
workspace: workspaceId,
integration,
},
update,
{
new: true,
upsert: true,
}
);
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken,
});
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt,
});
const update: Update = {
workspace: workspaceId,
integration
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
const integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
});
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
return integrationAuth;
}
}
return integrationAuth;
};
/**
* Sync/push environment variables in workspace with id [workspaceId] to
* all active integrations for that workspace
@@ -110,48 +109,54 @@ export const handleOAuthExchangeHelper = async ({
* @param {Object} obj.workspaceId - id of workspace
*/
export const syncIntegrationsHelper = async ({
workspaceId,
environment
workspaceId,
environment,
}: {
workspaceId: Types.ObjectId;
environment?: string;
workspaceId: Types.ObjectId;
environment?: string;
}) => {
const integrations = await Integration.find({
workspace: workspaceId,
...(environment ? {
environment
} : {}),
isActive: true,
app: { $ne: null }
const integrations = await Integration.find({
workspace: workspaceId,
...(environment
? {
environment,
}
: {}),
isActive: true,
app: { $ne: null },
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
// issue here?
workspaceId: integration.workspace,
environment: integration.environment,
secretPath: integration.secretPath,
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace,
environment: integration.environment
});
const integrationAuth = await IntegrationAuth.findById(
integration.integrationAuth
);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
if (!integrationAuth) throw new Error('Failed to find integration auth');
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth
});
// get integration auth access token
const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth,
});
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken
});
}
}
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken,
});
}
};
/**
* 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} refreshToken - decrypted refresh token
*/
export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
export const getIntegrationAuthRefreshHelper = async ({
integrationAuthId,
}: {
integrationAuthId: Types.ObjectId;
}) => {
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select("+refreshCiphertext +refreshIV +refreshTag");
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
});
return refreshToken;
}
const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string,
});
return refreshToken;
};
/**
* Return decrypted access token using the bot's copy
@@ -186,50 +198,65 @@ export const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { i
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
export const getIntegrationAuthAccessHelper = async ({
integrationAuthId,
}: {
integrationAuthId: Types.ObjectId;
}) => {
let accessId;
let accessToken;
const integrationAuth = await IntegrationAuth.findById(
integrationAuthId
).select(
"workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag"
);
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
if (!integrationAuth)
throw UnauthorizedRequestError({
message: "Failed to locate Integration Authentication credentials",
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// and refresh token to exchange with the OAuth2 server
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken
});
}
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string,
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// and refresh token to exchange with the OAuth2 server
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({
integrationAuthId,
});
accessToken = await exchangeRefresh({
integrationAuth,
refreshToken,
});
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
});
}
return ({
accessId,
accessToken
}
if (
integrationAuth?.accessIdCiphertext &&
integrationAuth?.accessIdIV &&
integrationAuth?.accessIdTag
) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string,
});
}
}
return {
accessId,
accessToken,
};
};
/**
* Encrypt refresh token [refreshToken] using the bot's copy
@@ -240,41 +267,43 @@ export const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { in
* @param {String} obj.refreshToken - refresh token
*/
export const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken
integrationAuthId,
refreshToken,
}: {
integrationAuthId: string;
refreshToken: string;
integrationAuthId: string;
refreshToken: string;
}) => {
let integrationAuth = await IntegrationAuth
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth;
}
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: refreshToken,
});
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
},
{
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
{
new: true,
}
);
return integrationAuth;
};
/**
* Encrypt access token [accessToken] and (optionally) access id [accessId]
* using the bot's copy of the workspace key for workspace belonging to
* using the bot's copy of the workspace key for workspace belonging to
* integration auth with id [integrationAuthId] and store it along with [accessExpiresAt]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
@@ -282,48 +311,52 @@ export const setIntegrationAuthRefreshHelper = async ({
* @param {Date} obj.accessExpiresAt - expiration date of access token
*/
export const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessId,
accessToken,
accessExpiresAt
integrationAuthId,
accessId,
accessToken,
accessExpiresAt,
}: {
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
integrationAuthId: string;
accessId: string | null;
accessToken: string;
accessExpiresAt: Date | undefined;
}) => {
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error("Failed to find integration auth");
const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken,
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId,
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{
_id: integrationAuthId,
},
{
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
},
{
new: true,
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth;
}
);
return integrationAuth;
};

View File

@@ -15,7 +15,7 @@ import {
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT
INTEGRATION_HASHICORP_VAULT,
} from "../variables";
export interface IIntegration {
@@ -33,23 +33,24 @@ export interface IIntegration {
targetServiceId: string;
path: string;
region: string;
secretPath: string;
integration:
| 'azure-key-vault'
| 'aws-parameter-store'
| 'aws-secret-manager'
| 'heroku'
| 'vercel'
| 'netlify'
| 'github'
| 'gitlab'
| 'render'
| 'railway'
| 'flyio'
| 'circleci'
| 'travisci'
| 'supabase'
| 'checkly'
| 'hashicorp-vault';
| "azure-key-vault"
| "aws-parameter-store"
| "aws-secret-manager"
| "heroku"
| "vercel"
| "netlify"
| "github"
| "gitlab"
| "render"
| "railway"
| "flyio"
| "circleci"
| "travisci"
| "supabase"
| "checkly"
| "hashicorp-vault";
integrationAuth: Types.ObjectId;
}
@@ -71,7 +72,7 @@ const integrationSchema = new Schema<IIntegration>(
url: {
// for custom self-hosted integrations (e.g. self-hosted GitHub enterprise)
type: String,
default: null
default: null,
},
app: {
// name of app in provider
@@ -90,17 +91,17 @@ const integrationSchema = new Schema<IIntegration>(
},
targetEnvironmentId: {
type: String,
default: null
default: null,
},
targetService: {
// railway-specific service
type: String,
default: null
default: null,
},
targetServiceId: {
// railway-specific service
type: String,
default: null
default: null,
},
owner: {
// github-specific repo owner-login
@@ -111,12 +112,12 @@ const integrationSchema = new Schema<IIntegration>(
// aws-parameter-store-specific path
// (also) vercel preview-branch
type: String,
default: null
default: null,
},
region: {
// aws-parameter-store-specific path
type: String,
default: null
default: null,
},
integration: {
type: String,
@@ -136,7 +137,7 @@ const integrationSchema = new Schema<IIntegration>(
INTEGRATION_TRAVISCI,
INTEGRATION_SUPABASE,
INTEGRATION_CHECKLY,
INTEGRATION_HASHICORP_VAULT
INTEGRATION_HASHICORP_VAULT,
],
required: true,
},
@@ -145,6 +146,11 @@ const integrationSchema = new Schema<IIntegration>(
ref: "IntegrationAuth",
required: true,
},
secretPath: {
type: String,
required: true,
default: "/",
},
},
{
timestamps: true,

View File

@@ -1,75 +1,77 @@
import express from 'express';
import express from "express";
const router = express.Router();
import {
requireAuth,
requireIntegrationAuth,
requireIntegrationAuthorizationAuth,
validateRequest
} from '../../middleware';
requireAuth,
requireIntegrationAuth,
requireIntegrationAuthorizationAuth,
validateRequest,
} from "../../middleware";
import {
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY
} from '../../variables';
import { body, param } from 'express-validator';
import { integrationController } from '../../controllers/v1';
ADMIN,
MEMBER,
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
} from "../../variables";
import { body, param } from "express-validator";
import { integrationController } from "../../controllers/v1";
router.post(
'/',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY]
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
location: 'body'
}),
body('integrationAuthId').exists().isString().trim(),
body('app').trim(),
body('isActive').exists().isBoolean(),
body('appId').trim(),
body('sourceEnvironment').trim(),
body('targetEnvironment').trim(),
body('targetEnvironmentId').trim(),
body('targetService').trim(),
body('targetServiceId').trim(),
body('owner').trim(),
body('path').trim(),
body('region').trim(),
validateRequest,
integrationController.createIntegration
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY],
}),
requireIntegrationAuthorizationAuth({
acceptedRoles: [ADMIN, MEMBER],
location: "body",
}),
body("integrationAuthId").exists().isString().trim(),
body("app").trim(),
body("isActive").exists().isBoolean(),
body("appId").trim(),
body("secretPath").default("/").isString().trim(),
body("sourceEnvironment").trim(),
body("targetEnvironment").trim(),
body("targetEnvironmentId").trim(),
body("targetService").trim(),
body("targetServiceId").trim(),
body("owner").trim(),
body("path").trim(),
body("region").trim(),
validateRequest,
integrationController.createIntegration
);
router.patch(
'/:integrationId',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationId').exists().trim(),
body('isActive').exists().isBoolean(),
body('app').exists().trim(),
body('environment').exists().trim(),
body('appId').exists(),
body('targetEnvironment').exists(),
body('owner').exists(),
validateRequest,
integrationController.updateIntegration
"/:integrationId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationId").exists().trim(),
body("isActive").exists().isBoolean(),
body("app").exists().trim(),
body("secretPath").default("/").isString().trim(),
body("environment").exists().trim(),
body("appId").exists(),
body("targetEnvironment").exists(),
body("owner").exists(),
validateRequest,
integrationController.updateIntegration
);
router.delete(
'/:integrationId',
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER]
}),
param('integrationId').exists().trim(),
validateRequest,
integrationController.deleteIntegration
"/:integrationId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT],
}),
requireIntegrationAuth({
acceptedRoles: [ADMIN, MEMBER],
}),
param("integrationId").exists().trim(),
validateRequest,
integrationController.deleteIntegration
);
export default router;

View File

@@ -1,110 +1,112 @@
import { Types } from 'mongoose';
import { Types } from "mongoose";
import {
getSecretsBotHelper,
encryptSymmetricHelper,
decryptSymmetricHelper,
getKey,
getIsWorkspaceE2EEHelper
} from '../helpers/bot';
getSecretsBotHelper,
encryptSymmetricHelper,
decryptSymmetricHelper,
getKey,
getIsWorkspaceE2EEHelper,
} from "../helpers/bot";
/**
* Class to handle bot actions
*/
class BotService {
/**
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
* @param workspaceId - id of workspace
* @returns {Boolean}
*/
static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
return await getIsWorkspaceE2EEHelper(workspaceId);
}
/**
* Return whether or not workspace with id [workspaceId] is end-to-end encrypted
* @param workspaceId - id of workspace
* @returns {Boolean}
*/
static async getIsWorkspaceE2EE(workspaceId: Types.ObjectId) {
return await getIsWorkspaceE2EEHelper(workspaceId);
}
/**
* Get workspace key for workspace with id [workspaceId] shared to bot.
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
* @returns
*/
static async getWorkspaceKeyWithBot({
workspaceId
}: {
workspaceId: Types.ObjectId;
}) {
return await getKey({
workspaceId
});
}
/**
* Return decrypted secrets for workspace with id [workspaceId] and
* environment [environmen] shared to bot.
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace of secrets
* @param {String} obj.environment - environment for secrets
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
*/
static async getSecrets({
workspaceId,
environment
}: {
workspaceId: Types.ObjectId;
environment: string;
}) {
return await getSecretsBotHelper({
workspaceId,
environment
});
}
/**
* Return symmetrically encrypted [plaintext] using the
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.plaintext - plaintext to encrypt
*/
static async encryptSymmetric({
workspaceId,
plaintext
}: {
workspaceId: Types.ObjectId;
plaintext: string;
}) {
return await encryptSymmetricHelper({
workspaceId,
plaintext
});
}
/**
* Return symmetrically decrypted [ciphertext] using the
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
*/
static async decryptSymmetric({
workspaceId,
ciphertext,
iv,
tag
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;
}) {
return await decryptSymmetricHelper({
workspaceId,
ciphertext,
iv,
tag
});
}
/**
* Get workspace key for workspace with id [workspaceId] shared to bot.
* @param {Object} obj
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get workspace key for
* @returns
*/
static async getWorkspaceKeyWithBot({
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await getKey({
workspaceId,
});
}
/**
* Return decrypted secrets for workspace with id [workspaceId] and
* environment [environmen] shared to bot.
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace of secrets
* @param {String} obj.environment - environment for secrets
* @returns {Object} secretObj - object where keys are secret keys and values are secret values
*/
static async getSecrets({
workspaceId,
environment,
secretPath,
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) {
return await getSecretsBotHelper({
workspaceId,
environment,
secretPath,
});
}
/**
* Return symmetrically encrypted [plaintext] using the
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.plaintext - plaintext to encrypt
*/
static async encryptSymmetric({
workspaceId,
plaintext,
}: {
workspaceId: Types.ObjectId;
plaintext: string;
}) {
return await encryptSymmetricHelper({
workspaceId,
plaintext,
});
}
/**
* Return symmetrically decrypted [ciphertext] using the
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
*/
static async decryptSymmetric({
workspaceId,
ciphertext,
iv,
tag,
}: {
workspaceId: Types.ObjectId;
ciphertext: string;
iv: string;
tag: string;
}) {
return await decryptSymmetricHelper({
workspaceId,
ciphertext,
iv,
tag,
});
}
}
export default BotService;
export default BotService;

View File

@@ -13,6 +13,7 @@ import {
BackupPrivateKey,
IntegrationAuth,
ServiceTokenData,
Integration,
} from "../../models";
import { generateKeyPair } from "../../utils/crypto";
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
@@ -424,3 +425,19 @@ export const backfillServiceToken = async () => {
);
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");
};

View File

@@ -13,6 +13,7 @@ import {
backfillEncryptionMetadata,
backfillSecretFolders,
backfillServiceToken,
backfillIntegration,
} from "./backfillData";
import {
reencryptBotPrivateKeys,
@@ -77,6 +78,7 @@ export const setup = async () => {
await backfillEncryptionMetadata();
await backfillSecretFolders();
await backfillServiceToken();
await backfillIntegration();
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY