diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index 6671e21ef..a750af53e 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -51,6 +51,10 @@ export const createFolder = async (req: Request, res: Response) => { nodes: folder.nodes, }); await folderVersion.save(); + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment, + }); return res.json({ folder: { id, name: folderName } }); } @@ -66,6 +70,12 @@ export const createFolder = async (req: Request, res: Response) => { }); await folderVersion.save(); + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment, + folderId: parentFolderId, + }); + return res.json({ folder }); }; @@ -96,7 +106,6 @@ export const updateFolderById = async (req: Request, res: Response) => { parentFolder.version += 1; folder.name = name; - folder.version += 1; await Folder.findByIdAndUpdate(folders._id, folders); const folderVersion = new FolderVersion({ @@ -106,19 +115,11 @@ export const updateFolderById = async (req: Request, res: Response) => { }); await folderVersion.save(); - // decision to take snapshot ot not - const secretCount = await Secret.count({ - workspace: workspaceId, + await EESecretService.takeSecretSnapshot({ + workspaceId, environment, - folder: folderId, + folderId: parentFolder.id, }); - if (secretCount || folder.children.length) { - await EESecretService.takeSecretSnapshot({ - workspaceId, - environment, - folderId: parentFolder.id, - }); - } return res.json({ message: "Successfully updated folder", @@ -148,23 +149,9 @@ export const deleteFolder = async (req: Request, res: Response) => { } const { deletedNode: delFolder, parent: parentFolder } = delOp; + parentFolder.version += 1; const delFolderIds = getAllFolderIds(delFolder); - // take a pre-delete snapshot if there are secrets inside the folders - const secretCount = await Secret.count({ - workspace: workspaceId, - environment, - folder: folderId, - }); - - if (secretCount || delFolder.children.length) { - await EESecretService.takeSecretSnapshot({ - workspaceId, - environment, - folderId: parentFolder.id, - }); - } - await Folder.findByIdAndUpdate(folders._id, folders); const folderVersion = new FolderVersion({ workspace: workspaceId, @@ -180,7 +167,6 @@ export const deleteFolder = async (req: Request, res: Response) => { }); } - // post delete snapshot await EESecretService.takeSecretSnapshot({ workspaceId, environment, diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index 5709d45e2..409da56b9 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,6 +1,6 @@ import { Request, Response } from "express"; import * as Sentry from "@sentry/node"; -import { Types } from "mongoose"; +import { PipelineStage, Types } from "mongoose"; import { Secret } from "../../../models"; import { SecretSnapshot, @@ -12,11 +12,8 @@ import { } from "../../models"; import { EESecretService } from "../../services"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; -import Folder from "../../../models/folder"; -import { - getAllFolderIds, - searchByFolderId, -} from "../../../services/FolderService"; +import Folder, { TFolderSchema } from "../../../models/folder"; +import { searchByFolderId } from "../../../services/FolderService"; /** * Return secret snapshots for workspace with id [workspaceId] @@ -214,17 +211,135 @@ export const rollbackWorkspaceSecretSnapshot = async ( if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); - // TODO: fix any - const oldSecretVersionsObj: any = secretSnapshot.secretVersions.reduce( - (accumulator, s) => ({ - ...accumulator, - [`${s.secret.toString()}`]: s, - }), - {} - ); + const snapshotFolderTree = secretSnapshot.folderVersion; + const latestFolderTree = await Folder.findOne({ + workspace: workspaceId, + environment, + }); + const latestFolderVersion = await FolderVersion.findOne({ + environment, + workspace: workspaceId, + "nodes.id": folderId, + }).sort({ "nodes.version": -1 }); + + const oldSecretVersionsObj: Record = {}; + const secretIds: Types.ObjectId[] = []; + const folderIds: string[] = [folderId]; + + secretSnapshot.secretVersions.forEach((snapSecVer) => { + oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer; + secretIds.push(snapSecVer.secret); + }); + + // the parent node from current latest one + // this will be modified according to the snapshot and latest snapshots + const newFolderTree = + latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId); + + if (newFolderTree) { + newFolderTree.children = snapshotFolderTree?.nodes?.children || []; + const queue = [newFolderTree]; + // a bfs algorithm in which we take the latest snapshots of all the folders in a level + while (queue.length) { + const groupByFolderId: Record = {}; + // the original queue is popped out completely to get what ever in a level + // subqueue is filled with all the children thus next level folders + // subQueue will then be transfered to the oriinal queue + const subQueue: TFolderSchema[] = []; + // get everything inside a level + while (queue.length) { + const folder = queue.pop() as TFolderSchema; + folder.children.forEach((el) => { + folderIds.push(el.id); // push ids and data into queu + subQueue.push(el); + // to modify the original tree very fast we keep a reference object + // key with folder id and pointing to the various nodes + groupByFolderId[el.id] = el; + }); + } + // get latest snapshots of all the folder + const matchWsFoldersPipeline = { + $match: { + workspace: new Types.ObjectId(workspaceId), + environment, + folderId: { + $in: Object.keys(groupByFolderId), + }, + }, + }; + const sortByFolderIdAndVersion: PipelineStage = { + $sort: { folderId: 1, version: -1 }, + }; + const pickLatestVersionOfEachFolder = { + $group: { + _id: "$folderId", + latestVersion: { $first: "$version" }, + doc: { + $first: "$$ROOT", + }, + }, + }; + const populateSecVersion = { + $lookup: { + from: SecretVersion.collection.name, + localField: "doc.secretVersions", + foreignField: "_id", + as: "doc.secretVersions", + }, + }; + const populateFolderVersion = { + $lookup: { + from: FolderVersion.collection.name, + localField: "doc.folderVersion", + foreignField: "_id", + as: "doc.folderVersion", + }, + }; + const unwindFolderVerField = { + $unwind: { + path: "$doc.folderVersion", + preserveNullAndEmptyArrays: true, + }, + }; + const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> = + await SecretSnapshot.aggregate([ + matchWsFoldersPipeline, + sortByFolderIdAndVersion, + pickLatestVersionOfEachFolder, + populateSecVersion, + populateFolderVersion, + unwindFolderVerField, + ]); + + // recursive snapshotting each level + latestSnapshotsByFolders.forEach((snap) => { + // mutate the folder tree to update the nodes to the latest version tree + // we are reconstructing the folder tree by latest snapshots here + if (groupByFolderId[snap.doc.folderId]) { + groupByFolderId[snap.doc.folderId].children = + snap.doc?.folderVersion?.nodes?.children || []; + } + + // push all children of next level snapshots + if (snap.doc.folderVersion?.nodes?.children) { + queue.push(...snap.doc.folderVersion.nodes.children); + } + + snap.doc.secretVersions.forEach((snapSecVer) => { + // record all the secrets + oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer; + secretIds.push(snapSecVer.secret); + }); + }); + + queue.push(...subQueue); + } + } + + // TODO: fix any const latestSecretVersionIds = await getLatestSecretVersionIds({ - secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret), + secretIds, }); // TODO: fix any @@ -245,27 +360,6 @@ export const rollbackWorkspaceSecretSnapshot = async ( {} ); - let folderIds: string[] = []; - - const folders = await Folder.findOne({ - workspace: workspaceId, - environment, - }).lean(); - const latestFolderVersion = await FolderVersion.findOne({ - environment, - workspace: workspaceId, - "nodes.id": folderId, - }).sort({ "nodes.version": -1 }); - - if (folders && folderId) { - const folder = searchByFolderId(folders.nodes, folderId); - if (folder) { - folderIds = getAllFolderIds(folder).map(({ id }) => id); - folder.children = secretSnapshot?.folderVersion?.nodes?.children || []; - folder.version = (latestFolderVersion?.nodes?.version || 0) + 1; - } - } - const secDelQuery: Record = { workspace: workspaceId, environment, @@ -283,9 +377,9 @@ export const rollbackWorkspaceSecretSnapshot = async ( // add secrets secrets = await Secret.insertMany( - secretSnapshot.secretVersions.map((sv) => { - const secretId = sv.secret; + Object.keys(oldSecretVersionsObj).map((sv) => { const { + secret: secretId, workspace, type, user, @@ -294,16 +388,14 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretKeyCiphertext, secretKeyIV, secretKeyTag, - secretKeyHash, secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash, createdAt, algorithm, keyEncoding, folder: secFolderId, - } = oldSecretVersionsObj[secretId.toString()]; + } = oldSecretVersionsObj[sv]; return { _id: secretId, @@ -316,11 +408,9 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretKeyCiphertext, secretKeyIV, secretKeyTag, - secretKeyHash, secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash, secretCommentCiphertext: "", secretCommentIV: "", secretCommentTag: "", @@ -346,11 +436,9 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretKeyCiphertext, secretKeyIV, secretKeyTag, - secretKeyHash, secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash, algorithm, keyEncoding, folder: secFolderId, @@ -367,11 +455,9 @@ export const rollbackWorkspaceSecretSnapshot = async ( secretKeyCiphertext, secretKeyIV, secretKeyTag, - secretKeyHash, secretValueCiphertext, secretValueIV, secretValueTag, - secretValueHash, algorithm, keyEncoding, folder: secFolderId, @@ -379,17 +465,18 @@ export const rollbackWorkspaceSecretSnapshot = async ( ) ); - if (folders) { - const newFolder = new Folder(folders); - newFolder._id = new Types.ObjectId(); - newFolder.isNew = true; - // when there is no - await newFolder.save(); + if (newFolderTree && latestFolderTree) { + // save the updated folder tree to the present one + newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1; + latestFolderTree._id = new Types.ObjectId(); + latestFolderTree.isNew = true; + await latestFolderTree.save(); + // create new folder version const newFolderVersion = new FolderVersion({ workspace: workspaceId, environment, - nodes: newFolder.nodes, + nodes: newFolderTree, }); await newFolderVersion.save(); } @@ -398,7 +485,9 @@ export const rollbackWorkspaceSecretSnapshot = async ( await SecretVersion.updateMany( { secret: { - $in: secretSnapshot.secretVersions.map((sv) => sv.secret), + $in: Object.keys(oldSecretVersionsObj).map( + (sv) => oldSecretVersionsObj[sv].secret + ), }, }, { diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index 72d9d260a..f7edab06e 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -1,10 +1,5 @@ import { Types } from "mongoose"; import { Secret, ISecret } from "../../models"; -import Folder from "../../models/folder"; -import { - getAllFolderIds, - searchByFolderId, -} from "../../services/FolderService"; import { SecretSnapshot, SecretVersion, @@ -30,25 +25,17 @@ const takeSecretSnapshotHelper = async ({ folderId?: string; }) => { // get all folder ids - let folderIds: string[] = []; - const folders = await Folder.findOne({ - workspace: workspaceId, - environment, - }).lean(); - if (folders && folderId) { - const folder = searchByFolderId(folders.nodes, folderId); - if (folder) folderIds = getAllFolderIds(folder).map(({ id }) => id); - } - - const secQuery: Record = { - workspace: workspaceId, - environment, - // undefined means root thus collect all secrets - }; - if (folderId !== "root") secQuery.folder = { $in: folderIds }; - const secretIds = (await Secret.find(secQuery, "_id").lean()).map( - (s) => s._id - ); + const secretIds = ( + await Secret.find( + { + workspace: workspaceId, + environment, + folder: folderId, + // undefined means root thus collect all secrets + }, + "_id" + ).lean() + ).map((s) => s._id); const latestSecretVersions = ( await SecretVersion.aggregate([ @@ -76,7 +63,7 @@ const takeSecretSnapshotHelper = async ({ const latestFolderVersion = await FolderVersion.findOne({ environment, workspace: workspaceId, - "nodes.id": folderId || "root", + "nodes.id": folderId, }).sort({ "nodes.version": -1 }); const latestSecretSnapshot = await SecretSnapshot.findOne({ @@ -132,5 +119,5 @@ const markDeletedSecretVersionsHelper = async ({ export { takeSecretSnapshotHelper, addSecretVersionsHelper, - markDeletedSecretVersionsHelper + markDeletedSecretVersionsHelper, }; diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index ea0fc37c5..ea2a9fd2a 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -25,6 +25,7 @@ export interface ISecretVersion { secretValueTag: string; algorithm: "aes-256-gcm"; keyEncoding: "utf8" | "base64"; + createdAt: string; folder?: string; } @@ -109,7 +110,7 @@ const secretVersionSchema = new Schema( }, folder: { type: String, - required: false, + required: true, }, }, { diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts index d1869f47e..aac2bc39a 100644 --- a/backend/src/services/FolderService.ts +++ b/backend/src/services/FolderService.ts @@ -33,6 +33,18 @@ export const searchByFolderId = ( } }; +export const folderBfsTraversal = async ( + root: TFolderSchema, + callback: (data: TFolderSchema) => void | Promise +) => { + const queue = [root]; + while (queue.length) { + const folder = queue.pop() as TFolderSchema; + await callback(folder); + queue.push(...folder.children); + } +}; + // bfs and then append to the folder const appendChild = (folders: TFolderSchema, folderName: string) => { const folder = folders.children.find(({ name }) => name === folderName); @@ -85,7 +97,6 @@ export const deleteFolderById = (folders: TFolderSchema, folderId: string) => { const queue = [folders]; while (queue.length) { const folder = queue.pop() as TFolderSchema; - folder.version += 1; const index = folder.children.findIndex(({ id }) => folderId === id); if (index !== -1) { const deletedFolder = folder.children.splice(index, 1); diff --git a/frontend/src/components/navigation/NavHeader.tsx b/frontend/src/components/navigation/NavHeader.tsx index ba9c1a965..c34bc5a1a 100644 --- a/frontend/src/components/navigation/NavHeader.tsx +++ b/frontend/src/components/navigation/NavHeader.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; import { useRouter } from 'next/router'; -import { faAngleRight, faHome } from '@fortawesome/free-solid-svg-icons'; +import { faAngleRight } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { useOrganization, useWorkspace } from '@app/context'; @@ -114,7 +114,7 @@ export default function NavHeader({ ) : ( - {name === 'root' ? : name} + {name === 'root' ? '/' : name} )} diff --git a/frontend/src/hooks/api/secretSnapshots/queries.tsx b/frontend/src/hooks/api/secretSnapshots/queries.tsx index b5daf62fd..92d7ed605 100644 --- a/frontend/src/hooks/api/secretSnapshots/queries.tsx +++ b/frontend/src/hooks/api/secretSnapshots/queries.tsx @@ -180,6 +180,7 @@ export const usePerformSecretRollback = () => { return data; }, onSuccess: (_, { workspaceId, environment, folderId }) => { + queryClient.invalidateQueries([{ workspaceId, environment }, 'secrets']); queryClient.invalidateQueries(secretSnapshotKeys.list(workspaceId, environment, folderId)); queryClient.invalidateQueries(secretSnapshotKeys.count(workspaceId, environment, folderId)); }