Begin developing bot, event, and integration pipeline

This commit is contained in:
Tuan Dang
2022-12-06 00:23:16 -05:00
parent c5a422fe64
commit 46fe724012
48 changed files with 1402 additions and 176 deletions
+7
View File
@@ -0,0 +1,7 @@
import {
actionPushToHeroku
} from './integration';
export {
actionPushToHeroku
}
+50
View File
@@ -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
}
+96
View File
@@ -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
});
};
+2
View File
@@ -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
-10
View File
@@ -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,
+13 -3
View File
@@ -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',
+5
View File
@@ -0,0 +1,5 @@
import { eventPushSecrets } from "./secret"
export {
eventPushSecrets
}
+44
View File
@@ -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
}
+49
View File
@@ -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
}
+77 -1
View File
@@ -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 };
+51 -1
View File
@@ -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
};
+1 -1
View File
@@ -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
+12
View File
@@ -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
}); });
+2
View File
@@ -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);
+96
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
import { exchangeCode } from './exchange';
import { exchangeRefresh } from './refresh';
export {
exchangeCode,
exchangeRefresh
}
+78
View File
@@ -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
}
+2
View File
@@ -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,
+45
View File
@@ -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) {
+7 -19
View File
@@ -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;
+57
View File
@@ -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;
+45
View File
@@ -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;
+38
View File
@@ -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;
+9
View File
@@ -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,
+39
View File
@@ -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;
+2
View File
@@ -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,
+1 -1
View File
@@ -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({
+4 -4
View File
@@ -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({
+54
View File
@@ -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;
+108
View File
@@ -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;
+75
View File
@@ -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;
+9 -1
View File
@@ -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
} }
+1
View File
@@ -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;
+16
View File
@@ -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,
-60
View File
@@ -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
};
+5
View File
@@ -0,0 +1,5 @@
const ACTION_PUSH_TO_HEROKU = 'pushToHeroku';
export {
ACTION_PUSH_TO_HEROKU
}
+14
View File
@@ -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
}
+7
View File
@@ -0,0 +1,7 @@
const EVENT_PUSH_SECRETS = 'pushSecrets';
const EVENT_PULL_SECRETS = 'pullSecrets';
export {
EVENT_PUSH_SECRETS,
EVENT_PULL_SECRETS
}
+65
View File
@@ -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
};
+18
View File
@@ -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
}
+24
View File
@@ -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
}
+8
View File
@@ -0,0 +1,8 @@
// secrets
const SECRET_SHARED = 'shared';
const SECRET_PERSONAL = 'personal';
export {
SECRET_SHARED,
SECRET_PERSONAL
}
+7
View File
@@ -0,0 +1,7 @@
const PLAN_STARTER = 'starter';
const PLAN_PRO = 'pro';
export {
PLAN_STARTER,
PLAN_PRO
}