feat(webhook): implemented api for webhooks

This commit is contained in:
akhilmhdh
2023-07-13 23:21:18 +05:30
parent 8a19cfe0c6
commit 0f81c78639
18 changed files with 748 additions and 364 deletions
+18 -16
View File
@@ -14,22 +14,24 @@ import * as userActionController from "./userActionController";
import * as userController from "./userController"; import * as userController from "./userController";
import * as workspaceController from "./workspaceController"; import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController";
export { export {
authController, authController,
botController, botController,
integrationAuthController, integrationAuthController,
integrationController, integrationController,
keyController, keyController,
membershipController, membershipController,
membershipOrgController, membershipOrgController,
organizationController, organizationController,
passwordController, passwordController,
secretController, secretController,
serviceTokenController, serviceTokenController,
signupController, signupController,
userActionController, userActionController,
userController, userController,
workspaceController, workspaceController,
secretScanningController secretScanningController,
webhookController
}; };
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Integration } from "../../models"; import { Integration } from "../../models";
import { EventService } from "../../services"; import { EventService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventPushSecrets, eventStartIntegration } from "../../events";
import Folder from "../../models/folder"; import Folder from "../../models/folder";
import { getFolderByPath } from "../../services/FolderService"; import { getFolderByPath } from "../../services/FolderService";
import { BadRequestError } from "../../utils/errors"; import { BadRequestError } from "../../utils/errors";
@@ -27,19 +27,19 @@ export const createIntegration = async (req: Request, res: Response) => {
owner, owner,
path, path,
region, region,
secretPath, secretPath
} = req.body; } = req.body;
const folders = await Folder.findOne({ const folders = await Folder.findOne({
workspace: req.integrationAuth.workspace._id, workspace: req.integrationAuth.workspace._id,
environment: sourceEnvironment, environment: sourceEnvironment
}); });
if (folders) { if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath); const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) { if (!folder) {
throw BadRequestError({ throw BadRequestError({
message: "Path for service token does not exist", message: "Path for service token does not exist"
}); });
} }
} }
@@ -62,21 +62,21 @@ export const createIntegration = async (req: Request, res: Response) => {
region, region,
secretPath, secretPath,
integration: req.integrationAuth.integration, integration: req.integrationAuth.integration,
integrationAuth: new Types.ObjectId(integrationAuthId), integrationAuth: new Types.ObjectId(integrationAuthId)
}).save(); }).save();
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventStartIntegration({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment: sourceEnvironment, environment: sourceEnvironment
}), })
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration, integration
}); });
}; };
@@ -97,26 +97,26 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, // github-specific integration param owner, // github-specific integration param
secretPath, secretPath
} = req.body; } = req.body;
const folders = await Folder.findOne({ const folders = await Folder.findOne({
workspace: req.integration.workspace, workspace: req.integration.workspace,
environment, environment
}); });
if (folders) { if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath); const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) { if (!folder) {
throw BadRequestError({ throw BadRequestError({
message: "Path for service token does not exist", message: "Path for service token does not exist"
}); });
} }
} }
const integration = await Integration.findOneAndUpdate( const integration = await Integration.findOneAndUpdate(
{ {
_id: req.integration._id, _id: req.integration._id
}, },
{ {
environment, environment,
@@ -125,25 +125,25 @@ export const updateIntegration = async (req: Request, res: Response) => {
appId, appId,
targetEnvironment, targetEnvironment,
owner, owner,
secretPath, secretPath
}, },
{ {
new: true, new: true
} }
); );
if (integration) { if (integration) {
// trigger event - push secrets // trigger event - push secrets
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventStartIntegration({
workspaceId: integration.workspace, workspaceId: integration.workspace,
environment, environment
}), })
}); });
} }
return res.status(200).send({ return res.status(200).send({
integration, integration
}); });
}; };
@@ -158,12 +158,12 @@ export const deleteIntegration = async (req: Request, res: Response) => {
const { integrationId } = req.params; const { integrationId } = req.params;
const 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");
return res.status(200).send({ return res.status(200).send({
integration, integration
}); });
}; };
@@ -80,7 +80,8 @@ export const pushSecrets = async (req: Request, res: Response) => {
EventService.handleEvent({ EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment environment,
secretPath: "/"
}) })
}); });
@@ -0,0 +1,140 @@
import { Request, Response } from "express";
import { Types } from "mongoose";
import { client, getRootEncryptionKey } from "../../config";
import { validateMembership } from "../../helpers";
import Webhook from "../../models/webhooks";
import { getWebhookPayload, triggerWebhookRequest } from "../../services/WebhookService";
import { BadRequestError } from "../../utils/errors";
import { ADMIN, ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, MEMBER } from "../../variables";
export const createWebhook = async (req: Request, res: Response) => {
const { webhookUrl, webhookSecretKey, environment, workspaceId, secretPath } = req.body;
const webhook = new Webhook({
workspace: workspaceId,
environment,
secretPath,
url: webhookUrl,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_BASE64
});
if (webhookSecretKey) {
const rootEncryptionKey = await getRootEncryptionKey();
const { ciphertext, iv, tag } = client.encryptSymmetric(webhookSecretKey, rootEncryptionKey);
webhook.iv = iv;
webhook.tag = tag;
webhook.encryptedSecretKey = ciphertext;
}
await webhook.save();
return res.status(200).send({
webhook,
message: "successfully created webhook"
});
};
export const updateWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const { isDisabled } = req.body;
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
// check that user is a member of the workspace
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
if (typeof isDisabled !== undefined) {
webhook.isDisabled = isDisabled;
}
await webhook.save();
return res.status(200).send({
webhook,
message: "successfully updated webhook"
});
};
export const deleteWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
await webhook.remove();
return res.status(200).send({
message: "successfully removed webhook"
});
};
export const testWebhook = async (req: Request, res: Response) => {
const { webhookId } = req.params;
const webhook = await Webhook.findById(webhookId);
if (!webhook) {
throw BadRequestError({ message: "Webhook not found!!" });
}
await validateMembership({
userId: req.user._id.toString(),
workspaceId: webhook.workspace,
acceptedRoles: [ADMIN, MEMBER]
});
try {
await triggerWebhookRequest(
webhook,
getWebhookPayload(
"test",
webhook.workspace.toString(),
webhook.environment,
webhook.secretPath
)
);
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "success",
lastRunErrorMessage: null
});
} catch (err) {
await Webhook.findByIdAndUpdate(webhookId, {
lastStatus: "failed",
lastRunErrorMessage: (err as Error).message
});
return res.status(400).send({
message: "Failed to receive response",
error: (err as Error).message
});
}
return res.status(200).send({
message: "Successfully received response"
});
};
export const listWebhooks = async (req: Request, res: Response) => {
const { environment, workspaceId, secretPath } = req.query;
const optionalFilters: Record<string, string> = {};
if (environment) optionalFilters.environment = environment as string;
if (secretPath) optionalFilters.secretPath = secretPath as string;
const webhooks = await Webhook.find({
workspace: new Types.ObjectId(workspaceId as string),
...optionalFilters
});
return res.status(200).send({
webhooks
});
};
+38 -22
View File
@@ -30,9 +30,11 @@ import Folder from "../../models/folder";
import { import {
getFolderByPath, getFolderByPath,
getFolderIdFromServiceToken, getFolderIdFromServiceToken,
searchByFolderId searchByFolderId,
searchByFolderIdWithDir
} from "../../services/FolderService"; } from "../../services/FolderService";
import { isValidScope } from "../../helpers/secrets"; import { isValidScope } from "../../helpers/secrets";
import path from "path";
/** /**
* Peform a batch of any specified CUD secret operations * Peform a batch of any specified CUD secret operations
@@ -47,14 +49,13 @@ export const batchSecrets = async (req: Request, res: Response) => {
const { const {
workspaceId, workspaceId,
environment, environment,
requests, requests
secretPath
}: { }: {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
requests: BatchSecretRequest[]; requests: BatchSecretRequest[];
secretPath: string;
} = req.body; } = req.body;
let secretPath = req.body.secretPath as string;
let folderId = req.body.folderId as string; let folderId = req.body.folderId as string;
const createSecrets: BatchSecret[] = []; const createSecrets: BatchSecret[] = [];
@@ -68,10 +69,6 @@ export const batchSecrets = async (req: Request, res: Response) => {
}); });
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (folders && folderId !== "root") {
const folder = searchByFolderId(folders.nodes, folderId as string);
if (!folder) throw BadRequestError({ message: "Folder not found" });
}
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
@@ -87,6 +84,15 @@ export const batchSecrets = async (req: Request, res: Response) => {
folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
} }
if (folders && folderId !== "root") {
const folder = searchByFolderIdWithDir(folders.nodes, folderId as string);
if (!folder?.folder) throw BadRequestError({ message: "Folder not found" });
secretPath = path.join(
"/",
...folder.dir.map(({ name }) => name).filter((name) => name !== "root")
);
}
for await (const request of requests) { for await (const request of requests) {
// do a validation // do a validation
@@ -319,7 +325,10 @@ export const batchSecrets = async (req: Request, res: Response) => {
// // trigger event - push secrets // // trigger event - push secrets
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId),
environment,
// root condition else this will be filled according to the path or folderid
secretPath: secretPath || "/"
}) })
}); });
@@ -535,7 +544,9 @@ export const createSecrets = async (req: Request, res: Response) => {
// trigger event - push secrets // trigger event - push secrets
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath: secretPath || "/"
}) })
}); });
}, 5000); }, 5000);
@@ -1033,13 +1044,16 @@ export const updateSecrets = async (req: Request, res: Response) => {
Object.keys(workspaceSecretObj).forEach(async (key) => { Object.keys(workspaceSecretObj).forEach(async (key) => {
// trigger event - push secrets // trigger event - push secrets
setTimeout(async () => { // This route is not used anymore thus keep it commented out as it does not expose environment
await EventService.handleEvent({ // it will end up creating a lot of requests from the server
event: eventPushSecrets({ // setTimeout(async () => {
workspaceId: new Types.ObjectId(key) // await EventService.handleEvent({
}) // event: eventPushSecrets({
}); // workspaceId: new Types.ObjectId(key),
}, 10000); // environment,
// })
// });
// }, 10000);
const updateAction = await EELogService.createAction({ const updateAction = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS, name: ACTION_UPDATE_SECRETS,
@@ -1174,11 +1188,13 @@ export const deleteSecrets = async (req: Request, res: Response) => {
Object.keys(workspaceSecretObj).forEach(async (key) => { Object.keys(workspaceSecretObj).forEach(async (key) => {
// trigger event - push secrets // trigger event - push secrets
await EventService.handleEvent({ // DEPRECIATED(akhilmhdh): as this would cause server to send so many request
event: eventPushSecrets({ // and this route is not used anymore thus like snapshot keeping it commented out
workspaceId: new Types.ObjectId(key) // await EventService.handleEvent({
}) // event: eventPushSecrets({
}); // workspaceId: new Types.ObjectId(key)
// })
// });
const deleteAction = await EELogService.createAction({ const deleteAction = await EELogService.createAction({
name: ACTION_DELETE_SECRETS, name: ACTION_DELETE_SECRETS,
userId: req.user?._id, userId: req.user?._id,
+91 -103
View File
@@ -1,34 +1,29 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Key, Membership, ServiceTokenData, Workspace } from "../../models";
import { import {
Key, pullSecrets as pull,
Membership, v2PushSecrets as push,
ServiceTokenData, reformatPullSecrets
Workspace,
} from "../../models";
import {
pullSecrets as pull,
v2PushSecrets as push,
reformatPullSecrets,
} from "../../helpers/secret"; } from "../../helpers/secret";
import { pushKeys } from "../../helpers/key"; import { pushKeys } from "../../helpers/key";
import { EventService, TelemetryService } from "../../services"; import { EventService, TelemetryService } from "../../services";
import { eventPushSecrets } from "../../events"; import { eventPushSecrets } from "../../events";
interface V2PushSecret { interface V2PushSecret {
type: string; // personal or shared type: string; // personal or shared
secretKeyCiphertext: string; secretKeyCiphertext: string;
secretKeyIV: string; secretKeyIV: string;
secretKeyTag: string; secretKeyTag: string;
secretKeyHash: string; secretKeyHash: string;
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretValueHash: string; secretValueHash: string;
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
} }
/** /**
@@ -39,7 +34,7 @@ interface V2PushSecret {
* @returns * @returns
*/ */
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]
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;
@@ -62,13 +57,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
environment, environment,
secrets, secrets,
channel: channel ? channel : "cli", channel: channel ? channel : "cli",
ipAddress: req.realIP, ipAddress: req.realIP
}); });
await pushKeys({ await pushKeys({
userId: req.user._id, userId: req.user._id,
workspaceId, workspaceId,
keys, keys
}); });
if (postHogClient) { if (postHogClient) {
@@ -79,8 +74,8 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : "cli", channel: channel ? channel : "cli"
}, }
}); });
} }
@@ -89,12 +84,13 @@ export const pushWorkspaceSecrets = async (req: Request, res: Response) => {
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath: "/"
})
}); });
return res.status(200).send({ return res.status(200).send({
message: "Successfully uploaded workspace secrets", message: "Successfully uploaded workspace secrets"
}); });
}; };
/** /**
@@ -105,7 +101,7 @@ 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; let secrets;
const postHogClient = await TelemetryService.getPostHogClient(); const postHogClient = await TelemetryService.getPostHogClient();
const environment: string = req.query.environment as string; const environment: string = req.query.environment as string;
const channel: string = req.query.channel as string; const channel: string = req.query.channel as string;
@@ -128,7 +124,7 @@ export const pullSecrets = async (req: Request, res: Response) => {
workspaceId, workspaceId,
environment, environment,
channel: channel ? channel : "cli", channel: channel ? channel : "cli",
ipAddress: req.realIP, ipAddress: req.realIP
}); });
if (channel !== "cli") { if (channel !== "cli") {
@@ -144,18 +140,18 @@ export const pullSecrets = async (req: Request, res: Response) => {
numberOfSecrets: secrets.length, numberOfSecrets: secrets.length,
environment, environment,
workspaceId, workspaceId,
channel: channel ? channel : "cli", channel: channel ? channel : "cli"
}, }
}); });
} }
return res.status(200).send({ return res.status(200).send({
secrets, secrets
}); });
}; };
export const getWorkspaceKey = async (req: Request, res: Response) => { export const getWorkspaceKey = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return encrypted project key' #swagger.summary = 'Return encrypted project key'
#swagger.description = 'Return encrypted project key' #swagger.description = 'Return encrypted project key'
@@ -183,43 +179,38 @@ export const getWorkspaceKey = async (req: Request, res: Response) => {
} }
} }
*/ */
let key; let key;
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");
return res.status(200).json(key); return res.status(200).json(key);
} };
export const getWorkspaceServiceTokenData = async ( export const getWorkspaceServiceTokenData = async (req: Request, res: Response) => {
req: Request,
res: Response
) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const serviceTokenData = await ServiceTokenData const serviceTokenData = await ServiceTokenData.find({
.find({ workspace: workspaceId
workspace: workspaceId, }).select("+encryptedKey +iv +tag");
})
.select("+encryptedKey +iv +tag");
return res.status(200).send({ return res.status(200).send({
serviceTokenData, serviceTokenData
}); });
} };
/** /**
* Return memberships for workspace with id [workspaceId] * Return memberships for workspace with id [workspaceId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const getWorkspaceMemberships = async (req: Request, res: Response) => { export const getWorkspaceMemberships = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Return project memberships' #swagger.summary = 'Return project memberships'
#swagger.description = 'Return project memberships' #swagger.description = 'Return project memberships'
@@ -255,22 +246,22 @@ export const getWorkspaceMemberships = async (req: Request, res: Response) => {
const { workspaceId } = req.params; const { workspaceId } = req.params;
const memberships = await Membership.find({ const memberships = await Membership.find({
workspace: workspaceId, workspace: workspaceId
}).populate("user", "+publicKey"); }).populate("user", "+publicKey");
return res.status(200).send({ return res.status(200).send({
memberships, memberships
}); });
} };
/** /**
* Update role of membership with id [membershipId] to role [role] * Update role of membership with id [membershipId] to role [role]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const updateWorkspaceMembership = async (req: Request, res: Response) => { export const updateWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Update project membership' #swagger.summary = 'Update project membership'
#swagger.description = 'Update project membership' #swagger.description = 'Update project membership'
@@ -323,33 +314,32 @@ export const updateWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
const { const { membershipId } = req.params;
membershipId,
} = req.params;
const { role } = req.body; const { role } = req.body;
const membership = await Membership.findByIdAndUpdate( const membership = await Membership.findByIdAndUpdate(
membershipId, membershipId,
{ {
role, role
}, { },
new: true, {
new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
membership, membership
}); });
} };
/** /**
* Delete workspace membership with id [membershipId] * Delete workspace membership with id [membershipId]
* @param req * @param req
* @param res * @param res
* @returns * @returns
*/ */
export const deleteWorkspaceMembership = async (req: Request, res: Response) => { export const deleteWorkspaceMembership = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Delete project membership' #swagger.summary = 'Delete project membership'
#swagger.description = 'Delete project membership' #swagger.description = 'Delete project membership'
@@ -385,23 +375,21 @@ export const deleteWorkspaceMembership = async (req: Request, res: Response) =>
} }
} }
*/ */
const { const { membershipId } = req.params;
membershipId,
} = req.params;
const 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
}); });
return res.status(200).send({ return res.status(200).send({
membership, membership
}); });
} };
/** /**
* Change autoCapitilzation Rule of workspace * Change autoCapitilzation Rule of workspace
@@ -415,18 +403,18 @@ export const toggleAutoCapitalization = async (req: Request, res: Response) => {
const workspace = await Workspace.findOneAndUpdate( const workspace = await Workspace.findOneAndUpdate(
{ {
_id: workspaceId, _id: workspaceId
}, },
{ {
autoCapitalization, autoCapitalization
}, },
{ {
new: true, new: true
} }
); );
return res.status(200).send({ return res.status(200).send({
message: "Successfully changed autoCapitalization setting", message: "Successfully changed autoCapitalization setting",
workspace, workspace
}); });
}; };
+54 -72
View File
@@ -21,22 +21,22 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
return res.status(200).send({ return res.status(200).send({
secrets: secrets.map((secret) => { secrets: secrets.map((secret) => {
const rep = repackageSecretToRaw({ const rep = repackageSecretToRaw({
secret, secret,
key, key
}); });
return rep; return rep;
}), })
}); });
}; };
@@ -58,54 +58,47 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret, secret,
key, key
}), })
}); });
}; };
/** /**
* Create secret with name [secretName] in plaintext * Create secret with name [secretName] in plaintext
* @param req * @param req
* @param res * @param res
*/ */
export const createSecretRaw = async (req: Request, res: Response) => { export const createSecretRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretValue, secretComment, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretValue,
secretComment,
secretPath = "/",
} = req.body;
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretKeyEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretName, plaintext: secretName,
key, key
}); });
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue, plaintext: secretValue,
key, key
}); });
const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretCommentEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretComment, plaintext: secretComment,
key, key
}); });
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
@@ -123,14 +116,15 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretPath, secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext, secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv, secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag, secretCommentTag: secretCommentEncrypted.tag
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
const secretWithoutBlindIndex = secret.toObject(); const secretWithoutBlindIndex = secret.toObject();
@@ -139,10 +133,10 @@ export const createSecretRaw = async (req: Request, res: Response) => {
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret: secretWithoutBlindIndex, secret: secretWithoutBlindIndex,
key, key
}), })
}); });
} };
/** /**
* Update secret with name [secretName] * Update secret with name [secretName]
@@ -151,21 +145,15 @@ export const createSecretRaw = async (req: Request, res: Response) => {
*/ */
export const updateSecretByNameRaw = async (req: Request, res: Response) => { export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretValue, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretValue,
secretPath = "/",
} = req.body;
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({ const secretValueEncrypted = encryptSymmetric128BitHexKeyUTF8({
plaintext: secretValue, plaintext: secretValue,
key, key
}); });
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
@@ -177,21 +165,22 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
secretValueCiphertext: secretValueEncrypted.ciphertext, secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv, secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag, secretValueTag: secretValueEncrypted.tag,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret, secret,
key, key
}), })
}); });
}; };
@@ -202,12 +191,7 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByNameRaw = async (req: Request, res: Response) => { export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretPath = "/",
} = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -215,25 +199,26 @@ export const deleteSecretByNameRaw = async (req: Request, res: Response) => {
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
const key = await BotService.getWorkspaceKeyWithBot({ const key = await BotService.getWorkspaceKeyWithBot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId)
}); });
return res.status(200).send({ return res.status(200).send({
secret: repackageSecretToRaw({ secret: repackageSecretToRaw({
secret, secret,
key, key
}), })
}); });
}; };
@@ -252,11 +237,11 @@ export const getSecrets = async (req: Request, res: Response) => {
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
return res.status(200).send({ return res.status(200).send({
secrets, secrets
}); });
}; };
@@ -278,11 +263,11 @@ export const getSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData, authData: req.authData
}); });
return res.status(200).send({ return res.status(200).send({
secret, secret
}); });
}; };
@@ -306,7 +291,7 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretPath = "/"
} = req.body; } = req.body;
const secret = await SecretService.createSecret({ const secret = await SecretService.createSecret({
@@ -324,25 +309,25 @@ export const createSecret = async (req: Request, res: Response) => {
secretPath, secretPath,
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
const secretWithoutBlindIndex = secret.toObject(); const secretWithoutBlindIndex = secret.toObject();
delete secretWithoutBlindIndex.secretBlindIndex; delete secretWithoutBlindIndex.secretBlindIndex;
return res.status(200).send({ return res.status(200).send({
secret: secretWithoutBlindIndex, secret: secretWithoutBlindIndex
}); });
}; };
/** /**
* Update secret with name [secretName] * Update secret with name [secretName]
* @param req * @param req
@@ -357,7 +342,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath = "/", secretPath = "/"
} = req.body; } = req.body;
const secret = await SecretService.updateSecret({ const secret = await SecretService.updateSecret({
@@ -369,18 +354,19 @@ export const updateSecretByName = async (req: Request, res: Response) => {
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
return res.status(200).send({ return res.status(200).send({
secret, secret
}); });
}; };
@@ -391,12 +377,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { secretName } = req.params; const { secretName } = req.params;
const { const { workspaceId, environment, type, secretPath = "/" } = req.body;
workspaceId,
environment,
type,
secretPath = "/",
} = req.body;
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
@@ -404,17 +385,18 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
secretPath, secretPath
}); });
await EventService.handleEvent({ await EventService.handleEvent({
event: eventPushSecrets({ event: eventPushSecrets({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
}), secretPath
})
}); });
return res.status(200).send({ return res.status(200).send({
secret, secret
}); });
}; };
+3 -4
View File
@@ -1,5 +1,4 @@
import { eventPushSecrets } from "./secret" import { eventPushSecrets } from "./secret";
import { eventStartIntegration } from "./integration";
export { export { eventPushSecrets, eventStartIntegration };
eventPushSecrets,
}
+23
View File
@@ -0,0 +1,23 @@
import { Types } from "mongoose";
import { EVENT_START_INTEGRATION } from "../variables";
/*
* Return event for starting integrations
* @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to push secrets to
* @returns
*/
export const eventStartIntegration = ({
workspaceId,
environment
}: {
workspaceId: Types.ObjectId;
environment: string;
}) => {
return {
name: EVENT_START_INTEGRATION,
workspaceId,
environment,
payload: {}
};
};
+33 -43
View File
@@ -1,64 +1,54 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import { EVENT_PULL_SECRETS, EVENT_PUSH_SECRETS } from "../variables";
EVENT_PULL_SECRETS,
EVENT_PUSH_SECRETS,
} from "../variables";
interface PushSecret { interface PushSecret {
ciphertextKey: string; ciphertextKey: string;
ivKey: string; ivKey: string;
tagKey: string; tagKey: string;
hashKey: string; hashKey: string;
ciphertextValue: string; ciphertextValue: string;
ivValue: string; ivValue: string;
tagValue: string; tagValue: string;
hashValue: string; hashValue: string;
type: "shared" | "personal"; type: "shared" | "personal";
} }
/** /**
* Return event for pushing secrets * Return event for pushing secrets
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to push secrets to * @param {String} obj.workspaceId - id of workspace to push secrets to
* @returns * @returns
*/ */
const eventPushSecrets = ({ const eventPushSecrets = ({
workspaceId,
environment,
secretPath
}: {
workspaceId: Types.ObjectId;
environment: string;
secretPath: string;
}) => {
return {
name: EVENT_PUSH_SECRETS,
workspaceId, workspaceId,
environment, environment,
}: { secretPath,
workspaceId: Types.ObjectId; payload: {}
environment?: string; };
}) => { };
return ({
name: EVENT_PUSH_SECRETS,
workspaceId,
environment,
payload: {
},
});
}
/** /**
* Return event for pulling secrets * Return event for pulling secrets
* @param {Object} obj * @param {Object} obj
* @param {String} obj.workspaceId - id of workspace to pull secrets from * @param {String} obj.workspaceId - id of workspace to pull secrets from
* @returns * @returns
*/ */
const eventPullSecrets = ({ const eventPullSecrets = ({ workspaceId }: { workspaceId: string }) => {
return {
name: EVENT_PULL_SECRETS,
workspaceId, workspaceId,
}: { payload: {}
workspaceId: string; };
}) => { };
return ({
name: EVENT_PULL_SECRETS,
workspaceId,
payload: {
}, export { eventPushSecrets };
});
}
export {
eventPushSecrets,
}
+22 -10
View File
@@ -1,12 +1,14 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { Bot } from "../models"; import { Bot } from "../models";
import { EVENT_PUSH_SECRETS } from "../variables"; import { EVENT_PUSH_SECRETS, EVENT_START_INTEGRATION } from "../variables";
import { IntegrationService } from "../services"; import { IntegrationService } from "../services";
import { triggerWebhook } from "../services/WebhookService";
interface Event { interface Event {
name: string; name: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment?: string; environment?: string;
secretPath?: string;
payload: any; payload: any;
} }
@@ -19,22 +21,32 @@ interface Event {
* @param {Object} obj.event.payload - payload of event (depends on event) * @param {Object} obj.event.payload - payload of event (depends on event)
*/ */
export const handleEventHelper = async ({ event }: { event: Event }) => { export const handleEventHelper = async ({ event }: { event: Event }) => {
const { workspaceId, environment } = event; const { workspaceId, environment, secretPath } = event;
// TODO: moduralize bot check into separate function // TODO: moduralize bot check into separate function
const bot = await Bot.findOne({ const bot = await Bot.findOne({
workspace: workspaceId, workspace: workspaceId,
isActive: true, isActive: true
}); });
if (!bot) return;
switch (event.name) { switch (event.name) {
case EVENT_PUSH_SECRETS: case EVENT_PUSH_SECRETS:
IntegrationService.syncIntegrations({ if (bot) {
workspaceId, await IntegrationService.syncIntegrations({
environment, workspaceId,
}); environment
});
}
console.log("-------", workspaceId, environment, secretPath);
triggerWebhook(workspaceId.toString(), environment || "", secretPath || "");
break;
case EVENT_START_INTEGRATION:
if (bot) {
IntegrationService.syncIntegrations({
workspaceId,
environment
});
}
break; break;
} }
}; };
+8 -8
View File
@@ -20,7 +20,7 @@ import {
organizations as eeOrganizationsRouter, organizations as eeOrganizationsRouter,
secret as eeSecretRouter, secret as eeSecretRouter,
secretSnapshot as eeSecretSnapshotRouter, secretSnapshot as eeSecretSnapshotRouter,
workspace as eeWorkspaceRouter, workspace as eeWorkspaceRouter
} from "./ee/routes/v1"; } from "./ee/routes/v1";
import { import {
auth as v1AuthRouter, auth as v1AuthRouter,
@@ -41,6 +41,7 @@ import {
userAction as v1UserActionRouter, userAction as v1UserActionRouter,
user as v1UserRouter, user as v1UserRouter,
workspace as v1WorkspaceRouter, workspace as v1WorkspaceRouter,
webhooks as v1WebhooksRouter
} from "./routes/v1"; } from "./routes/v1";
import { import {
auth as v2AuthRouter, auth as v2AuthRouter,
@@ -53,13 +54,13 @@ import {
serviceTokenData as v2ServiceTokenDataRouter, serviceTokenData as v2ServiceTokenDataRouter,
serviceAccounts as v2ServiceAccountsRouter, serviceAccounts as v2ServiceAccountsRouter,
environment as v2EnvironmentRouter, environment as v2EnvironmentRouter,
tags as v2TagsRouter, tags as v2TagsRouter
} from "./routes/v2"; } from "./routes/v2";
import { import {
auth as v3AuthRouter, auth as v3AuthRouter,
secrets as v3SecretsRouter, secrets as v3SecretsRouter,
signup as v3SignupRouter, signup as v3SignupRouter,
workspaces as v3WorkspacesRouter, workspaces as v3WorkspacesRouter
} from "./routes/v3"; } from "./routes/v3";
import { healthCheck } from "./routes/status"; import { healthCheck } from "./routes/status";
import { getLogger } from "./utils/logger"; import { getLogger } from "./utils/logger";
@@ -80,7 +81,7 @@ const main = async () => {
app.use( app.use(
cors({ cors({
credentials: true, credentials: true,
origin: await getSiteURL(), origin: await getSiteURL()
}) })
); );
@@ -126,6 +127,7 @@ const main = async () => {
app.use("/api/v1/integration-auth", v1IntegrationAuthRouter); app.use("/api/v1/integration-auth", v1IntegrationAuthRouter);
app.use("/api/v1/folders", v1SecretsFolder); app.use("/api/v1/folders", v1SecretsFolder);
app.use("/api/v1/secret-scanning", v1SecretScanningRouter); app.use("/api/v1/secret-scanning", v1SecretScanningRouter);
app.use("/api/v1/webhooks", v1WebhooksRouter);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
@@ -157,7 +159,7 @@ const main = async () => {
if (res.headersSent) return next(); if (res.headersSent) return next();
next( next(
RouteNotFoundError({ RouteNotFoundError({
message: `The requested source '(${req.method})${req.url}' was not found`, message: `The requested source '(${req.method})${req.url}' was not found`
}) })
); );
}); });
@@ -165,9 +167,7 @@ const main = async () => {
app.use(requestErrorHandler); app.use(requestErrorHandler);
const server = app.listen(await getPort(), async () => { const server = app.listen(await getPort(), async () => {
(await getLogger("backend-main")).info( (await getLogger("backend-main")).info(`Server started listening at port ${await getPort()}`);
`Server started listening at port ${await getPort()}`
);
}); });
// await createTestUserForDevelopment(); // await createTestUserForDevelopment();
+85
View File
@@ -0,0 +1,85 @@
import { Document, Schema, Types, model } from "mongoose";
import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_BASE64, ENCODING_SCHEME_UTF8 } from "../variables";
export interface IWebhook extends Document {
_id: Types.ObjectId;
workspace: Types.ObjectId;
environment: string;
secretPath: string;
url: string;
lastStatus: "success" | "failed";
lastRunErrorMessage?: string;
isDisabled: boolean;
encryptedSecretKey: string;
iv: string;
tag: string;
algorithm: "aes-256-gcm";
keyEncoding: "base64" | "utf8";
}
const WebhookSchema = new Schema<IWebhook>(
{
workspace: {
type: Schema.Types.ObjectId,
ref: "Workspace",
required: true
},
environment: {
type: String,
required: true
},
secretPath: {
type: String,
required: true,
default: "/"
},
url: {
type: String,
required: true
},
lastStatus: {
type: String,
enum: ["success", "failed"]
},
lastRunErrorMessage: {
type: String
},
isDisabled: {
type: Boolean,
default: false
},
// used for webhook signature
encryptedSecretKey: {
type: String,
select: false
},
iv: {
type: String,
select: false
},
tag: {
type: String,
select: false
},
algorithm: {
// the encryption algorithm used
type: String,
enum: [ALGORITHM_AES_256_GCM],
required: true,
select: false
},
keyEncoding: {
type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true,
select: false
}
},
{
timestamps: true
}
);
const Webhook = model<IWebhook>("Webhook", WebhookSchema);
export default Webhook;
+20 -18
View File
@@ -16,24 +16,26 @@ import integration from "./integration";
import integrationAuth from "./integrationAuth"; import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import secretScanning from "./secretScanning"; import secretScanning from "./secretScanning";
import webhooks from "./webhook";
export { export {
signup, signup,
auth, auth,
bot, bot,
user, user,
userAction, userAction,
organization, organization,
workspace, workspace,
membershipOrg, membershipOrg,
membership, membership,
key, key,
inviteOrg, inviteOrg,
secret, secret,
serviceToken, serviceToken,
password, password,
integration, integration,
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
secretScanning secretScanning,
webhooks
}; };
+75
View File
@@ -0,0 +1,75 @@
import express from "express";
const router = express.Router();
import { requireAuth, requireWorkspaceAuth, validateRequest } from "../../middleware";
import { body, param, query } from "express-validator";
import { ADMIN, AUTH_MODE_JWT, AUTH_MODE_SERVICE_ACCOUNT, MEMBER } from "../../variables";
import { webhookController } from "../../controllers/v1";
router.post(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body",
locationEnvironment: "body"
}),
body("workspaceId").exists().isString().trim(),
body("environment").exists().isString().trim(),
body("webhookUrl").exists().isString().isURL().trim(),
body("webhookSecretKey").isString().trim(),
body("secretPath").default("/").isString().trim(),
validateRequest,
webhookController.createWebhook
);
router.patch(
"/:webhookId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("webhookId").exists().isString().trim(),
body("isDisabled").default(false).isBoolean(),
validateRequest,
webhookController.updateWebhook
);
router.post(
"/:webhookId/test",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("webhookId").exists().isString().trim(),
validateRequest,
webhookController.testWebhook
);
router.delete(
"/:webhookId",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
param("webhookId").exists().isString().trim(),
validateRequest,
webhookController.deleteWebhook
);
router.get(
"/",
requireAuth({
acceptedAuthModes: [AUTH_MODE_JWT]
}),
requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query",
locationEnvironment: "query"
}),
query("workspaceId").exists().isString().trim(),
query("environment").optional().isString().trim(),
query("secretPath").optional().isString().trim(),
validateRequest,
webhookController.listWebhooks
);
export default router;
+20 -45
View File
@@ -5,7 +5,7 @@ import {
requireAuth, requireAuth,
requireSecretsAuth, requireSecretsAuth,
requireWorkspaceAuth, requireWorkspaceAuth,
validateRequest, validateRequest
} from "../../middleware"; } from "../../middleware";
import { validateClientForSecrets } from "../../validation"; import { validateClientForSecrets } from "../../validation";
import { body, query } from "express-validator"; import { body, query } from "express-validator";
@@ -20,22 +20,18 @@ import {
PERMISSION_READ_SECRETS, PERMISSION_READ_SECRETS,
PERMISSION_WRITE_SECRETS, PERMISSION_WRITE_SECRETS,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
import { BatchSecretRequest } from "../../types/secret"; import { BatchSecretRequest } from "../../types/secret";
router.post( router.post(
"/batch", "/batch",
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body", locationWorkspaceId: "body"
}), }),
body("workspaceId").exists().isString().trim(), body("workspaceId").exists().isString().trim(),
body("folderId").default("root").isString().trim(), body("folderId").default("root").isString().trim(),
@@ -52,10 +48,8 @@ router.post(
if (secretIds.length > 0) { if (secretIds.length > 0) {
req.secrets = await validateClientForSecrets({ req.secrets = await validateClientForSecrets({
authData: req.authData, authData: req.authData,
secretIds: secretIds.map( secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
(secretId: string) => new Types.ObjectId(secretId) requiredPermissions: []
),
requiredPermissions: [],
}); });
} }
} }
@@ -76,14 +70,11 @@ router.post(
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: create multiple secrets // case: create multiple secrets
if (value.length === 0) if (value.length === 0) throw new Error("secrets cannot be an empty array");
throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if ( if (
!secret.type || !secret.type ||
!( !(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED
) ||
!secret.secretKeyCiphertext || !secret.secretKeyCiphertext ||
!secret.secretKeyIV || !secret.secretKeyIV ||
!secret.secretKeyTag || !secret.secretKeyTag ||
@@ -108,9 +99,7 @@ router.post(
!value.secretValueIV || !value.secretValueIV ||
!value.secretValueTag !value.secretValueTag
) { ) {
throw new Error( throw new Error("secrets object is missing required secret properties");
"secrets object is missing required secret properties"
);
} }
} else { } else {
throw new Error("secrets must be an object or an array of objects"); throw new Error("secrets must be an object or an array of objects");
@@ -120,17 +109,13 @@ router.post(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "body", locationWorkspaceId: "body",
locationEnvironment: "body", locationEnvironment: "body",
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.createSecrets secretsController.createSecrets
); );
@@ -148,14 +133,14 @@ router.get(
AUTH_MODE_JWT, AUTH_MODE_JWT,
AUTH_MODE_API_KEY, AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_TOKEN,
AUTH_MODE_SERVICE_ACCOUNT, AUTH_MODE_SERVICE_ACCOUNT
], ]
}), }),
requireWorkspaceAuth({ requireWorkspaceAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
locationWorkspaceId: "query", locationWorkspaceId: "query",
locationEnvironment: "query", locationEnvironment: "query",
requiredPermissions: [PERMISSION_READ_SECRETS], requiredPermissions: [PERMISSION_READ_SECRETS]
}), }),
secretsController.getSecrets secretsController.getSecrets
); );
@@ -167,8 +152,7 @@ router.patch(
.custom((value) => { .custom((value) => {
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: update multiple secrets // case: update multiple secrets
if (value.length === 0) if (value.length === 0) throw new Error("secrets cannot be an empty array");
throw new Error("secrets cannot be an empty array");
for (const secret of value) { for (const secret of value) {
if (!secret.id) { if (!secret.id) {
throw new Error("Each secret must contain a ID property"); throw new Error("Each secret must contain a ID property");
@@ -187,15 +171,11 @@ router.patch(
}), }),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.updateSecrets secretsController.updateSecrets
); );
@@ -210,8 +190,7 @@ router.delete(
if (Array.isArray(value)) { if (Array.isArray(value)) {
// case: delete multiple secrets // case: delete multiple secrets
if (value.length === 0) if (value.length === 0) throw new Error("secrets cannot be an empty array");
throw new Error("secrets cannot be an empty array");
return value.every((id: string) => typeof id === "string"); return value.every((id: string) => typeof id === "string");
} }
@@ -221,15 +200,11 @@ router.delete(
.isEmpty(), .isEmpty(),
validateRequest, validateRequest,
requireAuth({ requireAuth({
acceptedAuthModes: [ acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
AUTH_MODE_JWT,
AUTH_MODE_API_KEY,
AUTH_MODE_SERVICE_TOKEN,
],
}), }),
requireSecretsAuth({ requireSecretsAuth({
acceptedRoles: [ADMIN, MEMBER], acceptedRoles: [ADMIN, MEMBER],
requiredPermissions: [PERMISSION_WRITE_SECRETS], requiredPermissions: [PERMISSION_WRITE_SECRETS]
}), }),
secretsController.deleteSecrets secretsController.deleteSecrets
); );
+93
View File
@@ -0,0 +1,93 @@
import axios from "axios";
import crypto from "crypto";
import { Types } from "mongoose";
import picomatch from "picomatch";
import { client, getRootEncryptionKey } from "../config";
import Webhook, { IWebhook } from "../models/webhooks";
export const triggerWebhookRequest = async (
{ url, encryptedSecretKey, iv, tag }: IWebhook,
payload: Record<string, unknown>
) => {
const headers: Record<string, string> = {};
payload["timestamp"] = Date.now();
if (encryptedSecretKey) {
const rootEncryptionKey = await getRootEncryptionKey();
const secretKey = client.decryptSymmetric(encryptedSecretKey, rootEncryptionKey, iv, tag);
const webhookSign = crypto
.createHmac("sha256", secretKey)
.update(JSON.stringify(payload))
.digest("hex");
headers["x-infisical-signature"] = `t=${payload["timestamp"]};${webhookSign}`;
}
const req = await axios.post(url, payload, { headers });
return req;
};
export const getWebhookPayload = (
eventName: string,
workspaceId: string,
environment: string,
secretPath?: string
) => ({
event: eventName,
project: {
workspaceId,
environment,
secretPath
}
});
export const triggerWebhook = async (
workspaceId: string,
environment: string,
secretPath: string
) => {
const webhooks = await Webhook.find({ workspace: workspaceId, environment, isDisabled: false });
// TODO(akhilmhdh): implement retry policy later, for that a cron job based approach is needed
// for exponential backoff
const toBeTriggeredHooks = webhooks.filter(({ secretPath: hookSecretPath }) =>
picomatch.isMatch(secretPath, hookSecretPath, { strictSlashes: false })
);
const webhooksTriggered = await Promise.allSettled(
toBeTriggeredHooks.map((hook) =>
triggerWebhookRequest(
hook,
getWebhookPayload("secrets.modified", workspaceId, environment, secretPath)
)
)
);
const successWebhooks: Types.ObjectId[] = [];
const failedWebhooks: Array<{ id: Types.ObjectId; error: string }> = [];
webhooksTriggered.forEach((data, index) => {
if (data.status === "rejected") {
failedWebhooks.push({ id: toBeTriggeredHooks[index]._id, error: data.reason.message });
return;
}
successWebhooks.push(toBeTriggeredHooks[index]._id);
});
// dont remove the workspaceid and environment filter. its used to reduce the dataset before $in check
await Webhook.bulkWrite([
{
updateMany: {
filter: { workspace: workspaceId, environment, _id: { $in: successWebhooks } },
update: { lastStatus: "success", lastRunErrorMessage: null }
}
},
...failedWebhooks.map(({ id, error }) => ({
updateOne: {
filter: {
workspace: workspaceId,
environment,
_id: id
},
update: {
lastStatus: "failed",
lastRunErrorMessage: error
}
}
}))
]);
};
+2 -1
View File
@@ -1,2 +1,3 @@
export const EVENT_PUSH_SECRETS = "pushSecrets"; export const EVENT_PUSH_SECRETS = "pushSecrets";
export const EVENT_PULL_SECRETS = "pullSecrets"; export const EVENT_PULL_SECRETS = "pullSecrets";
export const EVENT_START_INTEGRATION = "startIntegration";