feat(dashboard-v3): migrated folder, imports and snapshots to use only secret path and not folder id

This commit is contained in:
Akhil Mohan
2023-09-28 21:28:06 +05:30
parent d0b8aba990
commit 0ada343b6f
19 changed files with 1607 additions and 711 deletions
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
import { isValidScope } from "../../helpers"; import { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models"; import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService"; import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderWithPathFromId } from "../../services/FolderService"; import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { import {
BadRequestError, BadRequestError,
ResourceNotFoundError, ResourceNotFoundError,
@@ -95,37 +95,12 @@ export const createSecretImp = async (req: Request, res: Response) => {
*/ */
const { const {
body: { workspaceId, environment, folderId, secretImport } body: { workspaceId, environment, directory, secretImport }
} = await validateRequest(reqValidator.CreateSecretImportV1, req); } = await validateRequest(reqValidator.CreateSecretImportV1, req);
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && folderId !== "root") {
throw ResourceNotFoundError({
message: "Failed to find folder"
});
}
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
// root check // root check
let isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
isValidScopeAccess = isValidScope(
req.authData.authPayload,
secretImport.environment,
secretImport.secretPath
);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -133,27 +108,31 @@ export const createSecretImp = async (req: Request, res: Response) => {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: secretImport.environment,
secretPath: secretImport.secretPath
})
); );
} }
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/")
throw ResourceNotFoundError({ message: "Failed to find folder" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId folderId
}); });
const importToSecretPath = folders
? getFolderWithPathFromId(folders.nodes, folderId).folderPath
: "/";
if (!importSecDoc) { if (!importSecDoc) {
const doc = new SecretImport({ const doc = new SecretImport({
workspace: workspaceId, workspace: workspaceId,
@@ -173,7 +152,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
importFromEnvironment: secretImport.environment, importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath, importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment, importToEnvironment: environment,
importToSecretPath importToSecretPath: directory
} }
}, },
{ {
@@ -206,7 +185,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
importFromEnvironment: secretImport.environment, importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath, importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment, importToEnvironment: environment,
importToSecretPath importToSecretPath: directory
} }
}, },
{ {
@@ -563,8 +542,38 @@ export const getSecretImports = async (req: Request, res: Response) => {
} }
*/ */
const { const {
query: { workspaceId, environment, folderId } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetSecretImportsV1, req); } = await validateRequest(reqValidator.GetSecretImportsV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -575,41 +584,6 @@ export const getSecretImports = async (req: Request, res: Response) => {
return res.status(200).json({ secretImport: {} }); return res.status(200).json({ secretImport: {} });
} }
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
return res.status(200).json({ secretImport: importSecDoc }); return res.status(200).json({ secretImport: importSecDoc });
}; };
@@ -621,9 +595,39 @@ export const getSecretImports = async (req: Request, res: Response) => {
*/ */
export const getAllSecretsFromImport = async (req: Request, res: Response) => { export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { const {
query: { workspaceId, environment, folderId } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req); } = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -634,11 +638,6 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
return res.status(200).json({ secrets: [] }); return res.status(200).json({ secrets: [] });
} }
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/"; let secretPath = "/";
if (folders) { if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
@@ -9,12 +9,10 @@ import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder"; import { Folder } from "../../models/folder";
import { import {
appendFolder, appendFolder,
deleteFolderById,
generateFolderId, generateFolderId,
getAllFolderIds, getAllFolderIds,
getFolderByPath, getFolderByPath,
getFolderWithPathFromId, getFolderWithPathFromId,
getParentFromFolderId,
validateFolderName validateFolderName
} from "../../services/FolderService"; } from "../../services/FolderService";
import { import {
@@ -25,13 +23,9 @@ import {
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders"; import * as reqValidator from "../../validation/folders";
/** const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
* Create folder with name [folderName] for workspace with id [workspaceId]
* and environment [environment] // verify workspace id/environment
* @param req
* @param res
* @returns
*/
export const createFolder = async (req: Request, res: Response) => { export const createFolder = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Create a folder' #swagger.summary = 'Create a folder'
@@ -107,7 +101,7 @@ export const createFolder = async (req: Request, res: Response) => {
} }
*/ */
const { const {
body: { workspaceId, environment, folderName, parentFolderId } body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req); } = await validateRequest(reqValidator.CreateFolderV1, req);
if (!validateFolderName(folderName)) { if (!validateFolderName(folderName)) {
@@ -116,33 +110,29 @@ export const createFolder = async (req: Request, res: Response) => {
}); });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
// token check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// user check
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ const folders = await Folder.findOne({
workspace: workspaceId, workspace: workspaceId,
environment environment
}).lean(); }).lean();
if (req.user) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const secretPath =
folders && parentFolderId
? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath
: "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
// space has no folders initialized // space has no folders initialized
if (!folders) { if (!folders) {
if (req.authData.authPayload instanceof ServiceTokenData) { if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const id = generateFolderId(); const id = generateFolderId();
const folder = new Folder({ const folder = new Folder({
@@ -186,27 +176,10 @@ export const createFolder = async (req: Request, res: Response) => {
return res.json({ folder: { id, name: folderName } }); return res.json({ folder: { id, name: folderName } });
} }
const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
await Folder.findByIdAndUpdate(folders._id, folders);
const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId(
folders.nodes,
parentFolderId || "root"
);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = appendFolder(folders.nodes, { folderName, parentFolderId: parentFolder.id });
await Folder.findByIdAndUpdate(folders._id, folders); await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
@@ -219,11 +192,9 @@ export const createFolder = async (req: Request, res: Response) => {
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId: parentFolderId folderId: parentFolder.id
}); });
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -232,7 +203,7 @@ export const createFolder = async (req: Request, res: Response) => {
environment, environment,
folderId: folder.id, folderId: folder.id,
folderName, folderName,
folderPath folderPath: directory
} }
}, },
{ {
@@ -332,8 +303,8 @@ export const updateFolderById = async (req: Request, res: Response) => {
} }
*/ */
const { const {
body: { workspaceId, environment, name }, body: { workspaceId, environment, name, directory },
params: { folderId } params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req); } = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) { if (!validateFolderName(name)) {
@@ -342,38 +313,31 @@ export const updateFolderById = async (req: Request, res: Response) => {
}); });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) { if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
const parentFolder = getParentFromFolderId(folders.nodes, folderId); const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) { if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
if (req.user) { const folder = parentFolder.children.find(({ name }) => name === folderName);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); if (!folder) throw ERR_FOLDER_NOT_FOUND;
const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath;
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const oldFolderName = folder.name; const oldFolderName = folder.name;
parentFolder.version += 1; parentFolder.version += 1;
@@ -505,24 +469,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
} }
*/ */
const { const {
params: { folderId }, params: { folderName },
body: { environment, workspaceId } body: { environment, workspaceId, directory }
} = await validateRequest(reqValidator.DeleteFolderV1, req); } = await validateRequest(reqValidator.DeleteFolderV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { deletedNode: delFolder, parent: parentFolder } = delOp;
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
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, directory);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -531,12 +483,23 @@ export const deleteFolder = async (req: Request, res: Response) => {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
); );
} }
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) throw ERR_FOLDER_NOT_FOUND;
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
const deletedFolder = parentFolder.children.splice(index, 1)[0];
parentFolder.version += 1; parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder); const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders); await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
@@ -565,9 +528,9 @@ export const deleteFolder = async (req: Request, res: Response) => {
type: EventType.DELETE_FOLDER, type: EventType.DELETE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId, folderId: deletedFolder.id,
folderName: delFolder.name, folderName: deletedFolder.name,
folderPath: secretPath folderPath: directory
} }
}, },
{ {
@@ -575,7 +538,7 @@ export const deleteFolder = async (req: Request, res: Response) => {
} }
); );
res.send({ message: "successfully deleted folders", folders: delFolderIds }); return res.send({ message: "successfully deleted folders", folders: delFolderIds });
}; };
/** /**
@@ -677,69 +640,27 @@ export const getFolders = async (req: Request, res: Response) => {
} }
*/ */
const { const {
query: { workspaceId, environment, parentFolderId, parentFolderPath } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req); } = await validateRequest(reqValidator.GetFoldersV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
if (!folders) {
res.send({ folders: [], dir: [] });
return;
}
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }]
});
}
if (!parentFolderId) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,
name
}));
res.send({ folders: rootFolders });
return;
}
const { folder, folderPath, dir } = getFolderWithPathFromId(folders.nodes, parentFolderId);
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} else {
// check that user is a member of the workspace
await getUserProjectPermissions(req.user._id, workspaceId);
} }
res.send({ const folders = await Folder.findOne({ workspace: workspaceId, environment });
folders: folder.children.map(({ id, name }) => ({ id, name })), if (!folders) {
dir return res.send({ folders: [], dir: [] });
}
const folder = getFolderByPath(folders.nodes, directory);
return res.send({
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
}); });
}; };
+143 -8
View File
@@ -196,7 +196,15 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
export const createSecretRaw = async (req: Request, res: Response) => { export const createSecretRaw = async (req: Request, res: Response) => {
const { const {
params: { secretName }, params: { secretName },
body: { secretPath, environment, workspaceId, type, secretValue, secretComment } body: {
secretPath,
environment,
workspaceId,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.CreateSecretRawV3, req); } = await validateRequest(reqValidator.CreateSecretRawV3, req);
if (req.user?._id) { if (req.user?._id) {
@@ -249,7 +257,8 @@ 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,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -279,7 +288,7 @@ 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 { const {
params: { secretName }, params: { secretName },
body: { secretValue, environment, secretPath, type, workspaceId } body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
if (req.user?._id) { if (req.user?._id) {
@@ -316,7 +325,8 @@ 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,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -540,7 +550,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentTag, secretCommentTag,
secretKeyCiphertext, secretKeyCiphertext,
secretValueCiphertext, secretValueCiphertext,
secretCommentCiphertext secretCommentCiphertext,
skipMultilineEncoding
}, },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req); } = await validateRequest(reqValidator.CreateSecretV3, req);
@@ -577,7 +588,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
metadata metadata,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -610,11 +622,23 @@ export const updateSecretByName = async (req: Request, res: Response) => {
type, type,
environment, environment,
secretPath, secretPath,
workspaceId workspaceId,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
secretName: newSecretName,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
}, },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext))
throw BadRequestError({ message: "Missing encrypted key" });
if (req.user?._id) { if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
@@ -637,10 +661,19 @@ export const updateSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
newSecretName,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath secretPath,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -704,3 +737,105 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secret secret
}); });
}; };
export const createSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const createdSecrets = await SecretService.createSecretBatch({
secretPath,
environment,
workspaceId: new Types.ObjectId(workspaceId),
secrets,
authData: req.authData
});
return res.status(200).send({
secrets: createdSecrets
});
};
export const updateSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const updatedSecrets = await SecretService.updateSecretBatch({
secretPath,
environment,
workspaceId: new Types.ObjectId(workspaceId),
secrets,
authData: req.authData
});
return res.status(200).send({
secrets: updatedSecrets
});
};
export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
if (req.user?._id) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
} else {
await validateServiceTokenDataClientForWorkspace({
serviceTokenData: req.authData.authPayload as IServiceTokenData,
workspaceId: new Types.ObjectId(workspaceId),
environment,
secretPath,
requiredPermissions: [PERMISSION_WRITE_SECRETS]
});
}
const deletedSecrets = await SecretService.deleteSecretBatch({
secretPath,
environment,
workspaceId: new Types.ObjectId(workspaceId),
secrets,
authData: req.authData
});
return res.status(200).send({
secrets: deletedSecrets
});
};
@@ -27,7 +27,7 @@ import {
import { EESecretService } from "../../services"; import { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
// import Folder, { TFolderSchema } from "../../../models/folder"; // import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService"; import { getFolderByPath, searchByFolderId } from "../../../services/FolderService";
import { EEAuditLogService, EELicenseService } from "../../services"; import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { validateRequest } from "../../../helpers/validation"; import { validateRequest } from "../../../helpers/validation";
@@ -104,7 +104,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
*/ */
const { const {
params: { workspaceId }, params: { workspaceId },
query: { environment, folderId, offset, limit } query: { environment, directory, offset, limit }
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req); } = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -113,10 +113,20 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
const secretSnapshots = await SecretSnapshot.find({ const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root" folderId
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
@@ -135,7 +145,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
const { const {
params: { workspaceId }, params: { workspaceId },
query: { environment, folderId } query: { environment, directory }
} = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req); } = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -144,10 +154,20 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
const count = await SecretSnapshot.countDocuments({ const count = await SecretSnapshot.countDocuments({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root" folderId
}); });
return res.status(200).send({ return res.status(200).send({
@@ -215,7 +235,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
const { const {
params: { workspaceId }, params: { workspaceId },
body: { folderId, environment, version } body: { directory, environment, version }
} = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req); } = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -224,6 +244,16 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
// validate secret snapshot // validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({ const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId, workspace: workspaceId,
+43 -40
View File
@@ -1,47 +1,50 @@
export enum ActorType { export enum ActorType {
USER = "user", USER = "user",
SERVICE = "service" SERVICE = "service"
} }
export enum UserAgentType { export enum UserAgentType {
WEB = "web", WEB = "web",
CLI = "cli", CLI = "cli",
K8_OPERATOR = "k8-operator", K8_OPERATOR = "k8-operator",
OTHER = "other" OTHER = "other"
} }
export enum EventType { export enum EventType {
GET_SECRETS = "get-secrets", GET_SECRETS = "get-secrets",
GET_SECRET = "get-secret", GET_SECRET = "get-secret",
REVEAL_SECRET = "reveal-secret", REVEAL_SECRET = "reveal-secret",
CREATE_SECRET = "create-secret", CREATE_SECRET = "create-secret",
UPDATE_SECRET = "update-secret", CREATE_SECRETS = "create-secrets",
DELETE_SECRET = "delete-secret", UPDATE_SECRET = "update-secret",
GET_WORKSPACE_KEY = "get-workspace-key", UPDATE_SECRETS = "update-secrets",
AUTHORIZE_INTEGRATION = "authorize-integration", DELETE_SECRET = "delete-secret",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration", DELETE_SECRETS = "delete-secrets",
CREATE_INTEGRATION = "create-integration", GET_WORKSPACE_KEY = "get-workspace-key",
DELETE_INTEGRATION = "delete-integration", AUTHORIZE_INTEGRATION = "authorize-integration",
ADD_TRUSTED_IP = "add-trusted-ip", UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
UPDATE_TRUSTED_IP = "update-trusted-ip", CREATE_INTEGRATION = "create-integration",
DELETE_TRUSTED_IP = "delete-trusted-ip", DELETE_INTEGRATION = "delete-integration",
CREATE_SERVICE_TOKEN = "create-service-token", ADD_TRUSTED_IP = "add-trusted-ip",
DELETE_SERVICE_TOKEN = "delete-service-token", UPDATE_TRUSTED_IP = "update-trusted-ip",
CREATE_ENVIRONMENT = "create-environment", DELETE_TRUSTED_IP = "delete-trusted-ip",
UPDATE_ENVIRONMENT = "update-environment", CREATE_SERVICE_TOKEN = "create-service-token",
DELETE_ENVIRONMENT = "delete-environment", DELETE_SERVICE_TOKEN = "delete-service-token",
ADD_WORKSPACE_MEMBER = "add-workspace-member", CREATE_ENVIRONMENT = "create-environment",
REMOVE_WORKSPACE_MEMBER = "remove-workspace-member", UPDATE_ENVIRONMENT = "update-environment",
CREATE_FOLDER = "create-folder", DELETE_ENVIRONMENT = "delete-environment",
UPDATE_FOLDER = "update-folder", ADD_WORKSPACE_MEMBER = "add-workspace-member",
DELETE_FOLDER = "delete-folder", REMOVE_WORKSPACE_MEMBER = "remove-workspace-member",
CREATE_WEBHOOK = "create-webhook", CREATE_FOLDER = "create-folder",
UPDATE_WEBHOOK_STATUS = "update-webhook-status", UPDATE_FOLDER = "update-folder",
DELETE_WEBHOOK = "delete-webhook", DELETE_FOLDER = "delete-folder",
GET_SECRET_IMPORTS = "get-secret-imports", CREATE_WEBHOOK = "create-webhook",
CREATE_SECRET_IMPORT = "create-secret-import", UPDATE_WEBHOOK_STATUS = "update-webhook-status",
UPDATE_SECRET_IMPORT = "update-secret-import", DELETE_WEBHOOK = "delete-webhook",
DELETE_SECRET_IMPORT = "delete-secret-import", GET_SECRET_IMPORTS = "get-secret-imports",
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role", CREATE_SECRET_IMPORT = "create-secret-import",
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions" UPDATE_SECRET_IMPORT = "update-secret-import",
} DELETE_SECRET_IMPORT = "delete-secret-import",
UPDATE_USER_WORKSPACE_ROLE = "update-user-workspace-role",
UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS = "update-user-workspace-denied-permissions"
}
+318 -293
View File
@@ -1,403 +1,428 @@
import { import { ActorType, EventType } from "./enums";
ActorType,
EventType
} from "./enums";
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
email: string; email: string;
} }
interface ServiceActorMetadata { interface ServiceActorMetadata {
serviceId: string; serviceId: string;
name: string; name: string;
} }
export interface UserActor { export interface UserActor {
type: ActorType.USER; type: ActorType.USER;
metadata: UserActorMetadata; metadata: UserActorMetadata;
} }
export interface ServiceActor { export interface ServiceActor {
type: ActorType.SERVICE; type: ActorType.SERVICE;
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export type Actor = export type Actor = UserActor | ServiceActor;
| UserActor
| ServiceActor;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
numberOfSecrets: number; numberOfSecrets: number;
}; };
} }
interface GetSecretEvent { interface GetSecretEvent {
type: EventType.GET_SECRET; type: EventType.GET_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
}; };
} }
interface CreateSecretEvent { interface CreateSecretEvent {
type: EventType.CREATE_SECRET; type: EventType.CREATE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface CreateSecretBatchEvent {
type: EventType.CREATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface UpdateSecretEvent { interface UpdateSecretEvent {
type: EventType.UPDATE_SECRET; type: EventType.UPDATE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface UpdateSecretBatchEvent {
type: EventType.UPDATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface DeleteSecretEvent { interface DeleteSecretEvent {
type: EventType.DELETE_SECRET; type: EventType.DELETE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface DeleteSecretBatchEvent {
type: EventType.DELETE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface GetWorkspaceKeyEvent { interface GetWorkspaceKeyEvent {
type: EventType.GET_WORKSPACE_KEY, type: EventType.GET_WORKSPACE_KEY;
metadata: { metadata: {
keyId: string; keyId: string;
} };
} }
interface AuthorizeIntegrationEvent { interface AuthorizeIntegrationEvent {
type: EventType.AUTHORIZE_INTEGRATION; type: EventType.AUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface UnauthorizeIntegrationEvent { interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION; type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface CreateIntegrationEvent { interface CreateIntegrationEvent {
type: EventType.CREATE_INTEGRATION; type: EventType.CREATE_INTEGRATION;
metadata: { metadata: {
integrationId: string; integrationId: string;
integration: string; // TODO: fix type integration: string; // TODO: fix type
environment: string; environment: string;
secretPath: string; secretPath: string;
url?: string; url?: string;
app?: string; app?: string;
appId?: string; appId?: string;
targetEnvironment?: string; targetEnvironment?: string;
targetEnvironmentId?: string; targetEnvironmentId?: string;
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface DeleteIntegrationEvent { interface DeleteIntegrationEvent {
type: EventType.DELETE_INTEGRATION; type: EventType.DELETE_INTEGRATION;
metadata: { metadata: {
integrationId: string; integrationId: string;
integration: string; // TODO: fix type integration: string; // TODO: fix type
environment: string; environment: string;
secretPath: string; secretPath: string;
url?: string; url?: string;
app?: string; app?: string;
appId?: string; appId?: string;
targetEnvironment?: string; targetEnvironment?: string;
targetEnvironmentId?: string; targetEnvironmentId?: string;
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface AddTrustedIPEvent { interface AddTrustedIPEvent {
type: EventType.ADD_TRUSTED_IP; type: EventType.ADD_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface UpdateTrustedIPEvent { interface UpdateTrustedIPEvent {
type: EventType.UPDATE_TRUSTED_IP; type: EventType.UPDATE_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface DeleteTrustedIPEvent { interface DeleteTrustedIPEvent {
type: EventType.DELETE_TRUSTED_IP; type: EventType.DELETE_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface CreateServiceTokenEvent { interface CreateServiceTokenEvent {
type: EventType.CREATE_SERVICE_TOKEN; type: EventType.CREATE_SERVICE_TOKEN;
metadata: { metadata: {
name: string; name: string;
scopes: Array<{ scopes: Array<{
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
} };
} }
interface DeleteServiceTokenEvent { interface DeleteServiceTokenEvent {
type: EventType.DELETE_SERVICE_TOKEN; type: EventType.DELETE_SERVICE_TOKEN;
metadata: { metadata: {
name: string; name: string;
scopes: Array<{ scopes: Array<{
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
} };
} }
interface CreateEnvironmentEvent { interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT; type: EventType.CREATE_ENVIRONMENT;
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface UpdateEnvironmentEvent { interface UpdateEnvironmentEvent {
type: EventType.UPDATE_ENVIRONMENT; type: EventType.UPDATE_ENVIRONMENT;
metadata: { metadata: {
oldName: string; oldName: string;
newName: string; newName: string;
oldSlug: string; oldSlug: string;
newSlug: string; newSlug: string;
} };
} }
interface DeleteEnvironmentEvent { interface DeleteEnvironmentEvent {
type: EventType.DELETE_ENVIRONMENT; type: EventType.DELETE_ENVIRONMENT;
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface AddWorkspaceMemberEvent { interface AddWorkspaceMemberEvent {
type: EventType.ADD_WORKSPACE_MEMBER; type: EventType.ADD_WORKSPACE_MEMBER;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface RemoveWorkspaceMemberEvent { interface RemoveWorkspaceMemberEvent {
type: EventType.REMOVE_WORKSPACE_MEMBER; type: EventType.REMOVE_WORKSPACE_MEMBER;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface CreateFolderEvent { interface CreateFolderEvent {
type: EventType.CREATE_FOLDER; type: EventType.CREATE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface UpdateFolderEvent { interface UpdateFolderEvent {
type: EventType.UPDATE_FOLDER; type: EventType.UPDATE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
oldFolderName: string; oldFolderName: string;
newFolderName: string; newFolderName: string;
folderPath: string; folderPath: string;
} };
} }
interface DeleteFolderEvent { interface DeleteFolderEvent {
type: EventType.DELETE_FOLDER; type: EventType.DELETE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface CreateWebhookEvent { interface CreateWebhookEvent {
type: EventType.CREATE_WEBHOOK, type: EventType.CREATE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface UpdateWebhookStatusEvent { interface UpdateWebhookStatusEvent {
type: EventType.UPDATE_WEBHOOK_STATUS, type: EventType.UPDATE_WEBHOOK_STATUS;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface DeleteWebhookEvent { interface DeleteWebhookEvent {
type: EventType.DELETE_WEBHOOK, type: EventType.DELETE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface GetSecretImportsEvent { interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS, type: EventType.GET_SECRET_IMPORTS;
metadata: { metadata: {
environment: string; environment: string;
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
numberOfImports: number; numberOfImports: number;
} };
} }
interface CreateSecretImportEvent { interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT, type: EventType.CREATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importFromEnvironment: string; importFromEnvironment: string;
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateSecretImportEvent { interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT, type: EventType.UPDATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
orderBefore: { orderBefore: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[], }[];
orderAfter: { orderAfter: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[] }[];
} };
} }
interface DeleteSecretImportEvent { interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT, type: EventType.DELETE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importFromEnvironment: string; importFromEnvironment: string;
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateUserRole { interface UpdateUserRole {
type: EventType.UPDATE_USER_WORKSPACE_ROLE, type: EventType.UPDATE_USER_WORKSPACE_ROLE;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
oldRole: string; oldRole: string;
newRole: string; newRole: string;
} };
} }
interface UpdateUserDeniedPermissions { interface UpdateUserDeniedPermissions {
type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS, type: EventType.UPDATE_USER_WORKSPACE_DENIED_PERMISSIONS;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
deniedPermissions: { deniedPermissions: {
environmentSlug: string; environmentSlug: string;
ability: string; ability: string;
}[] }[];
} };
} }
export type Event = export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
| CreateSecretEvent | CreateSecretEvent
| UpdateSecretEvent | CreateSecretBatchEvent
| DeleteSecretEvent | UpdateSecretEvent
| GetWorkspaceKeyEvent | UpdateSecretBatchEvent
| AuthorizeIntegrationEvent | DeleteSecretEvent
| UnauthorizeIntegrationEvent | DeleteSecretBatchEvent
| CreateIntegrationEvent | GetWorkspaceKeyEvent
| DeleteIntegrationEvent | AuthorizeIntegrationEvent
| AddTrustedIPEvent | UnauthorizeIntegrationEvent
| UpdateTrustedIPEvent | CreateIntegrationEvent
| DeleteTrustedIPEvent | DeleteIntegrationEvent
| CreateServiceTokenEvent | AddTrustedIPEvent
| DeleteServiceTokenEvent | UpdateTrustedIPEvent
| CreateEnvironmentEvent | DeleteTrustedIPEvent
| UpdateEnvironmentEvent | CreateServiceTokenEvent
| DeleteEnvironmentEvent | DeleteServiceTokenEvent
| AddWorkspaceMemberEvent | CreateEnvironmentEvent
| RemoveWorkspaceMemberEvent | UpdateEnvironmentEvent
| CreateFolderEvent | DeleteEnvironmentEvent
| UpdateFolderEvent | AddWorkspaceMemberEvent
| DeleteFolderEvent | RemoveWorkspaceMemberEvent
| CreateWebhookEvent | CreateFolderEvent
| UpdateWebhookStatusEvent | UpdateFolderEvent
| DeleteWebhookEvent | DeleteFolderEvent
| GetSecretImportsEvent | CreateWebhookEvent
| CreateSecretImportEvent | UpdateWebhookStatusEvent
| UpdateSecretImportEvent | DeleteWebhookEvent
| DeleteSecretImportEvent | GetSecretImportsEvent
| UpdateUserRole | CreateSecretImportEvent
| UpdateUserDeniedPermissions; | UpdateSecretImportEvent
| DeleteSecretImportEvent
| UpdateUserRole
| UpdateUserDeniedPermissions;
+26 -24
View File
@@ -4,7 +4,7 @@ import {
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
export interface ISecretVersion { export interface ISecretVersion {
@@ -23,6 +23,7 @@ export interface ISecretVersion {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm"; algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64"; keyEncoding: "utf8" | "base64";
createdAt: string; createdAt: string;
@@ -36,95 +37,96 @@ const secretVersionSchema = new Schema<ISecretVersion>(
// could be deleted // could be deleted
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Secret", ref: "Secret",
required: true, required: true
}, },
version: { version: {
type: Number, type: Number,
default: 1, default: 1,
required: true, required: true
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
type: { type: {
type: String, type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL], enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true, required: true
}, },
user: { user: {
// user associated with the personal secret // user associated with the personal secret
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
environment: { environment: {
type: String, type: String,
required: true, required: true
}, },
isDeleted: { isDeleted: {
// consider removing field // consider removing field
type: Boolean, type: Boolean,
default: false, default: false,
required: true, required: true
}, },
secretBlindIndex: { secretBlindIndex: {
type: String, type: String,
select: false, select: false
}, },
secretKeyCiphertext: { secretKeyCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretKeyIV: { secretKeyIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyTag: { secretKeyTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueCiphertext: { secretValueCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretValueIV: { secretValueIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueTag: { secretValueTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
},
skipMultilineEncoding: {
type: Boolean,
required: false
}, },
algorithm: { algorithm: {
// the encryption algorithm used // the encryption algorithm used
type: String, type: String,
enum: [ALGORITHM_AES_256_GCM], enum: [ALGORITHM_AES_256_GCM],
required: true, required: true,
default: ALGORITHM_AES_256_GCM, default: ALGORITHM_AES_256_GCM
}, },
keyEncoding: { keyEncoding: {
type: String, type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true, required: true,
default: ENCODING_SCHEME_UTF8, default: ENCODING_SCHEME_UTF8
}, },
folder: { folder: {
type: String, type: String,
required: true, required: true
}, },
tags: { tags: {
ref: "Tag", ref: "Tag",
type: [Schema.Types.ObjectId], type: [Schema.Types.ObjectId],
default: [], default: []
} }
}, },
{ {
timestamps: true, timestamps: true
} }
); );
export const SecretVersion = model<ISecretVersion>( export const SecretVersion = model<ISecretVersion>("SecretVersion", secretVersionSchema);
"SecretVersion",
secretVersionSchema
);
+608 -5
View File
@@ -1,9 +1,12 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
CreateSecretBatchParams,
CreateSecretParams, CreateSecretParams,
DeleteSecretBatchParams,
DeleteSecretParams, DeleteSecretParams,
GetSecretParams, GetSecretParams,
GetSecretsParams, GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams UpdateSecretParams
} from "../interfaces/services/SecretService"; } from "../interfaces/services/SecretService";
import { import {
@@ -71,6 +74,8 @@ export function containsGlobPatterns(secretPath: string) {
return globChars.some((char) => normalizedPath.includes(char)); return globChars.some((char) => normalizedPath.includes(char));
} }
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
/** /**
* Returns an object containing secret [secret] but with its value, key, comment decrypted. * Returns an object containing secret [secret] but with its value, key, comment decrypted.
* *
@@ -330,7 +335,8 @@ export const createSecretHelper = async ({
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretPath = "/",
metadata metadata,
skipMultilineEncoding
}: CreateSecretParams) => { }: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
@@ -394,6 +400,7 @@ export const createSecretHelper = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
skipMultilineEncoding,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
@@ -416,6 +423,7 @@ export const createSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}); });
@@ -740,19 +748,50 @@ export const updateSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
newSecretName,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath secretPath,
tags,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding
}: UpdateSecretParams) => { }: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ // get secret blind index salt
secretName, const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
let secret: ISecret | null = null; let secret: ISecret | null = null;
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
let newSecretNameBlindIndex = undefined;
if (newSecretName) {
newSecretNameBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
const doesSecretAlreadyExist = await Secret.exists({
secretBlindIndex: newSecretNameBlindIndex,
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
type
});
if (!doesSecretAlreadyExist)
throw BadRequestError({ message: "Secret with the provided name already exist" });
}
if (type === SECRET_SHARED) { if (type === SECRET_SHARED) {
// case: update shared secret // case: update shared secret
secret = await Secret.findOneAndUpdate( secret = await Secret.findOneAndUpdate(
@@ -767,6 +806,15 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
$inc: { version: 1 } $inc: { version: 1 }
}, },
{ {
@@ -782,6 +830,9 @@ export const updateSecretHelper = async ({
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
folder: folderId, folder: folderId,
...getAuthDataPayloadUserObj(authData) ...getAuthDataPayloadUserObj(authData)
}, },
@@ -789,6 +840,9 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
tags,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
$inc: { version: 1 } $inc: { version: 1 }
}, },
{ {
@@ -805,16 +859,18 @@ export const updateSecretHelper = async ({
workspace: secret.workspace, workspace: secret.workspace,
folder: folderId, folder: folderId,
type, type,
tags,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment, environment: secret.environment,
isDeleted: false, isDeleted: false,
secretBlindIndex, secretBlindIndex: newSecretName ? newSecretNameBlindIndex : secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext, secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV, secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag, secretKeyTag: secret.secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}); });
@@ -1185,3 +1241,550 @@ export const expandSecrets = async (
return secrets; return secrets;
}; };
export const createSecretBatchHelper = async ({
secrets,
workspaceId,
authData,
secretPath,
environment
}: CreateSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type,
...getAuthDataPayloadUserObj(authData)
}))
)
.exec();
if (exists)
throw BadRequestError({
message: "Failed to create secret that already exists"
});
// create secret
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
secrets.map(
({
type,
secretName,
secretKeyIV,
metadata,
secretKeyTag,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretKeyCiphertext,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding
}) => ({
version: 1,
workspace: new Types.ObjectId(workspaceId),
environment,
type,
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
folder: folderId,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
metadata,
skipMultilineEncoding,
secretBlindIndex: secretBlindIndexes[secretName],
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
})
)
);
await EESecretService.addSecretVersions({
secretVersions: newlyCreatedSecrets.map(
(secret) =>
new SecretVersion({
secret: secret._id,
version: secret.version,
workspace: secret.workspace,
type: secret.type,
folder: folderId,
skipMultilineEncoding: secret?.skipMultilineEncoding,
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment,
isDeleted: false,
secretBlindIndex: secret.secretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8
})
)
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_ADD_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: newlyCreatedSecrets.map((n) => n._id)
});
action &&
(await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.CREATE_SECRETS,
metadata: {
environment,
secretPath,
secrets: newlyCreatedSecrets.map(({ secretBlindIndex, version, _id }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets added",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return newlyCreatedSecrets;
};
export const updateSecretBatchHelper = async ({
workspaceId,
environment,
authData,
secretPath,
secrets
}: UpdateSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const secretsToBeUpdated = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.select("+secretBlindIndex")
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.lean();
if (secretsToBeUpdated.length !== secrets.length)
throw BadRequestError({ message: "Some secrets not found" });
await Secret.bulkWrite(
secrets.map(
({
type,
secretName,
tags,
secretValueIV,
secretValueTag,
secretCommentIV,
secretCommentTag,
secretValueCiphertext,
secretCommentCiphertext,
skipMultilineEncoding
}) => ({
updateOne: {
filter: {
workspace: new Types.ObjectId(workspaceId),
environment,
folder: folderId,
secretBlindIndex: secretBlindIndexes[secretName],
type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
},
update: {
$inc: {
version: 1
},
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
tags,
skipMultilineEncoding
}
}
})
)
);
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, ISecret>>(
(prev, curr) => {
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
return prev;
},
{}
);
await EESecretService.addSecretVersions({
secretVersions: secrets.map((secret) => {
const {
_id,
version,
workspace,
type,
secretBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
} = secretsGroupedByBlindIndex[secretBlindIndexes[secret.secretName]];
return new SecretVersion({
secret: _id,
version: version + 1,
workspace: workspace,
type,
folder: folderId,
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment,
isDeleted: false,
secretBlindIndex: secretBlindIndex,
secretKeyCiphertext: secretKeyCiphertext,
secretKeyIV: secretKeyIV,
secretKeyTag: secretKeyTag,
secretValueCiphertext: secret.secretValueCiphertext,
secretValueIV: secret.secretValueIV,
secretValueTag: secret.secretValueTag,
algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8,
skipMultilineEncoding
});
})
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_UPDATE_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: secretsToBeUpdated.map(({ _id }) => _id)
});
action &&
(await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.UPDATE_SECRETS,
metadata: {
environment,
secretPath,
secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version + 1
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets modified",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: 1,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return;
};
export const deleteSecretBatchHelper = async ({
workspaceId,
environment,
authData,
secretPath = "/",
secrets
}: DeleteSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const deletedSecrets = await Secret.find({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.select({ secretBlindIndexes: 1 })
.lean()
.exec();
await Secret.deleteMany({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
}))
)
.exec();
await EESecretService.markDeletedSecretVersions({
secretIds: deletedSecrets.map((secret) => secret._id)
});
// (EE) create (audit) log
const action = await EELogService.createAction({
name: ACTION_DELETE_SECRETS,
...getAuthDataPayloadIdObj(authData),
workspaceId,
secretIds: deletedSecrets.map((secret) => secret._id)
});
action &&
(await EELogService.createLog({
...getAuthDataPayloadIdObj(authData),
workspaceId,
actions: [action],
channel: authData.userAgentType,
ipAddress: authData.ipAddress
}));
await EEAuditLogService.createAuditLog(
authData,
{
type: EventType.DELETE_SECRETS,
metadata: {
environment,
secretPath,
secrets: deletedSecrets.map(({ _id, version, secretBlindIndex }) => ({
secretId: _id.toString(),
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
secretVersion: version
}))
}
},
{
workspaceId
}
);
// (EE) take a secret snapshot
await EESecretService.takeSecretSnapshot({
workspaceId,
environment,
folderId
});
const postHogClient = await TelemetryService.getPostHogClient();
if (postHogClient) {
postHogClient.capture({
event: "secrets deleted",
distinctId: await TelemetryService.getDistinctId({
authData
}),
properties: {
numberOfSecrets: secrets.length,
environment,
workspaceId,
folderId,
channel: authData.userAgentType,
userAgent: authData.userAgent
}
});
}
return {
secrets: deletedSecrets
};
};
@@ -16,6 +16,7 @@ export interface CreateSecretParams {
secretCommentCiphertext?: string; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretPath: string; secretPath: string;
metadata?: { metadata?: {
source?: string; source?: string;
@@ -42,6 +43,10 @@ export interface GetSecretParams {
export interface UpdateSecretParams { export interface UpdateSecretParams {
secretName: string; secretName: string;
newSecretName?: string;
secretKeyCiphertext?: string;
secretKeyIV?: string;
secretKeyTag?: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
type: "shared" | "personal"; type: "shared" | "personal";
@@ -50,6 +55,11 @@ export interface UpdateSecretParams {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretPath: string; secretPath: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
} }
export interface DeleteSecretParams { export interface DeleteSecretParams {
@@ -60,3 +70,57 @@ export interface DeleteSecretParams {
authData: AuthData; authData: AuthData;
secretPath: string; secretPath: string;
} }
export interface CreateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
metadata?: {
source?: string;
};
}>;
}
export interface UpdateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
}>;
}
export interface DeleteSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
}>;
}
+32 -27
View File
@@ -4,7 +4,7 @@ import {
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../variables"; } from "../variables";
export interface ISecret { export interface ISecret {
@@ -12,7 +12,7 @@ export interface ISecret {
version: number; version: number;
workspace: Types.ObjectId; workspace: Types.ObjectId;
type: string; type: string;
user: Types.ObjectId; user?: Types.ObjectId;
environment: string; environment: string;
secretBlindIndex?: string; secretBlindIndex?: string;
secretKeyCiphertext: string; secretKeyCiphertext: string;
@@ -27,13 +27,14 @@ export interface ISecret {
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm"; algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64"; keyEncoding: "utf8" | "base64";
tags?: string[]; tags?: string[];
folder?: string; folder?: string;
metadata?: { metadata?: {
[key: string]: string; [key: string]: string;
} };
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
@@ -41,108 +42,112 @@ const secretSchema = new Schema<ISecret>(
version: { version: {
type: Number, type: Number,
required: true, required: true,
default: 1, default: 1
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
type: { type: {
type: String, type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL], enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true, required: true
}, },
user: { user: {
// user associated with the personal secret // user associated with the personal secret
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
tags: { tags: {
ref: "Tag", ref: "Tag",
type: [Schema.Types.ObjectId], type: [Schema.Types.ObjectId],
default: [], default: []
}, },
environment: { environment: {
type: String, type: String,
required: true, required: true
}, },
secretBlindIndex: { secretBlindIndex: {
type: String, type: String,
select: false, select: false
}, },
secretKeyCiphertext: { secretKeyCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretKeyIV: { secretKeyIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyTag: { secretKeyTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyHash: { secretKeyHash: {
type: String, type: String
}, },
secretValueCiphertext: { secretValueCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretValueIV: { secretValueIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueTag: { secretValueTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueHash: { secretValueHash: {
type: String, type: String
}, },
secretCommentCiphertext: { secretCommentCiphertext: {
type: String, type: String,
required: false, required: false
}, },
secretCommentIV: { secretCommentIV: {
type: String, // symmetric type: String, // symmetric
required: false, required: false
}, },
secretCommentTag: { secretCommentTag: {
type: String, // symmetric type: String, // symmetric
required: false, required: false
}, },
secretCommentHash: { secretCommentHash: {
type: String, type: String,
required: false, required: false
},
skipMultilineEncoding: {
type: Boolean,
required: false
}, },
algorithm: { algorithm: {
// the encryption algorithm used // the encryption algorithm used
type: String, type: String,
enum: [ALGORITHM_AES_256_GCM], enum: [ALGORITHM_AES_256_GCM],
required: true, required: true,
default: ALGORITHM_AES_256_GCM, default: ALGORITHM_AES_256_GCM
}, },
keyEncoding: { keyEncoding: {
type: String, type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true, required: true,
default: ENCODING_SCHEME_UTF8, default: ENCODING_SCHEME_UTF8
}, },
folder: { folder: {
type: String, type: String,
default: "root", default: "root"
}, },
metadata: { metadata: {
type: Schema.Types.Mixed type: Schema.Types.Mixed
} }
}, },
{ {
timestamps: true, timestamps: true
} }
); );
secretSchema.index({ tags: 1 }, { background: true }); secretSchema.index({ tags: 1 }, { background: true });
export const Secret = model<ISecret>("Secret", secretSchema); export const Secret = model<ISecret>("Secret", secretSchema);
+2 -2
View File
@@ -18,7 +18,7 @@ router.post(
); );
router.patch( router.patch(
"/:folderId", "/:folderName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
}), }),
@@ -26,7 +26,7 @@ router.patch(
); );
router.delete( router.delete(
"/:folderId", "/:folderName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
}), }),
+36 -8
View File
@@ -1,14 +1,8 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth, requireBlindIndicesEnabled, requireE2EEOff } from "../../middleware";
requireAuth,
requireBlindIndicesEnabled,
requireE2EEOff
} from "../../middleware";
import { secretsController } from "../../controllers/v3"; import { secretsController } from "../../controllers/v3";
import { import { AuthMode } from "../../variables";
AuthMode
} from "../../variables";
router.get( router.get(
"/raw", "/raw",
@@ -85,6 +79,40 @@ router.get(
secretsController.getSecrets secretsController.getSecrets
); );
// akhilmhdh: dont put batch router below the individual operation as those have arbitory name as params
router.post(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.createSecretByNameBatch
);
router.patch(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.updateSecretByNameBatch
);
router.delete(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.deleteSecretByNameBatch
);
router.post( router.post(
"/:secretName", "/:secretName",
requireAuth({ requireAuth({
@@ -108,14 +108,6 @@ export const getAllImportedSecrets = async (
type: "shared" type: "shared"
} }
}, },
{
$lookup: {
from: "tags", // note this is the name of the collection in the database, not the Mongoose model name
localField: "tags",
foreignField: "_id",
as: "tags"
}
},
{ {
$group: { $group: {
_id: { _id: {
+41 -31
View File
@@ -1,21 +1,27 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
CreateSecretParams, CreateSecretBatchParams,
DeleteSecretParams, CreateSecretParams,
GetSecretParams, DeleteSecretBatchParams,
GetSecretsParams, DeleteSecretParams,
UpdateSecretParams, GetSecretParams,
GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams
} from "../interfaces/services/SecretService"; } from "../interfaces/services/SecretService";
import { import {
createSecretBlindIndexDataHelper, createSecretBatchHelper,
createSecretHelper, createSecretBlindIndexDataHelper,
deleteSecretHelper, createSecretHelper,
generateSecretBlindIndexHelper, deleteSecretBatchHelper,
generateSecretBlindIndexWithSaltHelper, deleteSecretHelper,
getSecretBlindIndexSaltHelper, generateSecretBlindIndexHelper,
getSecretHelper, generateSecretBlindIndexWithSaltHelper,
getSecretsHelper, getSecretBlindIndexSaltHelper,
updateSecretHelper, getSecretHelper,
getSecretsHelper,
updateSecretBatchHelper,
updateSecretHelper
} from "../helpers/secrets"; } from "../helpers/secrets";
class SecretService { class SecretService {
@@ -26,13 +32,9 @@ class SecretService {
* @param {Buffer} obj.salt - 16-byte random salt * @param {Buffer} obj.salt - 16-byte random salt
* @param {Types.ObjectId} obj.workspaceId * @param {Types.ObjectId} obj.workspaceId
*/ */
static async createSecretBlindIndexData({ static async createSecretBlindIndexData({ workspaceId }: { workspaceId: Types.ObjectId }) {
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await createSecretBlindIndexDataHelper({ return await createSecretBlindIndexDataHelper({
workspaceId, workspaceId
}); });
} }
@@ -42,13 +44,9 @@ class SecretService {
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns * @returns
*/ */
static async getSecretBlindIndexSalt({ static async getSecretBlindIndexSalt({ workspaceId }: { workspaceId: Types.ObjectId }) {
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await getSecretBlindIndexSaltHelper({ return await getSecretBlindIndexSaltHelper({
workspaceId, workspaceId
}); });
} }
@@ -61,14 +59,14 @@ class SecretService {
*/ */
static async generateSecretBlindIndexWithSalt({ static async generateSecretBlindIndexWithSalt({
secretName, secretName,
salt, salt
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
}) { }) {
return await generateSecretBlindIndexWithSaltHelper({ return await generateSecretBlindIndexWithSaltHelper({
secretName, secretName,
salt, salt
}); });
} }
@@ -81,14 +79,14 @@ class SecretService {
*/ */
static async generateSecretBlindIndex({ static async generateSecretBlindIndex({
secretName, secretName,
workspaceId, workspaceId
}: { }: {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) { }) {
return await generateSecretBlindIndexHelper({ return await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId, workspaceId
}); });
} }
@@ -163,6 +161,18 @@ class SecretService {
static async deleteSecret(deleteSecretParams: DeleteSecretParams) { static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
return await deleteSecretHelper(deleteSecretParams); return await deleteSecretHelper(deleteSecretParams);
} }
static async createSecretBatch(createSecretParams: CreateSecretBatchParams) {
return await createSecretBatchHelper(createSecretParams);
}
static async updateSecretBatch(updateSecretParams: UpdateSecretBatchParams) {
return await updateSecretBatchHelper(updateSecretParams);
}
static async deleteSecretBatch(deleteSecretParams: DeleteSecretBatchParams) {
return await deleteSecretBatchHelper(deleteSecretParams);
}
} }
export default SecretService; export default SecretService;
+8 -7
View File
@@ -5,28 +5,30 @@ export const CreateFolderV1 = z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderName: z.string().trim(), folderName: z.string().trim(),
parentFolderId: z.string().trim().optional() directory: z.string().trim().default("/")
}) })
}); });
export const UpdateFolderV1 = z.object({ export const UpdateFolderV1 = z.object({
params: z.object({ params: z.object({
folderId: z.string().trim() folderName: z.string().trim()
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
name: z.string().trim() name: z.string().trim(),
directory: z.string().trim().default("/")
}) })
}); });
export const DeleteFolderV1 = z.object({ export const DeleteFolderV1 = z.object({
params: z.object({ params: z.object({
folderId: z.string().trim() folderName: z.string().trim()
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim() environment: z.string().trim(),
directory: z.string().trim().default("/")
}) })
}); });
@@ -34,7 +36,6 @@ export const GetFoldersV1 = z.object({
query: z.object({ query: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
parentFolderId: z.string().trim().optional(), directory: z.string().trim().default("/")
parentFolderPath: z.string().trim().optional()
}) })
}); });
+3 -3
View File
@@ -4,7 +4,7 @@ export const CreateSecretImportV1 = z.object({
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root"), directory: z.string().trim().default("/"),
secretImport: z.object({ secretImport: z.object({
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim() secretPath: z.string().trim()
@@ -40,7 +40,7 @@ export const GetSecretImportsV1 = z.object({
query: z.object({ query: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root") directory: z.string().trim().default("/")
}) })
}); });
@@ -48,6 +48,6 @@ export const GetAllSecretsFromImportV1 = z.object({
query: z.object({ query: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root") directory: z.string().trim().default("/")
}) })
}); });
+79 -2
View File
@@ -259,6 +259,7 @@ export const CreateSecretRawV3 = z.object({
secretPath: z.string().trim().default("/"), secretPath: z.string().trim().default("/"),
secretValue: z.string().trim(), secretValue: z.string().trim(),
secretComment: z.string().trim(), secretComment: z.string().trim(),
skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]) type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
}), }),
params: z.object({ params: z.object({
@@ -275,6 +276,7 @@ export const UpdateSecretByNameRawV3 = z.object({
environment: z.string().trim(), environment: z.string().trim(),
secretValue: z.string().trim(), secretValue: z.string().trim(),
secretPath: z.string().trim().default("/"), secretPath: z.string().trim().default("/"),
skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED) type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED)
}) })
}); });
@@ -335,7 +337,8 @@ export const CreateSecretV3 = z.object({
secretCommentCiphertext: z.string().trim().optional(), secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(), secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(), secretCommentTag: z.string().trim().optional(),
metadata: z.record(z.string()).optional() metadata: z.record(z.string()).optional(),
skipMultilineEncoding: z.boolean().optional()
}), }),
params: z.object({ params: z.object({
secretName: z.string().trim() secretName: z.string().trim()
@@ -350,7 +353,17 @@ export const UpdateSecretByNameV3 = z.object({
secretPath: z.string().trim().default("/"), secretPath: z.string().trim().default("/"),
secretValueCiphertext: z.string().trim(), secretValueCiphertext: z.string().trim(),
secretValueIV: z.string().trim(), secretValueIV: z.string().trim(),
secretValueTag: z.string().trim() secretValueTag: z.string().trim(),
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
tags: z.string().array().optional(),
skipMultilineEncoding: z.boolean().optional(),
// to update secret name
secretName: z.string().trim().optional(),
secretKeyIV: z.string().trim().optional(),
secretKeyTag: z.string().trim().optional(),
secretKeyCiphertext: z.string().trim().optional()
}), }),
params: z.object({ params: z.object({
secretName: z.string() secretName: z.string()
@@ -368,3 +381,67 @@ export const DeleteSecretByNameV3 = z.object({
secretName: z.string() secretName: z.string()
}) })
}); });
export const CreateSecretByNameBatchV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secrets: z
.object({
secretName: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretKeyCiphertext: z.string().trim(),
secretKeyIV: z.string().trim(),
secretKeyTag: z.string().trim(),
secretValueCiphertext: z.string().trim(),
secretValueIV: z.string().trim(),
secretValueTag: z.string().trim(),
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
metadata: z.record(z.string()).optional(),
skipMultilineEncoding: z.boolean().optional()
})
.array()
.min(1)
})
});
export const UpdateSecretByNameBatchV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secrets: z
.object({
secretName: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretValueCiphertext: z.string().trim(),
secretValueIV: z.string().trim(),
secretValueTag: z.string().trim(),
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
skipMultilineEncoding: z.boolean().optional(),
tags: z.string().array().optional()
})
.array()
.min(1)
})
});
export const DeleteSecretByNameBatchV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secrets: z
.object({
secretName: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
})
.array()
.min(1)
})
});
+7 -6
View File
@@ -33,9 +33,10 @@ export const validateClientForWorkspace = async ({
}) => { }) => {
const workspace = await Workspace.findById(workspaceId); const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({ if (!workspace)
message: "Failed to find workspace" throw WorkspaceNotFoundError({
}); message: "Failed to find workspace"
});
let membership; let membership;
switch (authData.actor.type) { switch (authData.actor.type) {
@@ -67,7 +68,7 @@ export const GetWorkspaceSecretSnapshotsV1 = z.object({
}), }),
query: z.object({ query: z.object({
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root"), directory: z.string().trim().default("/"),
offset: z.coerce.number(), offset: z.coerce.number(),
limit: z.coerce.number() limit: z.coerce.number()
}) })
@@ -79,7 +80,7 @@ export const GetWorkspaceSecretSnapshotsCountV1 = z.object({
}), }),
query: z.object({ query: z.object({
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root") directory: z.string().trim().default("/")
}) })
}); });
@@ -89,7 +90,7 @@ export const RollbackWorkspaceSecretSnapshotV1 = z.object({
}), }),
body: z.object({ body: z.object({
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root"), directory: z.string().trim().default("/"),
version: z.number() version: z.number()
}) })
}); });
+1 -1
View File
@@ -1,3 +1,3 @@
// secrets // secrets
export const SECRET_SHARED = "shared"; export const SECRET_SHARED = "shared";
export const SECRET_PERSONAL = "personal"; export const SECRET_PERSONAL = "personal";