Merge branch 'main' of github.com:reginaldbondoc/infisical

This commit is contained in:
Reginald Bondoc
2022-12-20 01:04:33 +01:00
97 changed files with 4294 additions and 1302 deletions
+230
View File
@@ -0,0 +1,230 @@
import * as Sentry from '@sentry/node';
import {
Bot,
BotKey,
Secret,
ISecret,
IUser
} from '../models';
import {
generateKeyPair,
encryptSymmetric,
decryptSymmetric,
decryptAsymmetric
} from '../utils/crypto';
import { decryptSecrets } from '../helpers/secret';
import { ENCRYPTION_KEY } from '../config';
import { SECRET_SHARED } from '../variables';
/**
* 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;
}
/**
* Return decrypted secrets for workspace with id [workspaceId]
* and [environment] using bot
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.environment - environment
*/
const getSecretsHelper = async ({
workspaceId,
environment
}: {
workspaceId: string;
environment: string;
}) => {
const content = {} as any;
try {
const key = await getKey({ workspaceId });
const secrets = await Secret.find({
workspaceId,
environment,
type: SECRET_SHARED
});
secrets.forEach((secret: ISecret) => {
const secretKey = decryptSymmetric({
ciphertext: secret.secretKeyCiphertext,
iv: secret.secretKeyIV,
tag: secret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: secret.secretValueCiphertext,
iv: secret.secretValueIV,
tag: secret.secretValueTag,
key
});
content[secretKey] = secretValue;
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get secrets');
}
return content;
}
/**
* Return bot's copy of the workspace key for workspace
* with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @returns {String} key - decrypted workspace key
*/
const getKey = async ({ workspaceId }: { workspaceId: string }) => {
let key;
try {
const botKey = await BotKey.findOne({
workspace: workspaceId
}).populate<{ sender: IUser }>('sender', 'publicKey');
if (!botKey) throw new Error('Failed to find bot key');
const bot = await Bot.findOne({
workspace: workspaceId
}).select('+encryptedPrivateKey +iv +tag');
if (!bot) throw new Error('Failed to find bot');
if (!bot.isActive) throw new Error('Bot is not active');
const privateKeyBot = decryptSymmetric({
ciphertext: bot.encryptedPrivateKey,
iv: bot.iv,
tag: bot.tag,
key: ENCRYPTION_KEY
});
key = decryptAsymmetric({
ciphertext: botKey.encryptedKey,
nonce: botKey.nonce,
publicKey: botKey.sender.publicKey as string,
privateKey: privateKeyBot
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get workspace key');
}
return key;
}
/**
* Return symmetrically encrypted [plaintext] using the
* key for workspace with id [workspaceId]
* @param {Object} obj1
* @param {String} obj1.workspaceId - id of workspace
* @param {String} obj1.plaintext - plaintext to encrypt
*/
const encryptSymmetricHelper = async ({
workspaceId,
plaintext
}: {
workspaceId: string;
plaintext: string;
}) => {
try {
const key = await getKey({ workspaceId });
const { ciphertext, iv, tag } = encryptSymmetric({
plaintext,
key
});
return ({
ciphertext,
iv,
tag
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric encryption with bot');
}
}
/**
* Return symmetrically decrypted [ciphertext] using the
* key for workspace with id [workspaceId]
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.ciphertext - ciphertext to decrypt
* @param {String} obj.iv - iv
* @param {String} obj.tag - tag
*/
const decryptSymmetricHelper = async ({
workspaceId,
ciphertext,
iv,
tag
}: {
workspaceId: string;
ciphertext: string;
iv: string;
tag: string;
}) => {
let plaintext;
try {
const key = await getKey({ workspaceId });
const plaintext = decryptSymmetric({
ciphertext,
iv,
tag,
key
});
return plaintext;
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to perform symmetric decryption with bot');
}
return plaintext;
}
export {
createBot,
getSecretsHelper,
encryptSymmetricHelper,
decryptSymmetricHelper
}
+51
View File
@@ -0,0 +1,51 @@
import { Bot, IBot } from '../models';
import * as Sentry from '@sentry/node';
import { EVENT_PUSH_SECRETS } from '../variables';
import { IntegrationService } from '../services';
interface Event {
name: string;
workspaceId: string;
payload: any;
}
/**
* Handle event [event]
* @param {Object} obj
* @param {Event} obj.event - an event
* @param {String} obj.event.name - name of event
* @param {String} obj.event.workspaceId - id of workspace that event is part of
* @param {Object} obj.event.payload - payload of event (depends on event)
*/
const handleEventHelper = async ({
event
}: {
event: Event;
}) => {
const { workspaceId } = event;
// TODO: moduralize bot check into separate function
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
if (!bot) return;
try {
switch (event.name) {
case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({
workspaceId
});
break;
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
}
export {
handleEventHelper
}
+350
View File
@@ -0,0 +1,350 @@
import * as Sentry from '@sentry/node';
import {
Bot,
Integration,
IIntegration,
IntegrationAuth,
IIntegrationAuth
} from '../models';
import { exchangeCode, exchangeRefresh, syncSecrets } from '../integrations';
import { BotService, IntegrationService } from '../services';
import {
ENV_DEV,
EVENT_PUSH_SECRETS,
INTEGRATION_VERCEL,
INTEGRATION_NETLIFY
} from '../variables';
interface Update {
workspace: string;
integration: string;
teamId?: string;
accountId?: string;
}
/**
* Perform OAuth2 code-token exchange for workspace with id [workspaceId] and integration
* named [integration]
* - Store integration access and refresh tokens returned from the OAuth2 code-token exchange
* - Add placeholder inactive integration
* - Create bot sequence for integration
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration
* @param {String} obj.code - code
*/
const handleOAuthExchangeHelper = async ({
workspaceId,
integration,
code
}: {
workspaceId: string;
integration: string;
code: string;
}) => {
let action;
let integrationAuth;
try {
const bot = await Bot.findOne({
workspace: workspaceId,
isActive: true
});
if (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange');
// exchange code for access and refresh tokens
const res = await exchangeCode({
integration,
code
});
const update: Update = {
workspace: workspaceId,
integration
}
switch (integration) {
case INTEGRATION_VERCEL:
update.teamId = res.teamId;
break;
case INTEGRATION_NETLIFY:
update.accountId = res.accountId;
break;
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: workspaceId,
integration
}, update, {
new: true,
upsert: true
});
if (res.refreshToken) {
// case: refresh token returned from exchange
// set integration auth refresh token
await setIntegrationAuthRefreshHelper({
integrationAuthId: integrationAuth._id.toString(),
refreshToken: res.refreshToken
});
}
if (res.accessToken) {
// case: access token returned from exchange
// set integration auth access token
await setIntegrationAuthAccessHelper({
integrationAuthId: integrationAuth._id.toString(),
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
// initialize new integration after exchange
await new Integration({
workspace: workspaceId,
environment: ENV_DEV,
isActive: false,
app: null,
integration,
integrationAuth: integrationAuth._id
}).save();
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
}
}
/**
* Sync/push environment variables in workspace with id [workspaceId] to
* all active integrations for that workspace
* @param {Object} obj
* @param {Object} obj.workspaceId - id of workspace
*/
const syncIntegrationsHelper = async ({
workspaceId
}: {
workspaceId: string;
}) => {
let integrations;
try {
integrations = await Integration.find({
workspace: workspaceId,
isActive: true,
app: { $ne: null }
});
// for each workspace integration, sync/push secrets
// to that integration
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({
workspaceId: integration.workspace.toString(),
environment: integration.environment
});
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
if (!integrationAuth) throw new Error('Failed to find integration auth');
// get integration auth access token
const accessToken = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth.toString()
});
// sync secrets to integration
await syncSecrets({
integration,
integrationAuth,
secrets,
accessToken
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integrations');
}
}
/**
* Return decrypted refresh token using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} refreshToken - decrypted refresh token
*/
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let refreshToken;
try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
if (!integrationAuth) throw new Error('Failed to find integration auth');
refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
ciphertext: integrationAuth.refreshCiphertext as string,
iv: integrationAuth.refreshIV as string,
tag: integrationAuth.refreshTag as string
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration refresh token');
}
return refreshToken;
}
/**
* Return decrypted access token using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @returns {String} accessToken - decrypted access token
*/
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: string }) => {
let accessToken;
try {
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext');
if (!integrationAuth) throw new Error('Failed to find integration auth');
accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string
});
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date
// and refresh token to exchange with the OAuth2 server
if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({
integration: integrationAuth.integration,
refreshToken
});
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get integration access token');
}
return accessToken;
}
/**
* Encrypt refresh token [refreshToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId] and store it
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.refreshToken - refresh token
*/
const setIntegrationAuthRefreshHelper = async ({
integrationAuthId,
refreshToken
}: {
integrationAuthId: string;
refreshToken: string;
}) => {
let integrationAuth;
try {
integrationAuth = await IntegrationAuth
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
plaintext: refreshToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv,
refreshTag: obj.tag
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to set integration auth refresh token');
}
return integrationAuth;
}
/**
* Encrypt access token [accessToken] using the bot's copy
* of the workspace key for workspace belonging to integration auth
* with id [integrationAuthId] and store it along with [accessExpiresAt]
* @param {Object} obj
* @param {String} obj.integrationAuthId - id of integration auth
* @param {String} obj.accessToken - access token
* @param {Date} obj.accessExpiresAt - expiration date of access token
*/
const setIntegrationAuthAccessHelper = async ({
integrationAuthId,
accessToken,
accessExpiresAt
}: {
integrationAuthId: string;
accessToken: string;
accessExpiresAt: Date;
}) => {
let integrationAuth;
try {
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace.toString(),
plaintext: accessToken
});
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessCiphertext: obj.ciphertext,
accessIV: obj.iv,
accessTag: obj.tag,
accessExpiresAt
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to save integration auth access token');
}
return integrationAuth;
}
export {
handleOAuthExchangeHelper,
syncIntegrationsHelper,
getIntegrationAuthRefreshHelper,
getIntegrationAuthAccessHelper,
setIntegrationAuthRefreshHelper,
setIntegrationAuthAccessHelper
}
-174
View File
@@ -1,174 +0,0 @@
import * as Sentry from '@sentry/node';
import axios from 'axios';
import { IntegrationAuth } from '../models';
import { encryptSymmetric, decryptSymmetric } from '../utils/crypto';
import { IIntegrationAuth } from '../models';
import {
ENCRYPTION_KEY,
OAUTH_CLIENT_SECRET_HEROKU,
OAUTH_TOKEN_URL_HEROKU
} from '../config';
/**
* Process token exchange and refresh responses from respective OAuth2 authorization servers by
* encrypting access and refresh tokens, computing new access token expiration times [accessExpiresAt],
* and upserting them into the DB for workspace with id [workspaceId] and integration [integration].
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace
* @param {String} obj.integration - name of integration (e.g. heroku)
* @param {Object} obj.res - response from OAuth2 authorization server
*/
const processOAuthTokenRes = async ({
workspaceId,
integration,
res
}: {
workspaceId: string;
integration: string;
res: any;
}): Promise<IIntegrationAuth> => {
let integrationAuth;
try {
// encrypt refresh + access tokens
const {
ciphertext: refreshCiphertext,
iv: refreshIV,
tag: refreshTag
} = encryptSymmetric({
plaintext: res.data.refresh_token,
key: ENCRYPTION_KEY
});
const {
ciphertext: accessCiphertext,
iv: accessIV,
tag: accessTag
} = encryptSymmetric({
plaintext: res.data.access_token,
key: ENCRYPTION_KEY
});
// compute access token expiration date
const accessExpiresAt = new Date();
accessExpiresAt.setSeconds(
accessExpiresAt.getSeconds() + res.data.expires_in
);
// create or replace integration authorization with encrypted tokens
// and access token expiration date
integrationAuth = await IntegrationAuth.findOneAndUpdate(
{ workspace: workspaceId, integration },
{
workspace: workspaceId,
integration,
refreshCiphertext,
refreshIV,
refreshTag,
accessCiphertext,
accessIV,
accessTag,
accessExpiresAt
},
{ upsert: true, new: true }
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
'Failed to process OAuth2 authorization server token response'
);
}
return integrationAuth;
};
/**
* Return access token for integration either by decrypting a non-expired access token [accessCiphertext] on
* the integration authorization document or by requesting a new one by decrypting and exchanging the
* refresh token [refreshCiphertext] with the respective OAuth2 authorization server.
* @param {Object} obj
* @param {IIntegrationAuth} obj.integrationAuth - an integration authorization document
* @returns {String} access token - new access token
*/
const getOAuthAccessToken = async ({
integrationAuth
}: {
integrationAuth: IIntegrationAuth;
}) => {
let accessToken;
try {
const {
refreshCiphertext,
refreshIV,
refreshTag,
accessCiphertext,
accessIV,
accessTag,
accessExpiresAt
} = integrationAuth;
if (
refreshCiphertext &&
refreshIV &&
refreshTag &&
accessCiphertext &&
accessIV &&
accessTag &&
accessExpiresAt
) {
if (accessExpiresAt < new Date()) {
// case: access token expired
// TODO: fetch another access token
let clientSecret;
switch (integrationAuth.integration) {
case 'heroku':
clientSecret = OAUTH_CLIENT_SECRET_HEROKU;
}
// record new access token and refresh token
// encrypt refresh + access tokens
const refreshToken = decryptSymmetric({
ciphertext: refreshCiphertext,
iv: refreshIV,
tag: refreshTag,
key: ENCRYPTION_KEY
});
// TODO: make route compatible with other integration types
const res = await axios.post(
OAUTH_TOKEN_URL_HEROKU, // maybe shouldn't be a config variable?
new URLSearchParams({
grant_type: 'refresh_token',
refresh_token: refreshToken,
client_secret: clientSecret
} as any)
);
accessToken = res.data.access_token;
await processOAuthTokenRes({
workspaceId: integrationAuth.workspace.toString(),
integration: integrationAuth.integration,
res
});
} else {
// case: access token still works
accessToken = decryptSymmetric({
ciphertext: accessCiphertext,
iv: accessIV,
tag: accessTag,
key: ENCRYPTION_KEY
});
}
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to get OAuth2 access token');
}
return accessToken;
};
export { processOAuthTokenRes, getOAuthAccessToken };
+51 -1
View File
@@ -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
};
+12
View File
@@ -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
});