mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 15:27:46 +00:00
Complete v1 loop for bot-based integrations
This commit is contained in:
@@ -47,7 +47,6 @@ SMTP_PASSWORD=
|
|||||||
# Integration
|
# Integration
|
||||||
# Optional only if integration is used
|
# Optional only if integration is used
|
||||||
OAUTH_CLIENT_SECRET_HEROKU=
|
OAUTH_CLIENT_SECRET_HEROKU=
|
||||||
OAUTH_TOKEN_URL_HEROKU=
|
|
||||||
|
|
||||||
# Sentry (optional) for monitoring errors
|
# Sentry (optional) for monitoring errors
|
||||||
SENTRY_DSN=
|
SENTRY_DSN=
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface BotKey {
|
|||||||
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||||
let bot;
|
let bot;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.body;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
bot = await Bot.findOne({
|
bot = await Bot.findOne({
|
||||||
workspace: workspaceId
|
workspace: workspaceId
|
||||||
@@ -58,13 +58,24 @@ export const setBotActiveState = async (req: Request, res: Response) => {
|
|||||||
|
|
||||||
if (isActive) {
|
if (isActive) {
|
||||||
// bot state set to active -> share workspace key with bot
|
// bot state set to active -> share workspace key with bot
|
||||||
await new BotKey({
|
if (!botKey?.encryptedKey || !botKey?.nonce) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to set bot state to active - missing bot key'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await BotKey.findOneAndUpdate({
|
||||||
|
workspace: req.bot.workspace
|
||||||
|
}, {
|
||||||
encryptedKey: botKey.encryptedKey,
|
encryptedKey: botKey.encryptedKey,
|
||||||
nonce: botKey.nonce,
|
nonce: botKey.nonce,
|
||||||
sender: req.user._id,
|
sender: req.user._id,
|
||||||
receiver: req.bot._id,
|
bot: req.bot._id,
|
||||||
workspace: req.bot.workspace
|
workspace: req.bot.workspace
|
||||||
}).save();
|
}, {
|
||||||
|
upsert: true,
|
||||||
|
new: true
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// case: bot state set to inactive -> delete bot's workspace key
|
// case: bot state set to inactive -> delete bot's workspace key
|
||||||
await BotKey.deleteOne({
|
await BotKey.deleteOne({
|
||||||
|
|||||||
@@ -3,15 +3,13 @@ import * as Sentry from '@sentry/node';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
import { IntegrationAuth, Integration } from '../models';
|
import { IntegrationAuth, Integration } from '../models';
|
||||||
import { processOAuthTokenRes } from '../helpers/integrationAuth';
|
|
||||||
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
import { INTEGRATION_SET, ENV_DEV } from '../variables';
|
||||||
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
||||||
|
|
||||||
import { IntegrationService } from '../services';
|
import { IntegrationService } from '../services';
|
||||||
|
import { getApps } from '../integrations';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
* Perform OAuth2 code-token exchange as part of integration [integration] for workspace with id [workspaceId]
|
||||||
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
@@ -21,8 +19,6 @@ export const oAuthExchange = async (
|
|||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// let clientSecret;
|
|
||||||
|
|
||||||
const { workspaceId, code, integration } = req.body;
|
const { workspaceId, code, integration } = req.body;
|
||||||
|
|
||||||
if (!INTEGRATION_SET.has(integration))
|
if (!INTEGRATION_SET.has(integration))
|
||||||
@@ -33,42 +29,6 @@ export const oAuthExchange = async (
|
|||||||
integration,
|
integration,
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
// // use correct client secret
|
|
||||||
// switch (integration) {
|
|
||||||
// case 'heroku':
|
|
||||||
// clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // TODO: unfinished - make compatible with other integration types
|
|
||||||
// const res = await axios.post( // this response may be different for each integration
|
|
||||||
// OAUTH_TOKEN_URL_HEROKU!,
|
|
||||||
// new URLSearchParams({
|
|
||||||
// grant_type: 'authorization_code',
|
|
||||||
// code: code,
|
|
||||||
// client_secret: clientSecret
|
|
||||||
// } as any)
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const integrationAuth = await processOAuthTokenRes({
|
|
||||||
// workspaceId,
|
|
||||||
// integration,
|
|
||||||
// res
|
|
||||||
// });
|
|
||||||
|
|
||||||
// // create or replace integration
|
|
||||||
// const integrationObj = await Integration.findOneAndUpdate(
|
|
||||||
// { workspace: workspaceId, integration },
|
|
||||||
// {
|
|
||||||
// workspace: workspaceId,
|
|
||||||
// environment: ENV_DEV,
|
|
||||||
// isActive: false,
|
|
||||||
// app: null,
|
|
||||||
// integration,
|
|
||||||
// integrationAuth: integrationAuth._id
|
|
||||||
// },
|
|
||||||
// { upsert: true, new: true }
|
|
||||||
// );
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -83,26 +43,25 @@ export const oAuthExchange = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return list of applications allowed for integration with id [integrationAuthId]
|
* Return list of applications allowed for integration with integration authorization id [integrationAuthId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
export const getIntegrationAuthApps = async (req: Request, res: Response) => {
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
let apps;
|
let apps;
|
||||||
try {
|
try {
|
||||||
const res = await axios.get('https://api.heroku.com/apps', {
|
apps = await getApps({
|
||||||
headers: {
|
integration: req.integrationAuth.integration,
|
||||||
Accept: 'application/vnd.heroku+json; version=3',
|
accessToken: req.accessToken
|
||||||
Authorization: 'Bearer ' + req.accessToken
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
} catch (err) {
|
||||||
apps = res.data.map((a: any) => ({
|
Sentry.setUser(null);
|
||||||
name: a.name
|
Sentry.captureException(err);
|
||||||
}));
|
return res.status(400).send({
|
||||||
} catch (err) {}
|
message: 'Failed to get integration authorization applications'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
apps
|
apps
|
||||||
|
|||||||
@@ -49,7 +49,6 @@ export const getIntegrations = async (req: Request, res: Response) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: deprecate
|
|
||||||
/**
|
/**
|
||||||
* Sync secrets [secrets] to integration with id [integrationId]
|
* Sync secrets [secrets] to integration with id [integrationId]
|
||||||
* @param req
|
* @param req
|
||||||
@@ -57,7 +56,10 @@ export const getIntegrations = async (req: Request, res: Response) => {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const syncIntegration = async (req: Request, res: Response) => {
|
export const syncIntegration = async (req: Request, res: Response) => {
|
||||||
// TODO: unfinished - make more versatile to accomodate for other integrations
|
// NOTE TO ALL DEVS: THIS FUNCTION IS BEING DEPRECATED. IGNORE IT BUT KEEP IT FOR NOW.
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
const { key, secrets }: { key: Key; secrets: PushSecret[] } = req.body;
|
||||||
const symmetricKey = decryptAsymmetric({
|
const symmetricKey = decryptAsymmetric({
|
||||||
@@ -106,6 +108,7 @@ export const syncIntegration = async (req: Request, res: Response) => {
|
|||||||
*/
|
*/
|
||||||
export const modifyIntegration = async (req: Request, res: Response) => {
|
export const modifyIntegration = async (req: Request, res: Response) => {
|
||||||
let integration;
|
let integration;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { update } = req.body;
|
const { update } = req.body;
|
||||||
|
|
||||||
|
|||||||
+183
-3
@@ -1,9 +1,20 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Bot
|
Bot,
|
||||||
|
BotKey,
|
||||||
|
Secret,
|
||||||
|
ISecret,
|
||||||
|
IUser
|
||||||
} from '../models';
|
} from '../models';
|
||||||
import { generateKeyPair, encryptSymmetric } from '../utils/crypto';
|
import {
|
||||||
|
generateKeyPair,
|
||||||
|
encryptSymmetric,
|
||||||
|
decryptSymmetric,
|
||||||
|
decryptAsymmetric
|
||||||
|
} from '../utils/crypto';
|
||||||
|
import { decryptSecrets } from '../helpers/secret';
|
||||||
import { ENCRYPTION_KEY } from '../config';
|
import { ENCRYPTION_KEY } from '../config';
|
||||||
|
import { SECRET_SHARED } from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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,6 +55,175 @@ const createBot = async ({
|
|||||||
return bot;
|
return bot;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted secrets for workspace with id [workspaceId]
|
||||||
|
* and [environment] using bot
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.environment - environment
|
||||||
|
*/
|
||||||
|
const getSecretsHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
environment
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
}) => {
|
||||||
|
let content = {} as any;
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const secrets = await Secret.find({
|
||||||
|
workspaceId,
|
||||||
|
type: SECRET_SHARED
|
||||||
|
});
|
||||||
|
|
||||||
|
secrets.forEach((secret: ISecret) => {
|
||||||
|
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) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get secrets');
|
||||||
|
}
|
||||||
|
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot's copy of the workspace key for workspace
|
||||||
|
* with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @returns {String} key - decrypted workspace key
|
||||||
|
*/
|
||||||
|
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
|
||||||
|
let key;
|
||||||
|
try {
|
||||||
|
const botKey = await BotKey.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).populate<{ sender: IUser }>('sender', 'publicKey');
|
||||||
|
|
||||||
|
if (!botKey) throw new Error('Failed to find bot key');
|
||||||
|
|
||||||
|
const bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
}).select('+encryptedPrivateKey +iv +tag');
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to find bot');
|
||||||
|
if (!bot.isActive) throw new Error('Bot is not active');
|
||||||
|
|
||||||
|
const privateKeyBot = decryptSymmetric({
|
||||||
|
ciphertext: bot.encryptedPrivateKey,
|
||||||
|
iv: bot.iv,
|
||||||
|
tag: bot.tag,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
key = decryptAsymmetric({
|
||||||
|
ciphertext: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
publicKey: botKey.sender.publicKey as string,
|
||||||
|
privateKey: privateKeyBot
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get workspace key');
|
||||||
|
}
|
||||||
|
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
|
* key for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {String} obj1.workspaceId - id of workspace
|
||||||
|
* @param {String} obj1.plaintext - plaintext to encrypt
|
||||||
|
*/
|
||||||
|
const encryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
plaintext
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
plaintext: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return ({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to perform symmetric encryption with bot');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
const decryptSymmetricHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
ciphertext: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}) => {
|
||||||
|
let plaintext;
|
||||||
|
try {
|
||||||
|
const key = await getKey({ workspaceId });
|
||||||
|
const plaintext = decryptSymmetric({
|
||||||
|
ciphertext,
|
||||||
|
iv,
|
||||||
|
tag,
|
||||||
|
key
|
||||||
|
});
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to perform symmetric decryption with bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return plaintext;
|
||||||
|
}
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createBot
|
createBot,
|
||||||
|
getSecretsHelper,
|
||||||
|
encryptSymmetricHelper,
|
||||||
|
decryptSymmetricHelper
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
import { Bot, IBot } from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||||
|
import { IntegrationService } from '../services';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle event [event]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Event} obj.event - an 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)
|
||||||
|
*/
|
||||||
|
const handleEventHelper = async ({
|
||||||
|
event
|
||||||
|
}: {
|
||||||
|
event: Event;
|
||||||
|
}) => {
|
||||||
|
const { workspaceId } = 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
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
handleEventHelper
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot,
|
||||||
|
Integration,
|
||||||
|
IIntegration,
|
||||||
|
IntegrationAuth,
|
||||||
|
IIntegrationAuth
|
||||||
|
} from '../models';
|
||||||
|
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
|
||||||
|
import { BotService, IntegrationService } from '../services';
|
||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
EVENT_PUSH_SECRETS
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
|
||||||
|
* named [integration]
|
||||||
|
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
|
||||||
|
* - Add placeholder inactive integration
|
||||||
|
* - 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.code - code
|
||||||
|
*/
|
||||||
|
const handleOAuthExchangeHelper = async ({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let action;
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
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
|
||||||
|
let res = await exchangeCode({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}, {
|
||||||
|
workspace: workspaceId,
|
||||||
|
integration
|
||||||
|
}, {
|
||||||
|
new: true,
|
||||||
|
upsert: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// set integration auth refresh token
|
||||||
|
await setIntegrationAuthRefreshHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
refreshToken: res.refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
// set integration auth access token
|
||||||
|
await setIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId: integrationAuth._id.toString(),
|
||||||
|
accessToken: res.accessToken,
|
||||||
|
accessExpiresAt: res.accessExpiresAt
|
||||||
|
});
|
||||||
|
|
||||||
|
// initializes an integration after exchange
|
||||||
|
await Integration.findOneAndUpdate(
|
||||||
|
{ workspace: workspaceId, integration },
|
||||||
|
{
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment: ENV_DEV,
|
||||||
|
isActive: false,
|
||||||
|
app: null,
|
||||||
|
integration,
|
||||||
|
integrationAuth: integrationAuth._id
|
||||||
|
},
|
||||||
|
{ upsert: true, new: true }
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to handle OAuth2 code-token exchange')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
|
* all active integrations for that workspace
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
const syncIntegrationsHelper = async ({
|
||||||
|
workspaceId
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let integrations;
|
||||||
|
try {
|
||||||
|
integrations = await Integration.find({
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: true, // TODO: filter so Integrations are ones with non-null apps
|
||||||
|
app: { $ne: null }
|
||||||
|
}).populate<{integrationAuth: IIntegrationAuth}>('integrationAuth', 'accessToken');
|
||||||
|
|
||||||
|
// 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({
|
||||||
|
workspaceId: integration.workspace.toString(),
|
||||||
|
environment: integration.environment
|
||||||
|
});
|
||||||
|
|
||||||
|
// get integration auth access token
|
||||||
|
const accessToken = await getIntegrationAuthAccessHelper({
|
||||||
|
integrationAuthId: integration.integrationAuth._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
|
// sync secrets to integration
|
||||||
|
await syncSecrets({
|
||||||
|
integration: integration.integration,
|
||||||
|
app: integration.app,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to integrations');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted refresh token 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} refreshToken - decrypted refresh token
|
||||||
|
*/
|
||||||
|
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||||
|
let refreshToken;
|
||||||
|
try {
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
|
.select('+refreshCiphertext +refreshIV +refreshTag');
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
refreshToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
ciphertext: integrationAuth.refreshCiphertext as string,
|
||||||
|
iv: integrationAuth.refreshIV as string,
|
||||||
|
tag: integrationAuth.refreshTag as string
|
||||||
|
});
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get integration refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return refreshToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted access token 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
|
||||||
|
* @returns {String} accessToken - decrypted access token
|
||||||
|
*/
|
||||||
|
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
const integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId)
|
||||||
|
.select('+accessCiphertext +accessIV +accessTag');
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
accessToken = await BotService.decryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
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({
|
||||||
|
integration: integrationAuth.integration,
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get integration access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||||
|
* of the workspace key for workspace belonging to integration auth
|
||||||
|
* with id [integrationAuthId] and store it
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
|
* @param {String} obj.refreshToken - refresh token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthRefreshHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth
|
||||||
|
.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: refreshToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
refreshCiphertext: obj.ciphertext,
|
||||||
|
refreshIV: obj.iv,
|
||||||
|
refreshTag: obj.tag
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to set integration auth refresh token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encrypt access token [accessToken] 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
|
||||||
|
* @param {String} obj.accessToken - access token
|
||||||
|
* @param {Date} obj.accessExpiresAt - expiration date of access token
|
||||||
|
*/
|
||||||
|
const setIntegrationAuthAccessHelper = async ({
|
||||||
|
integrationAuthId,
|
||||||
|
accessToken,
|
||||||
|
accessExpiresAt
|
||||||
|
}: {
|
||||||
|
integrationAuthId: string;
|
||||||
|
accessToken: string;
|
||||||
|
accessExpiresAt: Date;
|
||||||
|
}) => {
|
||||||
|
let integrationAuth;
|
||||||
|
try {
|
||||||
|
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
|
||||||
|
|
||||||
|
if (!integrationAuth) throw new Error('Failed to find integration auth');
|
||||||
|
|
||||||
|
const obj = await BotService.encryptSymmetric({
|
||||||
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
plaintext: accessToken
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationAuth = await IntegrationAuth.findOneAndUpdate({
|
||||||
|
_id: integrationAuthId
|
||||||
|
}, {
|
||||||
|
accessCiphertext: obj.ciphertext,
|
||||||
|
accessIV: obj.iv,
|
||||||
|
accessTag: obj.tag,
|
||||||
|
accessExpiresAt
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to save integration auth access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return integrationAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
handleOAuthExchangeHelper,
|
||||||
|
syncIntegrationsHelper,
|
||||||
|
getIntegrationAuthRefreshHelper,
|
||||||
|
getIntegrationAuthAccessHelper,
|
||||||
|
setIntegrationAuthRefreshHelper,
|
||||||
|
setIntegrationAuthAccessHelper
|
||||||
|
}
|
||||||
@@ -1,250 +0,0 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import axios from 'axios';
|
|
||||||
import { IntegrationAuth } from '../models';
|
|
||||||
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
|
|
||||||
import { IIntegrationAuth } from '../models';
|
|
||||||
import {
|
|
||||||
ENCRYPTION_KEY,
|
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
|
||||||
} from '../config';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Encrypt access and refresh tokens, compute new access token expiration times [accessExpiresAt],
|
|
||||||
* and upsert them into the DB for workspace with id [workspaceId] and integration [integration].
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
|
||||||
* @param {String} obj.integration - name of integration
|
|
||||||
* @param {String} obj.accessToken - access token for integration
|
|
||||||
* @param {Date} obj.accessExpiresAt - date of expiration for access token
|
|
||||||
* @param {String} obj.refreshToken - refresh token for integration
|
|
||||||
*/
|
|
||||||
const processOAuthTokenRes2 = async ({
|
|
||||||
workspaceId,
|
|
||||||
integration,
|
|
||||||
accessToken,
|
|
||||||
accessExpiresAt,
|
|
||||||
refreshToken,
|
|
||||||
}: {
|
|
||||||
workspaceId: string;
|
|
||||||
integration: string;
|
|
||||||
accessToken: string;
|
|
||||||
accessExpiresAt: Date;
|
|
||||||
refreshToken: string;
|
|
||||||
}) => {
|
|
||||||
|
|
||||||
let integrationAuth;
|
|
||||||
try {
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const {
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: refreshToken,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: accessToken,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// create or replace integration authorization with encrypted tokens
|
|
||||||
// and access token expiration date
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration,
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(
|
|
||||||
'Failed to process OAuth2 authorization server token response'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrationAuth;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: deprecate
|
|
||||||
/**
|
|
||||||
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
|
||||||
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
|
||||||
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {String} obj.workspaceId - id of workspace
|
|
||||||
* @param {String} obj.integration - name of integration (e.g. heroku)
|
|
||||||
* @param {Object} obj.res - response from OAuth2 authorization server
|
|
||||||
*/
|
|
||||||
const processOAuthTokenRes = async ({
|
|
||||||
workspaceId,
|
|
||||||
integration,
|
|
||||||
res
|
|
||||||
}: {
|
|
||||||
workspaceId: string;
|
|
||||||
integration: string;
|
|
||||||
res: any;
|
|
||||||
}): Promise<IIntegrationAuth> => {
|
|
||||||
let integrationAuth;
|
|
||||||
try {
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const {
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.refresh_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag
|
|
||||||
} = encryptSymmetric({
|
|
||||||
plaintext: res.data.access_token,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// compute access token expiration date
|
|
||||||
const accessExpiresAt = new Date();
|
|
||||||
accessExpiresAt.setSeconds(
|
|
||||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
|
||||||
);
|
|
||||||
|
|
||||||
// create or replace integration authorization with encrypted tokens
|
|
||||||
// and access token expiration date
|
|
||||||
integrationAuth = await IntegrationAuth.findOneAndUpdate(
|
|
||||||
{ workspace: workspaceId, integration },
|
|
||||||
{
|
|
||||||
workspace: workspaceId,
|
|
||||||
integration,
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
},
|
|
||||||
{ upsert: true, new: true }
|
|
||||||
);
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error(
|
|
||||||
'Failed to process OAuth2 authorization server token response'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return integrationAuth;
|
|
||||||
};
|
|
||||||
|
|
||||||
// TODO: deprecate
|
|
||||||
/**
|
|
||||||
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
|
||||||
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
|
||||||
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
|
|
||||||
* @param {Object} obj
|
|
||||||
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
|
|
||||||
* @returns {String} access token - new access token
|
|
||||||
*/
|
|
||||||
const getOAuthAccessToken = async ({
|
|
||||||
integrationAuth
|
|
||||||
}: {
|
|
||||||
integrationAuth: IIntegrationAuth;
|
|
||||||
}) => {
|
|
||||||
let accessToken;
|
|
||||||
try {
|
|
||||||
const {
|
|
||||||
refreshCiphertext,
|
|
||||||
refreshIV,
|
|
||||||
refreshTag,
|
|
||||||
accessCiphertext,
|
|
||||||
accessIV,
|
|
||||||
accessTag,
|
|
||||||
accessExpiresAt
|
|
||||||
} = integrationAuth;
|
|
||||||
|
|
||||||
if (
|
|
||||||
refreshCiphertext &&
|
|
||||||
refreshIV &&
|
|
||||||
refreshTag &&
|
|
||||||
accessCiphertext &&
|
|
||||||
accessIV &&
|
|
||||||
accessTag &&
|
|
||||||
accessExpiresAt
|
|
||||||
) {
|
|
||||||
if (accessExpiresAt < new Date()) {
|
|
||||||
// case: access token expired
|
|
||||||
// TODO: fetch another access token
|
|
||||||
|
|
||||||
let clientSecret;
|
|
||||||
switch (integrationAuth.integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// record new access token and refresh token
|
|
||||||
// encrypt refresh + access tokens
|
|
||||||
const refreshToken = decryptSymmetric({
|
|
||||||
ciphertext: refreshCiphertext,
|
|
||||||
iv: refreshIV,
|
|
||||||
tag: refreshTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO: make route compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'refresh_token',
|
|
||||||
refresh_token: refreshToken,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
accessToken = res.data.access_token;
|
|
||||||
|
|
||||||
await processOAuthTokenRes({
|
|
||||||
workspaceId: integrationAuth.workspace.toString(),
|
|
||||||
integration: integrationAuth.integration,
|
|
||||||
res
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// case: access token still works
|
|
||||||
accessToken = decryptSymmetric({
|
|
||||||
ciphertext: accessCiphertext,
|
|
||||||
iv: accessIV,
|
|
||||||
tag: accessTag,
|
|
||||||
key: ENCRYPTION_KEY
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
Sentry.setUser(null);
|
|
||||||
Sentry.captureException(err);
|
|
||||||
throw new Error('Failed to get OAuth2 access token');
|
|
||||||
}
|
|
||||||
|
|
||||||
return accessToken;
|
|
||||||
};
|
|
||||||
|
|
||||||
export { processOAuthTokenRes, processOAuthTokenRes2, getOAuthAccessToken };
|
|
||||||
|
|||||||
@@ -30,10 +30,10 @@ const createWorkspace = async ({
|
|||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
// const bot = await createBot({
|
const bot = await createBot({
|
||||||
// name: 'Infisical Bot',
|
name: 'Infisical Bot',
|
||||||
// workspaceId: workspace._id.toString()
|
workspaceId: workspace._id.toString()
|
||||||
// });
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_HEROKU_APPS_URL
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for integration named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.accessToken - access token for integration
|
||||||
|
* @returns {Object[]} apps - names of integration apps
|
||||||
|
* @returns {String} apps.name - name of integration app
|
||||||
|
*/
|
||||||
|
const getApps = async ({
|
||||||
|
integration,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
apps = await getAppsHeroku({
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return list of names of apps for Heroku integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.accessToken - access token for Heroku API
|
||||||
|
* @returns {Object[]} apps - names of Heroku apps
|
||||||
|
* @returns {String} apps.name - name of Heroku app
|
||||||
|
*/
|
||||||
|
const getAppsHeroku = async ({
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
let apps;
|
||||||
|
try {
|
||||||
|
const res = await axios.get(INTEGRATION_HEROKU_APPS_URL, {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: `Bearer ${accessToken}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
apps = res.data.map((a: any) => ({
|
||||||
|
name: a.name
|
||||||
|
}));
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get Heroku integration apps');
|
||||||
|
}
|
||||||
|
|
||||||
|
return apps;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
getApps
|
||||||
|
}
|
||||||
@@ -2,11 +2,11 @@ import axios from 'axios';
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
ACTION_PUSH_TO_HEROKU
|
ACTION_PUSH_TO_HEROKU
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import {
|
import {
|
||||||
OAUTH_CLIENT_SECRET_HEROKU,
|
OAUTH_CLIENT_SECRET_HEROKU
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
|
||||||
} from '../config';
|
} from '../config';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,13 +29,13 @@ const exchangeCode = async ({
|
|||||||
code: string;
|
code: string;
|
||||||
}) => {
|
}) => {
|
||||||
let obj = {} as any;
|
let obj = {} as any;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
switch (integration) {
|
switch (integration) {
|
||||||
case INTEGRATION_HEROKU:
|
case INTEGRATION_HEROKU:
|
||||||
obj = await exchangeCodeHeroku({
|
obj = await exchangeCodeHeroku({
|
||||||
code
|
code
|
||||||
});
|
});
|
||||||
obj['action'] = ACTION_PUSH_TO_HEROKU;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -63,10 +63,10 @@ const exchangeCodeHeroku = async ({
|
|||||||
code: string;
|
code: string;
|
||||||
}) => {
|
}) => {
|
||||||
let res: any;
|
let res: any;
|
||||||
let accessExpiresAt: any;
|
let accessExpiresAt = new Date();
|
||||||
try {
|
try {
|
||||||
res = await axios.post(
|
res = await axios.post(
|
||||||
OAUTH_TOKEN_URL_HEROKU!,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code: code,
|
code: code,
|
||||||
@@ -78,7 +78,6 @@ const exchangeCodeHeroku = async ({
|
|||||||
accessExpiresAt.getSeconds() + res.data.expires_in
|
accessExpiresAt.getSeconds() + res.data.expires_in
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('integrationHerokuExchange');
|
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { exchangeCode } from './exchange';
|
import { exchangeCode } from './exchange';
|
||||||
import { exchangeRefresh } from './refresh';
|
import { exchangeRefresh } from './refresh';
|
||||||
|
import { getApps } from './apps';
|
||||||
|
import { syncSecrets } from './sync';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
exchangeCode,
|
exchangeCode,
|
||||||
exchangeRefresh
|
exchangeRefresh,
|
||||||
|
getApps,
|
||||||
|
syncSecrets
|
||||||
}
|
}
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
OAUTH_CLIENT_SECRET_HEROKU
|
OAUTH_CLIENT_SECRET_HEROKU
|
||||||
} from '../config';
|
} from '../config';
|
||||||
import {
|
import {
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
INTEGRATION_HEROKU_TOKEN_URL
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,7 +55,7 @@ const exchangeRefreshHeroku = async ({
|
|||||||
let accessToken;
|
let accessToken;
|
||||||
try {
|
try {
|
||||||
const res = await axios.post(
|
const res = await axios.post(
|
||||||
OAUTH_TOKEN_URL_HEROKU,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
new URLSearchParams({
|
new URLSearchParams({
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
refresh_token: refreshToken,
|
refresh_token: refreshToken,
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { INTEGRATION_HEROKU } from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to [app] in integration named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Object} obj.integration - name of integration
|
||||||
|
* @param {Object} obj.app - app in integration
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
* @param {String} obj.accessToken - access token for integration
|
||||||
|
*/
|
||||||
|
const syncSecrets = async ({
|
||||||
|
integration,
|
||||||
|
app,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
app: string;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
await syncSecretsHeroku({
|
||||||
|
app,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to integration');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sync/push [secrets] to Heroku [app]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.app - app in integration
|
||||||
|
* @param {Object} obj.secrets - secrets to push to integration (object where keys are secret keys and values are secret values)
|
||||||
|
*/
|
||||||
|
const syncSecretsHeroku = async ({
|
||||||
|
app,
|
||||||
|
secrets,
|
||||||
|
accessToken
|
||||||
|
}: {
|
||||||
|
app: string;
|
||||||
|
secrets: any;
|
||||||
|
accessToken: string;
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
await axios.patch(
|
||||||
|
`https://api.heroku.com/apps/${app}/config-vars`,
|
||||||
|
secrets,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/vnd.heroku+json; version=3',
|
||||||
|
Authorization: 'Bearer ' + accessToken
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to sync secrets to Heroku');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
syncSecrets
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { Integration, IntegrationAuth, Membership } from '../models';
|
import { Bot, Integration, IntegrationAuth, Membership } from '../models';
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
import { IntegrationService } from '../services';
|
||||||
import { validateMembership } from '../helpers/membership';
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -51,7 +51,9 @@ const requireIntegrationAuth = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
req.integration = integration;
|
req.integration = integration;
|
||||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||||
|
integrationAuthId: integrationAuth._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { IntegrationAuth } from '../models';
|
import { IntegrationAuth } from '../models';
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
import { IntegrationService } from '../services';
|
||||||
import { validateMembership } from '../helpers/membership';
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -41,7 +41,10 @@ const requireIntegrationAuthorizationAuth = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
req.integrationAuth = integrationAuth;
|
req.integrationAuth = integrationAuth;
|
||||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
req.accessToken = await IntegrationService.getIntegrationAuthAccess({
|
||||||
|
integrationAuthId: integrationAuth._id.toString()
|
||||||
|
});
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ const botSchema = new Schema<IBot>(
|
|||||||
},
|
},
|
||||||
isActive: {
|
isActive: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: true
|
required: true,
|
||||||
|
default: false
|
||||||
},
|
},
|
||||||
publicKey: {
|
publicKey: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true
|
||||||
select: false
|
|
||||||
},
|
},
|
||||||
encryptedPrivateKey: {
|
encryptedPrivateKey: {
|
||||||
type: String,
|
type: String,
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
|
||||||
|
|
||||||
export interface IBotSequence {
|
|
||||||
_id: Types.ObjectId;
|
|
||||||
bot: Types.ObjectId;
|
|
||||||
name: string;
|
|
||||||
event: string;
|
|
||||||
action: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const botSequence = new Schema<IBotSequence>(
|
|
||||||
{
|
|
||||||
bot: {
|
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: 'Bot',
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
event: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
action: {
|
|
||||||
type: String,
|
|
||||||
required: true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
timestamps: true
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
const BotSequence = model<IBotSequence>('BotSequence', botSequence);
|
|
||||||
|
|
||||||
export default BotSequence;
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
||||||
import Bot, { IBot } from './bot';
|
import Bot, { IBot } from './bot';
|
||||||
import BotKey, { IBotKey } from './botKey';
|
import BotKey, { IBotKey } from './botKey';
|
||||||
import BotSequence, { IBotSequence } from './botSequence';
|
|
||||||
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
import IncidentContactOrg, { IIncidentContactOrg } from './incidentContactOrg';
|
||||||
import Integration, { IIntegration } from './integration';
|
import Integration, { IIntegration } from './integration';
|
||||||
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
import IntegrationAuth, { IIntegrationAuth } from './integrationAuth';
|
||||||
@@ -23,8 +22,6 @@ export {
|
|||||||
IBot,
|
IBot,
|
||||||
BotKey,
|
BotKey,
|
||||||
IBotKey,
|
IBotKey,
|
||||||
BotSequence,
|
|
||||||
IBotSequence,
|
|
||||||
IncidentContactOrg,
|
IncidentContactOrg,
|
||||||
IIncidentContactOrg,
|
IIncidentContactOrg,
|
||||||
Integration,
|
Integration,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import { body } from 'express-validator';
|
import { body, param } from 'express-validator';
|
||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireBotAuth,
|
requireBotAuth,
|
||||||
@@ -11,14 +11,13 @@ import { botController } from '../controllers';
|
|||||||
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/',
|
'/:workspaceId',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
acceptedStatuses: [COMPLETED, GRANTED],
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
location: 'body'
|
|
||||||
}),
|
}),
|
||||||
body('workspaceId').exists().trim().notEmpty(),
|
param('workspaceId').exists().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
botController.getBotByWorkspaceId
|
botController.getBotByWorkspaceId
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
import { ADMIN, MEMBER, GRANTED } from '../variables';
|
||||||
import { integrationAuthController } from '../controllers';
|
import { integrationAuthController } from '../controllers';
|
||||||
|
|
||||||
router.post( // semi-ok
|
router.post(
|
||||||
'/oauth-token',
|
'/oauth-token',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
@@ -25,7 +25,7 @@ router.post( // semi-ok
|
|||||||
integrationAuthController.oAuthExchange
|
integrationAuthController.oAuthExchange
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get( // not-ok
|
router.get(
|
||||||
'/:integrationAuthId/apps',
|
'/:integrationAuthId/apps',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuthorizationAuth({
|
requireIntegrationAuthorizationAuth({
|
||||||
@@ -37,7 +37,7 @@ router.get( // not-ok
|
|||||||
integrationAuthController.getIntegrationAuthApps
|
integrationAuthController.getIntegrationAuthApps
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete( // not-ok
|
router.delete(
|
||||||
'/:integrationAuthId',
|
'/:integrationAuthId',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuthorizationAuth({
|
requireIntegrationAuthorizationAuth({
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -1,19 +1,8 @@
|
|||||||
import {
|
|
||||||
Bot,
|
|
||||||
IBot,
|
|
||||||
BotKey,
|
|
||||||
IBotKey,
|
|
||||||
IUser,
|
|
||||||
Secret
|
|
||||||
} from '../models';
|
|
||||||
import * as Sentry from '@sentry/node';
|
|
||||||
import {
|
import {
|
||||||
decryptAsymmetric,
|
getSecretsHelper,
|
||||||
decryptSymmetric
|
encryptSymmetricHelper,
|
||||||
} from '../utils/crypto';
|
decryptSymmetricHelper
|
||||||
import {
|
} from '../helpers/bot';
|
||||||
ENCRYPTION_KEY
|
|
||||||
} from '../config';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to handle bot actions
|
* Class to handle bot actions
|
||||||
@@ -21,87 +10,72 @@ import {
|
|||||||
class BotService {
|
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 {Object} obj
|
||||||
* @param {String} obj.workspaceId - id of workspace of secrets
|
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||||
* @param {String} obj.environment - environment for 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,
|
workspaceId,
|
||||||
environment
|
environment
|
||||||
}: {
|
}: {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
}) {
|
}) {
|
||||||
|
return await getSecretsHelper({
|
||||||
let content: any = {};
|
workspaceId,
|
||||||
let bot;
|
environment
|
||||||
let botKey;
|
});
|
||||||
try {
|
}
|
||||||
|
|
||||||
// find bot
|
/**
|
||||||
bot = await Bot.findOne({
|
* Return symmetrically encrypted [plaintext] using the
|
||||||
workspace: workspaceId,
|
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||||
isActive: true
|
* @param {Object} obj
|
||||||
});
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
* @param {String} obj.plaintext - plaintext to encrypt
|
||||||
if (!bot) throw new Error('Failed to find bot');
|
*/
|
||||||
|
static async encryptSymmetric({
|
||||||
// find bot key
|
workspaceId,
|
||||||
botKey = await BotKey.findOne({
|
plaintext
|
||||||
workspace: workspaceId
|
}: {
|
||||||
}).populate<{ sender: IUser }>('sender');
|
workspaceId: string;
|
||||||
|
plaintext: string;
|
||||||
if (!botKey) throw new Error('Failed to find bot key');
|
}) {
|
||||||
|
return await encryptSymmetricHelper({
|
||||||
// decrypt bot private key
|
workspaceId,
|
||||||
const privateKey = decryptSymmetric({
|
plaintext
|
||||||
ciphertext: bot.encryptedPrivateKey,
|
});
|
||||||
iv: bot.iv,
|
}
|
||||||
tag: bot.tag,
|
|
||||||
key: ENCRYPTION_KEY
|
/**
|
||||||
});
|
* Return symmetrically decrypted [ciphertext] using the
|
||||||
|
* bot's copy of the workspace key for workspace with id [workspaceId]
|
||||||
// decrypt workspace key
|
* @param {Object} obj
|
||||||
const key = decryptAsymmetric({
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
ciphertext: botKey.encryptedKey,
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
nonce: botKey.nonce,
|
* @param {String} obj.iv - iv
|
||||||
publicKey: botKey.sender.publicKey as string,
|
* @param {String} obj.tag - tag
|
||||||
privateKey
|
*/
|
||||||
});
|
static async decryptSymmetric({
|
||||||
|
workspaceId,
|
||||||
// decrypt secrets
|
ciphertext,
|
||||||
const secrets = await Secret.find({
|
iv,
|
||||||
workspace: workspaceId,
|
tag
|
||||||
environment
|
}: {
|
||||||
});
|
workspaceId: string;
|
||||||
|
ciphertext: string;
|
||||||
secrets.forEach(secret => {
|
iv: string;
|
||||||
// KEY, VALUE
|
tag: string;
|
||||||
const secretKey = decryptSymmetric({
|
}) {
|
||||||
ciphertext: secret.secretKeyCiphertext,
|
return await decryptSymmetricHelper({
|
||||||
iv: secret.secretKeyIV,
|
workspaceId,
|
||||||
tag: secret.secretKeyTag,
|
ciphertext,
|
||||||
key
|
iv,
|
||||||
});
|
tag
|
||||||
|
});
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Bot, IBot, BotSequence } from '../models';
|
import { Bot, IBot } from '../models';
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import ActionService from './ActionService';
|
import { handleEventHelper } from '../helpers/event';
|
||||||
|
|
||||||
interface Event {
|
interface Event {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -9,12 +9,11 @@ interface Event {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to handle events. TODO: elaborate DOCSTRING.
|
* Class to handle events.
|
||||||
*/
|
*/
|
||||||
class EventService {
|
class EventService {
|
||||||
/**
|
/**
|
||||||
* Check if any bot sequences exist for event and forward
|
* Handle event [event]
|
||||||
* bot sequence details to ActionService for execution
|
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {Event} obj.event - an event
|
* @param {Event} obj.event - an event
|
||||||
* @param {String} obj.event.name - name of 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)
|
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||||
*/
|
*/
|
||||||
static async handleEvent({ event }: { event: Event }): Promise<void> {
|
static async handleEvent({ event }: { event: Event }): Promise<void> {
|
||||||
let botSequences;
|
await handleEventHelper({
|
||||||
let bot: IBot | null;
|
event
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +1,24 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Integration,
|
Integration
|
||||||
Bot,
|
|
||||||
BotSequence
|
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import {
|
||||||
|
handleOAuthExchangeHelper,
|
||||||
|
syncIntegrationsHelper,
|
||||||
|
getIntegrationAuthRefreshHelper,
|
||||||
|
getIntegrationAuthAccessHelper,
|
||||||
|
setIntegrationAuthRefreshHelper,
|
||||||
|
setIntegrationAuthAccessHelper,
|
||||||
|
} from '../helpers/integration';
|
||||||
import { exchangeCode } from '../integrations';
|
import { exchangeCode } from '../integrations';
|
||||||
import { processOAuthTokenRes2 } from '../helpers/integrationAuth';
|
|
||||||
import {
|
import {
|
||||||
ENV_DEV,
|
ENV_DEV,
|
||||||
EVENT_PUSH_SECRETS
|
EVENT_PUSH_SECRETS
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
|
|
||||||
|
// should sync stuff be here too? Probably.
|
||||||
|
// TODO: move bot functions to IntegrationService.
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Class to handle integrations
|
* Class to handle integrations
|
||||||
*/
|
*/
|
||||||
@@ -36,57 +44,101 @@ class IntegrationService {
|
|||||||
integration: string;
|
integration: string;
|
||||||
code: string;
|
code: string;
|
||||||
}) {
|
}) {
|
||||||
console.log('IntegrationService > handleO')
|
await handleOAuthExchangeHelper({
|
||||||
let action;
|
workspaceId,
|
||||||
try {
|
integration,
|
||||||
|
code
|
||||||
const bot = await Bot.find({
|
});
|
||||||
workspace: workspaceId,
|
}
|
||||||
isActive: true
|
|
||||||
});
|
/**
|
||||||
|
* Sync/push environment variables in workspace with id [workspaceId] to
|
||||||
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
|
* all associated integrations
|
||||||
|
* @param {Object} obj
|
||||||
// exchange code for access and refresh tokens
|
* @param {Object} obj.workspaceId - id of workspace
|
||||||
let res = await exchangeCode({
|
*/
|
||||||
integration,
|
static async syncIntegrations({
|
||||||
code
|
workspaceId
|
||||||
});
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
const integrationAuth = await processOAuthTokenRes2({
|
}) {
|
||||||
workspaceId,
|
return await syncIntegrationsHelper({
|
||||||
integration,
|
workspaceId
|
||||||
accessToken: res.accessToken,
|
});
|
||||||
accessExpiresAt: res.accessExpiresAt,
|
}
|
||||||
refreshToken: res.refreshToken
|
|
||||||
});
|
/**
|
||||||
|
* 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 },
|
* Encrypt refresh token [refreshToken] using the bot's copy
|
||||||
{
|
* of the workspace key for workspace belonging to integration auth
|
||||||
workspace: workspaceId,
|
* with id [integrationAuthId]
|
||||||
environment: ENV_DEV,
|
* @param {Object} obj
|
||||||
isActive: false,
|
* @param {String} obj.integrationAuthId - id of integration auth
|
||||||
app: null,
|
* @param {String} obj.refreshToken - refresh token
|
||||||
integration,
|
* @returns {IntegrationAuth} integrationAuth - updated integration auth
|
||||||
integrationAuth: integrationAuth._id
|
*/
|
||||||
},
|
static async setIntegrationAuthRefresh({
|
||||||
{ upsert: true, new: true }
|
integrationAuthId,
|
||||||
);
|
refreshToken
|
||||||
|
}: {
|
||||||
// add bot sequence
|
integrationAuthId: string;
|
||||||
await BotSequence.findOneAndUpdate({
|
refreshToken: string;
|
||||||
bot: bot._id,
|
}) {
|
||||||
name: integration + 'sequence',
|
return await setIntegrationAuthRefreshHelper({
|
||||||
event: EVENT_PUSH_SECRETS,
|
integrationAuthId,
|
||||||
action
|
refreshToken
|
||||||
});
|
});
|
||||||
} catch (err) {
|
}
|
||||||
console.error('IntegrationService error', err);
|
|
||||||
Sentry.setUser(null);
|
/**
|
||||||
Sentry.captureException(err);
|
* Encrypt access token [accessToken] using the bot's copy
|
||||||
throw new Error('Failed to handle OAuth2 code-token exchange')
|
* 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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import postHogClient from './PostHogClient';
|
import postHogClient from './PostHogClient';
|
||||||
import BotService from './BotService';
|
import BotService from './BotService';
|
||||||
import EventService from './EventService';
|
import EventService from './EventService';
|
||||||
import ActionService from './ActionService';
|
|
||||||
import IntegrationService from './IntegrationService';
|
import IntegrationService from './IntegrationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
postHogClient,
|
postHogClient,
|
||||||
BotService,
|
BotService,
|
||||||
EventService,
|
EventService,
|
||||||
ActionService,
|
|
||||||
IntegrationService
|
IntegrationService
|
||||||
}
|
}
|
||||||
@@ -96,7 +96,7 @@ const decryptAsymmetric = ({
|
|||||||
* Return symmetrically encrypted [plaintext] using [key].
|
* Return symmetrically encrypted [plaintext] using [key].
|
||||||
* @param {Object} obj
|
* @param {Object} obj
|
||||||
* @param {String} obj.plaintext - plaintext to encrypt
|
* @param {String} obj.plaintext - plaintext to encrypt
|
||||||
* @param {String} obj.key - 16-byte hex key
|
* @param {String} obj.key - hex key
|
||||||
*/
|
*/
|
||||||
const encryptSymmetric = ({
|
const encryptSymmetric = ({
|
||||||
plaintext,
|
plaintext,
|
||||||
@@ -129,7 +129,7 @@ const encryptSymmetric = ({
|
|||||||
* @param {String} obj.ciphertext - ciphertext to decrypt
|
* @param {String} obj.ciphertext - ciphertext to decrypt
|
||||||
* @param {String} obj.iv - iv
|
* @param {String} obj.iv - iv
|
||||||
* @param {String} obj.tag - tag
|
* @param {String} obj.tag - tag
|
||||||
* @param {String} obj.key - 32-byte hex key
|
* @param {String} obj.key - hex key
|
||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
const decryptSymmetric = ({
|
const decryptSymmetric = ({
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ import {
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_APPS_URL
|
||||||
} from './integration';
|
} from './integration';
|
||||||
import {
|
import {
|
||||||
OWNER,
|
OWNER,
|
||||||
@@ -58,7 +59,8 @@ export {
|
|||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
OAUTH_TOKEN_URL_HEROKU,
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_APPS_URL,
|
||||||
EVENT_PUSH_SECRETS,
|
EVENT_PUSH_SECRETS,
|
||||||
EVENT_PULL_SECRETS,
|
EVENT_PULL_SECRETS,
|
||||||
ACTION_PUSH_TO_HEROKU
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]);
|
|||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||||
|
|
||||||
// integration oauth endpoints
|
// integration oauth endpoints
|
||||||
const OAUTH_TOKEN_URL_HEROKU = 'https://id.heroku.com/oauth/token';
|
const INTEGRATION_HEROKU_TOKEN_URL = 'https://id.heroku.com/oauth/token';
|
||||||
|
|
||||||
|
// integration apps endpoints
|
||||||
|
const INTEGRATION_HEROKU_APPS_URL = 'https://api.heroku.com/apps';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
INTEGRATION_HEROKU,
|
INTEGRATION_HEROKU,
|
||||||
INTEGRATION_NETLIFY,
|
INTEGRATION_NETLIFY,
|
||||||
INTEGRATION_SET,
|
INTEGRATION_SET,
|
||||||
INTEGRATION_OAUTH2,
|
INTEGRATION_OAUTH2,
|
||||||
OAUTH_TOKEN_URL_HEROKU
|
INTEGRATION_HEROKU_TOKEN_URL,
|
||||||
|
INTEGRATION_HEROKU_APPS_URL
|
||||||
}
|
}
|
||||||
@@ -42,7 +42,7 @@ const getSecretsForProject = async ({
|
|||||||
publicKey: file.key.sender.publicKey,
|
publicKey: file.key.sender.publicKey,
|
||||||
privateKey: PRIVATE_KEY,
|
privateKey: PRIVATE_KEY,
|
||||||
});
|
});
|
||||||
|
|
||||||
file.secrets.map((secretPair) => {
|
file.secrets.map((secretPair) => {
|
||||||
// decrypt .env file with symmetric key
|
// decrypt .env file with symmetric key
|
||||||
const plainTextKey = decryptSymmetric({
|
const plainTextKey = decryptSymmetric({
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import SecurityClient from "~/utilities/SecurityClient.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function fetches the bot for a project
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const getBot = async ({ workspaceId }) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
"/api/v1/bot/" + workspaceId,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res.status == 200) {
|
||||||
|
return await res.json();
|
||||||
|
} else {
|
||||||
|
console.log("Failed to get bot for project");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default getBot;
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import SecurityClient from "~/utilities/SecurityClient.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function fetches the bot for a project
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const setBotActiveStatus = async ({ botId, isActive, botKey }) => {
|
||||||
|
return SecurityClient.fetchCall(
|
||||||
|
"/api/v1/bot/" + botId + "/active",
|
||||||
|
{
|
||||||
|
method: "PATCH",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
isActive,
|
||||||
|
botKey
|
||||||
|
})
|
||||||
|
}
|
||||||
|
).then(async (res) => {
|
||||||
|
if (res.status == 200) {
|
||||||
|
return await res.json();
|
||||||
|
} else {
|
||||||
|
console.log("Failed to get bot for project");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export default setBotActiveStatus;
|
||||||
@@ -29,6 +29,14 @@ import getIntegrations from "../api/integrations/GetIntegrations";
|
|||||||
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
import getWorkspaceAuthorizations from "../api/integrations/getWorkspaceAuthorizations";
|
||||||
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
import getWorkspaceIntegrations from "../api/integrations/getWorkspaceIntegrations";
|
||||||
import startIntegration from "../api/integrations/StartIntegration";
|
import startIntegration from "../api/integrations/StartIntegration";
|
||||||
|
import getBot from "../api/bot/getBot";
|
||||||
|
import setBotActiveStatus from "../api/bot/setBotActiveStatus";
|
||||||
|
import getLatestFileKey from "../api/workspace/getLatestFileKey";
|
||||||
|
|
||||||
|
const {
|
||||||
|
decryptAssymmetric,
|
||||||
|
encryptAssymmetric
|
||||||
|
} = require('../../components/utilities/cryptography/crypto');
|
||||||
|
|
||||||
const crypto = require("crypto");
|
const crypto = require("crypto");
|
||||||
|
|
||||||
@@ -169,6 +177,7 @@ export default function Integrations() {
|
|||||||
const [authorizations, setAuthorizations] = useState();
|
const [authorizations, setAuthorizations] = useState();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [csrfToken, setCsrfToken] = useState("");
|
const [csrfToken, setCsrfToken] = useState("");
|
||||||
|
const [bot, setBot] = useState(null);
|
||||||
|
|
||||||
useEffect(async () => {
|
useEffect(async () => {
|
||||||
const tempCSRFToken = crypto.randomBytes(16).toString("hex");
|
const tempCSRFToken = crypto.randomBytes(16).toString("hex");
|
||||||
@@ -184,6 +193,12 @@ export default function Integrations() {
|
|||||||
workspaceId: router.query.id,
|
workspaceId: router.query.id,
|
||||||
});
|
});
|
||||||
setProjectIntegrations(projectIntegrations);
|
setProjectIntegrations(projectIntegrations);
|
||||||
|
|
||||||
|
const bot = await getBot({
|
||||||
|
workspaceId: router.query.id
|
||||||
|
});
|
||||||
|
|
||||||
|
setBot(bot.bot);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const integrationsData = await getIntegrations();
|
const integrationsData = await getIntegrations();
|
||||||
@@ -193,6 +208,50 @@ export default function Integrations() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle activate/deactivate bot
|
||||||
|
*/
|
||||||
|
const handleBotActivate = async () => {
|
||||||
|
const k = await getLatestFileKey({ workspaceId: router.query.id });
|
||||||
|
try {
|
||||||
|
if (bot) {
|
||||||
|
let botKey;
|
||||||
|
if (!bot.isActive) {
|
||||||
|
// case: bot is active -> deactivate
|
||||||
|
|
||||||
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY');
|
||||||
|
const WORKSPACE_KEY = decryptAssymmetric({
|
||||||
|
ciphertext: k.latestKey.encryptedKey,
|
||||||
|
nonce: k.latestKey.nonce,
|
||||||
|
publicKey: k.latestKey.sender.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const { ciphertext, nonce } = encryptAssymmetric({
|
||||||
|
plaintext: WORKSPACE_KEY,
|
||||||
|
publicKey: bot.publicKey,
|
||||||
|
privateKey: PRIVATE_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
botKey = {
|
||||||
|
encryptedKey: ciphertext,
|
||||||
|
nonce
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// case: bot is not active
|
||||||
|
const bot2 = await setBotActiveStatus({
|
||||||
|
botId: bot._id,
|
||||||
|
isActive: bot.isActive ? false : true,
|
||||||
|
botKey
|
||||||
|
});
|
||||||
|
setBot(bot2.bot);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return integrations ? (
|
return integrations ? (
|
||||||
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
<div className="bg-bunker-800 max-h-screen flex flex-col justify-between text-white">
|
||||||
<Head>
|
<Head>
|
||||||
@@ -212,6 +271,9 @@ export default function Integrations() {
|
|||||||
<div className="flex flex-row justify-start items-center text-3xl">
|
<div className="flex flex-row justify-start items-center text-3xl">
|
||||||
<p className="font-semibold mr-4">Current Project Integrations</p>
|
<p className="font-semibold mr-4">Current Project Integrations</p>
|
||||||
</div>
|
</div>
|
||||||
|
<button onClick={() => handleBotActivate()}>
|
||||||
|
{(bot && bot?.isActive) ? 'Deactivate bot' : 'Activate bot'}
|
||||||
|
</button>
|
||||||
<p className="mr-4 text-base text-gray-400">
|
<p className="mr-4 text-base text-gray-400">
|
||||||
Manage your integrations of Infisical with third-party services.
|
Manage your integrations of Infisical with third-party services.
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user