diff --git a/backend/src/actions/index.ts b/backend/src/actions/index.ts new file mode 100644 index 000000000..25f85cc38 --- /dev/null +++ b/backend/src/actions/index.ts @@ -0,0 +1,7 @@ +import { + actionPushToHeroku +} from './integration'; + +export { + actionPushToHeroku +} \ No newline at end of file diff --git a/backend/src/actions/integration.ts b/backend/src/actions/integration.ts new file mode 100644 index 000000000..d9bff4fd1 --- /dev/null +++ b/backend/src/actions/integration.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/controllers/botController.ts b/backend/src/controllers/botController.ts new file mode 100644 index 000000000..d4bc90cf3 --- /dev/null +++ b/backend/src/controllers/botController.ts @@ -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 + }); +}; diff --git a/backend/src/controllers/index.ts b/backend/src/controllers/index.ts index 2d3debfb5..1da61835f 100644 --- a/backend/src/controllers/index.ts +++ b/backend/src/controllers/index.ts @@ -1,4 +1,5 @@ import * as authController from './authController'; +import * as botController from './botController'; import * as integrationAuthController from './integrationAuthController'; import * as integrationController from './integrationController'; import * as keyController from './keyController'; @@ -16,6 +17,7 @@ import * as workspaceController from './workspaceController'; export { authController, + botController, integrationAuthController, integrationController, keyController, diff --git a/backend/src/controllers/integrationAuthController.ts b/backend/src/controllers/integrationAuthController.ts index 009bcd391..fcdb40495 100644 --- a/backend/src/controllers/integrationAuthController.ts +++ b/backend/src/controllers/integrationAuthController.ts @@ -7,6 +7,8 @@ import { processOAuthTokenRes } from '../helpers/integrationAuth'; import { INTEGRATION_SET, ENV_DEV } from '../variables'; 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] * 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 * @returns */ -export const integrationAuthOauthExchange = async ( +export const oAuthExchange = async ( req: Request, res: Response ) => { try { - let clientSecret; + // let clientSecret; const { workspaceId, code, integration } = req.body; if (!INTEGRATION_SET.has(integration)) throw new Error('Failed to validate integration'); - - // use correct client secret - switch (integration) { - case 'heroku': - clientSecret = OAUTH_CLIENT_SECRET_HEROKU; - } - - // TODO: unfinished - make compatible with other integration types - const res = await axios.post( - OAUTH_TOKEN_URL_HEROKU!, - new URLSearchParams({ - grant_type: 'authorization_code', - code: code, - client_secret: clientSecret - } as any) - ); - - const integrationAuth = await processOAuthTokenRes({ + + await IntegrationService.handleOAuthExchange({ workspaceId, integration, - res + code }); - // 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 } - ); + // // use correct client secret + // switch (integration) { + // case 'heroku': + // clientSecret = OAUTH_CLIENT_SECRET_HEROKU; + // } + + // // TODO: unfinished - make compatible with other integration types + // const res = await axios.post( // this response may be different for each integration + // OAUTH_TOKEN_URL_HEROKU!, + // new URLSearchParams({ + // grant_type: 'authorization_code', + // code: code, + // client_secret: clientSecret + // } as any) + // ); + + // const integrationAuth = await processOAuthTokenRes({ + // workspaceId, + // integration, + // res + // }); + + // // create or replace integration + // const integrationObj = await Integration.findOneAndUpdate( + // { workspace: workspaceId, integration }, + // { + // workspace: workspaceId, + // environment: ENV_DEV, + // isActive: false, + // app: null, + // integration, + // integrationAuth: integrationAuth._id + // }, + // { upsert: true, new: true } + // ); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); return res.status(400).send({ - message: 'Failed to get OAuth2 token' + message: 'Failed to get OAuth2 code-token exchange' }); } diff --git a/backend/src/controllers/integrationController.ts b/backend/src/controllers/integrationController.ts index b75d9b74a..3c6ebba56 100644 --- a/backend/src/controllers/integrationController.ts +++ b/backend/src/controllers/integrationController.ts @@ -49,6 +49,7 @@ export const getIntegrations = async (req: Request, res: Response) => { }); }; +// TODO: deprecate /** * Sync secrets [secrets] to integration with id [integrationId] * @param req diff --git a/backend/src/controllers/keyController.ts b/backend/src/controllers/keyController.ts index 1c9b5e15c..778d44b60 100644 --- a/backend/src/controllers/keyController.ts +++ b/backend/src/controllers/keyController.ts @@ -17,16 +17,6 @@ export const uploadKey = async (req: Request, res: Response) => { const { workspaceId } = req.params; 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 const receiverMembership = await findMembership({ user: key.userId, diff --git a/backend/src/controllers/secretController.ts b/backend/src/controllers/secretController.ts index d1cf5f65d..cd91d3dbe 100644 --- a/backend/src/controllers/secretController.ts +++ b/backend/src/controllers/secretController.ts @@ -7,8 +7,9 @@ import { reformatPullSecrets } from '../helpers/secret'; import { pushKeys } from '../helpers/key'; +import { eventPushSecrets } from '../events'; +import { EventService } from '../services'; import { ENV_SET } from '../variables'; - import { postHogClient } from '../services'; interface PushSecret { @@ -60,7 +61,16 @@ export const pushSecrets = async (req: Request, res: Response) => { workspaceId, keys }); - + + // trigger event + EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId, + environment, + secrets + }) + }); + if (postHogClient) { postHogClient.capture({ event: 'secrets pushed', @@ -192,7 +202,7 @@ export const pullSecretsServiceToken = async (req: Request, res: Response) => { }; if (postHogClient) { - // capture secrets pushed event in production + // capture secrets pulled event in production postHogClient.capture({ distinctId: req.serviceToken.user.email, event: 'secrets pulled', diff --git a/backend/src/events/index.ts b/backend/src/events/index.ts new file mode 100644 index 000000000..461a3ece6 --- /dev/null +++ b/backend/src/events/index.ts @@ -0,0 +1,5 @@ +import { eventPushSecrets } from "./secret" + +export { + eventPushSecrets +} \ No newline at end of file diff --git a/backend/src/events/secret.ts b/backend/src/events/secret.ts new file mode 100644 index 000000000..771602d0e --- /dev/null +++ b/backend/src/events/secret.ts @@ -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 +} diff --git a/backend/src/helpers/bot.ts b/backend/src/helpers/bot.ts new file mode 100644 index 000000000..e66fe4489 --- /dev/null +++ b/backend/src/helpers/bot.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/helpers/integrationAuth.ts b/backend/src/helpers/integrationAuth.ts index 17f101676..dddad5c8a 100644 --- a/backend/src/helpers/integrationAuth.ts +++ b/backend/src/helpers/integrationAuth.ts @@ -9,6 +9,81 @@ import { OAUTH_TOKEN_URL_HEROKU } from '../config'; +/** + * Encrypt access and refresh tokens, compute new access token expiration times [accessExpiresAt], + * and upsert them into the DB for workspace with id [workspaceId] and integration [integration]. + * @param {Object} obj + * @param {String} obj.workspaceId - id of workspace + * @param {String} obj.integration - name of integration + * @param {String} obj.accessToken - access token for integration + * @param {Date} obj.accessExpiresAt - date of expiration for access token + * @param {String} obj.refreshToken - refresh token for integration +*/ +const processOAuthTokenRes2 = async ({ + workspaceId, + integration, + accessToken, + accessExpiresAt, + refreshToken, +}: { + workspaceId: string; + integration: string; + accessToken: string; + accessExpiresAt: Date; + refreshToken: string; +}) => { + + let integrationAuth; + try { + // encrypt refresh + access tokens + const { + ciphertext: refreshCiphertext, + iv: refreshIV, + tag: refreshTag + } = encryptSymmetric({ + plaintext: refreshToken, + key: ENCRYPTION_KEY + }); + + const { + ciphertext: accessCiphertext, + iv: accessIV, + tag: accessTag + } = encryptSymmetric({ + plaintext: accessToken, + key: ENCRYPTION_KEY + }); + + // create or replace integration authorization with encrypted tokens + // and access token expiration date + integrationAuth = await IntegrationAuth.findOneAndUpdate( + { workspace: workspaceId, integration }, + { + workspace: workspaceId, + integration, + refreshCiphertext, + refreshIV, + refreshTag, + accessCiphertext, + accessIV, + accessTag, + accessExpiresAt + }, + { upsert: true, new: true } + ); + + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error( + 'Failed to process OAuth2 authorization server token response' + ); + } + + return integrationAuth; +} + +// TODO: deprecate /** * Process token exchange and refresh responses from respective OAuth2 authorization servers by * encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt], @@ -82,6 +157,7 @@ const processOAuthTokenRes = async ({ return integrationAuth; }; +// TODO: deprecate /** * Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on * the integration authorization document or by requesting a new one by decrypting and exchanging the @@ -171,4 +247,4 @@ const getOAuthAccessToken = async ({ return accessToken; }; -export { processOAuthTokenRes, getOAuthAccessToken }; +export { processOAuthTokenRes, processOAuthTokenRes2, getOAuthAccessToken }; diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index 14cd567bb..1ff542f2a 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -1,6 +1,51 @@ import * as Sentry from '@sentry/node'; 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] * @param {Object} queryObj - query object @@ -97,4 +142,9 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { return deletedMembership; }; -export { addMemberships, findMembership, deleteMembership }; +export { + validateMembership, + addMemberships, + findMembership, + deleteMembership +}; diff --git a/backend/src/helpers/signup.ts b/backend/src/helpers/signup.ts index 410c442b3..3229910a5 100644 --- a/backend/src/helpers/signup.ts +++ b/backend/src/helpers/signup.ts @@ -33,7 +33,7 @@ const sendEmailVerification = async ({ email }: { email: string }) => { // send mail await sendMail({ template: 'emailVerification.handlebars', - subjectLine: 'Infisical workspace invitation', + subjectLine: 'Infisical confirmation code', recipients: [email], substitutions: { code: token diff --git a/backend/src/helpers/workspace.ts b/backend/src/helpers/workspace.ts index 52d7d227b..34d5a53e7 100644 --- a/backend/src/helpers/workspace.ts +++ b/backend/src/helpers/workspace.ts @@ -1,13 +1,16 @@ import * as Sentry from '@sentry/node'; import { Workspace, + Bot, Membership, Key, Secret } from '../models'; +import { createBot } from '../helpers/bot'; /** * 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} organizationId - id of organization to create workspace in * @param {Object} workspace - new workspace @@ -21,10 +24,16 @@ const createWorkspace = async ({ }) => { let workspace; try { + // create workspace workspace = await new Workspace({ name, organization: organizationId }).save(); + + // const bot = await createBot({ + // name: 'Infisical Bot', + // workspaceId: workspace._id.toString() + // }); } catch (err) { Sentry.setUser(null); Sentry.captureException(err); @@ -43,6 +52,9 @@ const createWorkspace = async ({ const deleteWorkspace = async ({ id }: { id: string }) => { try { await Workspace.deleteOne({ _id: id }); + await Bot.deleteOne({ + workspace: id + }); await Membership.deleteMany({ workspace: id }); diff --git a/backend/src/index.ts b/backend/src/index.ts index a5ae44969..61872184c 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -22,6 +22,7 @@ Sentry.init({ import { signup as signupRouter, auth as authRouter, + bot as botRouter, organization as organizationRouter, workspace as workspaceRouter, membershipOrg as membershipOrgRouter, @@ -71,6 +72,7 @@ app.use(express.json()); // routers app.use('/api/v1/signup', signupRouter); app.use('/api/v1/auth', authRouter); +app.use('/api/v1/bot', botRouter); app.use('/api/v1/user', userRouter); app.use('/api/v1/user-action', userActionRouter); app.use('/api/v1/organization', organizationRouter); diff --git a/backend/src/integrations/exchange.ts b/backend/src/integrations/exchange.ts new file mode 100644 index 000000000..96ca8d26d --- /dev/null +++ b/backend/src/integrations/exchange.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/integrations/index.ts b/backend/src/integrations/index.ts new file mode 100644 index 000000000..a97711cb5 --- /dev/null +++ b/backend/src/integrations/index.ts @@ -0,0 +1,7 @@ +import { exchangeCode } from './exchange'; +import { exchangeRefresh } from './refresh'; + +export { + exchangeCode, + exchangeRefresh +} \ No newline at end of file diff --git a/backend/src/integrations/refresh.ts b/backend/src/integrations/refresh.ts new file mode 100644 index 000000000..730c5a726 --- /dev/null +++ b/backend/src/integrations/refresh.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/middleware/index.ts b/backend/src/middleware/index.ts index e445b64cf..7fcba66e1 100644 --- a/backend/src/middleware/index.ts +++ b/backend/src/middleware/index.ts @@ -1,4 +1,5 @@ import requireAuth from './requireAuth'; +import requireBotAuth from './requireBotAuth'; import requireSignupAuth from './requireSignupAuth'; import requireWorkspaceAuth from './requireWorkspaceAuth'; import requireOrganizationAuth from './requireOrganizationAuth'; @@ -9,6 +10,7 @@ import validateRequest from './validateRequest'; export { requireAuth, + requireBotAuth, requireSignupAuth, requireWorkspaceAuth, requireOrganizationAuth, diff --git a/backend/src/middleware/requireBotAuth.ts b/backend/src/middleware/requireBotAuth.ts new file mode 100644 index 000000000..6c5a3820a --- /dev/null +++ b/backend/src/middleware/requireBotAuth.ts @@ -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; \ No newline at end of file diff --git a/backend/src/middleware/requireIntegrationAuth.ts b/backend/src/middleware/requireIntegrationAuth.ts index 70ca320c3..dacc0f863 100644 --- a/backend/src/middleware/requireIntegrationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuth.ts @@ -2,6 +2,7 @@ import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; import { Integration, IntegrationAuth, Membership } from '../models'; import { getOAuthAccessToken } from '../helpers/integrationAuth'; +import { validateMembership } from '../helpers/membership'; /** * Validate if user on request is a member of workspace with proper roles associated @@ -31,24 +32,14 @@ const requireIntegrationAuth = ({ if (!integration) { throw new Error('Failed to find integration'); } - - const membership = await Membership.findOne({ - user: req.user._id, - workspace: integration.workspace + + await validateMembership({ + userId: req.user._id.toString(), + 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({ _id: integration.integrationAuth }).select( diff --git a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts index 1f5c6dfc8..8fc5d329e 100644 --- a/backend/src/middleware/requireIntegrationAuthorizationAuth.ts +++ b/backend/src/middleware/requireIntegrationAuthorizationAuth.ts @@ -1,8 +1,8 @@ import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; -import { IntegrationAuth, Membership } from '../models'; -import { decryptSymmetric } from '../utils/crypto'; +import { IntegrationAuth } from '../models'; import { getOAuthAccessToken } from '../helpers/integrationAuth'; +import { validateMembership } from '../helpers/membership'; /** * Validate if user on request is a member of workspace with proper roles associated @@ -20,8 +20,6 @@ const requireIntegrationAuthorizationAuth = ({ acceptedStatuses: string[]; }) => { return async (req: Request, res: Response, next: NextFunction) => { - // (authorization) integration authorization middleware - try { const { integrationAuthId } = req.params; @@ -34,29 +32,15 @@ const requireIntegrationAuthorizationAuth = ({ if (!integrationAuth) { throw new Error('Failed to find integration authorization'); } - - const membership = await Membership.findOne({ - user: req.user._id, - workspace: integrationAuth.workspace + + await validateMembership({ + userId: req.user._id.toString(), + 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; - - // TODO: make compatible with other integration types since they won't necessarily have access tokens req.accessToken = await getOAuthAccessToken({ integrationAuth }); return next(); } catch (err) { diff --git a/backend/src/middleware/requireWorkspaceAuth.ts b/backend/src/middleware/requireWorkspaceAuth.ts index 03fb7357f..a27611f2d 100644 --- a/backend/src/middleware/requireWorkspaceAuth.ts +++ b/backend/src/middleware/requireWorkspaceAuth.ts @@ -1,6 +1,6 @@ import * as Sentry from '@sentry/node'; import { Request, Response, NextFunction } from 'express'; -import { Membership, IWorkspace } from '../models'; +import { validateMembership } from '../helpers/membership'; type req = 'params' | 'body' | 'query'; @@ -25,24 +25,12 @@ const requireWorkspaceAuth = ({ // workspace authorization middleware try { - // validate workspace membership - - const membership = await Membership.findOne({ - user: req.user._id, - workspace: req[location].workspaceId - }).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'); - } + const membership = await validateMembership({ + userId: req.user._id.toString(), + workspaceId: req[location].workspaceId, + acceptedRoles, + acceptedStatuses + }); req.membership = membership; diff --git a/backend/src/models/bot.ts b/backend/src/models/bot.ts new file mode 100644 index 000000000..f492b31ba --- /dev/null +++ b/backend/src/models/bot.ts @@ -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( + { + 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('Bot', botSchema); + +export default Bot; diff --git a/backend/src/models/botKey.ts b/backend/src/models/botKey.ts new file mode 100644 index 000000000..79555cd53 --- /dev/null +++ b/backend/src/models/botKey.ts @@ -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( + { + 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('BotKey', botKeySchema); + +export default BotKey; diff --git a/backend/src/models/botSequence.ts b/backend/src/models/botSequence.ts new file mode 100644 index 000000000..611214284 --- /dev/null +++ b/backend/src/models/botSequence.ts @@ -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( + { + 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('BotSequence', botSequence); + +export default BotSequence; \ No newline at end of file diff --git a/backend/src/models/index.ts b/backend/src/models/index.ts index 9b07f6766..eef3e5fbb 100644 --- a/backend/src/models/index.ts +++ b/backend/src/models/index.ts @@ -1,4 +1,7 @@ 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 Integration, { IIntegration } from './integration'; import IntegrationAuth, { IIntegrationAuth } from './integrationAuth'; @@ -16,6 +19,12 @@ import Workspace, { IWorkspace } from './workspace'; export { BackupPrivateKey, IBackupPrivateKey, + Bot, + IBot, + BotKey, + IBotKey, + BotSequence, + IBotSequence, IncidentContactOrg, IIncidentContactOrg, Integration, diff --git a/backend/src/routes/bot.ts b/backend/src/routes/bot.ts new file mode 100644 index 000000000..e9f9380e9 --- /dev/null +++ b/backend/src/routes/bot.ts @@ -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; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index cf015abfb..2dfe58baa 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,4 +1,5 @@ import signup from './signup'; +import bot from './bot'; import auth from './auth'; import user from './user'; import userAction from './userAction'; @@ -18,6 +19,7 @@ import integrationAuth from './integrationAuth'; export { signup, auth, + bot, user, userAction, organization, diff --git a/backend/src/routes/integration.ts b/backend/src/routes/integration.ts index d16154172..38b774ecb 100644 --- a/backend/src/routes/integration.ts +++ b/backend/src/routes/integration.ts @@ -11,7 +11,7 @@ import { integrationController } from '../controllers'; router.get('/integrations', requireAuth, integrationController.getIntegrations); -router.post( +router.post( // TODO: deprecate '/:integrationId/sync', requireAuth, requireIntegrationAuth({ diff --git a/backend/src/routes/integrationAuth.ts b/backend/src/routes/integrationAuth.ts index 61e5f56bf..dc60c7643 100644 --- a/backend/src/routes/integrationAuth.ts +++ b/backend/src/routes/integrationAuth.ts @@ -10,7 +10,7 @@ import { import { ADMIN, MEMBER, GRANTED } from '../variables'; import { integrationAuthController } from '../controllers'; -router.post( +router.post( // semi-ok '/oauth-token', requireAuth, requireWorkspaceAuth({ @@ -22,10 +22,10 @@ router.post( body('code').exists().trim().notEmpty(), body('integration').exists().trim().notEmpty(), validateRequest, - integrationAuthController.integrationAuthOauthExchange + integrationAuthController.oAuthExchange ); -router.get( +router.get( // not-ok '/:integrationAuthId/apps', requireAuth, requireIntegrationAuthorizationAuth({ @@ -37,7 +37,7 @@ router.get( integrationAuthController.getIntegrationAuthApps ); -router.delete( +router.delete( // not-ok '/:integrationAuthId', requireAuth, requireIntegrationAuthorizationAuth({ diff --git a/backend/src/services/ActionService.ts b/backend/src/services/ActionService.ts new file mode 100644 index 000000000..7d6de9dcb --- /dev/null +++ b/backend/src/services/ActionService.ts @@ -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; \ No newline at end of file diff --git a/backend/src/services/BotService.ts b/backend/src/services/BotService.ts new file mode 100644 index 000000000..a7bbd63bb --- /dev/null +++ b/backend/src/services/BotService.ts @@ -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; \ No newline at end of file diff --git a/backend/src/services/EventService.ts b/backend/src/services/EventService.ts new file mode 100644 index 000000000..bfceaaa82 --- /dev/null +++ b/backend/src/services/EventService.ts @@ -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 { + 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; \ No newline at end of file diff --git a/backend/src/services/IntegrationService.ts b/backend/src/services/IntegrationService.ts new file mode 100644 index 000000000..aa437e5c0 --- /dev/null +++ b/backend/src/services/IntegrationService.ts @@ -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; \ No newline at end of file diff --git a/backend/src/services/index.ts b/backend/src/services/index.ts index 54cdf94f4..c1cf42042 100644 --- a/backend/src/services/index.ts +++ b/backend/src/services/index.ts @@ -1,5 +1,13 @@ import postHogClient from './PostHogClient'; +import BotService from './BotService'; +import EventService from './EventService'; +import ActionService from './ActionService'; +import IntegrationService from './IntegrationService'; export { - postHogClient + postHogClient, + BotService, + EventService, + ActionService, + IntegrationService } \ No newline at end of file diff --git a/backend/src/types/express/index.d.ts b/backend/src/types/express/index.d.ts index 8fdb2b3fe..9a9e81449 100644 --- a/backend/src/types/express/index.d.ts +++ b/backend/src/types/express/index.d.ts @@ -11,6 +11,7 @@ declare global { membershipOrg: any; integration: any; integrationAuth: any; + bot: any; serviceToken: any; accessToken: any; query?: any; diff --git a/backend/src/utils/crypto.ts b/backend/src/utils/crypto.ts index 742e65e80..8172e7fc1 100644 --- a/backend/src/utils/crypto.ts +++ b/backend/src/utils/crypto.ts @@ -2,6 +2,21 @@ import nacl from 'tweetnacl'; import util from 'tweetnacl-util'; 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 * [publicKey] likely belongs to the recipient. @@ -139,6 +154,7 @@ const decryptSymmetric = ({ }; export { + generateKeyPair, encryptAsymmetric, decryptAsymmetric, encryptSymmetric, diff --git a/backend/src/variables.ts b/backend/src/variables.ts deleted file mode 100644 index cdd771b71..000000000 --- a/backend/src/variables.ts +++ /dev/null @@ -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 -}; diff --git a/backend/src/variables/action.ts b/backend/src/variables/action.ts new file mode 100644 index 000000000..1f913bbe9 --- /dev/null +++ b/backend/src/variables/action.ts @@ -0,0 +1,5 @@ +const ACTION_PUSH_TO_HEROKU = 'pushToHeroku'; + +export { + ACTION_PUSH_TO_HEROKU +} \ No newline at end of file diff --git a/backend/src/variables/environment.ts b/backend/src/variables/environment.ts new file mode 100644 index 000000000..44d7cdbb2 --- /dev/null +++ b/backend/src/variables/environment.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/variables/event.ts b/backend/src/variables/event.ts new file mode 100644 index 000000000..4477e8e02 --- /dev/null +++ b/backend/src/variables/event.ts @@ -0,0 +1,7 @@ +const EVENT_PUSH_SECRETS = 'pushSecrets'; +const EVENT_PULL_SECRETS = 'pullSecrets'; + +export { + EVENT_PUSH_SECRETS, + EVENT_PULL_SECRETS +} \ No newline at end of file diff --git a/backend/src/variables/index.ts b/backend/src/variables/index.ts new file mode 100644 index 000000000..135a45589 --- /dev/null +++ b/backend/src/variables/index.ts @@ -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 +}; \ No newline at end of file diff --git a/backend/src/variables/integration.ts b/backend/src/variables/integration.ts new file mode 100644 index 000000000..a63f2299f --- /dev/null +++ b/backend/src/variables/integration.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/variables/organization.ts b/backend/src/variables/organization.ts new file mode 100644 index 000000000..f91e1f5d3 --- /dev/null +++ b/backend/src/variables/organization.ts @@ -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 +} \ No newline at end of file diff --git a/backend/src/variables/secret.ts b/backend/src/variables/secret.ts new file mode 100644 index 000000000..31cbcf951 --- /dev/null +++ b/backend/src/variables/secret.ts @@ -0,0 +1,8 @@ +// secrets +const SECRET_SHARED = 'shared'; +const SECRET_PERSONAL = 'personal'; + +export { + SECRET_SHARED, + SECRET_PERSONAL +} \ No newline at end of file diff --git a/backend/src/variables/stripe.ts b/backend/src/variables/stripe.ts new file mode 100644 index 000000000..ecdbd98ae --- /dev/null +++ b/backend/src/variables/stripe.ts @@ -0,0 +1,7 @@ +const PLAN_STARTER = 'starter'; +const PLAN_PRO = 'pro'; + +export { + PLAN_STARTER, + PLAN_PRO +} \ No newline at end of file