feat: remove try-catch blocks for handling errors in middleware

This commit is contained in:
Spelchure
2023-06-05 21:15:35 +03:00
parent 08868681d8
commit bfee0a6d30
36 changed files with 3681 additions and 4753 deletions
+132 -166
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
@@ -34,47 +33,39 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
try { const {
const { email,
email, clientPublicKey
clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false }) }, { upsert: true, returnNewDocument: false })
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@@ -85,84 +76,76 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
try { const { email, clientProof } = req.body;
const { email, clientProof } = req.body; const user = await User.findOne({
const user = await User.findOne({ email
email }).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
}).select('+salt +verifier +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email }) const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: email })
if (!loginSRPDetailFromDB) { if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
} }
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// issue tokens // issue tokens
await checkUserDevice({ await checkUserDevice({
user, user,
ip: req.ip, ip: req.ip,
userAgent: req.headers['user-agent'] ?? '' userAgent: req.headers['user-agent'] ?? ''
}); });
const tokens = await issueAuthTokens({ userId: user._id.toString() }); const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: await getHttpsEnabled() secure: await getHttpsEnabled()
}); });
const loginAction = await EELogService.createAction({ const loginAction = await EELogService.createAction({
name: ACTION_LOGIN, name: ACTION_LOGIN,
userId: user._id userId: user._id
}); });
loginAction && await EELogService.createLog({ loginAction && await EELogService.createLog({
userId: user._id, userId: user._id,
actions: [loginAction], actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.ip
}); });
// return (access) token in response // return (access) token in response
return res.status(200).send({ return res.status(200).send({
token: tokens.token, token: tokens.token,
publicKey: user.publicKey, publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey, encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv, iv: user.iv,
tag: user.tag tag: user.tag
});
}
return res.status(400).send({
message: 'Failed to authenticate. Try again?'
}); });
} }
);
} catch (err) { return res.status(400).send({
Sentry.setUser(null); message: 'Failed to authenticate. Try again?'
Sentry.captureException(err); });
return res.status(400).send({ }
message: 'Failed to authenticate. Try again?' );
});
}
}; };
/** /**
@@ -172,38 +155,29 @@ export const login2 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const logout = async (req: Request, res: Response) => { export const logout = async (req: Request, res: Response) => {
try { await clearTokens({
await clearTokens({ userId: req.user._id.toString()
userId: req.user._id.toString() });
});
// clear httpOnly cookie // clear httpOnly cookie
res.cookie('jid', '', { res.cookie('jid', '', {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: (await getHttpsEnabled()) as boolean secure: (await getHttpsEnabled()) as boolean
}); });
const logoutAction = await EELogService.createAction({ const logoutAction = await EELogService.createAction({
name: ACTION_LOGOUT, name: ACTION_LOGOUT,
userId: req.user._id userId: req.user._id
}); });
logoutAction && await EELogService.createLog({ logoutAction && await EELogService.createLog({
userId: req.user._id, userId: req.user._id,
actions: [logoutAction], actions: [logoutAction],
channel: getChannelFromUserAgent(req.headers['user-agent']), channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip ipAddress: req.ip
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to logout'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully logged out.' message: 'Successfully logged out.'
@@ -229,43 +203,35 @@ export const checkAuth = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getNewToken = async (req: Request, res: Response) => { export const getNewToken = async (req: Request, res: Response) => {
try { const refreshToken = req.cookies.jid;
const refreshToken = req.cookies.jid;
if (!refreshToken) { if (!refreshToken) {
throw new Error('Failed to find token in request cookies'); throw new Error('Failed to find token in request cookies');
}
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const token = createToken({
payload: {
userId: decodedToken.userId
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
});
return res.status(200).send({
token
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Invalid request'
});
} }
const decodedToken = <jwt.UserIDJwtPayload>(
jwt.verify(refreshToken, await getJwtRefreshSecret())
);
const user = await User.findOne({
_id: decodedToken.userId
}).select('+publicKey');
if (!user) throw new Error('Failed to authenticate unfound user');
if (!user?.publicKey)
throw new Error('Failed to authenticate not fully set up account');
const token = createToken({
payload: {
userId: decodedToken.userId
},
expiresIn: await getJwtAuthLifetime(),
secret: await getJwtAuthSecret()
});
return res.status(200).send({
token
});
}; };
export const handleAuthProviderCallback = (req: Request, res: Response) => { export const handleAuthProviderCallback = (req: Request, res: Response) => {
+43 -63
View File
@@ -1,6 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { Bot, BotKey } from '../../models'; import { Bot, BotKey } from '../../models';
import { createBot } from '../../helpers/bot'; import { createBot } from '../../helpers/bot';
@@ -17,33 +16,24 @@ interface BotKey {
* @returns * @returns
*/ */
export const getBotByWorkspaceId = async (req: Request, res: Response) => { export const getBotByWorkspaceId = async (req: Request, res: Response) => {
let bot; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
bot = await Bot.findOne({ let bot = await Bot.findOne({
workspace: workspaceId workspace: workspaceId
}); });
if (!bot) { if (!bot) {
// case: bot doesn't exist for workspace with id [workspaceId] // case: bot doesn't exist for workspace with id [workspaceId]
// -> create a new bot and return it // -> create a new bot and return it
bot = await createBot({ bot = await createBot({
name: 'Infisical Bot', name: 'Infisical Bot',
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(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({ return res.status(200).send({
bot bot
}); });
}; };
/** /**
@@ -53,54 +43,44 @@ export const getBotByWorkspaceId = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const setBotActiveState = async (req: Request, res: Response) => { export const setBotActiveState = async (req: Request, res: Response) => {
let bot; const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
try {
const { isActive, botKey }: { isActive: boolean, botKey: BotKey } = req.body;
if (isActive) { if (isActive) {
// bot state set to active -> share workspace key with bot // bot state set to active -> share workspace key with bot
if (!botKey?.encryptedKey || !botKey?.nonce) { if (!botKey?.encryptedKey || !botKey?.nonce) {
return res.status(400).send({ return res.status(400).send({
message: 'Failed to set bot state to active - missing bot key' message: 'Failed to set bot state to active - missing bot key'
});
}
await BotKey.findOneAndUpdate({
workspace: req.bot.workspace
}, {
encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: req.user._id,
bot: req.bot._id,
workspace: req.bot.workspace
}, {
upsert: true,
new: true
});
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: req.bot._id
}); });
} }
bot = await Bot.findOneAndUpdate({ await BotKey.findOneAndUpdate({
_id: req.bot._id workspace: req.bot.workspace
}, { }, {
isActive encryptedKey: botKey.encryptedKey,
nonce: botKey.nonce,
sender: req.user._id,
bot: req.bot._id,
workspace: req.bot.workspace
}, { }, {
upsert: true,
new: true new: true
}); });
} else {
// case: bot state set to inactive -> delete bot's workspace key
await BotKey.deleteOne({
bot: req.bot._id
});
}
if (!bot) throw new Error('Failed to update bot active state'); let bot = await Bot.findOneAndUpdate({
_id: req.bot._id
}, {
isActive
}, {
new: true
});
} catch (err) { if (!bot) throw new Error('Failed to update bot active state');
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({ return res.status(200).send({
bot bot
@@ -1,6 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { import {
IntegrationAuth, IntegrationAuth,
Bot Bot
@@ -22,21 +21,12 @@ import { standardRequest } from '../../config/request';
* Return integration authorization with id [integrationAuthId] * Return integration authorization with id [integrationAuthId]
*/ */
export const getIntegrationAuth = async (req: Request, res: Response) => { export const getIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth; const { integrationAuthId } = req.params;
try { const integrationAuth = await IntegrationAuth.findById(integrationAuthId);
const { integrationAuthId } = req.params;
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) return res.status(400).send({ if (!integrationAuth) return res.status(400).send({
message: 'Failed to find integration authorization' message: 'Failed to find integration authorization'
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get integration authorization'
});
}
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth
@@ -61,33 +51,25 @@ export const oAuthExchange = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
try { const { workspaceId, code, integration } = req.body;
const { workspaceId, code, integration } = req.body; if (!INTEGRATION_SET.has(integration))
if (!INTEGRATION_SET.has(integration)) throw new Error('Failed to validate integration');
throw new Error('Failed to validate integration');
const environments = req.membership.workspace?.environments || []; const environments = req.membership.workspace?.environments || [];
if(environments.length === 0){ if(environments.length === 0){
throw new Error("Failed to get environments") throw new Error("Failed to get environments")
} }
const integrationAuth = await IntegrationService.handleOAuthExchange({ const integrationAuth = await IntegrationService.handleOAuthExchange({
workspaceId, workspaceId,
integration, integration,
code, code,
environment: environments[0].slug, environment: environments[0].slug,
}); });
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get OAuth2 code-token exchange'
});
}
}; };
/** /**
@@ -104,55 +86,47 @@ export const saveIntegrationAccessToken = async (
// TODO: check if access token is valid for each integration // TODO: check if access token is valid for each integration
let integrationAuth; let integrationAuth;
try { const {
const { workspaceId,
workspaceId, accessId,
accessId, accessToken,
accessToken, integration
integration }: {
}: { workspaceId: string;
workspaceId: string; accessId: string | null;
accessId: string | null; accessToken: string;
accessToken: string; integration: string;
integration: string; } = req.body;
} = req.body;
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
isActive: true isActive: true
}); });
if (!bot) throw new Error('Bot must be enabled to save integration access token'); if (!bot) throw new Error('Bot must be enabled to save integration access token');
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
integration integration
}, { }, {
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
integration, integration,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}, { }, {
new: true, new: true,
upsert: true upsert: true
}); });
// encrypt and save integration access details // encrypt and save integration access details
integrationAuth = await IntegrationService.setIntegrationAuthAccess({ integrationAuth = await IntegrationService.setIntegrationAuthAccess({
integrationAuthId: integrationAuth._id.toString(), integrationAuthId: integrationAuth._id.toString(),
accessId, accessId,
accessToken, accessToken,
accessExpiresAt: undefined accessExpiresAt: undefined
}); });
if (!integrationAuth) throw new Error('Failed to save integration access token'); if (!integrationAuth) throw new Error('Failed to save integration access token');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to save access token for integration'
});
}
return res.status(200).send({ return res.status(200).send({
integrationAuth integrationAuth
@@ -166,22 +140,13 @@ export const saveIntegrationAccessToken = async (
* @returns * @returns
*/ */
export const getIntegrationAuthApps = async (req: Request, res: Response) => { export const getIntegrationAuthApps = async (req: Request, res: Response) => {
let apps; const teamId = req.query.teamId as string;
try {
const teamId = req.query.teamId as string;
apps = await getApps({ const apps = await getApps({
integrationAuth: req.integrationAuth, integrationAuth: req.integrationAuth,
accessToken: req.accessToken, accessToken: req.accessToken,
...teamId && { teamId } ...teamId && { teamId }
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get integration authorization applications",
});
}
return res.status(200).send({ return res.status(200).send({
apps apps
@@ -402,19 +367,10 @@ export const getIntegrationAuthRailwayServices = async (req: Request, res: Respo
* @returns * @returns
*/ */
export const deleteIntegrationAuth = async (req: Request, res: Response) => { export const deleteIntegrationAuth = async (req: Request, res: Response) => {
let integrationAuth; const integrationAuth = await revokeAccess({
try { integrationAuth: req.integrationAuth,
integrationAuth = await revokeAccess({ accessToken: req.accessToken,
integrationAuth: req.integrationAuth, });
accessToken: req.accessToken,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration authorization",
});
}
return res.status(200).send({ return res.status(200).send({
integrationAuth, integrationAuth,
@@ -1,6 +1,5 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import * as Sentry from '@sentry/node';
import { import {
Integration Integration
} from '../../models'; } from '../../models';
@@ -14,62 +13,50 @@ import { eventPushSecrets } from '../../events';
* @returns * @returns
*/ */
export const createIntegration = async (req: Request, res: Response) => { export const createIntegration = async (req: Request, res: Response) => {
let integration; const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
} = req.body;
try { // TODO: validate [sourceEnvironment] and [targetEnvironment]
const {
integrationAuthId,
app,
appId,
isActive,
sourceEnvironment,
targetEnvironment,
targetEnvironmentId,
targetService,
targetServiceId,
owner,
path,
region
} = req.body;
// TODO: validate [sourceEnvironment] and [targetEnvironment] // initialize new integration after saving integration access token
const integration = await new Integration({
// initialize new integration after saving integration access token workspace: req.integrationAuth.workspace._id,
integration = await new Integration({ environment: sourceEnvironment,
workspace: req.integrationAuth.workspace._id, isActive,
environment: sourceEnvironment, app,
isActive, appId,
app, targetEnvironment,
appId, targetEnvironmentId,
targetEnvironment, targetService,
targetEnvironmentId, targetServiceId,
targetService, owner,
targetServiceId, path,
owner, region,
path, integration: req.integrationAuth.integration,
region, integrationAuth: new Types.ObjectId(integrationAuthId)
integration: req.integrationAuth.integration, }).save();
integrationAuth: new Types.ObjectId(integrationAuthId)
}).save();
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create integration'
});
}
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment: sourceEnvironment
})
});
}
return res.status(200).send({ return res.status(200).send({
integration, integration,
}); });
@@ -82,52 +69,43 @@ export const createIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateIntegration = async (req: Request, res: Response) => { export const updateIntegration = async (req: Request, res: Response) => {
let integration;
// TODO: add integration-specific validation to ensure that each // TODO: add integration-specific validation to ensure that each
// integration has the correct fields populated in [Integration] // integration has the correct fields populated in [Integration]
try { const {
const { environment,
isActive,
app,
appId,
targetEnvironment,
owner, // github-specific integration param
} = req.body;
const integration = await Integration.findOneAndUpdate(
{
_id: req.integration._id,
},
{
environment, environment,
isActive, isActive,
app, app,
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner,
} = req.body; },
{
integration = await Integration.findOneAndUpdate( new: true,
{
_id: req.integration._id,
},
{
environment,
isActive,
app,
appId,
targetEnvironment,
owner,
},
{
new: true,
}
);
if (integration) {
// trigger event - push secrets
EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
}),
});
} }
} catch (err) { );
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err); if (integration) {
return res.status(400).send({ // trigger event - push secrets
message: "Failed to update integration", EventService.handleEvent({
event: eventPushSecrets({
workspaceId: integration.workspace,
environment
}),
}); });
} }
@@ -144,22 +122,13 @@ export const updateIntegration = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteIntegration = async (req: Request, res: Response) => { export const deleteIntegration = async (req: Request, res: Response) => {
let integration; const { integrationId } = req.params;
try {
const { integrationId } = req.params;
integration = await Integration.findOneAndDelete({ const integration = await Integration.findOneAndDelete({
_id: integrationId, _id: integrationId,
}); });
if (!integration) throw new Error("Failed to find integration"); if (!integration) throw new Error("Failed to find integration");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete integration",
});
}
return res.status(200).send({ return res.status(200).send({
integration, integration,
+26 -44
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Key } from '../../models'; import { Key } from '../../models';
import { findMembership } from '../../helpers/membership'; import { findMembership } from '../../helpers/membership';
@@ -11,34 +10,26 @@ import { findMembership } from '../../helpers/membership';
* @returns * @returns
*/ */
export const uploadKey = async (req: Request, res: Response) => { export const uploadKey = async (req: Request, res: Response) => {
try { const { workspaceId } = req.params;
const { workspaceId } = req.params; const { key } = req.body;
const { key } = req.body;
// validate membership of receiver // validate membership of receiver
const receiverMembership = await findMembership({ const receiverMembership = await findMembership({
user: key.userId, user: key.userId,
workspace: workspaceId workspace: workspaceId
}); });
if (!receiverMembership) { if (!receiverMembership) {
throw new Error('Failed receiver membership validation for workspace'); throw new Error('Failed receiver membership validation for workspace');
} }
await new Key({ await new Key({
encryptedKey: key.encryptedKey, encryptedKey: key.encryptedKey,
nonce: key.nonce, nonce: key.nonce,
sender: req.user._id, sender: req.user._id,
receiver: key.userId, receiver: key.userId,
workspace: workspaceId workspace: workspaceId
}).save(); }).save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload key to workspace'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully uploaded key to workspace' message: 'Successfully uploaded key to workspace'
@@ -52,25 +43,16 @@ export const uploadKey = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getLatestKey = async (req: Request, res: Response) => { export const getLatestKey = async (req: Request, res: Response) => {
let latestKey; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
// get latest key // get latest key
latestKey = await Key.find({ const latestKey = await Key.find({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.limit(1) .limit(1)
.populate('sender', '+publicKey'); .populate('sender', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get latest key'
});
}
const resObj: any = {}; const resObj: any = {};
+111 -148
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { Membership, MembershipOrg, User, Key } from '../../models'; import { Membership, MembershipOrg, User, Key } from '../../models';
import { import {
@@ -16,25 +15,16 @@ import { getSiteURL } from '../../config';
* @returns * @returns
*/ */
export const validateMembership = async (req: Request, res: Response) => { export const validateMembership = async (req: Request, res: Response) => {
try { const { workspaceId } = req.params;
const { workspaceId } = req.params; // validate membership
const membership = await findMembership({
user: req.user._id,
workspace: workspaceId
});
// validate membership if (!membership) {
const membership = await findMembership({ throw new Error('Failed to validate membership');
user: req.user._id, }
workspace: workspaceId
});
if (!membership) {
throw new Error('Failed to validate membership');
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed workspace connection check'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Workspace membership confirmed' message: 'Workspace membership confirmed'
@@ -48,48 +38,39 @@ export const validateMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteMembership = async (req: Request, res: Response) => { export const deleteMembership = async (req: Request, res: Response) => {
let deletedMembership; const { membershipId } = req.params;
try {
const { membershipId } = req.params;
// check if membership to delete exists // check if membership to delete exists
const membershipToDelete = await Membership.findOne({ const membershipToDelete = await Membership.findOne({
_id: membershipId _id: membershipId
}).populate('user'); }).populate('user');
if (!membershipToDelete) { if (!membershipToDelete) {
throw new Error( throw new Error(
"Failed to delete workspace membership that doesn't exist" "Failed to delete workspace membership that doesn't exist"
); );
} }
// check if user is a member and admin of the workspace // check if user is a member and admin of the workspace
// whose membership we wish to delete // whose membership we wish to delete
const membership = await Membership.findOne({ const membership = await Membership.findOne({
user: req.user._id, user: req.user._id,
workspace: membershipToDelete.workspace workspace: membershipToDelete.workspace
}); });
if (!membership) { if (!membership) {
throw new Error('Failed to validate workspace membership'); throw new Error('Failed to validate workspace membership');
} }
if (membership.role !== ADMIN) { if (membership.role !== ADMIN) {
// user is not an admin member of the workspace // user is not an admin member of the workspace
throw new Error('Insufficient role for deleting workspace membership'); throw new Error('Insufficient role for deleting workspace membership');
} }
// delete workspace membership // delete workspace membership
deletedMembership = await deleteMember({ const deletedMembership = await deleteMember({
membershipId: membershipToDelete._id.toString() membershipId: membershipToDelete._id.toString()
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete membership'
});
}
return res.status(200).send({ return res.status(200).send({
deletedMembership deletedMembership
@@ -103,49 +84,40 @@ export const deleteMembership = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeMembershipRole = async (req: Request, res: Response) => { export const changeMembershipRole = async (req: Request, res: Response) => {
let membershipToChangeRole; const { membershipId } = req.params;
try { const { role } = req.body;
const { membershipId } = req.params;
const { role } = req.body;
if (![ADMIN, MEMBER].includes(role)) { if (![ADMIN, MEMBER].includes(role)) {
throw new Error('Failed to validate role'); throw new Error('Failed to validate role');
} }
// validate target membership // validate target membership
membershipToChangeRole = await findMembership({ const membershipToChangeRole = await findMembership({
_id: membershipId _id: membershipId
}); });
if (!membershipToChangeRole) { if (!membershipToChangeRole) {
throw new Error('Failed to find membership to change role'); throw new Error('Failed to find membership to change role');
} }
// check if user is a member and admin of target membership's // check if user is a member and admin of target membership's
// workspace // workspace
const membership = await findMembership({ const membership = await findMembership({
user: req.user._id, user: req.user._id,
workspace: membershipToChangeRole.workspace workspace: membershipToChangeRole.workspace
}); });
if (!membership) { if (!membership) {
throw new Error('Failed to validate membership'); throw new Error('Failed to validate membership');
} }
if (membership.role !== ADMIN) { if (membership.role !== ADMIN) {
// user is not an admin member of the workspace // user is not an admin member of the workspace
throw new Error('Insufficient role for changing member roles'); throw new Error('Insufficient role for changing member roles');
} }
membershipToChangeRole.role = role; membershipToChangeRole.role = role;
await membershipToChangeRole.save(); await membershipToChangeRole.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change membership role'
});
}
return res.status(200).send({ return res.status(200).send({
membership: membershipToChangeRole membership: membershipToChangeRole
@@ -159,72 +131,63 @@ export const changeMembershipRole = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const inviteUserToWorkspace = async (req: Request, res: Response) => { export const inviteUserToWorkspace = async (req: Request, res: Response) => {
let invitee, latestKey; const { workspaceId } = req.params;
try { const { email }: { email: string } = req.body;
const { workspaceId } = req.params;
const { email }: { email: string } = req.body;
invitee = await User.findOne({ const invitee = await User.findOne({
email email
}).select('+publicKey'); }).select('+publicKey');
if (!invitee || !invitee?.publicKey) if (!invitee || !invitee?.publicKey)
throw new Error('Failed to validate invitee'); throw new Error('Failed to validate invitee');
// validate invitee's workspace membership - ensure member isn't // validate invitee's workspace membership - ensure member isn't
// already a member of the workspace // already a member of the workspace
const inviteeMembership = await Membership.findOne({ const inviteeMembership = await Membership.findOne({
user: invitee._id, user: invitee._id,
workspace: workspaceId workspace: workspaceId
}); });
if (inviteeMembership) if (inviteeMembership)
throw new Error('Failed to add existing member of workspace'); throw new Error('Failed to add existing member of workspace');
// validate invitee's organization membership - ensure that only // validate invitee's organization membership - ensure that only
// (accepted) organization members can be added to the workspace // (accepted) organization members can be added to the workspace
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: invitee._id, user: invitee._id,
organization: req.membership.workspace.organization, organization: req.membership.workspace.organization,
status: ACCEPTED status: ACCEPTED
}); });
if (!membershipOrg) if (!membershipOrg)
throw new Error("Failed to validate invitee's organization membership"); throw new Error("Failed to validate invitee's organization membership");
// get latest key // get latest key
latestKey = await Key.findOne({ const latestKey = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.populate('sender', '+publicKey'); .populate('sender', '+publicKey');
// create new workspace membership // create new workspace membership
const m = await new Membership({ const m = await new Membership({
user: invitee._id, user: invitee._id,
workspace: workspaceId, workspace: workspaceId,
role: MEMBER role: MEMBER
}).save(); }).save();
await sendMail({ await sendMail({
template: 'workspaceInvitation.handlebars', template: 'workspaceInvitation.handlebars',
subjectLine: 'Infisical workspace invitation', subjectLine: 'Infisical workspace invitation',
recipients: [invitee.email], recipients: [invitee.email],
substitutions: { substitutions: {
inviterFirstName: req.user.firstName, inviterFirstName: req.user.firstName,
inviterEmail: req.user.email, inviterEmail: req.user.email,
workspaceName: req.membership.workspace.name, workspaceName: req.membership.workspace.name,
callback_url: (await getSiteURL()) + '/login' callback_url: (await getSiteURL()) + '/login'
} }
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to invite user to workspace'
});
}
return res.status(200).send({ return res.status(200).send({
invitee, invitee,
@@ -1,6 +1,5 @@
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { MembershipOrg, Organization, User } from '../../models'; import { MembershipOrg, Organization, User } from '../../models';
import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg'; import { deleteMembershipOrg as deleteMemberFromOrg } from '../../helpers/membershipOrg';
import { createToken } from '../../helpers/auth'; import { createToken } from '../../helpers/auth';
@@ -17,52 +16,43 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret, getSmtpConfigured
* @returns * @returns
*/ */
export const deleteMembershipOrg = async (req: Request, res: Response) => { export const deleteMembershipOrg = async (req: Request, res: Response) => {
let membershipOrgToDelete; const { membershipOrgId } = req.params;
try {
const { membershipOrgId } = req.params;
// check if organization membership to delete exists // check if organization membership to delete exists
membershipOrgToDelete = await MembershipOrg.findOne({ const membershipOrgToDelete = await MembershipOrg.findOne({
_id: membershipOrgId _id: membershipOrgId
}).populate('user'); }).populate('user');
if (!membershipOrgToDelete) { if (!membershipOrgToDelete) {
throw new Error( throw new Error(
"Failed to delete organization membership that doesn't exist" "Failed to delete organization membership that doesn't exist"
); );
} }
// check if user is a member and admin of the organization // check if user is a member and admin of the organization
// whose membership we wish to delete // whose membership we wish to delete
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: membershipOrgToDelete.organization organization: membershipOrgToDelete.organization
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); throw new Error('Failed to validate organization membership');
} }
if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) { if (membershipOrg.role !== OWNER && membershipOrg.role !== ADMIN) {
// user is not an admin member of the organization // user is not an admin member of the organization
throw new Error('Insufficient role for deleting organization membership'); throw new Error('Insufficient role for deleting organization membership');
} }
// delete organization membership // delete organization membership
const deletedMembershipOrg = await deleteMemberFromOrg({ const deletedMembershipOrg = await deleteMemberFromOrg({
membershipOrgId: membershipOrgToDelete._id.toString() membershipOrgId: membershipOrgToDelete._id.toString()
}); });
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membershipOrg.organization.toString() organizationId: membershipOrg.organization.toString()
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return membershipOrgToDelete; return membershipOrgToDelete;
}; };
@@ -78,14 +68,6 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
// [membershipOrgId] // [membershipOrgId]
let membershipToChangeRole; let membershipToChangeRole;
// try {
// } catch (err) {
// Sentry.setUser({ email: req.user.email });
// Sentry.captureException(err);
// return res.status(400).send({
// message: 'Failed to change organization membership role'
// });
// }
return res.status(200).send({ return res.status(200).send({
membershipOrg: membershipToChangeRole membershipOrg: membershipToChangeRole
@@ -101,106 +83,98 @@ export const changeMembershipOrgRole = async (req: Request, res: Response) => {
*/ */
export const inviteUserToOrganization = async (req: Request, res: Response) => { export const inviteUserToOrganization = async (req: Request, res: Response) => {
let invitee, inviteeMembershipOrg, completeInviteLink; let invitee, inviteeMembershipOrg, completeInviteLink;
try { const { organizationId, inviteeEmail } = req.body;
const { organizationId, inviteeEmail } = req.body; const host = req.headers.host;
const host = req.headers.host; const siteUrl = `${req.protocol}://${host}`;
const siteUrl = `${req.protocol}://${host}`;
// validate membership // validate membership
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: organizationId organization: organizationId
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error('Failed to validate organization membership'); throw new Error('Failed to validate organization membership');
} }
invitee = await User.findOne({ invitee = await User.findOne({
email: inviteeEmail email: inviteeEmail
}).select('+publicKey'); }).select('+publicKey');
if (invitee) { if (invitee) {
// case: invitee is an existing user // case: invitee is an existing user
inviteeMembershipOrg = await MembershipOrg.findOne({ inviteeMembershipOrg = await MembershipOrg.findOne({
user: invitee._id, user: invitee._id,
organization: organizationId organization: organizationId
}); });
if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) { if (inviteeMembershipOrg && inviteeMembershipOrg.status === ACCEPTED) {
throw new Error( throw new Error(
'Failed to invite an existing member of the organization' 'Failed to invite an existing member of the organization'
); );
} }
if (!inviteeMembershipOrg) { if (!inviteeMembershipOrg) {
await new MembershipOrg({ await new MembershipOrg({
user: invitee, user: invitee,
inviteEmail: inviteeEmail, inviteEmail: inviteeEmail,
organization: organizationId, organization: organizationId,
role: MEMBER, role: MEMBER,
status: INVITED status: INVITED
}).save(); }).save();
} }
} else { } else {
// check if invitee has been invited before // check if invitee has been invited before
inviteeMembershipOrg = await MembershipOrg.findOne({ inviteeMembershipOrg = await MembershipOrg.findOne({
inviteEmail: inviteeEmail, inviteEmail: inviteeEmail,
organization: organizationId organization: organizationId
}); });
if (!inviteeMembershipOrg) { if (!inviteeMembershipOrg) {
// case: invitee has never been invited before // case: invitee has never been invited before
await new MembershipOrg({ await new MembershipOrg({
inviteEmail: inviteeEmail, inviteEmail: inviteeEmail,
organization: organizationId, organization: organizationId,
role: MEMBER, role: MEMBER,
status: INVITED status: INVITED
}).save(); }).save();
} }
} }
const organization = await Organization.findOne({ _id: organizationId }); const organization = await Organization.findOne({ _id: organizationId });
if (organization) { if (organization) {
const token = await TokenService.createToken({ const token = await TokenService.createToken({
type: TOKEN_EMAIL_ORG_INVITATION, type: TOKEN_EMAIL_ORG_INVITATION,
email: inviteeEmail, email: inviteeEmail,
organizationId: organization._id organizationId: organization._id
}); });
await sendMail({ await sendMail({
template: 'organizationInvitation.handlebars', template: 'organizationInvitation.handlebars',
subjectLine: 'Infisical organization invitation', subjectLine: 'Infisical organization invitation',
recipients: [inviteeEmail], recipients: [inviteeEmail],
substitutions: { substitutions: {
inviterFirstName: req.user.firstName, inviterFirstName: req.user.firstName,
inviterEmail: req.user.email, inviterEmail: req.user.email,
organizationName: organization.name, organizationName: organization.name,
email: inviteeEmail, email: inviteeEmail,
organizationId: organization._id.toString(), organizationId: organization._id.toString(),
token, token,
callback_url: (await getSiteURL()) + '/signupinvite' callback_url: (await getSiteURL()) + '/signupinvite'
} }
}); });
if (!(await getSmtpConfigured())) { if (!(await getSmtpConfigured())) {
completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}` completeInviteLink = `${siteUrl + '/signupinvite'}?token=${token}&to=${inviteeEmail}&organization_id=${organization._id}`
} }
} }
await updateSubscriptionOrgQuantity({ organizationId }); await updateSubscriptionOrgQuantity({ organizationId });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send organization invite'
});
}
return res.status(200).send({ return res.status(200).send({
message: `Sent an invite link to ${req.body.inviteeEmail}`, message: `Sent an invite link to ${req.body.inviteeEmail}`,
@@ -216,70 +190,62 @@ export const inviteUserToOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const verifyUserToOrganization = async (req: Request, res: Response) => { export const verifyUserToOrganization = async (req: Request, res: Response) => {
let user, token; let user;
try { const {
const { email,
email, organizationId,
organizationId, code
code } = req.body;
} = req.body;
user = await User.findOne({ email }).select('+publicKey'); user = await User.findOne({ email }).select('+publicKey');
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email, inviteEmail: email,
status: INVITED, status: INVITED,
organization: new Types.ObjectId(organizationId) organization: new Types.ObjectId(organizationId)
}); });
if (!membershipOrg) if (!membershipOrg)
throw new Error('Failed to find any invitations for email'); throw new Error('Failed to find any invitations for email');
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_ORG_INVITATION, type: TOKEN_EMAIL_ORG_INVITATION,
email, email,
organizationId: membershipOrg.organization, organizationId: membershipOrg.organization,
token: code token: code
}); });
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
// membership can be approved and redirected to login/dashboard // membership can be approved and redirected to login/dashboard
membershipOrg.status = ACCEPTED; membershipOrg.status = ACCEPTED;
await membershipOrg.save(); await membershipOrg.save();
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId organizationId
}); });
return res.status(200).send({ return res.status(200).send({
message: 'Successfully verified email', message: 'Successfully verified email',
user, user,
}); });
} }
if (!user) { if (!user) {
// initialize user account // initialize user account
user = await new User({ user = await new User({
email email
}).save(); }).save();
} }
// generate temporary signup token // generate temporary signup token
token = createToken({ const token = createToken({
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret() secret: await getJwtSignupSecret()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email magic link verification for organization invitation'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully verified email', message: 'Successfully verified email',
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { import {
@@ -15,21 +14,12 @@ import _ from 'lodash';
import { getStripeSecretKey, getSiteURL } from '../../config'; import { getStripeSecretKey, getSiteURL } from '../../config';
export const getOrganizations = async (req: Request, res: Response) => { export const getOrganizations = async (req: Request, res: Response) => {
let organizations; const organizations = (
try { await MembershipOrg.find({
organizations = ( user: req.user._id,
await MembershipOrg.find({ status: ACCEPTED
user: req.user._id, }).populate('organization')
status: ACCEPTED ).map((m) => m.organization);
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organizations'
});
}
return res.status(200).send({ return res.status(200).send({
organizations organizations
@@ -44,33 +34,24 @@ export const getOrganizations = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createOrganization = async (req: Request, res: Response) => { export const createOrganization = async (req: Request, res: Response) => {
let organization; const { organizationName } = req.body;
try {
const { organizationName } = req.body;
if (organizationName.length < 1) { if (organizationName.length < 1) {
throw new Error('Organization names must be at least 1-character long'); throw new Error('Organization names must be at least 1-character long');
} }
// create organization and add user as member // create organization and add user as member
organization = await create({ const organization = await create({
email: req.user.email, email: req.user.email,
name: organizationName name: organizationName
}); });
await addMembershipsOrg({ await addMembershipsOrg({
userIds: [req.user._id.toString()], userIds: [req.user._id.toString()],
organizationId: organization._id.toString(), organizationId: organization._id.toString(),
roles: [OWNER], roles: [OWNER],
statuses: [ACCEPTED] statuses: [ACCEPTED]
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create organization'
});
}
return res.status(200).send({ return res.status(200).send({
organization organization
@@ -84,17 +65,7 @@ export const createOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganization = async (req: Request, res: Response) => { export const getOrganization = async (req: Request, res: Response) => {
let organization; const organization = req.organization
try {
organization = req.organization
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to find organization'
});
}
return res.status(200).send({ return res.status(200).send({
organization organization
}); });
@@ -107,20 +78,11 @@ export const getOrganization = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getOrganizationMembers = async (req: Request, res: Response) => { export const getOrganizationMembers = async (req: Request, res: Response) => {
let users; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
users = await MembershipOrg.find({ const users = await MembershipOrg.find({
organization: organizationId organization: organizationId
}).populate('user', '+publicKey'); }).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization members'
});
}
return res.status(200).send({ return res.status(200).send({
users users
@@ -137,35 +99,26 @@ export const getOrganizationWorkspaces = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let workspaces; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
const workspacesSet = new Set( const workspacesSet = new Set(
( (
await Workspace.find( await Workspace.find(
{ {
organization: organizationId organization: organizationId
}, },
'_id' '_id'
) )
).map((w) => w._id.toString()) ).map((w) => w._id.toString())
); );
workspaces = ( const workspaces = (
await Membership.find({ await Membership.find({
user: req.user._id user: req.user._id
}).populate('workspace') }).populate('workspace')
) )
.filter((m) => workspacesSet.has(m.workspace._id.toString())) .filter((m) => workspacesSet.has(m.workspace._id.toString()))
.map((m) => m.workspace); .map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get my workspaces'
});
}
return res.status(200).send({ return res.status(200).send({
workspaces workspaces
@@ -179,29 +132,20 @@ export const getOrganizationWorkspaces = async (
* @returns * @returns
*/ */
export const changeOrganizationName = async (req: Request, res: Response) => { export const changeOrganizationName = async (req: Request, res: Response) => {
let organization; const { organizationId } = req.params;
try { const { name } = req.body;
const { organizationId } = req.params;
const { name } = req.body;
organization = await Organization.findOneAndUpdate( const organization = await Organization.findOneAndUpdate(
{ {
_id: organizationId _id: organizationId
}, },
{ {
name name
}, },
{ {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change organization name'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed organization name', message: 'Successfully changed organization name',
@@ -219,20 +163,11 @@ export const getOrganizationIncidentContacts = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let incidentContactsOrg; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
incidentContactsOrg = await IncidentContactOrg.find({ const incidentContactsOrg = await IncidentContactOrg.find({
organization: organizationId organization: organizationId
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization incident contacts'
});
}
return res.status(200).send({ return res.status(200).send({
incidentContactsOrg incidentContactsOrg
@@ -249,23 +184,14 @@ export const addOrganizationIncidentContact = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let incidentContactOrg; const { organizationId } = req.params;
try { const { email } = req.body;
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndUpdate( const incidentContactOrg = await IncidentContactOrg.findOneAndUpdate(
{ email, organization: organizationId }, { email, organization: organizationId },
{ email, organization: organizationId }, { email, organization: organizationId },
{ upsert: true, new: true } { upsert: true, new: true }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to add incident contact for organization'
});
}
return res.status(200).send({ return res.status(200).send({
incidentContactOrg incidentContactOrg
@@ -282,22 +208,13 @@ export const deleteOrganizationIncidentContact = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let incidentContactOrg; const { organizationId } = req.params;
try { const { email } = req.body;
const { organizationId } = req.params;
const { email } = req.body;
incidentContactOrg = await IncidentContactOrg.findOneAndDelete({ const incidentContactOrg = await IncidentContactOrg.findOneAndDelete({
email, email,
organization: organizationId organization: organizationId
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization incident contact'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully deleted organization incident contact', message: 'Successfully deleted organization incident contact',
@@ -317,41 +234,33 @@ export const createOrganizationPortalSession = async (
res: Response res: Response
) => { ) => {
let session; let session;
try { const stripe = new Stripe(await getStripeSecretKey(), {
const stripe = new Stripe(await getStripeSecretKey(), { apiVersion: '2022-08-01'
apiVersion: '2022-08-01' });
});
// check if there is a payment method on file // check if there is a payment method on file
const paymentMethods = await stripe.paymentMethods.list({ const paymentMethods = await stripe.paymentMethods.list({
customer: req.organization.customerId, customer: req.organization.customerId,
type: 'card' type: 'card'
}); });
if (paymentMethods.data.length < 1) { if (paymentMethods.data.length < 1) {
// case: no payment method on file // case: no payment method on file
session = await stripe.checkout.sessions.create({ session = await stripe.checkout.sessions.create({
customer: req.organization.customerId, customer: req.organization.customerId,
mode: 'setup', mode: 'setup',
payment_method_types: ['card'], payment_method_types: ['card'],
success_url: (await getSiteURL()) + '/dashboard', success_url: (await getSiteURL()) + '/dashboard',
cancel_url: (await getSiteURL()) + '/dashboard' cancel_url: (await getSiteURL()) + '/dashboard'
}); });
} else { } else {
session = await stripe.billingPortal.sessions.create({ session = await stripe.billingPortal.sessions.create({
customer: req.organization.customerId, customer: req.organization.customerId,
return_url: (await getSiteURL()) + '/dashboard' return_url: (await getSiteURL()) + '/dashboard'
}); });
} }
return res.status(200).send({ url: session.url }); return res.status(200).send({ url: session.url });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to redirect to organization billing portal'
});
}
}; };
/** /**
@@ -364,22 +273,13 @@ export const getOrganizationSubscriptions = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let subscriptions; const stripe = new Stripe(await getStripeSecretKey(), {
try { apiVersion: '2022-08-01'
const stripe = new Stripe(await getStripeSecretKey(), { });
apiVersion: '2022-08-01'
});
subscriptions = await stripe.subscriptions.list({ const subscriptions = await stripe.subscriptions.list({
customer: req.organization.customerId customer: req.organization.customerId
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization subscriptions'
});
}
return res.status(200).send({ return res.status(200).send({
subscriptions subscriptions
+204 -264
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
// eslint-disable-next-line @typescript-eslint/no-var-requires // eslint-disable-next-line @typescript-eslint/no-var-requires
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
@@ -19,41 +18,32 @@ import { getSiteURL, getJwtSignupLifetime, getJwtSignupSecret } from '../../conf
* @returns * @returns
*/ */
export const emailPasswordReset = async (req: Request, res: Response) => { export const emailPasswordReset = async (req: Request, res: Response) => {
let email: string; const email = req.body.email;
try {
email = req.body.email;
const user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to send email verification for password reset' error: 'Failed to send email verification for password reset'
}); });
} }
const token = await TokenService.createToken({ const token = await TokenService.createToken({
type: TOKEN_EMAIL_PASSWORD_RESET, type: TOKEN_EMAIL_PASSWORD_RESET,
email email
}); });
await sendMail({ await sendMail({
template: 'passwordReset.handlebars', template: 'passwordReset.handlebars',
subjectLine: 'Infisical password reset', subjectLine: 'Infisical password reset',
recipients: [email], recipients: [email],
substitutions: { substitutions: {
email, email,
token, token,
callback_url: (await getSiteURL()) + '/password-reset' callback_url: (await getSiteURL()) + '/password-reset'
} }
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send email for account recovery'
});
}
return res.status(200).send({ return res.status(200).send({
message: `Sent an email for account recovery to ${email}` message: `Sent an email for account recovery to ${email}`
@@ -67,40 +57,31 @@ export const emailPasswordReset = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const emailPasswordResetVerify = async (req: Request, res: Response) => { export const emailPasswordResetVerify = async (req: Request, res: Response) => {
let user, token; const { email, code } = req.body;
try {
const { email, code } = req.body;
user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (!user || !user?.publicKey) { if (!user || !user?.publicKey) {
// case: user doesn't exist with email [email] or // case: user doesn't exist with email [email] or
// hasn't even completed their account // hasn't even completed their account
return res.status(403).send({ return res.status(403).send({
error: 'Failed email verification for password reset' error: 'Failed email verification for password reset'
}); });
} }
await TokenService.validateToken({ await TokenService.validateToken({
type: TOKEN_EMAIL_PASSWORD_RESET, type: TOKEN_EMAIL_PASSWORD_RESET,
email, email,
token: code token: code
}); });
// generate temporary password-reset token // generate temporary password-reset token
token = createToken({ const token = createToken({
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret() secret: await getJwtSignupSecret()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed email verification for password reset'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully verified email', message: 'Successfully verified email',
@@ -117,43 +98,35 @@ export const emailPasswordResetVerify = async (req: Request, res: Response) => {
*/ */
export const srp1 = async (req: Request, res: Response) => { export const srp1 = async (req: Request, res: Response) => {
// return salt, serverPublicKey as part of first step of SRP protocol // return salt, serverPublicKey as part of first step of SRP protocol
try { const { clientPublicKey } = req.body;
const { clientPublicKey } = req.body; const user = await User.findOne({
const user = await User.findOne({ email: req.user.email
email: req.user.email }).select('+salt +verifier');
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, { await LoginSRPDetail.findOneAndReplace({ email: req.user.email }, {
email: req.user.email, email: req.user.email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false }) }, { upsert: true, returnNewDocument: false })
return res.status(200).send({ return res.status(200).send({
serverPublicKey, serverPublicKey,
salt: user.salt salt: user.salt
}); });
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to start change password process'
});
}
}; };
/** /**
@@ -165,80 +138,72 @@ export const srp1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changePassword = async (req: Request, res: Response) => { export const changePassword = async (req: Request, res: Response) => {
try { const {
const { clientProof,
clientProof, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier
verifier } = req.body;
} = req.body;
const user = await User.findOne({ const user = await User.findOne({
email: req.user.email email: req.user.email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) { if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
} }
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey); server.setClientPublicKey(loginSRPDetailFromDB.clientPublicKey);
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// change password // change password
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed password' message: 'Successfully changed password'
}); });
} }
return res.status(400).send({ return res.status(400).send({
error: 'Failed to change password. Try again?' error: 'Failed to change password. Try again?'
}); });
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to change password. Try again?'
});
}
}; };
/** /**
@@ -252,69 +217,61 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
// requires verifying [clientProof] as part of second step of SRP protocol // requires verifying [clientProof] as part of second step of SRP protocol
// as initiated in /srp1 // as initiated in /srp1
try { const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } =
const { clientProof, encryptedPrivateKey, iv, tag, salt, verifier } = req.body;
req.body; const user = await User.findOne({
const user = await User.findOne({ email: req.user.email
email: req.user.email }).select('+salt +verifier');
}).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email }) const loginSRPDetailFromDB = await LoginSRPDetail.findOneAndDelete({ email: req.user.email })
if (!loginSRPDetailFromDB) { if (!loginSRPDetailFromDB) {
return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again")) return BadRequestError(Error("It looks like some details from the first login are not found. Please try login one again"))
} }
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier, verifier: user.verifier,
b: loginSRPDetailFromDB.serverBInt b: loginSRPDetailFromDB.serverBInt
}, },
async () => { async () => {
server.setClientPublicKey( server.setClientPublicKey(
loginSRPDetailFromDB.clientPublicKey loginSRPDetailFromDB.clientPublicKey
); );
// compare server and client shared keys // compare server and client shared keys
if (server.checkClientProof(clientProof)) { if (server.checkClientProof(clientProof)) {
// create new or replace backup private key // create new or replace backup private key
const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate( const backupPrivateKey = await BackupPrivateKey.findOneAndUpdate(
{ user: req.user._id }, { user: req.user._id },
{ {
user: req.user._id, user: req.user._id,
encryptedPrivateKey, encryptedPrivateKey,
iv, iv,
tag, tag,
salt, salt,
verifier verifier
}, },
{ upsert: true, new: true } { upsert: true, new: true }
).select('+user, encryptedPrivateKey'); ).select('+user, encryptedPrivateKey');
// issue tokens // issue tokens
return res.status(200).send({ return res.status(200).send({
message: 'Successfully updated backup private key', message: 'Successfully updated backup private key',
backupPrivateKey backupPrivateKey
}); });
} }
return res.status(400).send({ return res.status(400).send({
message: 'Failed to update backup private key' message: 'Failed to update backup private key'
}); });
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update backup private key'
});
}
}; };
/** /**
@@ -324,20 +281,11 @@ export const createBackupPrivateKey = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getBackupPrivateKey = async (req: Request, res: Response) => { export const getBackupPrivateKey = async (req: Request, res: Response) => {
let backupPrivateKey; const backupPrivateKey = await BackupPrivateKey.findOne({
try { user: req.user._id
backupPrivateKey = await BackupPrivateKey.findOne({ }).select('+encryptedPrivateKey +iv +tag');
user: req.user._id
}).select('+encryptedPrivateKey +iv +tag');
if (!backupPrivateKey) throw new Error('Failed to find backup private key'); if (!backupPrivateKey) throw new Error('Failed to find backup private key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({ return res.status(200).send({
backupPrivateKey backupPrivateKey
@@ -345,42 +293,34 @@ export const getBackupPrivateKey = async (req: Request, res: Response) => {
} }
export const resetPassword = async (req: Request, res: Response) => { export const resetPassword = async (req: Request, res: Response) => {
try { const {
const { protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier,
verifier, } = req.body;
} = req.body;
await User.findByIdAndUpdate( await User.findByIdAndUpdate(
req.user._id.toString(), req.user._id.toString(),
{ {
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
encryptedPrivateKey, encryptedPrivateKey,
iv: encryptedPrivateKeyIV, iv: encryptedPrivateKeyIV,
tag: encryptedPrivateKeyTag, tag: encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}, },
{ {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get backup private key'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully reset password' message: 'Successfully reset password'
+120 -150
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { Key, Secret } from '../../models'; import { Key, Secret } from '../../models';
import { import {
@@ -37,66 +36,56 @@ interface PushSecret {
*/ */
export const pushSecrets = async (req: Request, res: Response) => { export const pushSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
const postHogClient = await TelemetryService.getPostHogClient();
let { secrets }: { secrets: PushSecret[] } = req.body;
const { keys, environment, channel } = req.body;
const { workspaceId } = req.params;
try { // validate environment
const postHogClient = await TelemetryService.getPostHogClient(); const workspaceEnvs = req.membership.workspace.environments;
let { secrets }: { secrets: PushSecret[] } = req.body; if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
const { keys, environment, channel } = req.body; throw new Error('Failed to validate environment');
const { workspaceId } = req.params; }
// validate environment // sanitize secrets
const workspaceEnvs = req.membership.workspace.environments; secrets = secrets.filter(
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { (s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== ''
throw new Error('Failed to validate environment'); );
}
// sanitize secrets await push({
secrets = secrets.filter( userId: req.user._id,
(s: PushSecret) => s.ciphertextKey !== '' && s.ciphertextValue !== '' workspaceId,
); environment,
secrets
});
await push({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, keys
secrets });
});
await pushKeys({
userId: req.user._id,
workspaceId,
keys
});
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pushed', event: 'secrets pushed',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : 'cli'
} }
}); });
} }
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment
}) })
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully uploaded workspace secrets' message: 'Successfully uploaded workspace secrets'
@@ -111,59 +100,50 @@ export const pushSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; const postHogClient = await TelemetryService.getPostHogClient();
let key; const environment: string = req.query.environment as string;
try { const channel: string = req.query.channel as string;
const postHogClient = await TelemetryService.getPostHogClient(); const { workspaceId } = req.params;
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
secrets = await pull({ let secrets = await pull({
userId: req.user._id.toString(), userId: req.user._id.toString(),
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : 'cli', channel: channel ? channel : 'cli',
ipAddress: req.ip ipAddress: req.ip
}); });
key = await Key.findOne({ const key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.populate('sender', '+publicKey'); .populate('sender', '+publicKey');
if (channel !== 'cli') { if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets }); // FIX: Fix this any
} secrets = reformatPullSecrets({ secrets }) as any;
}
if (postHogClient) { if (postHogClient) {
// capture secrets pushed event in production // capture secrets pushed event in production
postHogClient.capture({ postHogClient.capture({
distinctId: req.user.email, distinctId: req.user.email,
event: 'secrets pulled', event: 'secrets pulled',
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : 'cli'
} }
}); });
} }
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
secrets, secrets,
@@ -180,58 +160,48 @@ export const pullSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecretsServiceToken = async (req: Request, res: Response) => { export const pullSecretsServiceToken = async (req: Request, res: Response) => {
let secrets; const postHogClient = await TelemetryService.getPostHogClient();
let key; const environment: string = req.query.environment as string;
try { const channel: string = req.query.channel as string;
const postHogClient = await TelemetryService.getPostHogClient(); const { workspaceId } = req.params;
const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
secrets = await pull({ const secrets = await pull({
userId: req.serviceToken.user._id.toString(), userId: req.serviceToken.user._id.toString(),
workspaceId, workspaceId,
environment, environment,
channel: 'cli', channel: 'cli',
ipAddress: req.ip ipAddress: req.ip
}); });
key = { const key = {
encryptedKey: req.serviceToken.encryptedKey, encryptedKey: req.serviceToken.encryptedKey,
nonce: req.serviceToken.nonce, nonce: req.serviceToken.nonce,
sender: { sender: {
publicKey: req.serviceToken.publicKey publicKey: req.serviceToken.publicKey
}, },
receiver: req.serviceToken.user, receiver: req.serviceToken.user,
workspace: req.serviceToken.workspace workspace: req.serviceToken.workspace
}; };
if (postHogClient) { if (postHogClient) {
// capture secrets pulled event in production // capture secrets pulled event in production
postHogClient.capture({ postHogClient.capture({
distinctId: req.serviceToken.user.email, distinctId: req.serviceToken.user.email,
event: 'secrets pulled', event: 'secrets pulled',
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : 'cli'
} }
}); });
} }
} catch (err) {
Sentry.setUser({ email: req.serviceToken.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
secrets: reformatPullSecrets({ secrets }), secrets: reformatPullSecrets({ secrets }),
+46 -64
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User } from '../../models'; import { User } from '../../models';
import { import {
sendEmailVerification, sendEmailVerification,
@@ -17,28 +16,19 @@ import { getInviteOnlySignup, getJwtSignupLifetime, getJwtSignupSecret, getSmtpC
* @returns * @returns
*/ */
export const beginEmailSignup = async (req: Request, res: Response) => { export const beginEmailSignup = async (req: Request, res: Response) => {
let email: string; const email = req.body.email;
try {
email = req.body.email;
const user = await User.findOne({ email }).select('+publicKey'); const user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to send email verification code for complete account' error: 'Failed to send email verification code for complete account'
}); });
} }
// send send verification email // send send verification email
await sendEmailVerification({ email }); await sendEmailVerification({ email });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to send email verification code'
});
}
return res.status(200).send({ return res.status(200).send({
message: `Sent an email verification code to ${email}` message: `Sent an email verification code to ${email}`
@@ -54,55 +44,47 @@ export const beginEmailSignup = async (req: Request, res: Response) => {
*/ */
export const verifyEmailSignup = async (req: Request, res: Response) => { export const verifyEmailSignup = async (req: Request, res: Response) => {
let user, token; let user, token;
try { const { email, code } = req.body;
const { email, code } = req.body;
// initialize user account // initialize user account
user = await User.findOne({ email }).select('+publicKey'); user = await User.findOne({ email }).select('+publicKey');
if (user && user?.publicKey) { if (user && user?.publicKey) {
// case: user has already completed account // case: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed email verification for complete user' error: 'Failed email verification for complete user'
}); });
} }
if (await getInviteOnlySignup()) { if (await getInviteOnlySignup()) {
// Only one user can create an account without being invited. The rest need to be invited in order to make an account // Only one user can create an account without being invited. The rest need to be invited in order to make an account
const userCount = await User.countDocuments({}) const userCount = await User.countDocuments({})
if (userCount != 0) { if (userCount != 0) {
throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." }) throw BadRequestError({ message: "New user sign ups are not allowed at this time. You must be invited to sign up." })
} }
} }
// verify email // verify email
if (await getSmtpConfigured()) { if (await getSmtpConfigured()) {
await checkEmailVerification({ await checkEmailVerification({
email, email,
code code
}); });
} }
if (!user) { if (!user) {
user = await new User({ user = await new User({
email email
}).save(); }).save();
} }
// generate temporary signup token // generate temporary signup token
token = createToken({ token = createToken({
payload: { payload: {
userId: user._id.toString() userId: user._id.toString()
}, },
expiresIn: await getJwtSignupLifetime(), expiresIn: await getJwtSignupLifetime(),
secret: await getJwtSignupSecret() secret: await getJwtSignupSecret()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed email verification'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfuly verified email', message: 'Successfuly verified email',
+10 -20
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../config'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../config';
* @returns * @returns
*/ */
export const handleWebhook = async (req: Request, res: Response) => { export const handleWebhook = async (req: Request, res: Response) => {
let event; // check request for valid stripe signature
try { const stripe = new Stripe(await getStripeSecretKey(), {
// check request for valid stripe signature apiVersion: '2022-08-01'
const stripe = new Stripe(await getStripeSecretKey(), { });
apiVersion: '2022-08-01'
});
const sig = req.headers['stripe-signature'] as string; const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent( const event = stripe.webhooks.constructEvent(
req.body, req.body,
sig, sig,
await getStripeWebhookSecret() await getStripeWebhookSecret()
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) { switch (event.type) {
case '': case '':
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { UserAction } from '../../models'; import { UserAction } from '../../models';
/** /**
@@ -11,28 +10,19 @@ import { UserAction } from '../../models';
export const addUserAction = async (req: Request, res: Response) => { export const addUserAction = async (req: Request, res: Response) => {
// add/record new action [action] for user with id [req.user._id] // add/record new action [action] for user with id [req.user._id]
let userAction; const { action } = req.body;
try {
const { action } = req.body;
userAction = await UserAction.findOneAndUpdate( const userAction = await UserAction.findOneAndUpdate(
{ {
user: req.user._id, user: req.user._id,
action action
}, },
{ user: req.user._id, action }, { user: req.user._id, action },
{ {
new: true, new: true,
upsert: true upsert: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to record user action'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully recorded user action', message: 'Successfully recorded user action',
@@ -48,21 +38,12 @@ export const addUserAction = async (req: Request, res: Response) => {
*/ */
export const getUserAction = async (req: Request, res: Response) => { export const getUserAction = async (req: Request, res: Response) => {
// get user action [action] for user with id [req.user._id] // get user action [action] for user with id [req.user._id]
let userAction; const action: string = req.query.action as string;
try {
const action: string = req.query.action as string;
userAction = await UserAction.findOne({ const userAction = await UserAction.findOne({
user: req.user._id, user: req.user._id,
action action
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get user action'
});
}
return res.status(200).send({ return res.status(200).send({
userAction userAction
+80 -170
View File
@@ -1,5 +1,4 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { import {
Workspace, Workspace,
Membership, Membership,
@@ -24,27 +23,18 @@ import { ADMIN } from "../../variables";
* @returns * @returns
*/ */
export const getWorkspacePublicKeys = async (req: Request, res: Response) => { export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
let publicKeys; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
publicKeys = ( const publicKeys = (
await Membership.find({ await Membership.find({
workspace: workspaceId, workspace: workspaceId,
}).populate<{ user: IUser }>("user", "publicKey") }).populate<{ user: IUser }>("user", "publicKey")
).map((member) => { ).map((member) => {
return { return {
publicKey: member.user.publicKey, publicKey: member.user.publicKey,
userId: member.user._id, userId: member.user._id,
}; };
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace member public keys",
});
}
return res.status(200).send({ return res.status(200).send({
publicKeys, publicKeys,
@@ -58,20 +48,11 @@ export const getWorkspacePublicKeys = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
let users; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
users = await Membership.find({ const users = await Membership.find({
workspace: workspaceId, workspace: workspaceId,
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace members",
});
}
return res.status(200).send({ return res.status(200).send({
users, users,
@@ -85,20 +66,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaces = async (req: Request, res: Response) => { export const getWorkspaces = async (req: Request, res: Response) => {
let workspaces; const workspaces = (
try { await Membership.find({
workspaces = ( user: req.user._id,
await Membership.find({ }).populate("workspace")
user: req.user._id, ).map((m) => m.workspace);
}).populate("workspace")
).map((m) => m.workspace);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspaces",
});
}
return res.status(200).send({ return res.status(200).send({
workspaces, workspaces,
@@ -112,20 +84,11 @@ export const getWorkspaces = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspace = async (req: Request, res: Response) => { export const getWorkspace = async (req: Request, res: Response) => {
let workspace; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
workspace = await Workspace.findOne({ const workspace = await Workspace.findOne({
_id: workspaceId, _id: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace",
});
}
return res.status(200).send({ return res.status(200).send({
workspace, workspace,
@@ -140,43 +103,34 @@ export const getWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const createWorkspace = async (req: Request, res: Response) => { export const createWorkspace = async (req: Request, res: Response) => {
let workspace; const { workspaceName, organizationId } = req.body;
try {
const { workspaceName, organizationId } = req.body;
// validate organization membership // validate organization membership
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
user: req.user._id, user: req.user._id,
organization: organizationId, organization: organizationId,
}); });
if (!membershipOrg) { if (!membershipOrg) {
throw new Error("Failed to validate organization membership"); throw new Error("Failed to validate organization membership");
}
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to create workspace",
});
} }
if (workspaceName.length < 1) {
throw new Error("Workspace names must be at least 1-character long");
}
// create workspace and add user as member
const workspace = await create({
name: workspaceName,
organizationId,
});
await addMemberships({
userIds: [req.user._id],
workspaceId: workspace._id.toString(),
roles: [ADMIN],
});
return res.status(200).send({ return res.status(200).send({
workspace, workspace,
}); });
@@ -189,20 +143,12 @@ export const createWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteWorkspace = async (req: Request, res: Response) => { export const deleteWorkspace = async (req: Request, res: Response) => {
try { const { workspaceId } = req.params;
const { workspaceId } = req.params;
// delete workspace // delete workspace
await deleteWork({ await deleteWork({
id: workspaceId, id: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to delete workspace",
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully deleted workspace", message: "Successfully deleted workspace",
@@ -216,29 +162,20 @@ export const deleteWorkspace = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const changeWorkspaceName = async (req: Request, res: Response) => { export const changeWorkspaceName = async (req: Request, res: Response) => {
let workspace; const { workspaceId } = req.params;
try { const { name } = req.body;
const { workspaceId } = req.params;
const { name } = req.body;
workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId, _id: workspaceId,
}, },
{ {
name, name,
}, },
{ {
new: true, new: true,
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to change workspace name",
});
}
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed workspace name", message: "Successfully changed workspace name",
@@ -253,20 +190,11 @@ export const changeWorkspaceName = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const getWorkspaceIntegrations = async (req: Request, res: Response) => { export const getWorkspaceIntegrations = async (req: Request, res: Response) => {
let integrations; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
integrations = await Integration.find({ const integrations = await Integration.find({
workspace: workspaceId, workspace: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integrations",
});
}
return res.status(200).send({ return res.status(200).send({
integrations, integrations,
@@ -283,20 +211,11 @@ export const getWorkspaceIntegrationAuthorizations = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let authorizations; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
authorizations = await IntegrationAuth.find({ const authorizations = await IntegrationAuth.find({
workspace: workspaceId, workspace: workspaceId,
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace integration authorizations",
});
}
return res.status(200).send({ return res.status(200).send({
authorizations, authorizations,
@@ -313,21 +232,12 @@ export const getWorkspaceServiceTokens = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let serviceTokens; const { workspaceId } = req.params;
try { // ?? FIX.
const { workspaceId } = req.params; const serviceTokens = await ServiceToken.find({
// ?? FIX. user: req.user._id,
serviceTokens = await ServiceToken.find({ workspace: workspaceId,
user: req.user._id, });
workspace: workspaceId,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace service tokens",
});
}
return res.status(200).send({ return res.status(200).send({
serviceTokens, serviceTokens,
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
@@ -14,18 +13,9 @@ import { getSaltRounds } from '../../config';
* @returns * @returns
*/ */
export const getAPIKeyData = async (req: Request, res: Response) => { export const getAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData; const apiKeyData = await APIKeyData.find({
try { user: req.user._id
apiKeyData = await APIKeyData.find({ });
user: req.user._id
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get API key data'
});
}
return res.status(200).send({ return res.status(200).send({
apiKeyData apiKeyData
@@ -38,38 +28,29 @@ export const getAPIKeyData = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const createAPIKeyData = async (req: Request, res: Response) => { export const createAPIKeyData = async (req: Request, res: Response) => {
let apiKey, apiKeyData; const { name, expiresIn } = req.body;
try {
const { name, expiresIn } = req.body;
const secret = crypto.randomBytes(16).toString('hex'); const secret = crypto.randomBytes(16).toString('hex');
const secretHash = await bcrypt.hash(secret, await getSaltRounds()); const secretHash = await bcrypt.hash(secret, await getSaltRounds());
const expiresAt = new Date(); const expiresAt = new Date();
expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn); expiresAt.setSeconds(expiresAt.getSeconds() + expiresIn);
apiKeyData = await new APIKeyData({ let apiKeyData = await new APIKeyData({
name, name,
lastUsed: new Date(), lastUsed: new Date(),
expiresAt, expiresAt,
user: req.user._id, user: req.user._id,
secretHash secretHash
}).save(); }).save();
// return api key data without sensitive data // return api key data without sensitive data
apiKeyData = await APIKeyData.findById(apiKeyData._id); // FIX: fix this any
apiKeyData = await APIKeyData.findById(apiKeyData._id) as any
if (!apiKeyData) throw new Error('Failed to find API key data'); if (!apiKeyData) throw new Error('Failed to find API key data');
apiKey = `ak.${apiKeyData._id.toString()}.${secret}`; const apiKey = `ak.${apiKeyData._id.toString()}.${secret}`;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to API key data'
});
}
return res.status(200).send({ return res.status(200).send({
apiKey, apiKey,
@@ -84,19 +65,8 @@ export const createAPIKeyData = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const deleteAPIKeyData = async (req: Request, res: Response) => { export const deleteAPIKeyData = async (req: Request, res: Response) => {
let apiKeyData; const { apiKeyDataId } = req.params;
try { const apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
const { apiKeyDataId } = req.params;
apiKeyData = await APIKeyData.findByIdAndDelete(apiKeyDataId);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete API key data'
});
}
return res.status(200).send({ return res.status(200).send({
apiKeyData apiKeyData
+158 -183
View File
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-var-requires */ /* eslint-disable @typescript-eslint/no-var-requires */
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import * as Sentry from '@sentry/node';
import * as bigintConversion from 'bigint-conversion'; import * as bigintConversion from 'bigint-conversion';
const jsrp = require('jsrp'); const jsrp = require('jsrp');
import { User, LoginSRPDetail } from '../../models'; import { User, LoginSRPDetail } from '../../models';
@@ -35,47 +34,40 @@ declare module 'jsonwebtoken' {
* @returns * @returns
*/ */
export const login1 = async (req: Request, res: Response) => { export const login1 = async (req: Request, res: Response) => {
try { const {
const { email,
email, clientPublicKey
clientPublicKey }: { email: string; clientPublicKey: string } = req.body;
}: { email: string; clientPublicKey: string } = req.body;
const user = await User.findOne({ const user = await User.findOne({
email email
}).select('+salt +verifier'); }).select('+salt +verifier');
if (!user) throw new Error('Failed to find user'); if (!user) throw new Error('Failed to find user');
const server = new jsrp.server(); const server = new jsrp.server();
server.init( server.init(
{ {
salt: user.salt, salt: user.salt,
verifier: user.verifier verifier: user.verifier
}, },
async () => { async () => {
// generate server-side public key // generate server-side public key
const serverPublicKey = server.getPublicKey(); const serverPublicKey = server.getPublicKey();
await LoginSRPDetail.findOneAndReplace({ email: email }, { await LoginSRPDetail.findOneAndReplace({ email: email }, {
email: email, email: email,
clientPublicKey: clientPublicKey, clientPublicKey: clientPublicKey,
serverBInt: bigintConversion.bigintToBuf(server.bInt), serverBInt: bigintConversion.bigintToBuf(server.bInt),
}, { upsert: true, returnNewDocument: false }); }, { upsert: true, returnNewDocument: false });
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
return res.status(200).send({
serverPublicKey,
salt: user.salt
});
}
);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to start authentication process'
});
}
}; };
/** /**
@@ -86,149 +78,140 @@ export const login1 = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const login2 = async (req: Request, res: Response) => { export const login2 = async (req: Request, res: Response) => {
try { if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' });
if (!req.headers['user-agent']) throw InternalServerError({ message: 'User-Agent header is required' }); const { email, clientProof } = req.body;
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
const { email, clientProof } = req.body; if (!user) throw new Error('Failed to find user');
const user = await User.findOne({
email
}).select('+salt +verifier +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag +publicKey +encryptedPrivateKey +iv +tag');
if (!user) throw new Error('Failed to find user'); const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email })
const loginSRPDetail = await LoginSRPDetail.findOneAndDelete({ email: email }) if (!loginSRPDetail) {
return BadRequestError(Error("Failed to find login details for SRP"))
}
if (!loginSRPDetail) { const server = new jsrp.server();
return BadRequestError(Error("Failed to find login details for SRP")) server.init(
} {
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
const server = new jsrp.server(); // compare server and client shared keys
server.init( if (server.checkClientProof(clientProof)) {
{
salt: user.salt,
verifier: user.verifier,
b: loginSRPDetail.serverBInt
},
async () => {
server.setClientPublicKey(loginSRPDetail.clientPublicKey);
// compare server and client shared keys if (user.isMfaEnabled) {
if (server.checkClientProof(clientProof)) { // case: user has MFA enabled
if (user.isMfaEnabled) { // generate temporary MFA token
// case: user has MFA enabled const token = createToken({
payload: {
// generate temporary MFA token userId: user._id.toString()
const token = createToken({ },
payload: { expiresIn: await getJwtMfaLifetime(),
userId: user._id.toString() secret: await getJwtMfaSecret()
},
expiresIn: await getJwtMfaLifetime(),
secret: await getJwtMfaSecret()
});
const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA,
email
});
// send MFA code [code] to [email]
await sendMail({
template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code',
recipients: [email],
substitutions: {
code
}
});
return res.status(200).send({
mfaEnabled: true,
token
});
}
await checkUserDevice({
user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens const code = await TokenService.createToken({
const tokens = await issueAuthTokens({ userId: user._id.toString() }); type: TOKEN_EMAIL_MFA,
email
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
}); });
// case: user does not have MFA enablgged // send MFA code [code] to [email]
// return (access) token in response await sendMail({
template: 'emailMfa.handlebars',
interface ResponseData { subjectLine: 'Infisical MFA code',
mfaEnabled: boolean; recipients: [email],
encryptionVersion: any; substitutions: {
protectedKey?: string; code
protectedKeyIV?: string; }
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
}); });
loginAction && await EELogService.createLog({ return res.status(200).send({
userId: user._id, mfaEnabled: true,
actions: [loginAction], token
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
}); });
return res.status(200).send(response);
} }
return res.status(400).send({ await checkUserDevice({
message: 'Failed to authenticate. Try again?' user,
ip: req.ip,
userAgent: req.headers['user-agent'] ?? ''
}); });
// issue tokens
const tokens = await issueAuthTokens({ userId: user._id.toString() });
// store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, {
httpOnly: true,
path: '/',
sameSite: 'strict',
secure: await getHttpsEnabled()
});
// case: user does not have MFA enablgged
// return (access) token in response
interface ResponseData {
mfaEnabled: boolean;
encryptionVersion: any;
protectedKey?: string;
protectedKeyIV?: string;
protectedKeyTag?: string;
token: string;
publicKey?: string;
encryptedPrivateKey?: string;
iv?: string;
tag?: string;
}
const response: ResponseData = {
mfaEnabled: false,
encryptionVersion: user.encryptionVersion,
token: tokens.token,
publicKey: user.publicKey,
encryptedPrivateKey: user.encryptedPrivateKey,
iv: user.iv,
tag: user.tag
}
if (
user?.protectedKey &&
user?.protectedKeyIV &&
user?.protectedKeyTag
) {
response.protectedKey = user.protectedKey;
response.protectedKeyIV = user.protectedKeyIV
response.protectedKeyTag = user.protectedKeyTag;
}
const loginAction = await EELogService.createAction({
name: ACTION_LOGIN,
userId: user._id
});
loginAction && await EELogService.createLog({
userId: user._id,
actions: [loginAction],
channel: getChannelFromUserAgent(req.headers['user-agent']),
ipAddress: req.ip
});
return res.status(200).send(response);
} }
);
} catch (err) { return res.status(400).send({
Sentry.setUser(null); message: 'Failed to authenticate. Try again?'
Sentry.captureException(err); });
return res.status(400).send({ }
message: 'Failed to authenticate. Try again?' );
});
}
}; };
/** /**
@@ -237,30 +220,22 @@ export const login2 = async (req: Request, res: Response) => {
* @param res * @param res
*/ */
export const sendMfaToken = async (req: Request, res: Response) => { export const sendMfaToken = async (req: Request, res: Response) => {
try { const { email } = req.body;
const { email } = req.body;
const code = await TokenService.createToken({ const code = await TokenService.createToken({
type: TOKEN_EMAIL_MFA, type: TOKEN_EMAIL_MFA,
email email
}); });
// send MFA code [code] to [email] // send MFA code [code] to [email]
await sendMail({ await sendMail({
template: 'emailMfa.handlebars', template: 'emailMfa.handlebars',
subjectLine: 'Infisical MFA code', subjectLine: 'Infisical MFA code',
recipients: [email], recipients: [email],
substitutions: { substitutions: {
code code
} }
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to send MFA code'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully sent new MFA code' message: 'Successfully sent new MFA code'
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { import {
Secret, Secret,
ServiceToken, ServiceToken,
@@ -25,30 +24,22 @@ export const createWorkspaceEnvironment = async (
) => { ) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentName, environmentSlug } = req.body; const { environmentName, environmentSlug } = req.body;
try { const workspace = await Workspace.findById(workspaceId).exec();
const workspace = await Workspace.findById(workspaceId).exec(); if (
if ( !workspace ||
!workspace || workspace?.environments.find(
workspace?.environments.find( ({ name, slug }) => slug === environmentSlug || environmentName === name
({ name, slug }) => slug === environmentSlug || environmentName === name )
) ) {
) { throw new Error('Failed to create workspace environment');
throw new Error('Failed to create workspace environment');
}
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to create new workspace environment',
});
} }
workspace?.environments.push({
name: environmentName,
slug: environmentSlug.toLowerCase(),
});
await workspace.save();
return res.status(200).send({ return res.status(200).send({
message: 'Successfully created new environment', message: 'Successfully created new environment',
workspace: workspaceId, workspace: workspaceId,
@@ -72,75 +63,67 @@ export const renameWorkspaceEnvironment = async (
) => { ) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body; const { environmentName, environmentSlug, oldEnvironmentSlug } = req.body;
try { // user should pass both new slug and env name
// user should pass both new slug and env name if (!environmentSlug || !environmentName) {
if (!environmentSlug || !environmentName) { throw new Error('Invalid environment given.');
throw new Error('Invalid environment given.');
}
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace environment',
});
} }
// atomic update the env to avoid conflict
const workspace = await Workspace.findById(workspaceId).exec();
if (!workspace) {
throw new Error('Failed to create workspace environment');
}
const isEnvExist = workspace.environments.some(
({ name, slug }) =>
slug !== oldEnvironmentSlug &&
(name === environmentName || slug === environmentSlug)
);
if (isEnvExist) {
throw new Error('Invalid environment given');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === oldEnvironmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments[envIndex].name = environmentName;
workspace.environments[envIndex].slug = environmentSlug.toLowerCase();
await workspace.save();
await Secret.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await SecretVersion.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceToken.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await ServiceTokenData.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug }
);
await Membership.updateMany(
{
workspace: workspaceId,
"deniedPermissions.environmentSlug": oldEnvironmentSlug
},
{ $set: { "deniedPermissions.$[element].environmentSlug": environmentSlug } },
{ arrayFilters: [{ "element.environmentSlug": oldEnvironmentSlug }] }
)
return res.status(200).send({ return res.status(200).send({
message: 'Successfully update environment', message: 'Successfully update environment',
workspace: workspaceId, workspace: workspaceId,
@@ -163,57 +146,48 @@ export const deleteWorkspaceEnvironment = async (
) => { ) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const { environmentSlug } = req.body; const { environmentSlug } = req.body;
try { // atomic update the env to avoid conflict
// atomic update the env to avoid conflict const workspace = await Workspace.findById(workspaceId).exec();
const workspace = await Workspace.findById(workspaceId).exec(); if (!workspace) {
if (!workspace) { throw new Error('Failed to create workspace environment');
throw new Error('Failed to create workspace environment');
}
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace environment',
});
} }
const envIndex = workspace?.environments.findIndex(
({ slug }) => slug === environmentSlug
);
if (envIndex === -1) {
throw new Error('Invalid environment given');
}
workspace.environments.splice(envIndex, 1);
await workspace.save();
// clean up
await Secret.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await SecretVersion.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceToken.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await ServiceTokenData.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Integration.deleteMany({
workspace: workspaceId,
environment: environmentSlug,
});
await Membership.updateMany(
{ workspace: workspaceId },
{ $pull: { deniedPermissions: { environmentSlug: environmentSlug } } }
)
return res.status(200).send({ return res.status(200).send({
message: 'Successfully deleted environment', message: 'Successfully deleted environment',
workspace: workspaceId, workspace: workspaceId,
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
MembershipOrg, MembershipOrg,
@@ -49,20 +48,11 @@ export const getOrganizationMemberships = async (req: Request, res: Response) =>
} }
} }
*/ */
let memberships; const { organizationId } = req.params;
try {
const { organizationId } = req.params;
memberships = await MembershipOrg.find({ const memberships = await MembershipOrg.find({
organization: organizationId organization: organizationId
}).populate('user', '+publicKey'); }).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get organization memberships'
});
}
return res.status(200).send({ return res.status(200).send({
memberships memberships
@@ -128,26 +118,17 @@ export const updateOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
let membership; const { membershipId } = req.params;
try { const { role } = req.body;
const { membershipId } = req.params;
const { role } = req.body;
membership = await MembershipOrg.findByIdAndUpdate( const membership = await MembershipOrg.findByIdAndUpdate(
membershipId, membershipId,
{ {
role role
}, { }, {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update organization membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
@@ -197,25 +178,16 @@ export const deleteOrganizationMembership = async (req: Request, res: Response)
} }
} }
*/ */
let membership; const { membershipId } = req.params;
try {
const { membershipId } = req.params;
// delete organization membership // delete organization membership
membership = await deleteMembershipOrg({ const membership = await deleteMembershipOrg({
membershipOrgId: membershipId membershipOrgId: membershipId
}); });
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString() organizationId: membership.organization.toString()
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete organization membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import crypto from 'crypto'; import crypto from 'crypto';
import bcrypt from 'bcrypt'; import bcrypt from 'bcrypt';
+189 -206
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { User, MembershipOrg } from '../../models'; import { User, MembershipOrg } from '../../models';
import { completeAccount } from '../../helpers/user'; import { completeAccount } from '../../helpers/user';
import { import {
@@ -20,136 +19,128 @@ import { updateSubscriptionOrgQuantity } from '../../helpers/organization';
*/ */
export const completeAccountSignup = async (req: Request, res: Response) => { export const completeAccountSignup = async (req: Request, res: Response) => {
let user, token, refreshToken; let user, token, refreshToken;
try { const {
const { email,
email, firstName,
firstName, lastName,
lastName, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, publicKey,
publicKey, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier,
verifier, organizationName
organizationName }: {
}: { email: string;
email: string; firstName: string;
firstName: string; lastName: string;
lastName: string; protectedKey: string;
protectedKey: string; protectedKeyIV: string;
protectedKeyIV: string; protectedKeyTag: string;
protectedKeyTag: string; publicKey: string;
publicKey: string; encryptedPrivateKey: string;
encryptedPrivateKey: string; encryptedPrivateKeyIV: string;
encryptedPrivateKeyIV: string; encryptedPrivateKeyTag: string;
encryptedPrivateKeyTag: string; salt: string;
salt: string; verifier: string;
verifier: string; organizationName: string;
organizationName: string; } = req.body;
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to complete account for complete user' error: 'Failed to complete account for complete user'
}); });
} }
// complete setting up user's account // complete setting up user's account
user = await completeAccount({ user = await completeAccount({
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}); });
if (!user) if (!user)
throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null throw new Error('Failed to complete account for non-existent user'); // ensure user is non-null
// initialize default organization and workspace // initialize default organization and workspace
await initializeDefaultOrg({ await initializeDefaultOrg({
organizationName, organizationName,
user user
}); });
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({ const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED
}); });
membershipsToUpdate.forEach(async (membership) => { membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString() organizationId: membership.organization.toString()
}); });
}); });
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
await MembershipOrg.updateMany( await MembershipOrg.updateMany(
{ {
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED
}, },
{ {
user, user,
status: ACCEPTED status: ACCEPTED
} }
); );
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id.toString()
}); });
token = tokens.token; token = tokens.token;
// sending a welcome email to new users // sending a welcome email to new users
if (await getLoopsApiKey()) { if (await getLoopsApiKey()) {
await standardRequest.post("https://app.loops.so/api/v1/events/send", { await standardRequest.post("https://app.loops.so/api/v1/events/send", {
"email": email, "email": email,
"eventName": "Sign Up", "eventName": "Sign Up",
"firstName": firstName, "firstName": firstName,
"lastName": lastName "lastName": lastName
}, { }, {
headers: { headers: {
"Accept": "application/json", "Accept": "application/json",
"Authorization": "Bearer " + (await getLoopsApiKey()) "Authorization": "Bearer " + (await getLoopsApiKey())
}, },
}); });
} }
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: await getHttpsEnabled() secure: await getHttpsEnabled()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: 'Successfully set up account',
@@ -167,105 +158,97 @@ export const completeAccountSignup = async (req: Request, res: Response) => {
*/ */
export const completeAccountInvite = async (req: Request, res: Response) => { export const completeAccountInvite = async (req: Request, res: Response) => {
let user, token, refreshToken; let user, token, refreshToken;
try { const {
const { email,
email, firstName,
firstName, lastName,
lastName, protectedKey,
protectedKey, protectedKeyIV,
protectedKeyIV, protectedKeyTag,
protectedKeyTag, publicKey,
publicKey, encryptedPrivateKey,
encryptedPrivateKey, encryptedPrivateKeyIV,
encryptedPrivateKeyIV, encryptedPrivateKeyTag,
encryptedPrivateKeyTag, salt,
salt, verifier
verifier } = req.body;
} = req.body;
// get user // get user
user = await User.findOne({ email }); user = await User.findOne({ email });
if (!user || (user && user?.publicKey)) { if (!user || (user && user?.publicKey)) {
// case 1: user doesn't exist. // case 1: user doesn't exist.
// case 2: user has already completed account // case 2: user has already completed account
return res.status(403).send({ return res.status(403).send({
error: 'Failed to complete account for complete user' error: 'Failed to complete account for complete user'
}); });
} }
const membershipOrg = await MembershipOrg.findOne({ const membershipOrg = await MembershipOrg.findOne({
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED
}); });
if (!membershipOrg) throw new Error('Failed to find invitations for email'); if (!membershipOrg) throw new Error('Failed to find invitations for email');
// complete setting up user's account // complete setting up user's account
user = await completeAccount({ user = await completeAccount({
userId: user._id.toString(), userId: user._id.toString(),
firstName, firstName,
lastName, lastName,
encryptionVersion: 2, encryptionVersion: 2,
protectedKey, protectedKey,
protectedKeyIV, protectedKeyIV,
protectedKeyTag, protectedKeyTag,
publicKey, publicKey,
encryptedPrivateKey, encryptedPrivateKey,
encryptedPrivateKeyIV, encryptedPrivateKeyIV,
encryptedPrivateKeyTag, encryptedPrivateKeyTag,
salt, salt,
verifier verifier
}); });
if (!user) if (!user)
throw new Error('Failed to complete account for non-existent user'); throw new Error('Failed to complete account for non-existent user');
// update organization membership statuses that are // update organization membership statuses that are
// invited to completed with user attached // invited to completed with user attached
const membershipsToUpdate = await MembershipOrg.find({ const membershipsToUpdate = await MembershipOrg.find({
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED
}); });
membershipsToUpdate.forEach(async (membership) => { membershipsToUpdate.forEach(async (membership) => {
await updateSubscriptionOrgQuantity({ await updateSubscriptionOrgQuantity({
organizationId: membership.organization.toString() organizationId: membership.organization.toString()
}); });
}); });
await MembershipOrg.updateMany( await MembershipOrg.updateMany(
{ {
inviteEmail: email, inviteEmail: email,
status: INVITED status: INVITED
}, },
{ {
user, user,
status: ACCEPTED status: ACCEPTED
} }
); );
// issue tokens // issue tokens
const tokens = await issueAuthTokens({ const tokens = await issueAuthTokens({
userId: user._id.toString() userId: user._id.toString()
}); });
token = tokens.token; token = tokens.token;
// store (refresh) token in httpOnly cookie // store (refresh) token in httpOnly cookie
res.cookie('jid', tokens.refreshToken, { res.cookie('jid', tokens.refreshToken, {
httpOnly: true, httpOnly: true,
path: '/', path: '/',
sameSite: 'strict', sameSite: 'strict',
secure: await getHttpsEnabled() secure: await getHttpsEnabled()
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to complete account setup'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully set up account', message: 'Successfully set up account',
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Membership, Secret, Membership, Secret,
+20 -48
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { import {
User, User,
MembershipOrg MembershipOrg
@@ -37,18 +36,9 @@ export const getMe = async (req: Request, res: Response) => {
} }
} }
*/ */
let user; const user = await User
try { .findById(req.user._id)
user = await User .select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
.findById(req.user._id)
.select('+salt +publicKey +encryptedPrivateKey +iv +tag +encryptionVersion +protectedKey +protectedKeyIV +protectedKeyTag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get current user'
});
}
return res.status(200).send({ return res.status(200).send({
user user
@@ -64,30 +54,21 @@ export const getMe = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const updateMyMfaEnabled = async (req: Request, res: Response) => { export const updateMyMfaEnabled = async (req: Request, res: Response) => {
let user; const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
try { req.user.isMfaEnabled = isMfaEnabled;
const { isMfaEnabled }: { isMfaEnabled: boolean } = req.body;
req.user.isMfaEnabled = isMfaEnabled;
if (isMfaEnabled) { if (isMfaEnabled) {
// TODO: adapt this route/controller // TODO: adapt this route/controller
// to work for different forms of MFA // to work for different forms of MFA
req.user.mfaMethods = ['email']; req.user.mfaMethods = ['email'];
} else { } else {
req.user.mfaMethods = []; req.user.mfaMethods = [];
}
await req.user.save();
user = req.user;
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to update current user's MFA status"
});
} }
await req.user.save();
const user = req.user;
return res.status(200).send({ return res.status(200).send({
user user
}); });
@@ -126,20 +107,11 @@ export const getMyOrganizations = async (req: Request, res: Response) => {
} }
} }
*/ */
let organizations; const organizations = (
try { await MembershipOrg.find({
organizations = ( user: req.user._id
await MembershipOrg.find({ }).populate('organization')
user: req.user._id ).map((m) => m.organization);
}).populate('organization')
).map((m) => m.organization);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get current user's organizations"
});
}
return res.status(200).send({ return res.status(200).send({
organizations organizations
+134 -206
View File
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Workspace, Workspace,
@@ -47,66 +46,57 @@ interface V2PushSecret {
*/ */
export const pushWorkspaceSecrets = async (req: Request, res: Response) => { export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
// upload (encrypted) secrets to workspace with id [workspaceId] // upload (encrypted) secrets to workspace with id [workspaceId]
try { const postHogClient = await TelemetryService.getPostHogClient();
const postHogClient = await TelemetryService.getPostHogClient(); let { secrets }: { secrets: V2PushSecret[] } = req.body;
let { secrets }: { secrets: V2PushSecret[] } = req.body; const { keys, environment, channel } = req.body;
const { keys, environment, channel } = req.body; const { workspaceId } = req.params;
const { workspaceId } = req.params;
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
// sanitize secrets // sanitize secrets
secrets = secrets.filter( secrets = secrets.filter(
(s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== '' (s: V2PushSecret) => s.secretKeyCiphertext !== '' && s.secretValueCiphertext !== ''
); );
await push({ await push({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
environment, environment,
secrets, secrets,
channel: channel ? channel : 'cli', channel: channel ? channel : 'cli',
ipAddress: req.ip ipAddress: req.ip
}); });
await pushKeys({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
keys keys
}); });
if (postHogClient) { if (postHogClient) {
postHogClient.capture({ postHogClient.capture({
event: 'secrets pushed', event: 'secrets pushed',
distinctId: req.user.email, distinctId: req.user.email,
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : 'cli'
} }
}); });
} }
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment
}) })
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to upload workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully uploaded workspace secrets' message: 'Successfully uploaded workspace secrets'
@@ -121,57 +111,49 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
* @returns * @returns
*/ */
export const pullSecrets = async (req: Request, res: Response) => { export const pullSecrets = async (req: Request, res: Response) => {
let secrets; const postHogClient = await TelemetryService.getPostHogClient();
try { const environment: string = req.query.environment as string;
const postHogClient = await TelemetryService.getPostHogClient(); const channel: string = req.query.channel as string;
const environment: string = req.query.environment as string; const { workspaceId } = req.params;
const channel: string = req.query.channel as string;
const { workspaceId } = req.params;
let userId; let userId;
if (req.user) { if (req.user) {
userId = req.user._id.toString(); userId = req.user._id.toString();
} else if (req.serviceTokenData) { } else if (req.serviceTokenData) {
userId = req.serviceTokenData.user.toString(); userId = req.serviceTokenData.user.toString();
} }
// validate environment // validate environment
const workspaceEnvs = req.membership.workspace.environments; const workspaceEnvs = req.membership.workspace.environments;
if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) { if (!workspaceEnvs.find(({ slug }: { slug: string }) => slug === environment)) {
throw new Error('Failed to validate environment'); throw new Error('Failed to validate environment');
} }
secrets = await pull({ let secrets = await pull({
userId, userId,
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : 'cli', channel: channel ? channel : 'cli',
ipAddress: req.ip ipAddress: req.ip
}); });
if (channel !== 'cli') { if (channel !== 'cli') {
secrets = reformatPullSecrets({ secrets }); // FIX: Fix this any
} secrets = reformatPullSecrets({ secrets }) as any;
}
if (postHogClient) { if (postHogClient) {
// capture secrets pushed event in production // capture secrets pushed event in production
postHogClient.capture({ postHogClient.capture({
distinctId: req.user.email, distinctId: req.user.email,
event: 'secrets pulled', event: 'secrets pulled',
properties: { properties: {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : 'cli' channel: channel ? channel : 'cli'
} }
}); });
} }
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to pull workspace secrets'
});
}
return res.status(200).send({ return res.status(200).send({
secrets secrets
@@ -208,22 +190,14 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
} }
*/ */
let key; let key;
try { const { workspaceId } = req.params;
const { workspaceId } = req.params;
key = await Key.findOne({ key = await Key.findOne({
workspace: workspaceId, workspace: workspaceId,
receiver: req.user._id receiver: req.user._id
}).populate('sender', '+publicKey'); }).populate('sender', '+publicKey');
if (!key) throw new Error('Failed to find workspace key'); if (!key) throw new Error('Failed to find workspace key');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace key'
});
}
return res.status(200).json(key); return res.status(200).json(key);
} }
@@ -231,23 +205,13 @@ export const getWorkspaceServiceTokenData = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let serviceTokenData; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData
.find({ .find({
workspace: workspaceId workspace: workspaceId
}) })
.select('+encryptedKey +iv +tag'); .select('+encryptedKey +iv +tag');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace service token data'
});
}
return res.status(200).send({ return res.status(200).send({
serviceTokenData serviceTokenData
@@ -294,20 +258,11 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
} }
} }
*/ */
let memberships; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
memberships = await Membership.find({ const memberships = await Membership.find({
workspace: workspaceId workspace: workspaceId
}).populate('user', '+publicKey'); }).populate('user', '+publicKey');
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to get workspace memberships'
});
}
return res.status(200).send({ return res.status(200).send({
memberships memberships
@@ -374,28 +329,19 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
let membership; const {
try { membershipId
const { } = req.params;
membershipId const { role } = req.body;
} = req.params;
const { role } = req.body;
membership = await Membership.findByIdAndUpdate( const membership = await Membership.findByIdAndUpdate(
membershipId, membershipId,
{ {
role role
}, { }, {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to update workspace membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
@@ -445,27 +391,18 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
let membership; const {
try { membershipId
const { } = req.params;
membershipId
} = req.params;
membership = await Membership.findByIdAndDelete(membershipId); const membership = await Membership.findByIdAndDelete(membershipId);
if (!membership) throw new Error('Failed to delete workspace membership'); if (!membership) throw new Error('Failed to delete workspace membership');
await Key.deleteMany({ await Key.deleteMany({
receiver: membership.user, receiver: membership.user,
workspace: membership.workspace workspace: membership.workspace
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to delete workspace membership'
});
}
return res.status(200).send({ return res.status(200).send({
membership membership
@@ -479,29 +416,20 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
* @returns * @returns
*/ */
export const toggleAutoCapitalization = async (req: Request, res: Response) => { export const toggleAutoCapitalization = async (req: Request, res: Response) => {
let workspace; const { workspaceId } = req.params;
try { const { autoCapitalization } = req.body;
const { workspaceId } = req.params;
const { autoCapitalization } = req.body;
workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId _id: workspaceId
}, },
{ {
autoCapitalization autoCapitalization
}, },
{ {
new: true new: true
} }
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: 'Failed to change autoCapitalization setting'
});
}
return res.status(200).send({ return res.status(200).send({
message: 'Successfully changed autoCapitalization setting', message: 'Successfully changed autoCapitalization setting',
@@ -1,5 +1,4 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import * as Sentry from '@sentry/node';
import { Action, SecretVersion } from '../../models'; import { Action, SecretVersion } from '../../models';
import { ActionNotFoundError } from '../../../utils/errors'; import { ActionNotFoundError } from '../../../utils/errors';
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import { EELicenseService } from '../../services'; import { EELicenseService } from '../../services';
import { getLicenseServerUrl } from '../../../config'; import { getLicenseServerUrl } from '../../../config';
@@ -12,19 +11,14 @@ import { licenseServerKeyRequest } from '../../../config/request';
* @returns * @returns
*/ */
export const getCloudProducts = async (req: Request, res: Response) => { export const getCloudProducts = async (req: Request, res: Response) => {
try { const billingCycle = req.query['billing-cycle'] as string;
const billingCycle = req.query['billing-cycle'] as string;
if (EELicenseService.instanceType === 'cloud') { if (EELicenseService.instanceType === 'cloud') {
const { data } = await licenseServerKeyRequest.get( const { data } = await licenseServerKeyRequest.get(
`${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}` `${await getLicenseServerUrl()}/api/license-server/v1/cloud-products?billing-cycle=${billingCycle}`
); );
return res.status(200).send(data); return res.status(200).send(data);
}
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
} }
return res.status(200).send({ return res.status(200).send({
@@ -1,5 +1,4 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { Secret } from "../../../models"; import { Secret } from "../../../models";
import { SecretVersion } from "../../models"; import { SecretVersion } from "../../models";
import { EESecretService } from "../../services"; import { EESecretService } from "../../services";
@@ -55,29 +54,20 @@ export const getSecretVersions = async (req: Request, res: Response) => {
} }
} }
*/ */
let secretVersions; const { secretId, workspaceId, environment, folderId } = req.params;
try {
const { secretId, workspaceId, environment, folderId } = req.params;
const offset: number = parseInt(req.query.offset as string); const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
secretVersions = await SecretVersion.find({ const secretVersions = await SecretVersion.find({
secret: secretId, secret: secretId,
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folder: folderId, folder: folderId,
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get secret versions",
});
}
return res.status(200).send({ return res.status(200).send({
secretVersions, secretVersions,
@@ -139,74 +129,45 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
} }
} }
*/ */
let secret; const { secretId } = req.params;
try { const { version } = req.body;
const { secretId } = req.params;
const { version } = req.body;
// validate secret version // validate secret version
const oldSecretVersion = await SecretVersion.findOne({ const oldSecretVersion = await SecretVersion.findOne({
secret: secretId, secret: secretId,
version, version,
}).select("+secretBlindIndex"); }).select("+secretBlindIndex");
if (!oldSecretVersion) throw new Error("Failed to find secret version"); if (!oldSecretVersion) throw new Error("Failed to find secret version");
const { const {
workspace, workspace,
type, type,
user, user,
environment, environment,
secretBlindIndex, secretBlindIndex,
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
secretKeyTag, secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
folder, algorithm,
algorithm, folder,
keyEncoding, keyEncoding,
} = oldSecretVersion; } = oldSecretVersion;
// update secret // update secret
secret = await Secret.findByIdAndUpdate( const secret = await Secret.findByIdAndUpdate(
secretId, secretId,
{ {
$inc: { $inc: {
version: 1, version: 1,
},
workspace,
type,
user,
environment,
...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folderId: folder,
algorithm,
keyEncoding,
}, },
{
new: true,
}
);
if (!secret) throw new Error("Failed to find and update secret");
// add new secret version
await new SecretVersion({
secret: secretId,
version: secret.version,
workspace, workspace,
type, type,
user, user,
environment, environment,
isDeleted: false,
...(secretBlindIndex ? { secretBlindIndex } : {}), ...(secretBlindIndex ? { secretBlindIndex } : {}),
secretKeyCiphertext, secretKeyCiphertext,
secretKeyIV, secretKeyIV,
@@ -214,24 +175,44 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
folder, folderId: folder,
algorithm, algorithm,
keyEncoding, keyEncoding,
}).save(); },
{
new: true,
}
);
// take secret snapshot if (!secret) throw new Error("Failed to find and update secret");
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace, // add new secret version
environment, await new SecretVersion({
folderId: folder, secret: secretId,
}); version: secret.version,
} catch (err) { workspace,
Sentry.setUser({ email: req.user.email }); type,
Sentry.captureException(err); user,
return res.status(400).send({ environment,
message: "Failed to roll back secret version", isDeleted: false,
}); ...(secretBlindIndex ? { secretBlindIndex } : {}),
} secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
folder,
algorithm,
keyEncoding,
}).save();
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: secret.workspace,
environment,
folderId: folder,
});
return res.status(200).send({ return res.status(200).send({
secret, secret,
@@ -1,5 +1,4 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { import {
ISecretVersion, ISecretVersion,
SecretSnapshot, SecretSnapshot,
@@ -13,23 +12,14 @@ import {
* @returns * @returns
*/ */
export const getSecretSnapshot = async (req: Request, res: Response) => { export const getSecretSnapshot = async (req: Request, res: Response) => {
let secretSnapshot; const { secretSnapshotId } = req.params;
try { const secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
const { secretSnapshotId } = req.params; .lean()
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
.lean()
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get secret snapshot",
});
}
const folderId = secretSnapshot.folderId; const folderId = secretSnapshot.folderId;
// to show only the folder required secrets // to show only the folder required secrets
secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter( secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter(
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Request, Response } from 'express'; import { Request, Response } from 'express';
import Stripe from 'stripe'; import Stripe from 'stripe';
import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config'; import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
@@ -10,26 +9,17 @@ import { getStripeSecretKey, getStripeWebhookSecret } from '../../../config';
* @returns * @returns
*/ */
export const handleWebhook = async (req: Request, res: Response) => { export const handleWebhook = async (req: Request, res: Response) => {
let event; const stripe = new Stripe(await getStripeSecretKey(), {
try { apiVersion: '2022-08-01'
const stripe = new Stripe(await getStripeSecretKey(), { });
apiVersion: '2022-08-01'
});
// check request for valid stripe signature // check request for valid stripe signature
const sig = req.headers['stripe-signature'] as string; const sig = req.headers['stripe-signature'] as string;
event = stripe.webhooks.constructEvent( const event = stripe.webhooks.constructEvent(
req.body, req.body,
sig, sig,
await getStripeWebhookSecret() await getStripeWebhookSecret()
); );
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
error: 'Failed to process webhook'
});
}
switch (event.type) { switch (event.type) {
case '': case '':
@@ -1,5 +1,4 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import * as Sentry from "@sentry/node";
import { PipelineStage, Types } from "mongoose"; import { PipelineStage, Types } from "mongoose";
import { Secret } from "../../../models"; import { Secret } from "../../../models";
import { import {
@@ -69,29 +68,20 @@ export const getWorkspaceSecretSnapshots = async (
} }
} }
*/ */
let secretSnapshots; const { workspaceId } = req.params;
try { const { environment, folderId } = req.query;
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
const offset: number = parseInt(req.query.offset as string); const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
secretSnapshots = await SecretSnapshot.find({ const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root", folderId: folderId || "root",
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
.limit(limit); .limit(limit);
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get secret snapshots",
});
}
return res.status(200).send({ return res.status(200).send({
secretSnapshots, secretSnapshots,
@@ -107,23 +97,14 @@ export const getWorkspaceSecretSnapshotsCount = async (
req: Request, req: Request,
res: Response res: Response
) => { ) => {
let count; const { workspaceId } = req.params;
try { const { environment, folderId } = req.query;
const { workspaceId } = req.params;
const { environment, folderId } = req.query;
count = await SecretSnapshot.countDocuments({ const count = await SecretSnapshot.countDocuments({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root", folderId: folderId || "root",
}); });
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to count number of secret snapshots",
});
}
return res.status(200).send({ return res.status(200).send({
count, count,
@@ -191,324 +172,315 @@ export const rollbackWorkspaceSecretSnapshot = async (
} }
*/ */
let secrets; const { workspaceId } = req.params;
try { const { version, environment, folderId = "root" } = req.body;
const { workspaceId } = req.params;
const { version, environment, folderId = "root" } = req.body;
// validate secret snapshot // validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({ const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId, workspace: workspaceId,
version, version,
environment, environment,
folderId: folderId, folderId: folderId,
})
.populate<{ secretVersions: ISecretVersion[] }>({
path: "secretVersions",
select: "+secretBlindIndex",
}) })
.populate<{ secretVersions: ISecretVersion[] }>({ .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
path: "secretVersions",
select: "+secretBlindIndex",
})
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
const snapshotFolderTree = secretSnapshot.folderVersion; const snapshotFolderTree = secretSnapshot.folderVersion;
const latestFolderTree = await Folder.findOne({ const latestFolderTree = await Folder.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
}); });
const latestFolderVersion = await FolderVersion.findOne({ const latestFolderVersion = await FolderVersion.findOne({
environment, environment,
workspace: workspaceId, workspace: workspaceId,
"nodes.id": folderId, "nodes.id": folderId,
}).sort({ "nodes.version": -1 }); }).sort({ "nodes.version": -1 });
const oldSecretVersionsObj: Record<string, ISecretVersion> = {}; const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
const secretIds: Types.ObjectId[] = []; const secretIds: Types.ObjectId[] = [];
const folderIds: string[] = [folderId]; const folderIds: string[] = [folderId];
secretSnapshot.secretVersions.forEach((snapSecVer) => { secretSnapshot.secretVersions.forEach((snapSecVer) => {
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer; oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
secretIds.push(snapSecVer.secret); secretIds.push(snapSecVer.secret);
}); });
// the parent node from current latest one // the parent node from current latest one
// this will be modified according to the snapshot and latest snapshots // this will be modified according to the snapshot and latest snapshots
const newFolderTree = const newFolderTree =
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
if (newFolderTree) { if (newFolderTree) {
newFolderTree.children = snapshotFolderTree?.nodes?.children || []; newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
const queue = [newFolderTree]; const queue = [newFolderTree];
// a bfs algorithm in which we take the latest snapshots of all the folders in a level // a bfs algorithm in which we take the latest snapshots of all the folders in a level
while (queue.length) {
const groupByFolderId: Record<string, TFolderSchema> = {};
// the original queue is popped out completely to get what ever in a level
// subqueue is filled with all the children thus next level folders
// subQueue will then be transfered to the oriinal queue
const subQueue: TFolderSchema[] = [];
// get everything inside a level
while (queue.length) { while (queue.length) {
const groupByFolderId: Record<string, TFolderSchema> = {}; const folder = queue.pop() as TFolderSchema;
// the original queue is popped out completely to get what ever in a level folder.children.forEach((el) => {
// subqueue is filled with all the children thus next level folders folderIds.push(el.id); // push ids and data into queu
// subQueue will then be transfered to the oriinal queue subQueue.push(el);
const subQueue: TFolderSchema[] = []; // to modify the original tree very fast we keep a reference object
// get everything inside a level // key with folder id and pointing to the various nodes
while (queue.length) { groupByFolderId[el.id] = el;
const folder = queue.pop() as TFolderSchema;
folder.children.forEach((el) => {
folderIds.push(el.id); // push ids and data into queu
subQueue.push(el);
// to modify the original tree very fast we keep a reference object
// key with folder id and pointing to the various nodes
groupByFolderId[el.id] = el;
});
}
// get latest snapshots of all the folder
const matchWsFoldersPipeline = {
$match: {
workspace: new Types.ObjectId(workspaceId),
environment,
folderId: {
$in: Object.keys(groupByFolderId),
},
},
};
const sortByFolderIdAndVersion: PipelineStage = {
$sort: { folderId: 1, version: -1 },
};
const pickLatestVersionOfEachFolder = {
$group: {
_id: "$folderId",
latestVersion: { $first: "$version" },
doc: {
$first: "$$ROOT",
},
},
};
const populateSecVersion = {
$lookup: {
from: SecretVersion.collection.name,
localField: "doc.secretVersions",
foreignField: "_id",
as: "doc.secretVersions",
},
};
const populateFolderVersion = {
$lookup: {
from: FolderVersion.collection.name,
localField: "doc.folderVersion",
foreignField: "_id",
as: "doc.folderVersion",
},
};
const unwindFolderVerField = {
$unwind: {
path: "$doc.folderVersion",
preserveNullAndEmptyArrays: true,
},
};
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
await SecretSnapshot.aggregate([
matchWsFoldersPipeline,
sortByFolderIdAndVersion,
pickLatestVersionOfEachFolder,
populateSecVersion,
populateFolderVersion,
unwindFolderVerField,
]);
// recursive snapshotting each level
latestSnapshotsByFolders.forEach((snap) => {
// mutate the folder tree to update the nodes to the latest version tree
// we are reconstructing the folder tree by latest snapshots here
if (groupByFolderId[snap.doc.folderId]) {
groupByFolderId[snap.doc.folderId].children =
snap.doc?.folderVersion?.nodes?.children || [];
}
// push all children of next level snapshots
if (snap.doc.folderVersion?.nodes?.children) {
queue.push(...snap.doc.folderVersion.nodes.children);
}
snap.doc.secretVersions.forEach((snapSecVer) => {
// record all the secrets
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
secretIds.push(snapSecVer.secret);
});
}); });
queue.push(...subQueue);
} }
} // get latest snapshots of all the folder
const matchWsFoldersPipeline = {
// TODO: fix any $match: {
const latestSecretVersionIds = await getLatestSecretVersionIds({ workspace: new Types.ObjectId(workspaceId),
secretIds, environment,
}); folderId: {
$in: Object.keys(groupByFolderId),
// TODO: fix any
const latestSecretVersions: any = (
await SecretVersion.find(
{
_id: {
$in: latestSecretVersionIds.map((s) => s.versionId),
}, },
}, },
"secret version" };
) const sortByFolderIdAndVersion: PipelineStage = {
).reduce( $sort: { folderId: 1, version: -1 },
(accumulator, s) => ({ };
...accumulator, const pickLatestVersionOfEachFolder = {
[`${s.secret.toString()}`]: s, $group: {
}), _id: "$folderId",
{} latestVersion: { $first: "$version" },
); doc: {
$first: "$$ROOT",
},
},
};
const populateSecVersion = {
$lookup: {
from: SecretVersion.collection.name,
localField: "doc.secretVersions",
foreignField: "_id",
as: "doc.secretVersions",
},
};
const populateFolderVersion = {
$lookup: {
from: FolderVersion.collection.name,
localField: "doc.folderVersion",
foreignField: "_id",
as: "doc.folderVersion",
},
};
const unwindFolderVerField = {
$unwind: {
path: "$doc.folderVersion",
preserveNullAndEmptyArrays: true,
},
};
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
await SecretSnapshot.aggregate([
matchWsFoldersPipeline,
sortByFolderIdAndVersion,
pickLatestVersionOfEachFolder,
populateSecVersion,
populateFolderVersion,
unwindFolderVerField,
]);
const secDelQuery: Record<string, unknown> = { // recursive snapshotting each level
workspace: workspaceId, latestSnapshotsByFolders.forEach((snap) => {
environment, // mutate the folder tree to update the nodes to the latest version tree
// undefined means root thus collect all secrets // we are reconstructing the folder tree by latest snapshots here
}; if (groupByFolderId[snap.doc.folderId]) {
if (folderId !== "root" && folderIds.length) groupByFolderId[snap.doc.folderId].children =
secDelQuery.folder = { $in: folderIds }; snap.doc?.folderVersion?.nodes?.children || [];
}
// delete existing secrets // push all children of next level snapshots
await Secret.deleteMany(secDelQuery); if (snap.doc.folderVersion?.nodes?.children) {
await Folder.deleteOne({ queue.push(...snap.doc.folderVersion.nodes.children);
workspace: workspaceId, }
environment,
});
// add secrets snap.doc.secretVersions.forEach((snapSecVer) => {
secrets = await Secret.insertMany( // record all the secrets
Object.keys(oldSecretVersionsObj).map((sv) => { oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
const { secretIds.push(snapSecVer.secret);
secret: secretId, });
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
} = oldSecretVersionsObj[sv];
return {
_id: secretId,
version: latestSecretVersions[secretId.toString()].version + 1,
workspace,
type,
user,
environment,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext: "",
secretCommentIV: "",
secretCommentTag: "",
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
};
})
);
// add secret versions
const secretV = await SecretVersion.insertMany(
secrets.map(
({
_id,
version,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
})
)
);
if (newFolderTree && latestFolderTree) {
// save the updated folder tree to the present one
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
latestFolderTree._id = new Types.ObjectId();
latestFolderTree.isNew = true;
await latestFolderTree.save();
// create new folder version
const newFolderVersion = new FolderVersion({
workspace: workspaceId,
environment,
nodes: newFolderTree,
}); });
await newFolderVersion.save();
}
// update secret versions of restored secrets as not deleted queue.push(...subQueue);
await SecretVersion.updateMany( }
}
// TODO: fix any
const latestSecretVersionIds = await getLatestSecretVersionIds({
secretIds,
});
// TODO: fix any
const latestSecretVersions: any = (
await SecretVersion.find(
{ {
secret: { _id: {
$in: Object.keys(oldSecretVersionsObj).map( $in: latestSecretVersionIds.map((s) => s.versionId),
(sv) => oldSecretVersionsObj[sv].secret
),
}, },
}, },
{ "secret version"
isDeleted: false, )
} ).reduce(
); (accumulator, s) => ({
...accumulator,
[`${s.secret.toString()}`]: s,
}),
{}
);
// take secret snapshot const secDelQuery: Record<string, unknown> = {
await EESecretService.takeSecretSnapshot({ workspace: workspaceId,
workspaceId: new Types.ObjectId(workspaceId), environment,
// undefined means root thus collect all secrets
};
if (folderId !== "root" && folderIds.length)
secDelQuery.folder = { $in: folderIds };
// delete existing secrets
await Secret.deleteMany(secDelQuery);
await Folder.deleteOne({
workspace: workspaceId,
environment,
});
// add secrets
const secrets = await Secret.insertMany(
Object.keys(oldSecretVersionsObj).map((sv) => {
const {
secret: secretId,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
} = oldSecretVersionsObj[sv];
return {
_id: secretId,
version: latestSecretVersions[secretId.toString()].version + 1,
workspace,
type,
user,
environment,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext: "",
secretCommentIV: "",
secretCommentTag: "",
createdAt,
algorithm,
keyEncoding,
folder: secFolderId,
};
})
);
// add secret versions
const secretV = await SecretVersion.insertMany(
secrets.map(
({
_id,
version,
workspace,
type,
user,
environment,
secretBlindIndex,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
}) => ({
_id: new Types.ObjectId(),
secret: _id,
version,
workspace,
type,
user,
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex ?? undefined,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
algorithm,
keyEncoding,
folder: secFolderId,
})
)
);
if (newFolderTree && latestFolderTree) {
// save the updated folder tree to the present one
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
latestFolderTree._id = new Types.ObjectId();
latestFolderTree.isNew = true;
await latestFolderTree.save();
// create new folder version
const newFolderVersion = new FolderVersion({
workspace: workspaceId,
environment, environment,
folderId, nodes: newFolderTree,
});
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to roll back secret snapshot",
}); });
await newFolderVersion.save();
} }
// update secret versions of restored secrets as not deleted
await SecretVersion.updateMany(
{
secret: {
$in: Object.keys(oldSecretVersionsObj).map(
(sv) => oldSecretVersionsObj[sv].secret
),
},
},
{
isDeleted: false,
}
);
// take secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId),
environment,
folderId,
});
return res.status(200).send({ return res.status(200).send({
secrets, secrets,
}); });
@@ -587,39 +559,30 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
} }
} }
*/ */
let logs; const { workspaceId } = req.params;
try {
const { workspaceId } = req.params;
const offset: number = parseInt(req.query.offset as string); const offset: number = parseInt(req.query.offset as string);
const limit: number = parseInt(req.query.limit as string); const limit: number = parseInt(req.query.limit as string);
const sortBy: string = req.query.sortBy as string; const sortBy: string = req.query.sortBy as string;
const userId: string = req.query.userId as string; const userId: string = req.query.userId as string;
const actionNames: string = req.query.actionNames as string; const actionNames: string = req.query.actionNames as string;
logs = await Log.find({ const logs = await Log.find({
workspace: workspaceId, workspace: workspaceId,
...(userId ? { user: userId } : {}), ...(userId ? { user: userId } : {}),
...(actionNames ...(actionNames
? { ? {
actionNames: { actionNames: {
$in: actionNames.split(","), $in: actionNames.split(","),
}, },
} }
: {}), : {}),
}) })
.sort({ createdAt: sortBy === "recent" ? -1 : 1 }) .sort({ createdAt: sortBy === "recent" ? -1 : 1 })
.skip(offset) .skip(offset)
.limit(limit) .limit(limit)
.populate("actions") .populate("actions")
.populate("user serviceAccount serviceTokenData"); .populate("user serviceAccount serviceTokenData");
} catch (err) {
Sentry.setUser({ email: req.user.email });
Sentry.captureException(err);
return res.status(400).send({
message: "Failed to get workspace logs",
});
}
return res.status(200).send({ return res.status(200).send({
logs, logs,
+17 -24
View File
@@ -1,5 +1,4 @@
import NodeCache from 'node-cache'; import NodeCache from 'node-cache';
import * as Sentry from '@sentry/node';
import { import {
getLicenseKey, getLicenseKey,
getLicenseServerKey, getLicenseServerKey,
@@ -98,35 +97,29 @@ class EELicenseService {
const licenseServerKey = await getLicenseServerKey(); const licenseServerKey = await getLicenseServerKey();
const licenseKey = await getLicenseKey(); const licenseKey = await getLicenseKey();
try { if (licenseServerKey) {
if (licenseServerKey) { // license server key is present -> validate it
// license server key is present -> validate it const token = await refreshLicenseServerKeyToken()
const token = await refreshLicenseServerKeyToken()
if (token) { if (token) {
this.instanceType = 'cloud'; this.instanceType = 'cloud';
}
return;
} }
if (licenseKey) { return;
// license key is present -> validate it }
const token = await refreshLicenseKeyToken();
if (token) { if (licenseKey) {
const { data: { currentPlan } } = await licenseKeyRequest.get( // license key is present -> validate it
`${await getLicenseServerUrl()}/api/license/v1/plan` const token = await refreshLicenseKeyToken();
);
this.globalFeatureSet = currentPlan; if (token) {
this.instanceType = 'enterprise-self-hosted'; const { data: { currentPlan } } = await licenseKeyRequest.get(
} `${await getLicenseServerUrl()}/api/license/v1/plan`
);
this.globalFeatureSet = currentPlan;
this.instanceType = 'enterprise-self-hosted';
} }
} catch (err) {
// case: self-hosted free
Sentry.setUser(null);
Sentry.captureException(err);
} }
} }
+162 -213
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { import {
Bot, Bot,
@@ -48,66 +47,59 @@ const handleOAuthExchangeHelper = async ({
code: string; code: string;
environment: string; environment: string;
}) => { }) => {
let integrationAuth; const bot = await Bot.findOne({
try { workspace: workspaceId,
const bot = await Bot.findOne({ isActive: true
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;
}
const 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 (!bot) throw new Error('Bot must be enabled for OAuth2 code-token exchange'); if (res.accessToken) {
// case: access token returned from exchange
// exchange code for access and refresh tokens // set integration auth access token
const res = await exchangeCode({ await setIntegrationAuthAccessHelper({
integration, integrationAuthId: integrationAuth._id.toString(),
code accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
}); });
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(),
accessId: null,
accessToken: res.accessToken,
accessExpiresAt: res.accessExpiresAt
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to handle OAuth2 code-token exchange')
} }
return integrationAuth; return integrationAuth;
@@ -125,47 +117,40 @@ const syncIntegrationsHelper = async ({
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
}) => { }) => {
let integrations; const integrations = await Integration.find({
try { workspace: workspaceId,
integrations = await Integration.find({ ...(environment ? {
workspace: workspaceId, environment
...(environment ? { } : {}),
environment isActive: true,
} : {}), app: { $ne: null }
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({ // issue here?
workspaceId: integration.workspace,
environment: integration.environment
}); });
// for each workspace integration, sync/push secrets const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth);
// to that integration if (!integrationAuth) throw new Error('Failed to find integration auth');
for await (const integration of integrations) {
// get workspace, environment (shared) secrets
const secrets = await BotService.getSecrets({ // issue here?
workspaceId: integration.workspace,
environment: integration.environment
});
const integrationAuth = await IntegrationAuth.findById(integration.integrationAuth); // get integration auth access token
if (!integrationAuth) throw new Error('Failed to find integration auth'); const access = await getIntegrationAuthAccessHelper({
integrationAuthId: integration.integrationAuth
});
// get integration auth access token // sync secrets to integration
const access = await getIntegrationAuthAccessHelper({ await syncSecrets({
integrationAuthId: integration.integrationAuth integration,
}); integrationAuth,
secrets,
// sync secrets to integration accessId: access.accessId === undefined ? null : access.accessId,
await syncSecrets({ accessToken: access.accessToken
integration, });
integrationAuth,
secrets,
accessId: access.accessId === undefined ? null : access.accessId,
accessToken: access.accessToken
});
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to sync secrets to integrations');
} }
} }
@@ -178,30 +163,18 @@ const syncIntegrationsHelper = async ({
* @param {String} refreshToken - decrypted refresh token * @param {String} refreshToken - decrypted refresh token
*/ */
const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { const getIntegrationAuthRefreshHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let refreshToken; const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
try { if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
const integrationAuth = await IntegrationAuth
.findById(integrationAuthId)
.select('+refreshCiphertext +refreshIV +refreshTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); const refreshToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
refreshToken = await BotService.decryptSymmetric({ ciphertext: integrationAuth.refreshCiphertext as string,
workspaceId: integrationAuth.workspace, iv: integrationAuth.refreshIV as string,
ciphertext: integrationAuth.refreshCiphertext as string, tag: integrationAuth.refreshTag as string
iv: integrationAuth.refreshIV as string, });
tag: integrationAuth.refreshTag as string
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
if(err instanceof RequestError)
throw err
else
throw new Error('Failed to get integration refresh token');
}
return refreshToken; return refreshToken;
} }
@@ -217,50 +190,40 @@ const syncIntegrationsHelper = async ({
const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => { const getIntegrationAuthAccessHelper = async ({ integrationAuthId }: { integrationAuthId: Types.ObjectId }) => {
let accessId; let accessId;
let accessToken; let accessToken;
try { const integrationAuth = await IntegrationAuth
const integrationAuth = await IntegrationAuth .findById(integrationAuthId)
.findById(integrationAuthId) .select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
.select('workspace integration +accessCiphertext +accessIV +accessTag +accessExpiresAt + refreshCiphertext +accessIdCiphertext +accessIdIV +accessIdTag');
if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'}); if (!integrationAuth) throw UnauthorizedRequestError({message: 'Failed to locate Integration Authentication credentials'});
accessToken = await BotService.decryptSymmetric({ accessToken = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessCiphertext as string, ciphertext: integrationAuth.accessCiphertext as string,
iv: integrationAuth.accessIV as string, iv: integrationAuth.accessIV as string,
tag: integrationAuth.accessTag as string tag: integrationAuth.accessTag as string
}); });
if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) { if (integrationAuth?.accessExpiresAt && integrationAuth?.refreshCiphertext) {
// there is a access token expiration date // there is a access token expiration date
// and refresh token to exchange with the OAuth2 server // and refresh token to exchange with the OAuth2 server
if (integrationAuth.accessExpiresAt < new Date()) { if (integrationAuth.accessExpiresAt < new Date()) {
// access token is expired // access token is expired
const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId }); const refreshToken = await getIntegrationAuthRefreshHelper({ integrationAuthId });
accessToken = await exchangeRefresh({ accessToken = await exchangeRefresh({
integrationAuth, integrationAuth,
refreshToken refreshToken
});
}
}
if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
accessId = await BotService.decryptSymmetric({
workspaceId: integrationAuth.workspace,
ciphertext: integrationAuth.accessIdCiphertext as string,
iv: integrationAuth.accessIdIV as string,
tag: integrationAuth.accessIdTag as string
}); });
} }
}
} catch (err) { if (integrationAuth?.accessIdCiphertext && integrationAuth?.accessIdIV && integrationAuth?.accessIdTag) {
Sentry.setUser(null); accessId = await BotService.decryptSymmetric({
Sentry.captureException(err); workspaceId: integrationAuth.workspace,
if(err instanceof RequestError) ciphertext: integrationAuth.accessIdCiphertext as string,
throw err iv: integrationAuth.accessIdIV as string,
else tag: integrationAuth.accessIdTag as string
throw new Error('Failed to get integration access token'); });
} }
return ({ return ({
@@ -285,34 +248,27 @@ const setIntegrationAuthRefreshHelper = async ({
refreshToken: string; refreshToken: string;
}) => { }) => {
let integrationAuth; let integrationAuth = await IntegrationAuth
try { .findById(integrationAuthId);
integrationAuth = await IntegrationAuth
.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw new Error('Failed to find integration auth');
const obj = await BotService.encryptSymmetric({ const obj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
plaintext: refreshToken plaintext: refreshToken
}); });
integrationAuth = await IntegrationAuth.findOneAndUpdate({ integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId _id: integrationAuthId
}, { }, {
refreshCiphertext: obj.ciphertext, refreshCiphertext: obj.ciphertext,
refreshIV: obj.iv, refreshIV: obj.iv,
refreshTag: obj.tag, refreshTag: obj.tag,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}, { }, {
new: true new: true
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to set integration auth refresh token');
}
return integrationAuth; return integrationAuth;
} }
@@ -337,46 +293,39 @@ const setIntegrationAuthAccessHelper = async ({
accessToken: string; accessToken: string;
accessExpiresAt: Date | undefined; accessExpiresAt: Date | undefined;
}) => { }) => {
let integrationAuth; let integrationAuth = await IntegrationAuth.findById(integrationAuthId);
try {
integrationAuth = await IntegrationAuth.findById(integrationAuthId);
if (!integrationAuth) throw new Error('Failed to find integration auth'); if (!integrationAuth) throw new Error('Failed to find integration auth');
const encryptedAccessTokenObj = await BotService.encryptSymmetric({ const encryptedAccessTokenObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessToken
});
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace, workspaceId: integrationAuth.workspace,
plaintext: accessToken plaintext: accessId
}); });
let encryptedAccessIdObj;
if (accessId) {
encryptedAccessIdObj = await BotService.encryptSymmetric({
workspaceId: integrationAuth.workspace,
plaintext: accessId
});
}
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to save integration auth access token');
} }
integrationAuth = await IntegrationAuth.findOneAndUpdate({
_id: integrationAuthId
}, {
accessIdCiphertext: encryptedAccessIdObj?.ciphertext ?? undefined,
accessIdIV: encryptedAccessIdObj?.iv ?? undefined,
accessIdTag: encryptedAccessIdObj?.tag ?? undefined,
accessCiphertext: encryptedAccessTokenObj.ciphertext,
accessIV: encryptedAccessTokenObj.iv,
accessTag: encryptedAccessTokenObj.tag,
accessExpiresAt,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
}, {
new: true
});
return integrationAuth; return integrationAuth;
} }
+35 -58
View File
@@ -1,7 +1,6 @@
import * as Sentry from '@sentry/node'; import { Types } from "mongoose";
import { Types } from 'mongoose'; import { Membership, Key } from "../models";
import { Membership, Key } from '../models'; import { MembershipNotFoundError, BadRequestError } from "../utils/errors";
import { MembershipNotFoundError, BadRequestError } from '../utils/errors';
/** /**
* Validate that user with id [userId] is a member of workspace with id [workspaceId] * Validate that user with id [userId] is a member of workspace with id [workspaceId]
@@ -18,23 +17,23 @@ const validateMembership = async ({
}: { }: {
userId: Types.ObjectId | string; userId: Types.ObjectId | string;
workspaceId: Types.ObjectId | string; workspaceId: Types.ObjectId | string;
acceptedRoles?: Array<'admin' | 'member'>; acceptedRoles?: Array<"admin" | "member">;
}) => { }) => {
const membership = await Membership.findOne({ const membership = await Membership.findOne({
user: userId, user: userId,
workspace: workspaceId, workspace: workspaceId,
}).populate('workspace'); }).populate("workspace");
if (!membership) { if (!membership) {
throw MembershipNotFoundError({ throw MembershipNotFoundError({
message: 'Failed to find workspace membership', message: "Failed to find workspace membership",
}); });
} }
if (acceptedRoles) { if (acceptedRoles) {
if (!acceptedRoles.includes(membership.role)) { if (!acceptedRoles.includes(membership.role)) {
throw BadRequestError({ throw BadRequestError({
message: 'Failed authorization for membership role', message: "Failed authorization for membership role",
}); });
} }
} }
@@ -48,15 +47,7 @@ const validateMembership = async ({
* @return {Object} membership - membership * @return {Object} membership - membership
*/ */
const findMembership = async (queryObj: any) => { const findMembership = async (queryObj: any) => {
let membership; const membership = await Membership.findOne(queryObj);
try {
membership = await Membership.findOne(queryObj);
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to find membership');
}
return membership; return membership;
}; };
@@ -77,31 +68,24 @@ const addMemberships = async ({
workspaceId: string; workspaceId: string;
roles: string[]; roles: string[];
}): Promise<void> => { }): Promise<void> => {
try { const operations = userIds.map((userId, idx) => {
const operations = userIds.map((userId, idx) => { return {
return { updateOne: {
updateOne: { filter: {
filter: { user: userId,
user: userId, workspace: workspaceId,
workspace: workspaceId, role: roles[idx],
role: roles[idx],
},
update: {
user: userId,
workspace: workspaceId,
role: roles[idx],
},
upsert: true,
}, },
}; update: {
}); user: userId,
workspace: workspaceId,
await Membership.bulkWrite(operations as any); role: roles[idx],
} catch (err) { },
Sentry.setUser(null); upsert: true,
Sentry.captureException(err); },
throw new Error('Failed to add users to workspace'); };
} });
await Membership.bulkWrite(operations as any);
}; };
/** /**
@@ -110,24 +94,17 @@ const addMemberships = async ({
* @param {String} obj.membershipId - id of membership to delete * @param {String} obj.membershipId - id of membership to delete
*/ */
const deleteMembership = async ({ membershipId }: { membershipId: string }) => { const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
let deletedMembership; const deletedMembership = await Membership.findOneAndDelete({
try { _id: membershipId,
deletedMembership = await Membership.findOneAndDelete({ });
_id: membershipId,
});
// delete keys associated with the membership // delete keys associated with the membership
if (deletedMembership?.user) { if (deletedMembership?.user) {
// case: membership had a registered user // case: membership had a registered user
await Key.deleteMany({ await Key.deleteMany({
receiver: deletedMembership.user, receiver: deletedMembership.user,
workspace: deletedMembership.workspace, workspace: deletedMembership.workspace,
}); });
}
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete membership');
} }
return deletedMembership; return deletedMembership;
+12 -18
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import fs from 'fs'; import fs from 'fs';
import path from 'path'; import path from 'path';
import handlebars from 'handlebars'; import handlebars from 'handlebars';
@@ -26,24 +25,19 @@ const sendMail = async ({
substitutions: any; substitutions: any;
}) => { }) => {
if (await getSmtpConfigured()) { if (await getSmtpConfigured()) {
try { const html = fs.readFileSync(
const html = fs.readFileSync( path.resolve(__dirname, '../templates/' + template),
path.resolve(__dirname, '../templates/' + template), 'utf8'
'utf8' );
); const temp = handlebars.compile(html);
const temp = handlebars.compile(html); const htmlToSend = temp(substitutions);
const htmlToSend = temp(substitutions);
await smtpTransporter.sendMail({ await smtpTransporter.sendMail({
from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`, from: `"${await getSmtpFromName()}" <${await getSmtpFromAddress()}>`,
to: recipients.join(', '), to: recipients.join(', '),
subject: subjectLine, subject: subjectLine,
html: htmlToSend html: htmlToSend
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
}
} }
}; };
+18 -33
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { IUser } from '../models'; import { IUser } from '../models';
import { createOrganization } from './organization'; import { createOrganization } from './organization';
import { addMembershipsOrg } from './membershipOrg'; import { addMembershipsOrg } from './membershipOrg';
@@ -15,28 +14,20 @@ import { TOKEN_EMAIL_CONFIRMATION } from '../variables';
* @returns {Boolean} success - whether or not operation was successful * @returns {Boolean} success - whether or not operation was successful
*/ */
const sendEmailVerification = async ({ email }: { email: string }) => { const sendEmailVerification = async ({ email }: { email: string }) => {
try { const token = await TokenService.createToken({
const token = await TokenService.createToken({ type: TOKEN_EMAIL_CONFIRMATION,
type: TOKEN_EMAIL_CONFIRMATION, email
email });
});
// send mail // send mail
await sendMail({ await sendMail({
template: 'emailVerification.handlebars', template: 'emailVerification.handlebars',
subjectLine: 'Infisical confirmation code', subjectLine: 'Infisical confirmation code',
recipients: [email], recipients: [email],
substitutions: { substitutions: {
code: token code: token
} }
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error(
"Ouch. We weren't able to send your email verification code"
);
}
}; };
/** /**
@@ -52,17 +43,11 @@ const checkEmailVerification = async ({
email: string; email: string;
code: string; code: string;
}) => { }) => {
try { await TokenService.validateToken({
await TokenService.validateToken({ type: TOKEN_EMAIL_CONFIRMATION,
type: TOKEN_EMAIL_CONFIRMATION, email,
email, token: code
token: code });
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error("Oops. We weren't able to verify");
}
}; };
/** /**
+28 -42
View File
@@ -1,4 +1,3 @@
import * as Sentry from '@sentry/node';
import { import {
Workspace, Workspace,
Bot, Bot,
@@ -23,31 +22,24 @@ const createWorkspace = async ({
name: string; name: string;
organizationId: string; organizationId: string;
}) => { }) => {
let workspace; // create workspace
try { const workspace = await new Workspace({
// create workspace name,
workspace = await new Workspace({ organization: organizationId,
name, autoCapitalization: true
organization: organizationId, }).save();
autoCapitalization: true
}).save();
// initialize bot for workspace // initialize bot for workspace
await createBot({ await createBot({
name: 'Infisical Bot', name: 'Infisical Bot',
workspaceId: workspace._id workspaceId: workspace._id
}); });
// initialize blind index salt for workspace // initialize blind index salt for workspace
await SecretService.createSecretBlindIndexData({ await SecretService.createSecretBlindIndexData({
workspaceId: workspace._id workspaceId: workspace._id
}); });
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to create workspace');
}
return workspace; return workspace;
}; };
@@ -59,25 +51,19 @@ const createWorkspace = async ({
* @param {String} obj.id - id of workspace to delete * @param {String} obj.id - id of workspace to delete
*/ */
const deleteWorkspace = async ({ id }: { id: string }) => { const deleteWorkspace = async ({ id }: { id: string }) => {
try { await Workspace.deleteOne({ _id: id });
await Workspace.deleteOne({ _id: id }); await Bot.deleteOne({
await Bot.deleteOne({ workspace: id
workspace: id });
}); await Membership.deleteMany({
await Membership.deleteMany({ workspace: id
workspace: id });
}); await Secret.deleteMany({
await Secret.deleteMany({ workspace: id
workspace: id });
}); await Key.deleteMany({
await Key.deleteMany({ workspace: id
workspace: id });
});
} catch (err) {
Sentry.setUser(null);
Sentry.captureException(err);
throw new Error('Failed to delete workspace');
}
}; };
export { export {
File diff suppressed because it is too large Load Diff