mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 15:27:27 +00:00
Begin developing bot, event, and integration pipeline
This commit is contained in:
@@ -0,0 +1,7 @@
|
|||||||
|
import {
|
||||||
|
actionPushToHeroku
|
||||||
|
} from './integration';
|
||||||
|
|
||||||
|
export {
|
||||||
|
actionPushToHeroku
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import {
|
||||||
|
Key,
|
||||||
|
Bot,
|
||||||
|
IBot,
|
||||||
|
Integration,
|
||||||
|
IntegrationAuth
|
||||||
|
} from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { BotService } from '../services';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Push secrets to Heroku
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {Event} obj.event
|
||||||
|
* @param {IBot} obj.bot
|
||||||
|
*/
|
||||||
|
const actionPushToHeroku = ({
|
||||||
|
event,
|
||||||
|
bot
|
||||||
|
}: {
|
||||||
|
event: Event,
|
||||||
|
bot: IBot
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
// TODO: push secrets in [event]
|
||||||
|
// event: name, workspaceId, payload (environment, secrets)
|
||||||
|
try {
|
||||||
|
|
||||||
|
// 1. Bot needs to decrypt their project key
|
||||||
|
// 2. Bot needs to decrypt secrets
|
||||||
|
// 3. Query IntegrationAuth for credentials
|
||||||
|
// 4. Decrypt integration refresh and token
|
||||||
|
// 5. Query Integration for integration details
|
||||||
|
// 6. Push to integration
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
actionPushToHeroku
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Bot, BotKey } from '../models';
|
||||||
|
import { createBot } from '../helpers/bot';
|
||||||
|
|
||||||
|
interface BotKey {
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot for workspace with id [workspaceId]. If a workspace bot doesn't exist,
|
||||||
|
* then create and return a new bot.
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const getBotByWorkspaceId = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { workspaceId } = req.body;
|
||||||
|
|
||||||
|
bot = await Bot.findOne({
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
// case: bot doesn't exist for workspace with id [workspaceId]
|
||||||
|
// -> create a new bot and return it
|
||||||
|
bot = await createBot({
|
||||||
|
name: 'Infisical Bot',
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to get bot for workspace'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return bot with id [req.bot._id] with active state set to [isActive].
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const setBotActiveState = async (req: Request, res: Response) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
|
||||||
|
|
||||||
|
if (isActive) {
|
||||||
|
// bot state set to active -> share workspace key with bot
|
||||||
|
await new BotKey({
|
||||||
|
encryptedKey: botKey.encryptedKey,
|
||||||
|
nonce: botKey.nonce,
|
||||||
|
sender: req.user._id,
|
||||||
|
receiver: req.bot._id,
|
||||||
|
workspace: req.bot.workspace
|
||||||
|
}).save();
|
||||||
|
} else {
|
||||||
|
// case: bot state set to inactive -> delete bot's workspace key
|
||||||
|
await BotKey.deleteOne({
|
||||||
|
bot: req.bot._id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
bot = await Bot.findOneAndUpdate({
|
||||||
|
_id: req.bot._id
|
||||||
|
}, {
|
||||||
|
isActive
|
||||||
|
}, {
|
||||||
|
new: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!bot) throw new Error('Failed to update bot active state');
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser({ email: req.user.email });
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(400).send({
|
||||||
|
message: 'Failed to update bot active state'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).send({
|
||||||
|
bot
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import * as authController from './authController';
|
import * as authController from './authController';
|
||||||
|
import * as botController from './botController';
|
||||||
import * as integrationAuthController from './integrationAuthController';
|
import * as integrationAuthController from './integrationAuthController';
|
||||||
import * as integrationController from './integrationController';
|
import * as integrationController from './integrationController';
|
||||||
import * as keyController from './keyController';
|
import * as keyController from './keyController';
|
||||||
@@ -16,6 +17,7 @@ import * as workspaceController from './workspaceController';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
authController,
|
authController,
|
||||||
|
botController,
|
||||||
integrationAuthController,
|
integrationAuthController,
|
||||||
integrationController,
|
integrationController,
|
||||||
keyController,
|
keyController,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ 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';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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
|
* Note: integration [integration] must be set up compatible/designed for OAuth2
|
||||||
@@ -14,58 +16,64 @@ import { OAUTH_CLIENT_SECRET_HEROKU, OAUTH_TOKEN_URL_HEROKU } from '../config';
|
|||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
export const integrationAuthOauthExchange = async (
|
export const oAuthExchange = async (
|
||||||
req: Request,
|
req: Request,
|
||||||
res: Response
|
res: Response
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
let clientSecret;
|
// let clientSecret;
|
||||||
|
|
||||||
const { workspaceId, code, integration } = req.body;
|
const { workspaceId, code, integration } = req.body;
|
||||||
|
|
||||||
if (!INTEGRATION_SET.has(integration))
|
if (!INTEGRATION_SET.has(integration))
|
||||||
throw new Error('Failed to validate integration');
|
throw new Error('Failed to validate integration');
|
||||||
|
|
||||||
// use correct client secret
|
await IntegrationService.handleOAuthExchange({
|
||||||
switch (integration) {
|
|
||||||
case 'heroku':
|
|
||||||
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: unfinished - make compatible with other integration types
|
|
||||||
const res = await axios.post(
|
|
||||||
OAUTH_TOKEN_URL_HEROKU!,
|
|
||||||
new URLSearchParams({
|
|
||||||
grant_type: 'authorization_code',
|
|
||||||
code: code,
|
|
||||||
client_secret: clientSecret
|
|
||||||
} as any)
|
|
||||||
);
|
|
||||||
|
|
||||||
const integrationAuth = await processOAuthTokenRes({
|
|
||||||
workspaceId,
|
workspaceId,
|
||||||
integration,
|
integration,
|
||||||
res
|
code
|
||||||
});
|
});
|
||||||
|
|
||||||
// create or replace integration
|
// // use correct client secret
|
||||||
const integrationObj = await Integration.findOneAndUpdate(
|
// switch (integration) {
|
||||||
{ workspace: workspaceId, integration },
|
// case 'heroku':
|
||||||
{
|
// clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
|
||||||
workspace: workspaceId,
|
// }
|
||||||
environment: ENV_DEV,
|
|
||||||
isActive: false,
|
// // TODO: unfinished - make compatible with other integration types
|
||||||
app: null,
|
// const res = await axios.post( // this response may be different for each integration
|
||||||
integration,
|
// OAUTH_TOKEN_URL_HEROKU!,
|
||||||
integrationAuth: integrationAuth._id
|
// new URLSearchParams({
|
||||||
},
|
// grant_type: 'authorization_code',
|
||||||
{ upsert: true, new: true }
|
// 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);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get OAuth2 token'
|
message: 'Failed to get OAuth2 code-token exchange'
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ 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
|
||||||
|
|||||||
@@ -17,16 +17,6 @@ export const uploadKey = async (req: Request, res: Response) => {
|
|||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { key } = req.body;
|
const { key } = req.body;
|
||||||
|
|
||||||
// validate membership of sender
|
|
||||||
const senderMembership = await findMembership({
|
|
||||||
user: req.user._id,
|
|
||||||
workspace: workspaceId
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!senderMembership) {
|
|
||||||
throw new Error('Failed sender membership validation for workspace');
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate membership of receiver
|
// validate membership of receiver
|
||||||
const receiverMembership = await findMembership({
|
const receiverMembership = await findMembership({
|
||||||
user: key.userId,
|
user: key.userId,
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import {
|
|||||||
reformatPullSecrets
|
reformatPullSecrets
|
||||||
} from '../helpers/secret';
|
} from '../helpers/secret';
|
||||||
import { pushKeys } from '../helpers/key';
|
import { pushKeys } from '../helpers/key';
|
||||||
|
import { eventPushSecrets } from '../events';
|
||||||
|
import { EventService } from '../services';
|
||||||
import { ENV_SET } from '../variables';
|
import { ENV_SET } from '../variables';
|
||||||
|
|
||||||
import { postHogClient } from '../services';
|
import { postHogClient } from '../services';
|
||||||
|
|
||||||
interface PushSecret {
|
interface PushSecret {
|
||||||
@@ -60,7 +61,16 @@ export const pushSecrets = async (req: Request, res: Response) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
keys
|
keys
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// trigger event
|
||||||
|
EventService.handleEvent({
|
||||||
|
event: eventPushSecrets({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secrets
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pushed',
|
event: 'secrets pushed',
|
||||||
@@ -192,7 +202,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (postHogClient) {
|
if (postHogClient) {
|
||||||
// capture secrets pushed event in production
|
// capture secrets pulled event in production
|
||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
distinctId: req.serviceToken.user.email,
|
distinctId: req.serviceToken.user.email,
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { eventPushSecrets } from "./secret"
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||||
|
|
||||||
|
interface PushSecret {
|
||||||
|
ciphertextKey: string;
|
||||||
|
ivKey: string;
|
||||||
|
tagKey: string;
|
||||||
|
hashKey: string;
|
||||||
|
ciphertextValue: string;
|
||||||
|
ivValue: string;
|
||||||
|
tagValue: string;
|
||||||
|
hashValue: string;
|
||||||
|
type: 'shared' | 'personal';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return event for pushing secrets
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace to push secrets to
|
||||||
|
* @param {String} obj.environment - environment for secrets
|
||||||
|
* @param {PushSecret[]} obj.secrets - secrets to push
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const eventPushSecrets = ({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
secrets
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
secrets: PushSecret[];
|
||||||
|
}) => {
|
||||||
|
return ({
|
||||||
|
name: EVENT_PUSH_SECRETS,
|
||||||
|
workspaceId,
|
||||||
|
payload: {
|
||||||
|
environment,
|
||||||
|
secrets
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
eventPushSecrets
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Bot
|
||||||
|
} from '../models';
|
||||||
|
import { generateKeyPair, encryptSymmetric } from '../utils/crypto';
|
||||||
|
import { ENCRYPTION_KEY } from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an inactive bot with name [name] for workspace with id [workspaceId]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.name - name of bot
|
||||||
|
* @param {String} obj.workspaceId - id of workspace that bot belongs to
|
||||||
|
*/
|
||||||
|
const createBot = async ({
|
||||||
|
name,
|
||||||
|
workspaceId,
|
||||||
|
}: {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
}) => {
|
||||||
|
let bot;
|
||||||
|
try {
|
||||||
|
const { publicKey, privateKey } = generateKeyPair();
|
||||||
|
const { ciphertext, iv, tag } = encryptSymmetric({
|
||||||
|
plaintext: privateKey,
|
||||||
|
key: ENCRYPTION_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
bot = await new Bot({
|
||||||
|
name,
|
||||||
|
workspace: workspaceId,
|
||||||
|
isActive: false,
|
||||||
|
publicKey,
|
||||||
|
encryptedPrivateKey: ciphertext,
|
||||||
|
iv,
|
||||||
|
tag
|
||||||
|
}).save();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to create bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
return bot;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
createBot
|
||||||
|
}
|
||||||
@@ -9,6 +9,81 @@ import {
|
|||||||
OAUTH_TOKEN_URL_HEROKU
|
OAUTH_TOKEN_URL_HEROKU
|
||||||
} from '../config';
|
} 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
|
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
|
||||||
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
|
||||||
@@ -82,6 +157,7 @@ const processOAuthTokenRes = async ({
|
|||||||
return integrationAuth;
|
return integrationAuth;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: deprecate
|
||||||
/**
|
/**
|
||||||
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
|
* 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
|
* the integration authorization document or by requesting a new one by decrypting and exchanging the
|
||||||
@@ -171,4 +247,4 @@ const getOAuthAccessToken = async ({
|
|||||||
return accessToken;
|
return accessToken;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { processOAuthTokenRes, getOAuthAccessToken };
|
export { processOAuthTokenRes, processOAuthTokenRes2, getOAuthAccessToken };
|
||||||
|
|||||||
@@ -1,6 +1,51 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Membership, Key } from '../models';
|
import { Membership, Key } from '../models';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
|
* and has at least one of the roles in [acceptedRoles] and statuses in [acceptedStatuses]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.userId - id of user to validate
|
||||||
|
* @param {String} obj.workspaceId - id of workspace
|
||||||
|
*/
|
||||||
|
const validateMembership = async ({
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
}: {
|
||||||
|
userId: string;
|
||||||
|
workspaceId: string;
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
}) => {
|
||||||
|
|
||||||
|
let membership;
|
||||||
|
try {
|
||||||
|
membership = await Membership.findOne({
|
||||||
|
user: userId,
|
||||||
|
workspace: workspaceId
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!membership) throw new Error('Failed to find membership');
|
||||||
|
|
||||||
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
|
throw new Error('Failed to validate membership role');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!acceptedStatuses.includes(membership.status)) {
|
||||||
|
throw new Error('Failed to validate membership status');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to validate membership');
|
||||||
|
}
|
||||||
|
|
||||||
|
return membership;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return membership matching criteria specified in query [queryObj]
|
* Return membership matching criteria specified in query [queryObj]
|
||||||
* @param {Object} queryObj - query object
|
* @param {Object} queryObj - query object
|
||||||
@@ -97,4 +142,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
export { addMemberships, findMembership, deleteMembership };
|
export {
|
||||||
|
validateMembership,
|
||||||
|
addMemberships,
|
||||||
|
findMembership,
|
||||||
|
deleteMembership
|
||||||
|
};
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => {
|
|||||||
// send mail
|
// send mail
|
||||||
await sendMail({
|
await sendMail({
|
||||||
template: 'emailVerification.handlebars',
|
template: 'emailVerification.handlebars',
|
||||||
subjectLine: 'Infisical workspace invitation',
|
subjectLine: 'Infisical confirmation code',
|
||||||
recipients: [email],
|
recipients: [email],
|
||||||
substitutions: {
|
substitutions: {
|
||||||
code: token
|
code: token
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import {
|
import {
|
||||||
Workspace,
|
Workspace,
|
||||||
|
Bot,
|
||||||
Membership,
|
Membership,
|
||||||
Key,
|
Key,
|
||||||
Secret
|
Secret
|
||||||
} from '../models';
|
} from '../models';
|
||||||
|
import { createBot } from '../helpers/bot';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a workspace with name [name] in organization with id [organizationId]
|
* Create a workspace with name [name] in organization with id [organizationId]
|
||||||
|
* and a bot for it.
|
||||||
* @param {String} name - name of workspace to create.
|
* @param {String} name - name of workspace to create.
|
||||||
* @param {String} organizationId - id of organization to create workspace in
|
* @param {String} organizationId - id of organization to create workspace in
|
||||||
* @param {Object} workspace - new workspace
|
* @param {Object} workspace - new workspace
|
||||||
@@ -21,10 +24,16 @@ const createWorkspace = async ({
|
|||||||
}) => {
|
}) => {
|
||||||
let workspace;
|
let workspace;
|
||||||
try {
|
try {
|
||||||
|
// create workspace
|
||||||
workspace = await new Workspace({
|
workspace = await new Workspace({
|
||||||
name,
|
name,
|
||||||
organization: organizationId
|
organization: organizationId
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
|
// const bot = await createBot({
|
||||||
|
// name: 'Infisical Bot',
|
||||||
|
// workspaceId: workspace._id.toString()
|
||||||
|
// });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser(null);
|
Sentry.setUser(null);
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
@@ -43,6 +52,9 @@ const createWorkspace = async ({
|
|||||||
const deleteWorkspace = async ({ id }: { id: string }) => {
|
const deleteWorkspace = async ({ id }: { id: string }) => {
|
||||||
try {
|
try {
|
||||||
await Workspace.deleteOne({ _id: id });
|
await Workspace.deleteOne({ _id: id });
|
||||||
|
await Bot.deleteOne({
|
||||||
|
workspace: id
|
||||||
|
});
|
||||||
await Membership.deleteMany({
|
await Membership.deleteMany({
|
||||||
workspace: id
|
workspace: id
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ Sentry.init({
|
|||||||
import {
|
import {
|
||||||
signup as signupRouter,
|
signup as signupRouter,
|
||||||
auth as authRouter,
|
auth as authRouter,
|
||||||
|
bot as botRouter,
|
||||||
organization as organizationRouter,
|
organization as organizationRouter,
|
||||||
workspace as workspaceRouter,
|
workspace as workspaceRouter,
|
||||||
membershipOrg as membershipOrgRouter,
|
membershipOrg as membershipOrgRouter,
|
||||||
@@ -71,6 +72,7 @@ app.use(express.json());
|
|||||||
// routers
|
// routers
|
||||||
app.use('/api/v1/signup', signupRouter);
|
app.use('/api/v1/signup', signupRouter);
|
||||||
app.use('/api/v1/auth', authRouter);
|
app.use('/api/v1/auth', authRouter);
|
||||||
|
app.use('/api/v1/bot', botRouter);
|
||||||
app.use('/api/v1/user', userRouter);
|
app.use('/api/v1/user', userRouter);
|
||||||
app.use('/api/v1/user-action', userActionRouter);
|
app.use('/api/v1/user-action', userActionRouter);
|
||||||
app.use('/api/v1/organization', organizationRouter);
|
app.use('/api/v1/organization', organizationRouter);
|
||||||
|
|||||||
@@ -0,0 +1,96 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
} from '../variables';
|
||||||
|
import {
|
||||||
|
OAUTH_CLIENT_SECRET_HEROKU,
|
||||||
|
OAUTH_TOKEN_URL_HEROKU
|
||||||
|
} from '../config';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for OAuth2
|
||||||
|
* code-token exchange for integration named [integration]
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {String} obj1.integration - name of integration
|
||||||
|
* @param {String} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.accessToken - access token for integration
|
||||||
|
* @returns {String} obj.refreshToken - refresh token for integration
|
||||||
|
* @returns {Date} obj.accessExpiresAt - date of expiration for access token
|
||||||
|
* @returns {String} obj.action - integration action for bot sequence
|
||||||
|
*/
|
||||||
|
const exchangeCode = async ({
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let obj = {} as any;
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
obj = await exchangeCodeHeroku({
|
||||||
|
code
|
||||||
|
});
|
||||||
|
obj['action'] = ACTION_PUSH_TO_HEROKU;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange');
|
||||||
|
}
|
||||||
|
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return [accessToken], [accessExpiresAt], and [refreshToken] for Heroku
|
||||||
|
* OAuth2 code-token exchange
|
||||||
|
* @param {Object} obj1
|
||||||
|
* @param {Object} obj1.code - code for code-token exchange
|
||||||
|
* @returns {Object} obj2
|
||||||
|
* @returns {String} obj2.accessToken - access token for Heroku API
|
||||||
|
* @returns {String} obj2.refreshToken - refresh token for Heroku API
|
||||||
|
* @returns {Date} obj2.accessExpiresAt - date of expiration for access token
|
||||||
|
*/
|
||||||
|
const exchangeCodeHeroku = async ({
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
code: string;
|
||||||
|
}) => {
|
||||||
|
let res: any;
|
||||||
|
let accessExpiresAt: any;
|
||||||
|
try {
|
||||||
|
res = await axios.post(
|
||||||
|
OAUTH_TOKEN_URL_HEROKU!,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'authorization_code',
|
||||||
|
code: code,
|
||||||
|
client_secret: OAUTH_CLIENT_SECRET_HEROKU
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
accessExpiresAt.setSeconds(
|
||||||
|
accessExpiresAt.getSeconds() + res.data.expires_in
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('integrationHerokuExchange');
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed OAuth2 code-token exchange with Heroku');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ({
|
||||||
|
accessToken: res.data.access_token,
|
||||||
|
refreshToken: res.data.refresh_token,
|
||||||
|
accessExpiresAt
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
exchangeCode
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import { exchangeCode } from './exchange';
|
||||||
|
import { exchangeRefresh } from './refresh';
|
||||||
|
|
||||||
|
export {
|
||||||
|
exchangeCode,
|
||||||
|
exchangeRefresh
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { INTEGRATION_HEROKU } from '../variables';
|
||||||
|
import {
|
||||||
|
OAUTH_CLIENT_SECRET_HEROKU
|
||||||
|
} from '../config';
|
||||||
|
import {
|
||||||
|
OAUTH_TOKEN_URL_HEROKU
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for integration
|
||||||
|
* named [integration]
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.integration - name of integration
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||||
|
*/
|
||||||
|
const exchangeRefresh = async ({
|
||||||
|
integration,
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
integration: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
switch (integration) {
|
||||||
|
case INTEGRATION_HEROKU:
|
||||||
|
accessToken = await exchangeRefreshHeroku({
|
||||||
|
refreshToken
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get new OAuth2 access token');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new access token by exchanging refresh token [refreshToken] for the
|
||||||
|
* Heroku integration
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.refreshToken - refresh token to use to get new access token for Heroku
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const exchangeRefreshHeroku = async ({
|
||||||
|
refreshToken
|
||||||
|
}: {
|
||||||
|
refreshToken: string;
|
||||||
|
}) => {
|
||||||
|
let accessToken;
|
||||||
|
try {
|
||||||
|
const res = await axios.post(
|
||||||
|
OAUTH_TOKEN_URL_HEROKU,
|
||||||
|
new URLSearchParams({
|
||||||
|
grant_type: 'refresh_token',
|
||||||
|
refresh_token: refreshToken,
|
||||||
|
client_secret: OAUTH_CLIENT_SECRET_HEROKU
|
||||||
|
} as any)
|
||||||
|
);
|
||||||
|
|
||||||
|
accessToken = res.data.access_token;
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
throw new Error('Failed to get new OAuth2 access token for Heroku');
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
export {
|
||||||
|
exchangeRefresh
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import requireAuth from './requireAuth';
|
import requireAuth from './requireAuth';
|
||||||
|
import requireBotAuth from './requireBotAuth';
|
||||||
import requireSignupAuth from './requireSignupAuth';
|
import requireSignupAuth from './requireSignupAuth';
|
||||||
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
import requireWorkspaceAuth from './requireWorkspaceAuth';
|
||||||
import requireOrganizationAuth from './requireOrganizationAuth';
|
import requireOrganizationAuth from './requireOrganizationAuth';
|
||||||
@@ -9,6 +10,7 @@ import validateRequest from './validateRequest';
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
|
requireBotAuth,
|
||||||
requireSignupAuth,
|
requireSignupAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
requireOrganizationAuth,
|
requireOrganizationAuth,
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { Bot } from '../models';
|
||||||
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
|
const requireBotAuth = ({
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses,
|
||||||
|
location = 'params'
|
||||||
|
}: {
|
||||||
|
acceptedRoles: string[];
|
||||||
|
acceptedStatuses: string[];
|
||||||
|
location?: req;
|
||||||
|
}) => {
|
||||||
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
const bot = await Bot.findOne({ _id: req[location].botId });
|
||||||
|
|
||||||
|
if (!bot) {
|
||||||
|
throw new Error('Failed to find bot');
|
||||||
|
}
|
||||||
|
|
||||||
|
await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
|
workspaceId: bot.workspace.toString(),
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
|
});
|
||||||
|
|
||||||
|
req.bot = bot;
|
||||||
|
|
||||||
|
next();
|
||||||
|
} catch (err) {
|
||||||
|
Sentry.setUser(null);
|
||||||
|
Sentry.captureException(err);
|
||||||
|
return res.status(401).send({
|
||||||
|
error: 'Failed bot authorization'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default requireBotAuth;
|
||||||
@@ -2,6 +2,7 @@ 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 { Integration, IntegrationAuth, Membership } from '../models';
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||||
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member of workspace with proper roles associated
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
@@ -31,24 +32,14 @@ const requireIntegrationAuth = ({
|
|||||||
if (!integration) {
|
if (!integration) {
|
||||||
throw new Error('Failed to find integration');
|
throw new Error('Failed to find integration');
|
||||||
}
|
}
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
await validateMembership({
|
||||||
user: req.user._id,
|
userId: req.user._id.toString(),
|
||||||
workspace: integration.workspace
|
workspaceId: integration.workspace.toString(),
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Failed to find integration workspace membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
const integrationAuth = await IntegrationAuth.findOne({
|
const integrationAuth = await IntegrationAuth.findOne({
|
||||||
_id: integration.integrationAuth
|
_id: integration.integrationAuth
|
||||||
}).select(
|
}).select(
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
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, Membership } from '../models';
|
import { IntegrationAuth } from '../models';
|
||||||
import { decryptSymmetric } from '../utils/crypto';
|
|
||||||
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
import { getOAuthAccessToken } from '../helpers/integrationAuth';
|
||||||
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate if user on request is a member of workspace with proper roles associated
|
* Validate if user on request is a member of workspace with proper roles associated
|
||||||
@@ -20,8 +20,6 @@ const requireIntegrationAuthorizationAuth = ({
|
|||||||
acceptedStatuses: string[];
|
acceptedStatuses: string[];
|
||||||
}) => {
|
}) => {
|
||||||
return async (req: Request, res: Response, next: NextFunction) => {
|
return async (req: Request, res: Response, next: NextFunction) => {
|
||||||
// (authorization) integration authorization middleware
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { integrationAuthId } = req.params;
|
const { integrationAuthId } = req.params;
|
||||||
|
|
||||||
@@ -34,29 +32,15 @@ const requireIntegrationAuthorizationAuth = ({
|
|||||||
if (!integrationAuth) {
|
if (!integrationAuth) {
|
||||||
throw new Error('Failed to find integration authorization');
|
throw new Error('Failed to find integration authorization');
|
||||||
}
|
}
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
await validateMembership({
|
||||||
user: req.user._id,
|
userId: req.user._id.toString(),
|
||||||
workspace: integrationAuth.workspace
|
workspaceId: integrationAuth.workspace.toString(),
|
||||||
|
acceptedRoles,
|
||||||
|
acceptedStatuses
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error(
|
|
||||||
'Failed to find integration authorization workspace membership'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.integrationAuth = integrationAuth;
|
req.integrationAuth = integrationAuth;
|
||||||
|
|
||||||
// TODO: make compatible with other integration types since they won't necessarily have access tokens
|
|
||||||
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
req.accessToken = await getOAuthAccessToken({ integrationAuth });
|
||||||
return next();
|
return next();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
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 { Membership, IWorkspace } from '../models';
|
import { validateMembership } from '../helpers/membership';
|
||||||
|
|
||||||
type req = 'params' | 'body' | 'query';
|
type req = 'params' | 'body' | 'query';
|
||||||
|
|
||||||
@@ -25,24 +25,12 @@ const requireWorkspaceAuth = ({
|
|||||||
// workspace authorization middleware
|
// workspace authorization middleware
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// validate workspace membership
|
const membership = await validateMembership({
|
||||||
|
userId: req.user._id.toString(),
|
||||||
const membership = await Membership.findOne({
|
workspaceId: req[location].workspaceId,
|
||||||
user: req.user._id,
|
acceptedRoles,
|
||||||
workspace: req[location].workspaceId
|
acceptedStatuses
|
||||||
}).populate<{ workspace: IWorkspace }>('workspace');
|
});
|
||||||
|
|
||||||
if (!membership) {
|
|
||||||
throw new Error('Failed to find workspace membership');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
|
||||||
throw new Error('Failed to validate workspace membership role');
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!acceptedStatuses.includes(membership.status)) {
|
|
||||||
throw new Error('Failed to validate workspace membership status');
|
|
||||||
}
|
|
||||||
|
|
||||||
req.membership = membership;
|
req.membership = membership;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IBot {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
name: string;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
isActive: boolean;
|
||||||
|
publicKey: string;
|
||||||
|
encryptedPrivateKey: string;
|
||||||
|
iv: string;
|
||||||
|
tag: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botSchema = new Schema<IBot>(
|
||||||
|
{
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isActive: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
publicKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
encryptedPrivateKey: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
iv: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
},
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
select: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const Bot = model<IBot>('Bot', botSchema);
|
||||||
|
|
||||||
|
export default Bot;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import { Schema, model, Types } from 'mongoose';
|
||||||
|
|
||||||
|
export interface IBotKey {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
encryptedKey: string;
|
||||||
|
nonce: string;
|
||||||
|
sender: Types.ObjectId;
|
||||||
|
bot: Types.ObjectId;
|
||||||
|
workspace: Types.ObjectId;
|
||||||
|
}
|
||||||
|
|
||||||
|
const botKeySchema = new Schema<IBotKey>(
|
||||||
|
{
|
||||||
|
encryptedKey: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
nonce: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
sender: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'User',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
bot: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Bot',
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: 'Workspace',
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const BotKey = model<IBotKey>('BotKey', botKeySchema);
|
||||||
|
|
||||||
|
export default BotKey;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
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,4 +1,7 @@
|
|||||||
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
import BackupPrivateKey, { IBackupPrivateKey } from './backupPrivateKey';
|
||||||
|
import Bot, { IBot } from './bot';
|
||||||
|
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';
|
||||||
@@ -16,6 +19,12 @@ import Workspace, { IWorkspace } from './workspace';
|
|||||||
export {
|
export {
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
IBackupPrivateKey,
|
IBackupPrivateKey,
|
||||||
|
Bot,
|
||||||
|
IBot,
|
||||||
|
BotKey,
|
||||||
|
IBotKey,
|
||||||
|
BotSequence,
|
||||||
|
IBotSequence,
|
||||||
IncidentContactOrg,
|
IncidentContactOrg,
|
||||||
IIncidentContactOrg,
|
IIncidentContactOrg,
|
||||||
Integration,
|
Integration,
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import express from 'express';
|
||||||
|
const router = express.Router();
|
||||||
|
import { body } from 'express-validator';
|
||||||
|
import {
|
||||||
|
requireAuth,
|
||||||
|
requireBotAuth,
|
||||||
|
requireWorkspaceAuth,
|
||||||
|
validateRequest
|
||||||
|
} from '../middleware';
|
||||||
|
import { botController } from '../controllers';
|
||||||
|
import { ADMIN, MEMBER, COMPLETED, GRANTED } from '../variables';
|
||||||
|
|
||||||
|
router.get(
|
||||||
|
'/',
|
||||||
|
requireAuth,
|
||||||
|
requireWorkspaceAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED],
|
||||||
|
location: 'body'
|
||||||
|
}),
|
||||||
|
body('workspaceId').exists().trim().notEmpty(),
|
||||||
|
validateRequest,
|
||||||
|
botController.getBotByWorkspaceId
|
||||||
|
);
|
||||||
|
|
||||||
|
router.patch(
|
||||||
|
'/:botId/active',
|
||||||
|
requireAuth,
|
||||||
|
requireBotAuth({
|
||||||
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
|
acceptedStatuses: [COMPLETED, GRANTED]
|
||||||
|
}),
|
||||||
|
body('isActive').isBoolean(),
|
||||||
|
body('botKey'),
|
||||||
|
validateRequest,
|
||||||
|
botController.setBotActiveState
|
||||||
|
);
|
||||||
|
|
||||||
|
export default router;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import signup from './signup';
|
import signup from './signup';
|
||||||
|
import bot from './bot';
|
||||||
import auth from './auth';
|
import auth from './auth';
|
||||||
import user from './user';
|
import user from './user';
|
||||||
import userAction from './userAction';
|
import userAction from './userAction';
|
||||||
@@ -18,6 +19,7 @@ import integrationAuth from './integrationAuth';
|
|||||||
export {
|
export {
|
||||||
signup,
|
signup,
|
||||||
auth,
|
auth,
|
||||||
|
bot,
|
||||||
user,
|
user,
|
||||||
userAction,
|
userAction,
|
||||||
organization,
|
organization,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { integrationController } from '../controllers';
|
|||||||
|
|
||||||
router.get('/integrations', requireAuth, integrationController.getIntegrations);
|
router.get('/integrations', requireAuth, integrationController.getIntegrations);
|
||||||
|
|
||||||
router.post(
|
router.post( // TODO: deprecate
|
||||||
'/:integrationId/sync',
|
'/:integrationId/sync',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuth({
|
requireIntegrationAuth({
|
||||||
|
|||||||
@@ -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(
|
router.post( // semi-ok
|
||||||
'/oauth-token',
|
'/oauth-token',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
@@ -22,10 +22,10 @@ router.post(
|
|||||||
body('code').exists().trim().notEmpty(),
|
body('code').exists().trim().notEmpty(),
|
||||||
body('integration').exists().trim().notEmpty(),
|
body('integration').exists().trim().notEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
integrationAuthController.integrationAuthOauthExchange
|
integrationAuthController.oAuthExchange
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get( // not-ok
|
||||||
'/:integrationAuthId/apps',
|
'/:integrationAuthId/apps',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuthorizationAuth({
|
requireIntegrationAuthorizationAuth({
|
||||||
@@ -37,7 +37,7 @@ router.get(
|
|||||||
integrationAuthController.getIntegrationAuthApps
|
integrationAuthController.getIntegrationAuthApps
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
router.delete( // not-ok
|
||||||
'/:integrationAuthId',
|
'/:integrationAuthId',
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireIntegrationAuthorizationAuth({
|
requireIntegrationAuthorizationAuth({
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
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;
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle bot actions
|
||||||
|
*/
|
||||||
|
class BotService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return decrypted secrets using bot
|
||||||
|
* @param {Object} obj
|
||||||
|
* @param {String} obj.workspaceId - id of workspace of secrets
|
||||||
|
* @param {String} obj.environment - environment for secrets
|
||||||
|
*/
|
||||||
|
static async decryptSecrets({
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BotService;
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Bot, IBot, BotSequence } from '../models';
|
||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import ActionService from './ActionService';
|
||||||
|
|
||||||
|
interface Event {
|
||||||
|
name: string;
|
||||||
|
workspaceId: string;
|
||||||
|
payload: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle events. TODO: elaborate DOCSTRING.
|
||||||
|
*/
|
||||||
|
class EventService {
|
||||||
|
/**
|
||||||
|
* Check if any bot sequences exist for event and forward
|
||||||
|
* bot sequence details to ActionService for execution
|
||||||
|
* @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)
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EventService;
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import * as Sentry from '@sentry/node';
|
||||||
|
import {
|
||||||
|
Integration,
|
||||||
|
Bot,
|
||||||
|
BotSequence
|
||||||
|
} from '../models';
|
||||||
|
import { exchangeCode } from '../integrations';
|
||||||
|
import { processOAuthTokenRes2 } from '../helpers/integrationAuth';
|
||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
EVENT_PUSH_SECRETS
|
||||||
|
} from '../variables';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class to handle integrations
|
||||||
|
*/
|
||||||
|
class IntegrationService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*/
|
||||||
|
static async handleOAuthExchange({
|
||||||
|
workspaceId,
|
||||||
|
integration,
|
||||||
|
code
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
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 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')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default IntegrationService;
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
import postHogClient from './PostHogClient';
|
import postHogClient from './PostHogClient';
|
||||||
|
import BotService from './BotService';
|
||||||
|
import EventService from './EventService';
|
||||||
|
import ActionService from './ActionService';
|
||||||
|
import IntegrationService from './IntegrationService';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
postHogClient
|
postHogClient,
|
||||||
|
BotService,
|
||||||
|
EventService,
|
||||||
|
ActionService,
|
||||||
|
IntegrationService
|
||||||
}
|
}
|
||||||
Vendored
+1
@@ -11,6 +11,7 @@ declare global {
|
|||||||
membershipOrg: any;
|
membershipOrg: any;
|
||||||
integration: any;
|
integration: any;
|
||||||
integrationAuth: any;
|
integrationAuth: any;
|
||||||
|
bot: any;
|
||||||
serviceToken: any;
|
serviceToken: any;
|
||||||
accessToken: any;
|
accessToken: any;
|
||||||
query?: any;
|
query?: any;
|
||||||
|
|||||||
@@ -2,6 +2,21 @@ import nacl from 'tweetnacl';
|
|||||||
import util from 'tweetnacl-util';
|
import util from 'tweetnacl-util';
|
||||||
import AesGCM from './aes-gcm';
|
import AesGCM from './aes-gcm';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return new base64, NaCl, public-private key pair.
|
||||||
|
* @returns {Object} obj
|
||||||
|
* @returns {String} obj.publicKey - base64, NaCl, public key
|
||||||
|
* @returns {String} obj.privateKey - base64, NaCl, private key
|
||||||
|
*/
|
||||||
|
const generateKeyPair = () => {
|
||||||
|
const pair = nacl.box.keyPair();
|
||||||
|
|
||||||
|
return ({
|
||||||
|
publicKey: util.encodeBase64(pair.publicKey),
|
||||||
|
privateKey: util.encodeBase64(pair.secretKey)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
* Return assymmetrically encrypted [plaintext] using [publicKey] where
|
||||||
* [publicKey] likely belongs to the recipient.
|
* [publicKey] likely belongs to the recipient.
|
||||||
@@ -139,6 +154,7 @@ const decryptSymmetric = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
generateKeyPair,
|
||||||
encryptAsymmetric,
|
encryptAsymmetric,
|
||||||
decryptAsymmetric,
|
decryptAsymmetric,
|
||||||
encryptSymmetric,
|
encryptSymmetric,
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
// membership roles
|
|
||||||
const OWNER = 'owner';
|
|
||||||
const ADMIN = 'admin';
|
|
||||||
const MEMBER = 'member';
|
|
||||||
|
|
||||||
// membership statuses
|
|
||||||
const INVITED = 'invited';
|
|
||||||
|
|
||||||
// -- organization
|
|
||||||
const ACCEPTED = 'accepted';
|
|
||||||
|
|
||||||
// -- workspace
|
|
||||||
const COMPLETED = 'completed';
|
|
||||||
const GRANTED = 'granted';
|
|
||||||
|
|
||||||
// subscriptions
|
|
||||||
const PLAN_STARTER = 'starter';
|
|
||||||
const PLAN_PRO = 'pro';
|
|
||||||
|
|
||||||
// secrets
|
|
||||||
const SECRET_SHARED = 'shared';
|
|
||||||
const SECRET_PERSONAL = 'personal';
|
|
||||||
|
|
||||||
// environments
|
|
||||||
const ENV_DEV = 'dev';
|
|
||||||
const ENV_TESTING = 'test';
|
|
||||||
const ENV_STAGING = 'staging';
|
|
||||||
const ENV_PROD = 'prod';
|
|
||||||
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
|
||||||
|
|
||||||
// integrations
|
|
||||||
const INTEGRATION_HEROKU = 'heroku';
|
|
||||||
const INTEGRATION_NETLIFY = 'netlify';
|
|
||||||
const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]);
|
|
||||||
|
|
||||||
// integration types
|
|
||||||
const INTEGRATION_OAUTH2 = 'oauth2';
|
|
||||||
|
|
||||||
export {
|
|
||||||
OWNER,
|
|
||||||
ADMIN,
|
|
||||||
MEMBER,
|
|
||||||
INVITED,
|
|
||||||
ACCEPTED,
|
|
||||||
COMPLETED,
|
|
||||||
GRANTED,
|
|
||||||
PLAN_STARTER,
|
|
||||||
PLAN_PRO,
|
|
||||||
SECRET_SHARED,
|
|
||||||
SECRET_PERSONAL,
|
|
||||||
ENV_DEV,
|
|
||||||
ENV_TESTING,
|
|
||||||
ENV_STAGING,
|
|
||||||
ENV_PROD,
|
|
||||||
ENV_SET,
|
|
||||||
INTEGRATION_HEROKU,
|
|
||||||
INTEGRATION_NETLIFY,
|
|
||||||
INTEGRATION_SET,
|
|
||||||
INTEGRATION_OAUTH2
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
const ACTION_PUSH_TO_HEROKU = 'pushToHeroku';
|
||||||
|
|
||||||
|
export {
|
||||||
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
// environments
|
||||||
|
const ENV_DEV = 'dev';
|
||||||
|
const ENV_TESTING = 'test';
|
||||||
|
const ENV_STAGING = 'staging';
|
||||||
|
const ENV_PROD = 'prod';
|
||||||
|
const ENV_SET = new Set([ENV_DEV, ENV_TESTING, ENV_STAGING, ENV_PROD]);
|
||||||
|
|
||||||
|
export {
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const EVENT_PUSH_SECRETS = 'pushSecrets';
|
||||||
|
const EVENT_PULL_SECRETS = 'pullSecrets';
|
||||||
|
|
||||||
|
export {
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import {
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET
|
||||||
|
} from './environment';
|
||||||
|
import {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
OAUTH_TOKEN_URL_HEROKU
|
||||||
|
} from './integration';
|
||||||
|
import {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED
|
||||||
|
} from './organization';
|
||||||
|
import {
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL
|
||||||
|
} from './secret';
|
||||||
|
import {
|
||||||
|
PLAN_STARTER,
|
||||||
|
PLAN_PRO
|
||||||
|
} from './stripe';
|
||||||
|
import {
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS
|
||||||
|
} from './event';
|
||||||
|
import {
|
||||||
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
} from './action';
|
||||||
|
|
||||||
|
export {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED,
|
||||||
|
PLAN_STARTER,
|
||||||
|
PLAN_PRO,
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL,
|
||||||
|
ENV_DEV,
|
||||||
|
ENV_TESTING,
|
||||||
|
ENV_STAGING,
|
||||||
|
ENV_PROD,
|
||||||
|
ENV_SET,
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
OAUTH_TOKEN_URL_HEROKU,
|
||||||
|
EVENT_PUSH_SECRETS,
|
||||||
|
EVENT_PULL_SECRETS,
|
||||||
|
ACTION_PUSH_TO_HEROKU
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
// integrations
|
||||||
|
const INTEGRATION_HEROKU = 'heroku';
|
||||||
|
const INTEGRATION_NETLIFY = 'netlify';
|
||||||
|
const INTEGRATION_SET = new Set([INTEGRATION_HEROKU, INTEGRATION_NETLIFY]);
|
||||||
|
|
||||||
|
// integration types
|
||||||
|
const INTEGRATION_OAUTH2 = 'oauth2';
|
||||||
|
|
||||||
|
// integration oauth endpoints
|
||||||
|
const OAUTH_TOKEN_URL_HEROKU = 'https://id.heroku.com/oauth/token';
|
||||||
|
|
||||||
|
export {
|
||||||
|
INTEGRATION_HEROKU,
|
||||||
|
INTEGRATION_NETLIFY,
|
||||||
|
INTEGRATION_SET,
|
||||||
|
INTEGRATION_OAUTH2,
|
||||||
|
OAUTH_TOKEN_URL_HEROKU
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// membership roles
|
||||||
|
const OWNER = 'owner';
|
||||||
|
const ADMIN = 'admin';
|
||||||
|
const MEMBER = 'member';
|
||||||
|
|
||||||
|
// membership statuses
|
||||||
|
const INVITED = 'invited';
|
||||||
|
|
||||||
|
// -- organization
|
||||||
|
const ACCEPTED = 'accepted';
|
||||||
|
|
||||||
|
// -- workspace
|
||||||
|
const COMPLETED = 'completed';
|
||||||
|
const GRANTED = 'granted';
|
||||||
|
|
||||||
|
export {
|
||||||
|
OWNER,
|
||||||
|
ADMIN,
|
||||||
|
MEMBER,
|
||||||
|
INVITED,
|
||||||
|
ACCEPTED,
|
||||||
|
COMPLETED,
|
||||||
|
GRANTED
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// secrets
|
||||||
|
const SECRET_SHARED = 'shared';
|
||||||
|
const SECRET_PERSONAL = 'personal';
|
||||||
|
|
||||||
|
export {
|
||||||
|
SECRET_SHARED,
|
||||||
|
SECRET_PERSONAL
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
const PLAN_STARTER = 'starter';
|
||||||
|
const PLAN_PRO = 'pro';
|
||||||
|
|
||||||
|
export {
|
||||||
|
PLAN_STARTER,
|
||||||
|
PLAN_PRO
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user