mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 12:27:31 +00:00
feat(webhook): implemented api for webhooks
This commit is contained in:
@@ -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
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { eventPushSecrets } from "./secret"
|
import { eventPushSecrets } from "./secret";
|
||||||
|
import { eventStartIntegration } from "./integration";
|
||||||
|
|
||||||
export {
|
export { eventPushSecrets, eventStartIntegration };
|
||||||
eventPushSecrets,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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: {}
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -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,
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
]);
|
||||||
|
};
|
||||||
@@ -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";
|
||||||
|
|||||||
Reference in New Issue
Block a user