mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-15 14:27:23 +00:00
Merge branch 'main' of github.com:reginaldbondoc/infisical
This commit is contained in:
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { Bot, IBot } from '../models';
|
||||
import * as Sentry from '@sentry/node';
|
||||
import { EVENT_PUSH_SECRETS } from '../variables';
|
||||
import { IntegrationService } from '../services';
|
||||
|
||||
interface Event {
|
||||
name: string;
|
||||
workspaceId: string;
|
||||
payload: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle event [event]
|
||||
* @param {Object} obj
|
||||
* @param {Event} obj.event - an event
|
||||
* @param {String} obj.event.name - name of event
|
||||
* @param {String} obj.event.workspaceId - id of workspace that event is part of
|
||||
* @param {Object} obj.event.payload - payload of event (depends on event)
|
||||
*/
|
||||
const handleEventHelper = async ({
|
||||
event
|
||||
}: {
|
||||
event: Event;
|
||||
}) => {
|
||||
const { workspaceId } = event;
|
||||
|
||||
// TODO: moduralize bot check into separate function
|
||||
const bot = await Bot.findOne({
|
||||
workspace: workspaceId,
|
||||
isActive: true
|
||||
});
|
||||
|
||||
if (!bot) return;
|
||||
|
||||
try {
|
||||
switch (event.name) {
|
||||
case EVENT_PUSH_SECRETS:
|
||||
IntegrationService.syncIntegrations({
|
||||
workspaceId
|
||||
});
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
Sentry.setUser(null);
|
||||
Sentry.captureException(err);
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
handleEventHelper
|
||||
}
|
||||
@@ -0,0 +1,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
|
||||
}
|
||||
@@ -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 };
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user