Complete v1 loop for bot-based integrations

This commit is contained in:
Tuan Dang
2022-12-08 23:22:44 -05:00
parent 46fe724012
commit 1757f0d690
33 changed files with 1085 additions and 638 deletions
-54
View File
@@ -1,54 +0,0 @@
import * as Sentry from '@sentry/node';
import { IBot } from '../models';
import { ACTION_PUSH_TO_HEROKU } from '../variables';
import { actionPushToHeroku } from '../actions';
interface Event {
name: string;
workspaceId: string;
payload: any;
}
/**
* Class to handle actions
*/
class ActionService {
/**
* @param {Object} obj
* @param {String} action - name of action to trigger
* @param {Event} event
* @param {String} obj.event.name - name of event
* @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event)
* @param bot
* @returns
*/
static async handleAction({
action,
event,
bot
}: {
action: string;
event: Event;
bot: IBot;
}) {
try {
switch (action) {
case ACTION_PUSH_TO_HEROKU:
actionPushToHeroku({
event,
bot
});
return;
default:
return;
}
} catch (err) {
console.error('EventService err', err);
Sentry.setUser(null);
Sentry.captureException(err);
}
}
}
export default ActionService;
+60 -86
View File
@@ -1,19 +1,8 @@
import {
Bot,
IBot,
BotKey,
IBotKey,
IUser,
Secret
} from '../models';
import * as Sentry from '@sentry/node';
import {
decryptAsymmetric,
decryptSymmetric
} from '../utils/crypto';
import {
ENCRYPTION_KEY
} from '../config';
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
} from '../helpers/bot';
/**
* Class to handle bot actions
@@ -21,87 +10,72 @@ import {
class BotService {
/**
* Return decrypted secrets using bot
* 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 decryptSecrets({
static async getSecrets({
workspaceId,
environment
}: {
workspaceId: string;
environment: string;
}) {
let content: any = {};
let bot;
let botKey;
try {
// find bot
bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
if (!bot) throw new Error('Failed to find bot');
// find bot key
botKey = await BotKey.findOne({
workspace: workspaceId
}).populate<{ sender: IUser }>('sender');
if (!botKey) throw new Error('Failed to find bot key');
// decrypt bot private key
const privateKey = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: ENCRYPTION_KEY
});
// decrypt workspace key
const key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey
});
// decrypt secrets
const secrets = await Secret.find({
workspace: workspaceId,
environment
});
secrets.forEach(secret => {
// KEY, VALUE
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
content[secretKey] = secretValue;
});
} catch (err) {
console.error('BotService');
Sentry.setUser(null);
Sentry.captureException(err);
}
return content;
return await getSecretsHelper({
workspaceId,
environment
});
}
/**
* Return symmetrically encrypted [plaintext] using the
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.plaintext - plaintext to encrypt
*/
static async encryptSymmetric({
workspaceId,
plaintext
}: {
workspaceId: string;
plaintext: string;
}) {
return await encryptSymmetricHelper({
workspaceId,
plaintext
});
}
/**
* Return symmetrically decrypted [ciphertext] using the
* bot's copy of the workspace key for workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
*/
static async decryptSymmetric({
workspaceId,
ciphertext,
iv,
tag
}: {
workspaceId: string;
ciphertext: string;
iv: string;
tag: string;
}) {
return await decryptSymmetricHelper({
workspaceId,
ciphertext,
iv,
tag
});
}
}
+7 -52
View File
@@ -1,6 +1,6 @@
import { Bot, IBot, BotSequence } from '../models';
import { Bot, IBot } from '../models';
import * as Sentry from '@sentry/node';
import ActionService from './ActionService';
import { handleEventHelper } from '../helpers/event';
interface Event {
name: string;
@@ -9,12 +9,11 @@ interface Event {
}
/**
* Class to handle events. TODO: elaborate DOCSTRING.
* Class to handle events.
*/
class EventService {
/**
* Check if any bot sequences exist for event and forward
* bot sequence details to ActionService for execution
* Handle event [event]
* @param {Object} obj
* @param {Event} obj.event - an event
* @param {String} obj.event.name - name of event
@@ -22,53 +21,9 @@ class EventService {
* @param {Object} obj.event.payload - payload of event (depends on event)
*/
static async handleEvent({ event }: { event: Event }): Promise<void> {
let botSequences;
let bot: IBot | null;
try {
console.log('EventService');
const { workspaceId } = event;
bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
console.log('A', bot);
// case: bot doesn't exist
if (!bot) {
return;
}
botSequences = await BotSequence.find({
bot: bot._id,
event: event.name
});
console.log('B', botSequences);
// case: bot sequences don't exist
if (botSequences.length === 0) return;
console.log('C');
return;
// // execute event sequences
// botSequences.forEach(botSequence => {
// // sequence.actions
// ActionService.handleAction({
// action: botSequence.action,
// event,
// bot: bot as IBot
// });
// });
} catch (err) {
console.error('EventService err', err);
Sentry.setUser(null);
Sentry.captureException(err);
}
await handleEventHelper({
event
});
}
}
+106 -54
View File
@@ -1,16 +1,24 @@
import * as Sentry from '@sentry/node';
import {
Integration,
Bot,
BotSequence
Integration
} from '../models';
import {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper,
} from '../helpers/integration';
import { exchangeCode } from '../integrations';
import { processOAuthTokenRes2 } from '../helpers/integrationAuth';
import {
ENV_DEV,
EVENT_PUSH_SECRETS
} from '../variables';
// should sync stuff be here too? Probably.
// TODO: move bot functions to IntegrationService.
/**
* Class to handle integrations
*/
@@ -36,57 +44,101 @@ class IntegrationService {
integration: string;
code: string;
}) {
console.log('IntegrationService > handleO')
let action;
try {
const bot = await Bot.find({
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
let res = await exchangeCode({
integration,
code
});
const integrationAuth = await processOAuthTokenRes2({
workspaceId,
integration,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt,
refreshToken: res.refreshToken
});
await handleOAuthExchangeHelper({
workspaceId,
integration,
code
});
}
/**
* Sync/push environment variables in workspace with id [workspaceId] to
* all associated integrations
* @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace
*/
static async syncIntegrations({
workspaceId
}: {
workspaceId: string;
}) {
return await syncIntegrationsHelper({
workspaceId
});
}
/**
* Return decrypted refresh token for integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
static async getIntegrationAuthRefresh({ integrationAuthId }: { integrationAuthId: string}) {
return await getIntegrationAuthRefreshHelper({
integrationAuthId
});
}
/**
* Return decrypted access token for integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} accessToken - decrypted access token
*/
static async getIntegrationAuthAccess({ integrationAuthId }: { integrationAuthId: string}) {
return await getIntegrationAuthAccessHelper({
integrationAuthId
});
}
await Integration.findOneAndUpdate(
{ workspace: workspaceId, integration },
{
workspace: workspaceId,
environment: ENV_DEV,
isActive: false,
app: null,
integration,
integrationAuth: integrationAuth._id
},
{ upsert: true, new: true }
);
// add bot sequence
await BotSequence.findOneAndUpdate({
bot: bot._id,
name: integration + 'sequence',
event: EVENT_PUSH_SECRETS,
action
});
} catch (err) {
console.error('IntegrationService error', err);
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
/**
* Encrypt refresh token [refreshToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.refreshToken - refresh token
* @returns {IntegrationAuth} integrationAuth - updated integration auth
*/
static async setIntegrationAuthRefresh({
integrationAuthId,
refreshToken
}: {
integrationAuthId: string;
refreshToken: string;
}) {
return await setIntegrationAuthRefreshHelper({
integrationAuthId,
refreshToken
});
}
/**
* Encrypt access token [accessToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessToken - access token
* @param {String} obj.accessExpiresAt - expiration date of access token
* @returns {IntegrationAuth} - updated integration auth
*/
static async setIntegrationAuthAccess({
integrationAuthId,
accessToken,
accessExpiresAt
}: {
integrationAuthId: string;
accessToken: string;
accessExpiresAt: Date;
}) {
return await setIntegrationAuthAccessHelper({
integrationAuthId,
accessToken,
accessExpiresAt
});
}
}
-2
View File
@@ -1,13 +1,11 @@
import postHogClient from './PostHogClient';
import BotService from './BotService';
import EventService from './EventService';
import ActionService from './ActionService';
import IntegrationService from './IntegrationService';
export {
postHogClient,
BotService,
EventService,
ActionService,
IntegrationService
}