From deaa85cbe715cbf9908d43fbda8bc08c37ed1f67 Mon Sep 17 00:00:00 2001 From: akhilmhdh Date: Mon, 29 May 2023 22:23:03 +0530 Subject: [PATCH] feat(folders): added support for snapshot by env and folder --- backend/package-lock.json | 38 +- backend/package.json | 2 +- .../controllers/v1/secretsFolderController.ts | 260 ++- .../src/controllers/v2/secretsController.ts | 1812 +++++++++-------- .../src/ee/controllers/v1/secretController.ts | 266 +-- .../v1/secretSnapshotController.ts | 65 +- .../ee/controllers/v1/workspaceController.ts | 603 +++--- backend/src/ee/helpers/secret.ts | 54 +- backend/src/ee/helpers/secretVersion.ts | 130 +- backend/src/ee/models/folderVersion.ts | 60 + backend/src/ee/models/index.ts | 29 +- backend/src/ee/models/secretSnapshot.ts | 69 +- backend/src/ee/models/secretVersion.ts | 220 +- backend/src/ee/routes/v1/workspace.ts | 128 +- backend/src/ee/services/EESecretService.ts | 119 +- backend/src/helpers/auth.ts | 2 +- backend/src/helpers/membership.ts | 184 +- backend/src/helpers/secret.ts | 16 +- backend/src/helpers/secrets.ts | 1195 +++++------ backend/src/index.ts | 41 +- .../services/SecretService/index.ts | 78 +- backend/src/middleware/requestErrorHandler.ts | 5 + backend/src/models/folder.ts | 76 +- backend/src/models/secret.ts | 277 ++- backend/src/routes/v1/secretsFolder.ts | 66 +- backend/src/routes/v2/secrets.ts | 373 ++-- backend/src/services/FolderService.ts | 178 ++ backend/src/types/secret/index.d.ts | 71 +- backend/src/utils/folder.ts | 142 +- backend/src/utils/patchAsyncRoutes.js | 69 + backend/src/utils/setup/backfillData.ts | 611 +++--- backend/src/utils/setup/index.ts | 110 +- 32 files changed, 4059 insertions(+), 3290 deletions(-) create mode 100644 backend/src/ee/models/folderVersion.ts create mode 100644 backend/src/services/FolderService.ts create mode 100644 backend/src/utils/patchAsyncRoutes.js diff --git a/backend/package-lock.json b/backend/package-lock.json index 305e0df90..9edf8b98e 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -29,7 +29,6 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.1", "express": "^4.18.1", - "express-async-errors": "^3.1.1", "express-rate-limit": "^6.7.0", "express-validator": "^6.14.2", "handlebars": "^4.7.7", @@ -42,6 +41,7 @@ "lodash": "^4.17.21", "mongoose": "^6.10.5", "node-cache": "^5.1.2", + "nanoid": "^3.3.6", "nodemailer": "^6.8.0", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", @@ -5146,14 +5146,6 @@ "node": ">= 0.10.0" } }, - "node_modules/express-async-errors": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", - "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", - "peerDependencies": { - "express": "^4.16.2" - } - }, "node_modules/express-rate-limit": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", @@ -7388,6 +7380,23 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -16230,12 +16239,6 @@ } } }, - "express-async-errors": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/express-async-errors/-/express-async-errors-3.1.1.tgz", - "integrity": "sha512-h6aK1da4tpqWSbyCa3FxB/V6Ehd4EEB15zyQq9qe75OZBp0krinNKuH4rAY+S/U/2I36vdLAUFSjQJ+TFmODng==", - "requires": {} - }, "express-rate-limit": { "version": "6.7.0", "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz", @@ -17909,6 +17912,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/backend/package.json b/backend/package.json index d049b8015..468f444f6 100644 --- a/backend/package.json +++ b/backend/package.json @@ -20,7 +20,6 @@ "crypto-js": "^4.1.1", "dotenv": "^16.0.1", "express": "^4.18.1", - "express-async-errors": "^3.1.1", "express-rate-limit": "^6.7.0", "express-validator": "^6.14.2", "handlebars": "^4.7.7", @@ -33,6 +32,7 @@ "lodash": "^4.17.21", "mongoose": "^6.10.5", "node-cache": "^5.1.2", + "nanoid": "^3.3.6", "nodemailer": "^6.8.0", "passport": "^0.6.0", "passport-google-oauth20": "^2.0.0", diff --git a/backend/src/controllers/v1/secretsFolderController.ts b/backend/src/controllers/v1/secretsFolderController.ts index d20456577..5f167e337 100644 --- a/backend/src/controllers/v1/secretsFolderController.ts +++ b/backend/src/controllers/v1/secretsFolderController.ts @@ -1,107 +1,233 @@ -import { Request, Response } from 'express'; -import { Secret } from '../../models'; -import Folder from '../../models/folder'; -import { BadRequestError } from '../../utils/errors'; -import { ROOT_FOLDER_PATH, getFolderPath, getParentPath, normalizePath, validateFolderName } from '../../utils/folder'; -import { ADMIN, MEMBER } from '../../variables'; -import { validateMembership } from '../../helpers/membership'; +import { Request, Response } from "express"; +import { Secret } from "../../models"; +import Folder from "../../models/folder"; +import { BadRequestError } from "../../utils/errors"; +import { + appendFolder, + deleteFolderById, + getAllFolderIds, + searchByFolderIdWithDir, + searchByFolderId, + validateFolderName, + generateFolderId, + getParentFromFolderId, +} from "../../services/FolderService"; +import { ADMIN, MEMBER } from "../../variables"; +import { validateMembership } from "../../helpers/membership"; +import { FolderVersion } from "../../ee/models"; +import { EESecretService } from "../../ee/services"; // TODO // verify workspace id/environment export const createFolder = async (req: Request, res: Response) => { - const { workspaceId, environment, folderName, parentFolderId } = req.body + const { workspaceId, environment, folderName, parentFolderId } = req.body; if (!validateFolderName(folderName)) { - throw BadRequestError({ message: "Folder name cannot contain spaces. Only underscore and dashes" }) + throw BadRequestError({ + message: "Folder name cannot contain spaces. Only underscore and dashes", + }); } - if (parentFolderId) { - const parentFolder = await Folder.find({ environment: environment, workspace: workspaceId, id: parentFolderId }); - if (!parentFolder) { - throw BadRequestError({ message: "The parent folder doesn't exist" }) - } - } - - let completePath = await getFolderPath(parentFolderId) - if (completePath == ROOT_FOLDER_PATH) { - completePath = "" - } - - const currentFolderPath = completePath + "/" + folderName // construct new path with current folder to be created - const normalizedCurrentPath = normalizePath(currentFolderPath) - const normalizedParentPath = getParentPath(normalizedCurrentPath) - - const existingFolder = await Folder.findOne({ - name: folderName, + const folders = await Folder.findOne({ workspace: workspaceId, - environment: environment, - parent: parentFolderId, - path: normalizedCurrentPath + environment, + }).lean(); + // space has no folders initialized + if (!folders) { + const id = generateFolderId(); + const folder = new Folder({ + workspace: workspaceId, + environment, + nodes: { + id: "root", + name: "root", + version: 1, + children: [{ id, name: folderName, children: [], version: 1 }], + }, + }); + await folder.save(); + const folderVersion = new FolderVersion({ + workspace: workspaceId, + environment, + nodes: folder.nodes, + }); + await folderVersion.save(); + return res.json({ folder: { id, name: folderName } }); + } + + const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); + await Folder.findByIdAndUpdate(folders._id, folders); + + const parentFolder = searchByFolderId(folders.nodes, parentFolderId); + const folderVersion = new FolderVersion({ + workspace: workspaceId, + environment, + // root condition + nodes: parentFolder || folders.nodes, + }); + await folderVersion.save(); + + return res.json({ folder }); +}; + +export const updateFolderById = async (req: Request, res: Response) => { + const { folderId } = req.params; + const { name, workspaceId, environment } = req.body; + + const folders = await Folder.findOne({ workspace: workspaceId, environment }); + if (!folders) { + throw BadRequestError({ message: "The folder doesn't exist" }); + } + + // check that user is a member of the workspace + await validateMembership({ + userId: req.user._id.toString(), + workspaceId, + acceptedRoles: [ADMIN, MEMBER], }); - if (existingFolder) { - return res.json(existingFolder) + const parentFolder = getParentFromFolderId(folders.nodes, folderId); + if (!parentFolder) { + throw BadRequestError({ message: "The folder doesn't exist" }); + } + const folder = parentFolder.children.find(({ id }) => id === folderId); + if (!folder) { + throw BadRequestError({ message: "The folder doesn't exist" }); } - const newFolder = new Folder({ - name: folderName, + parentFolder.version += 1; + folder.name = name; + folder.version += 1; + + await Folder.findByIdAndUpdate(folders._id, folders); + const folderVersion = new FolderVersion({ workspace: workspaceId, - environment: environment, - parent: parentFolderId, - path: normalizedCurrentPath, - parentPath: normalizedParentPath + environment, + nodes: parentFolder, }); + await folderVersion.save(); - await newFolder.save(); + // decision to take snapshot ot not + const secretCount = await Secret.count({ + workspace: workspaceId, + environment, + folder: folderId, + }); + if (secretCount || folder.children.length) { + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment, + folderId: parentFolder.name === "root" ? undefined : parentFolder.id, + }); + } - return res.json(newFolder) -} + return res.json({ + message: "Successfully updated folder", + folder: { name: folder.name, id: folder.id }, + }); +}; export const deleteFolder = async (req: Request, res: Response) => { - const { folderId } = req.params - const queue: any[] = [folderId]; + const { folderId } = req.params; + const { workspaceId, environment } = req.body; - const folder = await Folder.findById(folderId); - if (!folder) { - throw BadRequestError({ message: "The folder doesn't exist" }) + const folders = await Folder.findOne({ workspace: workspaceId, environment }); + if (!folders) { + throw BadRequestError({ message: "The folder doesn't exist" }); } // check that user is a member of the workspace await validateMembership({ userId: req.user._id.toString(), - workspaceId: folder.workspace as any, - acceptedRoles: [ADMIN, MEMBER] + workspaceId, + acceptedRoles: [ADMIN, MEMBER], }); - while (queue.length > 0) { - const currentFolderId = queue.shift(); + const delOp = deleteFolderById(folders.nodes, folderId); + if (!delOp) { + throw BadRequestError({ message: "The folder doesn't exist" }); + } + const { deletedNode: delFolder, parent: parentFolder } = delOp; - const childFolders = await Folder.find({ parent: currentFolderId }); - for (const childFolder of childFolders) { - queue.push(childFolder._id); - } + const delFolderIds = getAllFolderIds(delFolder); - await Secret.deleteMany({ folder: currentFolderId }); + // take a pre-delete snapshot if there are secrets inside the folders + const secretCount = await Secret.count({ + workspace: workspaceId, + environment, + folder: folderId, + }); - await Folder.deleteOne({ _id: currentFolderId }); + if (secretCount || delFolder.children.length) { + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment, + folderId: parentFolder.id, + }); } - res.send() -} + await Folder.findByIdAndUpdate(folders._id, folders); + const folderVersion = new FolderVersion({ + workspace: workspaceId, + environment, + nodes: parentFolder, + }); + await folderVersion.save(); + if (delFolderIds.length) { + await Secret.deleteMany({ + folder: { $in: delFolderIds.map(({ id }) => id) }, + workspace: workspaceId, + environment, + }); + } + + // post delete snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment, + folderId: parentFolder.id, + }); + + res.send({ message: "successfully deleted folders", folders: delFolderIds }); +}; // TODO: validate workspace -export const getFolderById = async (req: Request, res: Response) => { - const { folderId } = req.params +export const getFolders = async (req: Request, res: Response) => { + const { workspaceId, environment, parentFolderId } = req.query as { + workspaceId: string; + environment: string; + parentFolderId?: string; + }; - const folder = await Folder.findById(folderId); - if (!folder) { - throw BadRequestError({ message: "The folder doesn't exist" }) + const folders = await Folder.findOne({ workspace: workspaceId, environment }); + if (!folders) { + res.send({ folders: [], dir: [] }); + return; } + // check that user is a member of the workspace await validateMembership({ userId: req.user._id.toString(), - workspaceId: folder.workspace as any, - acceptedRoles: [ADMIN, MEMBER] + workspaceId, + acceptedRoles: [ADMIN, MEMBER], }); - res.send({ folder }) -} \ No newline at end of file + if (!parentFolderId) { + const rootFolders = folders.nodes.children.map(({ id, name }) => ({ + id, + name, + })); + res.send({ folders: rootFolders }); + return; + } + + const folderBySearch = searchByFolderIdWithDir(folders.nodes, parentFolderId); + if (!folderBySearch) { + throw BadRequestError({ message: "The folder doesn't exist" }); + } + const { folder, dir } = folderBySearch; + + res.send({ + folders: folder.children.map(({ id, name }) => ({ id, name })), + dir, + }); +}; diff --git a/backend/src/controllers/v2/secretsController.ts b/backend/src/controllers/v2/secretsController.ts index 4a5618454..f9d8ea217 100644 --- a/backend/src/controllers/v2/secretsController.ts +++ b/backend/src/controllers/v2/secretsController.ts @@ -1,337 +1,343 @@ -import { Types } from 'mongoose'; -import { Request, Response } from 'express'; -import { ISecret, Secret, Workspace } from '../../models'; -import { IAction, SecretVersion } from '../../ee/models'; +import { Types } from "mongoose"; +import { Request, Response } from "express"; +import { ISecret, Secret } from "../../models"; +import { IAction, SecretVersion } from "../../ee/models"; import { - SECRET_PERSONAL, - ACTION_ADD_SECRETS, - ACTION_READ_SECRETS, - ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8 -} from '../../variables'; -import { UnauthorizedRequestError, WorkspaceNotFoundError } from '../../utils/errors'; -import { EventService } from '../../services'; -import { eventPushSecrets } from '../../events'; -import { EESecretService, EELogService, EELicenseService } from '../../ee/services'; -import { TelemetryService, SecretService } from '../../services'; -import { getChannelFromUserAgent } from '../../utils/posthog'; -import { PERMISSION_WRITE_SECRETS } from '../../variables'; -import { userHasNoAbility, userHasWorkspaceAccess, userHasWriteOnlyAbility } from '../../ee/helpers/checkMembershipPermissions'; -import Tag from '../../models/tag'; -import _ from 'lodash'; + SECRET_PERSONAL, + ACTION_ADD_SECRETS, + ACTION_READ_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_DELETE_SECRETS, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, +} from "../../variables"; +import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; +import { EventService } from "../../services"; +import { eventPushSecrets } from "../../events"; +import { EESecretService, EELogService } from "../../ee/services"; +import { TelemetryService, SecretService } from "../../services"; +import { getChannelFromUserAgent } from "../../utils/posthog"; +import { PERMISSION_WRITE_SECRETS } from "../../variables"; import { - BatchSecretRequest, - BatchSecret -} from '../../types/secret'; -import { getFolderPath, getFoldersInDirectory, normalizePath } from '../../utils/folder'; -import { ROOT_FOLDER_PATH } from '../../utils/folder'; + userHasNoAbility, + userHasWorkspaceAccess, + userHasWriteOnlyAbility, +} from "../../ee/helpers/checkMembershipPermissions"; +import Tag from "../../models/tag"; +import _ from "lodash"; +import { BatchSecretRequest, BatchSecret } from "../../types/secret"; +import Folder from "../../models/folder"; +import { + getFolderByPath, + searchByFolderId, +} from "../../services/FolderService"; /** * Peform a batch of any specified CUD secret operations * (used by dashboard) - * @param req - * @param res + * @param req + * @param res */ export const batchSecrets = async (req: Request, res: Response) => { + const channel = getChannelFromUserAgent(req.headers["user-agent"]); + const postHogClient = await TelemetryService.getPostHogClient(); - const channel = getChannelFromUserAgent(req.headers['user-agent']); - const postHogClient = await TelemetryService.getPostHogClient(); + const { + workspaceId, + environment, + folderId, + requests, + }: { + workspaceId: string; + environment: string; + folderId: string; + requests: BatchSecretRequest[]; + } = req.body; - const { - workspaceId, - environment, - requests - }: { - workspaceId: string; - environment: string; - requests: BatchSecretRequest[]; - } = req.body; - - const workspace = await Workspace.findById(workspaceId); - if (!workspace) throw WorkspaceNotFoundError(); - - const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier >= 1; + const createSecrets: BatchSecret[] = []; + const updateSecrets: BatchSecret[] = []; + const deleteSecrets: Types.ObjectId[] = []; + const actions: IAction[] = []; - const createSecrets: BatchSecret[] = []; - const updateSecrets: BatchSecret[] = []; - const deleteSecrets: Types.ObjectId[] = []; - const actions: IAction[] = []; + // get secret blind index salt + const salt = await SecretService.getSecretBlindIndexSalt({ + workspaceId: new Types.ObjectId(workspaceId), + }); - // get secret blind index salt - const salt = await SecretService.getSecretBlindIndexSalt({ - workspaceId: new Types.ObjectId(workspaceId) + const folders = await Folder.findOne({ workspace: workspaceId, environment }); + if (folders && folderId !== "root") { + const folder = searchByFolderId(folders.nodes, folderId as string); + if (!folder) throw BadRequestError({ message: "Folder not found" }); + } + + for await (const request of requests) { + // do a validation + + let secretBlindIndex = ""; + switch (request.method) { + case "POST": + secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( + { + secretName: request.secret.secretName, + salt, + } + ); + + createSecrets.push({ + ...request.secret, + version: 1, + user: request.secret.type === SECRET_PERSONAL ? req.user : undefined, + environment, + workspace: new Types.ObjectId(workspaceId), + folder: folderId, + secretBlindIndex, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }); + break; + case "PATCH": + secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt( + { + secretName: request.secret.secretName, + salt, + } + ); + + updateSecrets.push({ + ...request.secret, + _id: new Types.ObjectId(request.secret._id), + secretBlindIndex, + folder: folderId, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }); + break; + case "DELETE": + deleteSecrets.push(new Types.ObjectId(request.secret._id)); + break; + } + } + + // handle create secrets + let createdSecrets: ISecret[] = []; + if (createSecrets.length > 0) { + createdSecrets = await Secret.insertMany(createSecrets); + // (EE) add secret versions for new secrets + await EESecretService.addSecretVersions({ + secretVersions: createdSecrets.map((n: any) => { + return { + ...n._doc, + _id: new Types.ObjectId(), + secret: n._id, + isDeleted: false, + }; + }), }); - for await (const request of requests) { - const folderId = request.secret.folderId + const addAction = (await EELogService.createAction({ + name: ACTION_ADD_SECRETS, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(workspaceId), + secretIds: createdSecrets.map((n) => n._id), + })) as IAction; + actions.push(addAction); - // TODO: need to auth folder - const fullFolderPath = await getFolderPath(folderId) - - let secretBlindIndex = ''; - switch (request.method) { - case 'POST': - secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ - secretName: request.secret.secretName, - salt - }); - - createSecrets.push({ - ...request.secret, - version: 1, - user: request.secret.type === SECRET_PERSONAL ? req.user : undefined, - environment, - workspace: new Types.ObjectId(workspaceId), - path: fullFolderPath, - folder: folderId, - secretBlindIndex, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }); - break; - case 'PATCH': - secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ - secretName: request.secret.secretName, - salt, - }); - - updateSecrets.push({ - ...request.secret, - _id: new Types.ObjectId(request.secret._id), - secretBlindIndex, - folder: folderId, - path: fullFolderPath, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }); - break; - case 'DELETE': - deleteSecrets.push(new Types.ObjectId(request.secret._id)); - break; - } + if (postHogClient) { + postHogClient.capture({ + event: "secrets added", + distinctId: req.user.email, + properties: { + numberOfSecrets: createdSecrets.length, + environment, + workspaceId, + channel, + userAgent: req.headers?.["user-agent"], + }, + }); } + } - // handle create secrets - let createdSecrets: ISecret[] = []; - if (createSecrets.length > 0) { - createdSecrets = await Secret.insertMany(createSecrets); - // (EE) add secret versions for new secrets - await EESecretService.addSecretVersions({ - secretVersions: createdSecrets.map((n: any) => { - return ({ - ...n._doc, - _id: new Types.ObjectId(), - secret: n._id, - isDeleted: false - }); - }) - }); + // handle update secrets + let updatedSecrets: ISecret[] = []; + if (updateSecrets.length > 0 && req.secrets) { + // construct object containing all secrets + let listedSecretsObj: { + [key: string]: { + version: number; + type: string; + }; + } = {}; - const addAction = await EELogService.createAction({ - name: ACTION_ADD_SECRETS, - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(workspaceId), - secretIds: createdSecrets.map((n) => n._id) - }) as IAction; - actions.push(addAction); + listedSecretsObj = req.secrets.reduce( + (obj: any, secret: ISecret) => ({ + ...obj, + [secret._id.toString()]: secret, + }), + {} + ); - if (postHogClient) { - postHogClient.capture({ - event: 'secrets added', - distinctId: req.user.email, - properties: { - numberOfSecrets: createdSecrets.length, - environment, - workspaceId, - channel, - userAgent: req.headers?.['user-agent'], - isPaid, - email: req.user.email - } - }); - } - } + const updateOperations = updateSecrets.map((u) => ({ + updateOne: { + filter: { + _id: new Types.ObjectId(u._id), + workspace: new Types.ObjectId(workspaceId), + }, + update: { + $inc: { + version: 1, + }, + ...u, + _id: new Types.ObjectId(u._id), + }, + }, + })); - // handle update secrets - let updatedSecrets: ISecret[] = []; - if (updateSecrets.length > 0 && req.secrets) { - // construct object containing all secrets - let listedSecretsObj: { - [key: string]: { - version: number; - type: string; - } - } = {}; + await Secret.bulkWrite(updateOperations); - listedSecretsObj = req.secrets.reduce((obj: any, secret: ISecret) => ({ - ...obj, - [secret._id.toString()]: secret - }), {}); - - const updateOperations = updateSecrets.map((u) => ({ - updateOne: { - filter: { - _id: new Types.ObjectId(u._id), - workspace: new Types.ObjectId(workspaceId) - }, - update: { - $inc: { - version: 1 - }, - ...u, - _id: new Types.ObjectId(u._id) - } - } - })); - - await Secret.bulkWrite(updateOperations); - - const secretVersions = updateSecrets.map((u) => new SecretVersion({ - secret: new Types.ObjectId(u._id), - version: listedSecretsObj[u._id.toString()].version, - workspace: new Types.ObjectId(workspaceId), - type: listedSecretsObj[u._id.toString()].type, - environment, - isDeleted: false, - secretBlindIndex: u.secretBlindIndex, - secretKeyCiphertext: u.secretKeyCiphertext, - secretKeyIV: u.secretKeyIV, - secretKeyTag: u.secretKeyTag, - secretValueCiphertext: u.secretValueCiphertext, - secretValueIV: u.secretValueIV, - secretValueTag: u.secretValueTag, - secretCommentCiphertext: u.secretCommentCiphertext, - secretCommentIV: u.secretCommentIV, - secretCommentTag: u.secretCommentTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - tags: u.tags - })); - - await EESecretService.addSecretVersions({ - secretVersions - }); - - updatedSecrets = await Secret.find({ - _id: { - $in: updateSecrets.map((u) => new Types.ObjectId(u._id)) - } - }); - - const updateAction = await EELogService.createAction({ - name: ACTION_UPDATE_SECRETS, - userId: req.user._id, - workspaceId: new Types.ObjectId(workspaceId), - secretIds: updatedSecrets.map((u) => u._id) - }) as IAction; - actions.push(updateAction); - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets modified', - distinctId: req.user.email, - properties: { - numberOfSecrets: updateSecrets.length, - environment, - workspaceId, - channel, - userAgent: req.headers?.['user-agent'], - isPaid, - email: req.user.email - } - }); - } - } - - // handle delete secrets - if (deleteSecrets.length > 0) { - await Secret.deleteMany({ - _id: { - $in: deleteSecrets - } - }); - - await EESecretService.markDeletedSecretVersions({ - secretIds: deleteSecrets - }); - - const deleteAction = await EELogService.createAction({ - name: ACTION_DELETE_SECRETS, - userId: req.user._id, - workspaceId: new Types.ObjectId(workspaceId), - secretIds: deleteSecrets - }) as IAction; - actions.push(deleteAction); - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets deleted', - distinctId: req.user.email, - properties: { - numberOfSecrets: deleteSecrets.length, - environment, - workspaceId, - channel: channel, - userAgent: req.headers?.['user-agent'], - isPaid, - email: req.user.email - } - }); - } - } - - if (actions.length > 0) { - // (EE) create (audit) log - await EELogService.createLog({ - userId: req.user._id.toString(), - workspaceId: new Types.ObjectId(workspaceId), - actions, - channel, - ipAddress: req.ip - }); - } - - // // trigger event - push secrets - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId) + const secretVersions = updateSecrets.map( + (u) => + new SecretVersion({ + secret: new Types.ObjectId(u._id), + version: listedSecretsObj[u._id.toString()].version, + workspace: new Types.ObjectId(workspaceId), + type: listedSecretsObj[u._id.toString()].type, + environment, + isDeleted: false, + secretBlindIndex: u.secretBlindIndex, + secretKeyCiphertext: u.secretKeyCiphertext, + secretKeyIV: u.secretKeyIV, + secretKeyTag: u.secretKeyTag, + secretValueCiphertext: u.secretValueCiphertext, + secretValueIV: u.secretValueIV, + secretValueTag: u.secretValueTag, + secretCommentCiphertext: u.secretCommentCiphertext, + secretCommentIV: u.secretCommentIV, + secretCommentTag: u.secretCommentTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + tags: u.tags, }) + ); + + await EESecretService.addSecretVersions({ + secretVersions, }); - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(workspaceId) + updatedSecrets = await Secret.find({ + _id: { + $in: updateSecrets.map((u) => new Types.ObjectId(u._id)), + }, }); - const resObj: { [key: string]: ISecret[] | string[] } = {} + const updateAction = (await EELogService.createAction({ + name: ACTION_UPDATE_SECRETS, + userId: req.user._id, + workspaceId: new Types.ObjectId(workspaceId), + secretIds: updatedSecrets.map((u) => u._id), + })) as IAction; + actions.push(updateAction); - if (createSecrets.length > 0) { - resObj['createdSecrets'] = createdSecrets; + if (postHogClient) { + postHogClient.capture({ + event: "secrets modified", + distinctId: req.user.email, + properties: { + numberOfSecrets: updateSecrets.length, + environment, + workspaceId, + channel, + userAgent: req.headers?.["user-agent"], + }, + }); } + } - if (updateSecrets.length > 0) { - resObj['updatedSecrets'] = updatedSecrets; + // handle delete secrets + if (deleteSecrets.length > 0) { + await Secret.deleteMany({ + _id: { + $in: deleteSecrets, + }, + }); + + await EESecretService.markDeletedSecretVersions({ + secretIds: deleteSecrets, + }); + + const deleteAction = (await EELogService.createAction({ + name: ACTION_DELETE_SECRETS, + userId: req.user._id, + workspaceId: new Types.ObjectId(workspaceId), + secretIds: deleteSecrets, + })) as IAction; + actions.push(deleteAction); + + if (postHogClient) { + postHogClient.capture({ + event: "secrets deleted", + distinctId: req.user.email, + properties: { + numberOfSecrets: deleteSecrets.length, + environment, + workspaceId, + channel: channel, + userAgent: req.headers?.["user-agent"], + }, + }); } + } - if (deleteSecrets.length > 0) { - resObj['deletedSecrets'] = deleteSecrets.map((d) => d.toString()); - } + if (actions.length > 0) { + // (EE) create (audit) log + await EELogService.createLog({ + userId: req.user._id.toString(), + workspaceId: new Types.ObjectId(workspaceId), + actions, + channel, + ipAddress: req.ip, + }); + } - return res.status(200).send(resObj); -} + // // trigger event - push secrets + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + }), + }); + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + folderId, + }); + + const resObj: { [key: string]: ISecret[] | string[] } = {}; + + if (createSecrets.length > 0) { + resObj["createdSecrets"] = createdSecrets; + } + + if (updateSecrets.length > 0) { + resObj["updatedSecrets"] = updatedSecrets; + } + + if (deleteSecrets.length > 0) { + resObj["deletedSecrets"] = deleteSecrets.map((d) => d.toString()); + } + + return res.status(200).send(resObj); +}; /** * Create secret(s) for workspace with id [workspaceId] and environment [environment] - * @param req - * @param res + * @param req + * @param res */ export const createSecrets = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Create new secret(s)' #swagger.description = 'Create one or many secrets for a given project and environment.' @@ -384,204 +390,219 @@ export const createSecrets = async (req: Request, res: Response) => { } */ - const channel = getChannelFromUserAgent(req.headers['user-agent']) - const { workspaceId, environment }: { workspaceId: string, environment: string } = req.body; + const channel = getChannelFromUserAgent(req.headers["user-agent"]); + const { + workspaceId, + environment, + folderId, + }: { workspaceId: string; environment: string; folderId: string } = req.body; - if (req.user) { - const hasAccess = await userHasWorkspaceAccess(req.user, new Types.ObjectId(workspaceId), environment, PERMISSION_WRITE_SECRETS) - if (!hasAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } - } - - const workspace = await Workspace.findById(workspaceId); - if (!workspace) throw WorkspaceNotFoundError(); - - const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier >= 1; - - let listOfSecretsToCreate; - if (Array.isArray(req.body.secrets)) { - // case: create multiple secrets - listOfSecretsToCreate = req.body.secrets; - } else if (typeof req.body.secrets === 'object') { - // case: create 1 secret - listOfSecretsToCreate = [req.body.secrets]; - } - - // get secret blind index salt - const salt = await SecretService.getSecretBlindIndexSalt({ - workspaceId: new Types.ObjectId(workspaceId) - }); - - type secretsToCreateType = { - type: string; - secretName?: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - tags: string[] - } - - const secretsToInsert: ISecret[] = await Promise.all( - listOfSecretsToCreate.map(async ({ - type, - secretName, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - tags - }: secretsToCreateType) => { - let secretBlindIndex; - if (secretName) { - secretBlindIndex = await SecretService.generateSecretBlindIndexWithSalt({ - secretName, - salt - }); - } - - return ({ - version: 1, - workspace: new Types.ObjectId(workspaceId), - type, - ...(secretBlindIndex ? { secretBlindIndex } : {}), - user: (req.user && type === SECRET_PERSONAL) ? req.user : undefined, - environment, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - tags - }); - }) + if (req.user) { + const hasAccess = await userHasWorkspaceAccess( + req.user, + new Types.ObjectId(workspaceId), + environment, + PERMISSION_WRITE_SECRETS ); - - const newlyCreatedSecrets: ISecret[] = (await Secret.insertMany(secretsToInsert)).map((insertedSecret) => insertedSecret.toObject()); - - setTimeout(async () => { - // trigger event - push secrets - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(workspaceId) - }) - }); - }, 5000); - - // (EE) add secret versions for new secrets - await EESecretService.addSecretVersions({ - secretVersions: newlyCreatedSecrets.map(({ - _id, - version, - workspace, - type, - user, - environment, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag - }) => new SecretVersion({ - secret: _id, - version, - workspace, - type, - user, - environment, - secretBlindIndex, - isDeleted: false, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - })) - }); - - const addAction = await EELogService.createAction({ - name: ACTION_ADD_SECRETS, - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(workspaceId), - secretIds: newlyCreatedSecrets.map((n) => n._id) - }); - - // (EE) create (audit) log - addAction && await EELogService.createLog({ - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(workspaceId), - actions: [addAction], - channel, - ipAddress: req.ip - }); - - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(workspaceId) - }); - - const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { - postHogClient.capture({ - event: 'secrets added', - distinctId: await TelemetryService.getDistinctId({ - authData: req.authData - }), - properties: { - numberOfSecrets: listOfSecretsToCreate.length, - environment, - workspaceId, - channel: channel, - userAgent: req.headers?.['user-agent'], - isPaid, - email: await TelemetryService.getDistinctId({ - authData: req.authData - }) - } - }); + if (!hasAccess) { + throw UnauthorizedRequestError({ + message: + "You do not have the necessary permission(s) perform this action", + }); } + } - return res.status(200).send({ - secrets: newlyCreatedSecrets + let listOfSecretsToCreate; + if (Array.isArray(req.body.secrets)) { + // case: create multiple secrets + listOfSecretsToCreate = req.body.secrets; + } else if (typeof req.body.secrets === "object") { + // case: create 1 secret + listOfSecretsToCreate = [req.body.secrets]; + } + + // get secret blind index salt + const salt = await SecretService.getSecretBlindIndexSalt({ + workspaceId: new Types.ObjectId(workspaceId), + }); + + type secretsToCreateType = { + type: string; + secretName?: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentCiphertext: string; + secretCommentIV: string; + secretCommentTag: string; + tags: string[]; + }; + + const secretsToInsert: ISecret[] = await Promise.all( + listOfSecretsToCreate.map( + async ({ + type, + secretName, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + tags, + }: secretsToCreateType) => { + let secretBlindIndex; + if (secretName) { + secretBlindIndex = + await SecretService.generateSecretBlindIndexWithSalt({ + secretName, + salt, + }); + } + + return { + version: 1, + workspace: new Types.ObjectId(workspaceId), + type, + folderId, + ...(secretBlindIndex ? { secretBlindIndex } : {}), + user: req.user && type === SECRET_PERSONAL ? req.user : undefined, + environment, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + tags, + }; + } + ) + ); + + const newlyCreatedSecrets: ISecret[] = ( + await Secret.insertMany(secretsToInsert) + ).map((insertedSecret) => insertedSecret.toObject()); + + setTimeout(async () => { + // trigger event - push secrets + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(workspaceId), + }), }); -} + }, 5000); + + // (EE) add secret versions for new secrets + await EESecretService.addSecretVersions({ + secretVersions: newlyCreatedSecrets.map( + ({ + _id, + version, + workspace, + type, + user, + environment, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + }) => + new SecretVersion({ + secret: _id, + version, + workspace, + type, + user, + environment, + secretBlindIndex, + isDeleted: false, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + folder: folderId, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }) + ), + }); + + const addAction = await EELogService.createAction({ + name: ACTION_ADD_SECRETS, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(workspaceId), + secretIds: newlyCreatedSecrets.map((n) => n._id), + }); + + // (EE) create (audit) log + addAction && + (await EELogService.createLog({ + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(workspaceId), + actions: [addAction], + channel, + ipAddress: req.ip, + })); + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + folderId, + }); + + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "secrets added", + distinctId: await TelemetryService.getDistinctId({ + authData: req.authData, + }), + properties: { + numberOfSecrets: listOfSecretsToCreate.length, + environment, + workspaceId, + channel: channel, + userAgent: req.headers?.["user-agent"], + }, + }); + } + + return res.status(200).send({ + secrets: newlyCreatedSecrets, + }); +}; /** * Return secret(s) for workspace with id [workspaceId], environment [environment] and user * with id [req.user._id] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getSecrets = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Read secrets' #swagger.description = 'Read secrets from a project and environment' @@ -621,172 +642,180 @@ export const getSecrets = async (req: Request, res: Response) => { } */ - const { tagSlugs, secretsPath } = req.query; - const workspaceId = req.query.workspaceId as string; - const environment = req.query.environment as string; - const normalizedPath = normalizePath(secretsPath as string) - const folders = await getFoldersInDirectory(workspaceId as string, environment as string, normalizedPath) + const { tagSlugs, secretPath } = req.query; + let { folderId } = req.query; + const workspaceId = req.query.workspaceId as string; + const environment = req.query.environment as string; - const workspace = await Workspace.findById(workspaceId); - if (!workspace) throw WorkspaceNotFoundError(); - - const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier >= 1; + const folders = await Folder.findOne({ workspace: workspaceId, environment }); + if ( + (!folders && folderId !== "root" && folderId) || + (!folders && secretPath) + ) { + throw BadRequestError({ message: "Folder not found" }); + } + if (folders && folderId !== "root") { + const folder = searchByFolderId(folders.nodes, folderId as string); + if (!folder) throw BadRequestError({ message: "Folder not found" }); + } - // secrets to return - let secrets: ISecret[] = []; - - // query tags table to get all tags ids for the tag names for the given workspace - let tagIds = []; - const tagNamesList = typeof tagSlugs === 'string' && tagSlugs !== '' ? tagSlugs.split(',') : []; - if (tagNamesList != undefined && tagNamesList.length != 0) { - const workspaceFromDB = await Tag.find({ workspace: workspaceId }); - tagIds = _.map(tagNamesList, (tagName) => { - const tag = _.find(workspaceFromDB, { slug: tagName }); - return tag ? tag.id : null; - }); + if (folders && secretPath) { + if (!folders) throw BadRequestError({ message: "Folder not found" }); + const folder = getFolderByPath(folders.nodes, folderId as string); + if (!folder) { + throw BadRequestError({ message: "Secret path not found" }); } + folderId = folder.id; + } - if (req.user) { - // case: client authorization is via JWT - const hasWriteOnlyAccess = await userHasWriteOnlyAbility(req.user._id, new Types.ObjectId(workspaceId), environment) - const hasNoAccess = await userHasNoAbility(req.user._id, new Types.ObjectId(workspaceId), environment) - if (hasNoAccess) { - throw UnauthorizedRequestError({ message: "You do not have the necessary permission(s) perform this action" }) - } + // secrets to return + let secrets: ISecret[] = []; - const secretQuery: any = { - workspace: workspaceId, - environment, - $or: [ - { user: req.user._id }, // personal secrets for this user - { user: { $exists: false } } // shared secrets from workspace - ] - } - - if (normalizedPath == ROOT_FOLDER_PATH) { - secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } - } else if (normalizedPath) { - secretQuery.path = normalizedPath - } - - if (tagIds.length > 0) { - secretQuery.tags = { $in: tagIds }; - } - - if (hasWriteOnlyAccess) { - // only return the secret keys and not the values since user does not have right to see values - secrets = await Secret.find(secretQuery).select("secretKeyCiphertext secretKeyIV secretKeyTag").populate("tags") - } else { - secrets = await Secret.find(secretQuery).populate("tags") - } - } - - // case: client authorization is via service token - if (req.serviceTokenData) { - const userId = req.serviceTokenData.user; - - const secretQuery: any = { - workspace: workspaceId, - environment, - $or: [ - { user: userId }, // personal secrets for this user - { user: { $exists: false } } // shared secrets from workspace - ] - } - - // TODO: check if user can query for given path - if (normalizedPath == ROOT_FOLDER_PATH) { - secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } - } else if (normalizedPath) { - secretQuery.path = normalizedPath - } - - if (tagIds.length > 0) { - secretQuery.tags = { $in: tagIds }; - } - - // TODO check if service token has write only permission - - secrets = await Secret.find(secretQuery).populate("tags"); - } - - // case: client authorization is via service account - if (req.serviceAccount) { - const secretQuery: any = { - workspace: workspaceId, - environment, - user: { $exists: false } // shared secrets only from workspace - } - - if (normalizedPath == ROOT_FOLDER_PATH) { - secretQuery.path = { $in: [ROOT_FOLDER_PATH, null, undefined] } - } else if (normalizedPath) { - secretQuery.path = normalizedPath - } - - if (tagIds.length > 0) { - secretQuery.tags = { $in: tagIds }; - } - - secrets = await Secret.find(secretQuery).populate("tags"); - } - - const channel = getChannelFromUserAgent(req.headers['user-agent']) - - const readAction = await EELogService.createAction({ - name: ACTION_READ_SECRETS, - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(workspaceId as string), - secretIds: secrets.map((n: any) => n._id) + // query tags table to get all tags ids for the tag names for the given workspace + let tagIds = []; + const tagNamesList = + typeof tagSlugs === "string" && tagSlugs !== "" ? tagSlugs.split(",") : []; + if (tagNamesList != undefined && tagNamesList.length != 0) { + const workspaceFromDB = await Tag.find({ workspace: workspaceId }); + tagIds = _.map(tagNamesList, (tagName: string) => { + const tag = _.find(workspaceFromDB, { slug: tagName }); + return tag ? tag.id : null; }); + } - readAction && await EELogService.createLog({ - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(workspaceId as string), - actions: [readAction], + if (req.user) { + // case: client authorization is via JWT + const hasWriteOnlyAccess = await userHasWriteOnlyAbility( + req.user._id, + new Types.ObjectId(workspaceId), + environment + ); + const hasNoAccess = await userHasNoAbility( + req.user._id, + new Types.ObjectId(workspaceId), + environment + ); + if (hasNoAccess) { + throw UnauthorizedRequestError({ + message: + "You do not have the necessary permission(s) perform this action", + }); + } + + const secretQuery: any = { + workspace: workspaceId, + environment, + folder: folderId, + $or: [ + { user: req.user._id }, // personal secrets for this user + { user: { $exists: false } }, // shared secrets from workspace + ], + }; + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + if (hasWriteOnlyAccess) { + // only return the secret keys and not the values since user does not have right to see values + secrets = await Secret.find(secretQuery) + .select("secretKeyCiphertext secretKeyIV secretKeyTag") + .populate("tags"); + } else { + secrets = await Secret.find(secretQuery).populate("tags"); + } + } + + // case: client authorization is via service token + if (req.serviceTokenData) { + const userId = req.serviceTokenData.user; + + const secretQuery: any = { + workspace: workspaceId, + folder: folderId, + environment, + $or: [ + { user: userId }, // personal secrets for this user + { user: { $exists: false } }, // shared secrets from workspace + ], + }; + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + // TODO check if service token has write only permission + + secrets = await Secret.find(secretQuery).populate("tags"); + } + + // case: client authorization is via service account + if (req.serviceAccount) { + const secretQuery: any = { + workspace: workspaceId, + environment, + folder: folderId, + user: { $exists: false }, // shared secrets only from workspace + }; + + if (tagIds.length > 0) { + secretQuery.tags = { $in: tagIds }; + } + + secrets = await Secret.find(secretQuery).populate("tags"); + } + + const channel = getChannelFromUserAgent(req.headers["user-agent"]); + + const readAction = await EELogService.createAction({ + name: ACTION_READ_SECRETS, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(workspaceId as string), + secretIds: secrets.map((n: any) => n._id), + }); + + readAction && + (await EELogService.createLog({ + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(workspaceId as string), + actions: [readAction], + channel, + ipAddress: req.ip, + })); + + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "secrets pulled", + distinctId: await TelemetryService.getDistinctId({ + authData: req.authData, + }), + properties: { + numberOfSecrets: secrets.length, + environment, + workspaceId, channel, - ipAddress: req.ip + userAgent: req.headers?.["user-agent"], + }, }); + } - const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { - postHogClient.capture({ - event: 'secrets pulled', - distinctId: await TelemetryService.getDistinctId({ - authData: req.authData - }), - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel, - userAgent: req.headers?.['user-agent'], - isPaid, - email: await TelemetryService.getDistinctId({ - authData: req.authData - }) - } - }); - } - - return res.status(200).send({ - secrets, - folders - }); -} + return res.status(200).send({ + secrets, + }); +}; /** * Update secret(s) - * @param req - * @param res + * @param req + * @param res */ export const updateSecrets = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Update secret(s)' #swagger.description = 'Update secret(s)' @@ -830,204 +859,211 @@ export const updateSecrets = async (req: Request, res: Response) => { } } */ - const channel = req.headers?.['user-agent']?.toLowerCase().includes('mozilla') ? 'web' : 'cli'; + const channel = req.headers?.["user-agent"]?.toLowerCase().includes("mozilla") + ? "web" + : "cli"; - interface PatchSecret { - id: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - tags: string[] - } + interface PatchSecret { + id: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentCiphertext: string; + secretCommentIV: string; + secretCommentTag: string; + tags: string[]; + } - const updateOperationsToPerform = req.body.secrets.map((secret: PatchSecret) => { - const { + const updateOperationsToPerform = req.body.secrets.map( + (secret: PatchSecret) => { + const { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + tags, + } = secret; + + return { + updateOne: { + filter: { _id: new Types.ObjectId(secret.id) }, + update: { + $inc: { + version: 1, + }, secretKeyCiphertext, secretKeyIV, secretKeyTag, secretValueCiphertext, secretValueIV, secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - tags - } = secret; - - return ({ - updateOne: { - filter: { _id: new Types.ObjectId(secret.id) }, - update: { - $inc: { - version: 1 - }, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - tags, - ...(( - secretCommentCiphertext !== undefined && - secretCommentIV && - secretCommentTag - ) ? { - secretCommentCiphertext, - secretCommentIV, - secretCommentTag - } : {}), + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + tags, + ...(secretCommentCiphertext !== undefined && + secretCommentIV && + secretCommentTag + ? { + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, } - } - }); - }); - - await Secret.bulkWrite(updateOperationsToPerform); - - const secretModificationsBySecretId: { [key: string]: PatchSecret } = {}; - req.body.secrets.forEach((secret: PatchSecret) => { - secretModificationsBySecretId[secret.id] = secret; - }); - - const ListOfSecretsBeforeModifications = req.secrets - const secretVersions = { - secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => { - const { - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - tags - } = secretModificationsBySecretId[secret._id.toString()] - - return ({ - secret: secret._id, - version: secret.version + 1, - workspace: secret.workspace, - type: secret.type, - environment: secret.environment, - secretKeyCiphertext: secretKeyCiphertext ? secretKeyCiphertext : secret.secretKeyCiphertext, - secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV, - secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag, - secretValueCiphertext: secretValueCiphertext ? secretValueCiphertext : secret.secretValueCiphertext, - secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV, - secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag, - secretCommentCiphertext: secretCommentCiphertext ? secretCommentCiphertext : secret.secretCommentCiphertext, - secretCommentIV: secretCommentIV ? secretCommentIV : secret.secretCommentIV, - secretCommentTag: secretCommentTag ? secretCommentTag : secret.secretCommentTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8, - tags: tags ? tags : secret.tags - }); - }) + : {}), + }, + }, + }; } + ); - await EESecretService.addSecretVersions(secretVersions); + await Secret.bulkWrite(updateOperationsToPerform); + const secretModificationsBySecretId: { [key: string]: PatchSecret } = {}; + req.body.secrets.forEach((secret: PatchSecret) => { + secretModificationsBySecretId[secret.id] = secret; + }); - // group secrets into workspaces so updated secrets can - // be logged and snapshotted separately for each workspace - const workspaceSecretObj: any = {}; - req.secrets.forEach((s: any) => { - if (s.workspace.toString() in workspaceSecretObj) { - workspaceSecretObj[s.workspace.toString()].push(s); - } else { - workspaceSecretObj[s.workspace.toString()] = [s] - } + const ListOfSecretsBeforeModifications = req.secrets; + const secretVersions = { + secretVersions: ListOfSecretsBeforeModifications.map((secret: ISecret) => { + const { + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + tags, + } = secretModificationsBySecretId[secret._id.toString()]; + + return { + secret: secret._id, + version: secret.version + 1, + workspace: secret.workspace, + type: secret.type, + environment: secret.environment, + secretKeyCiphertext: secretKeyCiphertext + ? secretKeyCiphertext + : secret.secretKeyCiphertext, + secretKeyIV: secretKeyIV ? secretKeyIV : secret.secretKeyIV, + secretKeyTag: secretKeyTag ? secretKeyTag : secret.secretKeyTag, + secretValueCiphertext: secretValueCiphertext + ? secretValueCiphertext + : secret.secretValueCiphertext, + secretValueIV: secretValueIV ? secretValueIV : secret.secretValueIV, + secretValueTag: secretValueTag ? secretValueTag : secret.secretValueTag, + secretCommentCiphertext: secretCommentCiphertext + ? secretCommentCiphertext + : secret.secretCommentCiphertext, + secretCommentIV: secretCommentIV + ? secretCommentIV + : secret.secretCommentIV, + secretCommentTag: secretCommentTag + ? secretCommentTag + : secret.secretCommentTag, + tags: tags ? tags : secret.tags, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }; + }), + }; + + await EESecretService.addSecretVersions(secretVersions); + + // group secrets into workspaces so updated secrets can + // be logged and snapshotted separately for each workspace + const workspaceSecretObj: any = {}; + req.secrets.forEach((s: any) => { + if (s.workspace.toString() in workspaceSecretObj) { + workspaceSecretObj[s.workspace.toString()].push(s); + } else { + workspaceSecretObj[s.workspace.toString()] = [s]; + } + }); + + Object.keys(workspaceSecretObj).forEach(async (key) => { + // trigger event - push secrets + setTimeout(async () => { + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(key), + }), + }); + }, 10000); + + const updateAction = await EELogService.createAction({ + name: ACTION_UPDATE_SECRETS, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(key), + secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id), }); - Object.keys(workspaceSecretObj).forEach(async (key) => { - // trigger event - push secrets - setTimeout(async () => { - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(key) - }) - }); - }, 10000); + // (EE) create (audit) log + updateAction && + (await EELogService.createLog({ + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(key), + actions: [updateAction], + channel, + ipAddress: req.ip, + })); - const updateAction = await EELogService.createAction({ - name: ACTION_UPDATE_SECRETS, - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(key), - secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) - }); + // (EE) take a secret snapshot + // IMP(akhilmhdh): commented out due to unknown where the environment is + // await EESecretService.takeSecretSnapshot({ + // workspaceId: new Types.ObjectId(key), + // environment, + // folderId, + // }); - // (EE) create (audit) log - updateAction && await EELogService.createLog({ - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(key), - actions: [updateAction], - channel, - ipAddress: req.ip - }); + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "secrets modified", + distinctId: await TelemetryService.getDistinctId({ + authData: req.authData, + }), + properties: { + numberOfSecrets: workspaceSecretObj[key].length, + environment: workspaceSecretObj[key][0].environment, + workspaceId: key, + channel: channel, + userAgent: req.headers?.["user-agent"], + }, + }); + } + }); - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(key) - }) - - const workspace = await Workspace.findById(key); - if (!workspace) throw WorkspaceNotFoundError(); - - const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier >= 1; - - const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { - postHogClient.capture({ - event: 'secrets modified', - distinctId: await TelemetryService.getDistinctId({ - authData: req.authData - }), - properties: { - numberOfSecrets: workspaceSecretObj[key].length, - environment: workspaceSecretObj[key][0].environment, - workspaceId: key, - channel: channel, - userAgent: req.headers?.['user-agent'], - isPaid, - email: await TelemetryService.getDistinctId({ - authData: req.authData - }) - } - }); - } - }); - - return res.status(200).send({ - secrets: await Secret.find({ - _id: { - $in: req.secrets.map((secret: ISecret) => secret._id) - } - }) - }); -} + return res.status(200).send({ + secrets: await Secret.find({ + _id: { + $in: req.secrets.map((secret: ISecret) => secret._id), + }, + }), + }); +}; /** * Delete secret(s) - * @param req - * @param res + * @param req + * @param res */ export const deleteSecrets = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Delete secret(s)' #swagger.description = 'Delete one or many secrets by their ID(s)' @@ -1072,91 +1108,83 @@ export const deleteSecrets = async (req: Request, res: Response) => { } */ - const channel = getChannelFromUserAgent(req.headers['user-agent']) - const toDelete = req.secrets.map((s: any) => s._id); + const channel = getChannelFromUserAgent(req.headers["user-agent"]); + const toDelete = req.secrets.map((s: any) => s._id); - await Secret.deleteMany({ - _id: { - $in: toDelete - } + await Secret.deleteMany({ + _id: { + $in: toDelete, + }, + }); + + await EESecretService.markDeletedSecretVersions({ + secretIds: toDelete, + }); + + // group secrets into workspaces so deleted secrets can + // be logged and snapshotted separately for each workspace + const workspaceSecretObj: any = {}; + req.secrets.forEach((s: any) => { + if (s.workspace.toString() in workspaceSecretObj) { + workspaceSecretObj[s.workspace.toString()].push(s); + } else { + workspaceSecretObj[s.workspace.toString()] = [s]; + } + }); + + Object.keys(workspaceSecretObj).forEach(async (key) => { + // trigger event - push secrets + await EventService.handleEvent({ + event: eventPushSecrets({ + workspaceId: new Types.ObjectId(key), + }), + }); + const deleteAction = await EELogService.createAction({ + name: ACTION_DELETE_SECRETS, + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(key), + secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id), }); - await EESecretService.markDeletedSecretVersions({ - secretIds: toDelete - }); + // (EE) create (audit) log + deleteAction && + (await EELogService.createLog({ + userId: req.user?._id, + serviceAccountId: req.serviceAccount?._id, + serviceTokenDataId: req.serviceTokenData?._id, + workspaceId: new Types.ObjectId(key), + actions: [deleteAction], + channel, + ipAddress: req.ip, + })); - // group secrets into workspaces so deleted secrets can - // be logged and snapshotted separately for each workspace - const workspaceSecretObj: any = {}; - req.secrets.forEach((s: any) => { - if (s.workspace.toString() in workspaceSecretObj) { - workspaceSecretObj[s.workspace.toString()].push(s); - } else { - workspaceSecretObj[s.workspace.toString()] = [s] - } - }); + // (EE) take a secret snapshot + // IMP(akhilmhdh): Not sure how to take secretSnapshot + // await EESecretService.takeSecretSnapshot({ + // workspaceId: new Types.ObjectId(key), + // }); - Object.keys(workspaceSecretObj).forEach(async (key) => { - // trigger event - push secrets - await EventService.handleEvent({ - event: eventPushSecrets({ - workspaceId: new Types.ObjectId(key) - }) - }); - const deleteAction = await EELogService.createAction({ - name: ACTION_DELETE_SECRETS, - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(key), - secretIds: workspaceSecretObj[key].map((secret: ISecret) => secret._id) - }); + const postHogClient = await TelemetryService.getPostHogClient(); + if (postHogClient) { + postHogClient.capture({ + event: "secrets deleted", + distinctId: await TelemetryService.getDistinctId({ + authData: req.authData, + }), + properties: { + numberOfSecrets: workspaceSecretObj[key].length, + environment: workspaceSecretObj[key][0].environment, + workspaceId: key, + channel: channel, + userAgent: req.headers?.["user-agent"], + }, + }); + } + }); - // (EE) create (audit) log - deleteAction && await EELogService.createLog({ - userId: req.user?._id, - serviceAccountId: req.serviceAccount?._id, - serviceTokenDataId: req.serviceTokenData?._id, - workspaceId: new Types.ObjectId(key), - actions: [deleteAction], - channel, - ipAddress: req.ip - }); - - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(key) - }); - - const workspace = await Workspace.findById(key); - if (!workspace) throw WorkspaceNotFoundError(); - - const orgPlan = await EELicenseService.getOrganizationPlan(workspace.organization.toString()); - const isPaid = orgPlan.tier >= 1; - - const postHogClient = await TelemetryService.getPostHogClient(); - if (postHogClient) { - postHogClient.capture({ - event: 'secrets deleted', - distinctId: await TelemetryService.getDistinctId({ - authData: req.authData - }), - properties: { - numberOfSecrets: workspaceSecretObj[key].length, - environment: workspaceSecretObj[key][0].environment, - workspaceId: key, - channel: channel, - userAgent: req.headers?.['user-agent'], - isPaid, - email: await TelemetryService.getDistinctId({ - authData: req.authData - }) - } - }); - } - }); - - return res.status(200).send({ - secrets: req.secrets - }); -} \ No newline at end of file + return res.status(200).send({ + secrets: req.secrets, + }); +}; diff --git a/backend/src/ee/controllers/v1/secretController.ts b/backend/src/ee/controllers/v1/secretController.ts index 8e3b39b54..3d1e4b3b3 100644 --- a/backend/src/ee/controllers/v1/secretController.ts +++ b/backend/src/ee/controllers/v1/secretController.ts @@ -1,16 +1,16 @@ -import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; -import { Secret } from '../../../models'; -import { SecretVersion } from '../../models'; -import { EESecretService } from '../../services'; +import { Request, Response } from "express"; +import * as Sentry from "@sentry/node"; +import { Secret } from "../../../models"; +import { SecretVersion } from "../../models"; +import { EESecretService } from "../../services"; /** * Return secret versions for secret with id [secretId] - * @param req - * @param res + * @param req + * @param res */ - export const getSecretVersions = async (req: Request, res: Response) => { - /* +export const getSecretVersions = async (req: Request, res: Response) => { + /* #swagger.summary = 'Return secret versions' #swagger.description = 'Return secret versions' @@ -55,41 +55,43 @@ import { EESecretService } from '../../services'; } } */ - let secretVersions; - try { - const { secretId } = req.params; + let secretVersions; + try { + const { secretId, workspaceId, environment, folderId } = req.params; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - - secretVersions = await SecretVersion.find({ - secret: secretId - }) - .sort({ createdAt: -1 }) - .skip(offset) - .limit(limit); + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get secret versions' - }); - } - - return res.status(200).send({ - secretVersions - }); -} + secretVersions = await SecretVersion.find({ + secret: secretId, + workspace: workspaceId, + environment, + folder: folderId, + }) + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get secret versions", + }); + } + + return res.status(200).send({ + secretVersions, + }); +}; /** * Roll back secret with id [secretId] to version [version] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const rollbackSecretVersion = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Roll back secret to a version.' #swagger.description = 'Roll back secret to a version.' @@ -137,97 +139,101 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => { } } */ - let secret; - try { - const { secretId } = req.params; - const { version } = req.body; - - // validate secret version - const oldSecretVersion = await SecretVersion.findOne({ - secret: secretId, - version - }).select('+secretBlindIndex') - - if (!oldSecretVersion) throw new Error('Failed to find secret version'); - - const { - workspace, - type, - user, - environment, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm, - keyEncoding - } = oldSecretVersion; - - // update secret - secret = await Secret.findByIdAndUpdate( - secretId, - { - $inc: { - version: 1 - }, - workspace, - type, - user, - environment, - ...(secretBlindIndex ? { secretBlindIndex } : {}), - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm, - keyEncoding - }, - { - new: true - } - ); - - if (!secret) throw new Error('Failed to find and update secret'); + let secret; + try { + const { secretId } = req.params; + const { version } = req.body; - // add new secret version - await new SecretVersion({ - secret: secretId, - version: secret.version, - workspace, - type, - user, - environment, - isDeleted: false, - ...(secretBlindIndex ? { secretBlindIndex } : {}), - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm, - keyEncoding - }).save(); - - // take secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: secret.workspace - }); - - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to roll back secret version' - }); - } - - return res.status(200).send({ - secret - }); -} \ No newline at end of file + // validate secret version + const oldSecretVersion = await SecretVersion.findOne({ + secret: secretId, + version, + }).select("+secretBlindIndex"); + + if (!oldSecretVersion) throw new Error("Failed to find secret version"); + + const { + workspace, + type, + user, + environment, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + folder, + algorithm, + keyEncoding, + } = oldSecretVersion; + + // update secret + secret = await Secret.findByIdAndUpdate( + secretId, + { + $inc: { + version: 1, + }, + workspace, + type, + user, + environment, + ...(secretBlindIndex ? { secretBlindIndex } : {}), + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + folderId: folder, + algorithm, + keyEncoding, + }, + { + new: true, + } + ); + + if (!secret) throw new Error("Failed to find and update secret"); + + // add new secret version + await new SecretVersion({ + secret: secretId, + version: secret.version, + workspace, + type, + user, + environment, + isDeleted: false, + ...(secretBlindIndex ? { secretBlindIndex } : {}), + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + folder, + algorithm, + keyEncoding, + }).save(); + + // take secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: secret.workspace, + environment, + folderId: folder, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to roll back secret version", + }); + } + + return res.status(200).send({ + secret, + }); +}; diff --git a/backend/src/ee/controllers/v1/secretSnapshotController.ts b/backend/src/ee/controllers/v1/secretSnapshotController.ts index 15b346f98..a6ee2ce4d 100644 --- a/backend/src/ee/controllers/v1/secretSnapshotController.ts +++ b/backend/src/ee/controllers/v1/secretSnapshotController.ts @@ -1,33 +1,48 @@ -import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; -import { SecretSnapshot } from '../../models'; +import { Request, Response } from "express"; +import * as Sentry from "@sentry/node"; +import { + ISecretVersion, + SecretSnapshot, + TFolderRootVersionSchema, +} from "../../models"; /** * Return secret snapshot with id [secretSnapshotId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getSecretSnapshot = async (req: Request, res: Response) => { - let secretSnapshot; - try { - const { secretSnapshotId } = req.params; + let secretSnapshot; + try { + const { secretSnapshotId } = req.params; - secretSnapshot = await SecretSnapshot - .findById(secretSnapshotId) - .populate('secretVersions'); - - if (!secretSnapshot) throw new Error('Failed to find secret snapshot'); + secretSnapshot = await SecretSnapshot.findById(secretSnapshotId) + .lean() + .populate<{ secretVersions: ISecretVersion[] }>("secretVersions") + .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get secret snapshot' - }); - } - - return res.status(200).send({ - secretSnapshot + if (!secretSnapshot) throw new Error("Failed to find secret snapshot"); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get secret snapshot", }); -} \ No newline at end of file + } + const folderId = secretSnapshot.folderId; + // to show only the folder required secrets + secretSnapshot.secretVersions = secretSnapshot.secretVersions.filter( + ({ folder }) => folder === folderId + ); + + secretSnapshot.folderVersion = + secretSnapshot?.folderVersion?.nodes?.children?.map(({ id, name }) => ({ + id, + name, + })) as any; + + return res.status(200).send({ + secretSnapshot, + }); +}; diff --git a/backend/src/ee/controllers/v1/workspaceController.ts b/backend/src/ee/controllers/v1/workspaceController.ts index e3f46eb71..64309abce 100644 --- a/backend/src/ee/controllers/v1/workspaceController.ts +++ b/backend/src/ee/controllers/v1/workspaceController.ts @@ -1,25 +1,33 @@ -import { Request, Response } from 'express'; -import * as Sentry from '@sentry/node'; -import { Types } from 'mongoose'; +import { Request, Response } from "express"; +import * as Sentry from "@sentry/node"; +import { Types } from "mongoose"; +import { Secret } from "../../../models"; import { - Secret -} from '../../../models'; -import { - SecretSnapshot, - Log, - SecretVersion, - ISecretVersion -} from '../../models'; -import { EESecretService } from '../../services'; -import { getLatestSecretVersionIds } from '../../helpers/secretVersion'; + SecretSnapshot, + Log, + SecretVersion, + ISecretVersion, + FolderVersion, + TFolderRootVersionSchema, +} from "../../models"; +import { EESecretService } from "../../services"; +import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; +import Folder from "../../../models/folder"; +import { + getAllFolderIds, + searchByFolderId, +} from "../../../services/FolderService"; /** * Return secret snapshots for workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ - export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) => { - /* +export const getWorkspaceSecretSnapshots = async ( + req: Request, + res: Response +) => { + /* #swagger.summary = 'Return project secret snapshot ids' #swagger.description = 'Return project secret snapshots ids' @@ -64,66 +72,78 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion'; } } */ - let secretSnapshots; - try { - const { workspaceId } = req.params; + let secretSnapshots; + try { + const { workspaceId } = req.params; + const { environment, folderId } = req.query; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - - secretSnapshots = await SecretSnapshot.find({ - workspace: workspaceId - }) - .sort({ createdAt: -1 }) - .skip(offset) - .limit(limit); + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get secret snapshots' - }); - } - - return res.status(200).send({ - secretSnapshots - }); -} + secretSnapshots = await SecretSnapshot.find({ + workspace: workspaceId, + environment, + folderId: folderId || "root", + }) + .sort({ createdAt: -1 }) + .skip(offset) + .limit(limit); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get secret snapshots", + }); + } + + return res.status(200).send({ + secretSnapshots, + }); +}; /** * Return count of secret snapshots for workspace with id [workspaceId] - * @param req - * @param res + * @param req + * @param res */ -export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { - let count; - try { - const { workspaceId } = req.params; - count = await SecretSnapshot.countDocuments({ - workspace: workspaceId - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to count number of secret snapshots' - }); - } - - return res.status(200).send({ - count - }); -} +export const getWorkspaceSecretSnapshotsCount = async ( + req: Request, + res: Response +) => { + let count; + try { + const { workspaceId } = req.params; + const { environment, folderId } = req.query; + + count = await SecretSnapshot.countDocuments({ + workspace: workspaceId, + environment, + folderId: folderId || "root", + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to count number of secret snapshots", + }); + } + + return res.status(200).send({ + count, + }); +}; /** * Rollback secret snapshot with id [secretSnapshotId] to version [version] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ -export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Response) => { - /* +export const rollbackWorkspaceSecretSnapshot = async ( + req: Request, + res: Response +) => { + /* #swagger.summary = 'Roll back project secrets to those captured in a secret snapshot version.' #swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.' @@ -173,168 +193,235 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons } } */ - - let secrets; - try { - const { workspaceId } = req.params; - const { version } = req.body; - - // validate secret snapshot - const secretSnapshot = await SecretSnapshot.findOne({ - workspace: workspaceId, - version - }).populate<{ secretVersions: ISecretVersion[]}>({ - path: 'secretVersions', - select: '+secretBlindIndex' - }); - - 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 latestSecretVersionIds = await getLatestSecretVersionIds({ - secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret) - }); - - // TODO: fix any - const latestSecretVersions: any = (await SecretVersion.find({ - _id: { - $in: latestSecretVersionIds.map((s) => s.versionId) - } - }, 'secret version')) - .reduce((accumulator, s) => ({ - ...accumulator, - [`${s.secret.toString()}`]: s - }), {}); - - // delete existing secrets - await Secret.deleteMany({ - workspace: workspaceId - }); + let secrets; + try { + const { workspaceId } = req.params; + const { version, environment, folderId = "root" } = req.body; - // add secrets - secrets = await Secret.insertMany( - secretSnapshot.secretVersions.map((sv) => { - const secretId = sv.secret; - const { - workspace, - type, - user, - environment, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - createdAt - } = oldSecretVersionsObj[secretId.toString()]; - - return ({ - _id: secretId, - version: latestSecretVersions[secretId.toString()].version + 1, - workspace, - type, - user, - environment, - secretBlindIndex: secretBlindIndex ?? undefined, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash, - secretCommentCiphertext: '', - secretCommentIV: '', - secretCommentTag: '', - createdAt - }); - }) - ); - - // add secret versions - const secretV = await SecretVersion.insertMany( - secrets.map(({ - _id, - version, - workspace, - type, - user, - environment, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash - }) => ({ - _id: new Types.ObjectId(), - secret: _id, - version, - workspace, - type, - user, - environment, - isDeleted: false, - secretBlindIndex: secretBlindIndex ?? undefined, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretKeyHash, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretValueHash - })) - ); - - // update secret versions of restored secrets as not deleted - await SecretVersion.updateMany({ - secret: { - $in: secretSnapshot.secretVersions.map((sv) => sv.secret) - } - }, { - isDeleted: false - }); - - // take secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId: new Types.ObjectId(workspaceId) - }); - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to roll back secret snapshot' - }); + // validate secret snapshot + const secretSnapshot = await SecretSnapshot.findOne({ + workspace: workspaceId, + version, + environment, + folderId: folderId, + }) + .populate<{ secretVersions: ISecretVersion[] }>({ + path: "secretVersions", + select: "+secretBlindIndex", + }) + .populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion"); + + 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 latestSecretVersionIds = await getLatestSecretVersionIds({ + secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret), + }); + + // TODO: fix any + const latestSecretVersions: any = ( + await SecretVersion.find( + { + _id: { + $in: latestSecretVersionIds.map((s) => s.versionId), + }, + }, + "secret version" + ) + ).reduce( + (accumulator, s) => ({ + ...accumulator, + [`${s.secret.toString()}`]: s, + }), + {} + ); + + 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; + } } - - return res.status(200).send({ - secrets - }); -} + + const secDelQuery: Record = { + workspace: workspaceId, + environment, + // undefined means root thus collect all secrets + }; + if (folderId !== "root" && folderIds.length) + secDelQuery.folder = { $in: folderIds }; + + // delete existing secrets + await Secret.deleteMany(secDelQuery); + await Folder.deleteOne({ + workspace: workspaceId, + environment, + }); + + // add secrets + secrets = await Secret.insertMany( + secretSnapshot.secretVersions.map((sv) => { + const secretId = sv.secret; + const { + workspace, + type, + user, + environment, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + createdAt, + folder: secFolderId, + } = oldSecretVersionsObj[secretId.toString()]; + + return { + _id: secretId, + version: latestSecretVersions[secretId.toString()].version + 1, + workspace, + type, + user, + environment, + secretBlindIndex: secretBlindIndex ?? undefined, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + secretCommentCiphertext: "", + secretCommentIV: "", + secretCommentTag: "", + createdAt, + folder: secFolderId, + }; + }) + ); + + // add secret versions + const secretV = await SecretVersion.insertMany( + secrets.map( + ({ + _id, + version, + workspace, + type, + user, + environment, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + folder: secFolderId, + }) => ({ + _id: new Types.ObjectId(), + secret: _id, + version, + workspace, + type, + user, + environment, + isDeleted: false, + secretBlindIndex: secretBlindIndex ?? undefined, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretKeyHash, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretValueHash, + folder: secFolderId, + }) + ) + ); + + const newFolder = new Folder(folders); + newFolder._id = new Types.ObjectId(); + newFolder.isNew = true; + await newFolder.save(); + // create new folder version + const newFolderVersion = new FolderVersion({ + workspace: workspaceId, + environment, + nodes: newFolder.nodes, + }); + await newFolderVersion.save(); + + // update secret versions of restored secrets as not deleted + await SecretVersion.updateMany( + { + secret: { + $in: secretSnapshot.secretVersions.map((sv) => sv.secret), + }, + }, + { + isDeleted: false, + } + ); + + // take secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId: new Types.ObjectId(workspaceId), + environment, + folderId, + }); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to roll back secret snapshot", + }); + } + + return res.status(200).send({ + secrets, + }); +}; /** * Return (audit) logs for workspace with id [workspaceId] - * @param req - * @param res - * @returns + * @param req + * @param res + * @returns */ export const getWorkspaceLogs = async (req: Request, res: Response) => { - /* + /* #swagger.summary = 'Return project (audit) logs' #swagger.description = 'Return project (audit) logs' @@ -400,43 +487,41 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => { } } */ - let logs - try { - const { workspaceId } = req.params; + let logs; + try { + const { workspaceId } = req.params; - const offset: number = parseInt(req.query.offset as string); - const limit: number = parseInt(req.query.limit as string); - const sortBy: string = req.query.sortBy as string; - const userId: string = req.query.userId as string; - const actionNames: string = req.query.actionNames as string; - - logs = await Log.find({ - workspace: workspaceId, - ...( userId ? { user: userId } : {}), - ...( - actionNames - ? { - actionNames: { - $in: actionNames.split(',') - } - } : {} - ) - }) - .sort({ createdAt: sortBy === 'recent' ? -1 : 1 }) - .skip(offset) - .limit(limit) - .populate('actions') - .populate('user serviceAccount serviceTokenData'); + const offset: number = parseInt(req.query.offset as string); + const limit: number = parseInt(req.query.limit as string); + const sortBy: string = req.query.sortBy as string; + const userId: string = req.query.userId as string; + const actionNames: string = req.query.actionNames as string; - } catch (err) { - Sentry.setUser({ email: req.user.email }); - Sentry.captureException(err); - return res.status(400).send({ - message: 'Failed to get workspace logs' - }); - } - - return res.status(200).send({ - logs - }); -} \ No newline at end of file + logs = await Log.find({ + workspace: workspaceId, + ...(userId ? { user: userId } : {}), + ...(actionNames + ? { + actionNames: { + $in: actionNames.split(","), + }, + } + : {}), + }) + .sort({ createdAt: sortBy === "recent" ? -1 : 1 }) + .skip(offset) + .limit(limit) + .populate("actions") + .populate("user serviceAccount serviceTokenData"); + } catch (err) { + Sentry.setUser({ email: req.user.email }); + Sentry.captureException(err); + return res.status(400).send({ + message: "Failed to get workspace logs", + }); + } + + return res.status(200).send({ + logs, + }); +}; diff --git a/backend/src/ee/helpers/secret.ts b/backend/src/ee/helpers/secret.ts index c60b1a18c..72d9d260a 100644 --- a/backend/src/ee/helpers/secret.ts +++ b/backend/src/ee/helpers/secret.ts @@ -1,6 +1,16 @@ import { Types } from "mongoose"; import { Secret, ISecret } from "../../models"; -import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models"; +import Folder from "../../models/folder"; +import { + getAllFolderIds, + searchByFolderId, +} from "../../services/FolderService"; +import { + SecretSnapshot, + SecretVersion, + ISecretVersion, + FolderVersion, +} from "../models"; /** * Save a secret snapshot that is a copy of the current state of secrets in workspace with id @@ -12,22 +22,40 @@ import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models"; */ const takeSecretSnapshotHelper = async ({ workspaceId, + environment, + folderId = "root", }: { workspaceId: Types.ObjectId; + environment: string; + folderId?: string; }) => { - const secretIds = ( - await Secret.find( - { - workspace: workspaceId, - }, - "_id" - ) - ).map((s) => s._id); + // 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 latestSecretVersions = ( await SecretVersion.aggregate([ { $match: { + environment, + workspace: new Types.ObjectId(workspaceId), secret: { $in: secretIds, }, @@ -45,6 +73,11 @@ const takeSecretSnapshotHelper = async ({ }, ]).exec() ).map((s) => s.versionId); + const latestFolderVersion = await FolderVersion.findOne({ + environment, + workspace: workspaceId, + "nodes.id": folderId || "root", + }).sort({ "nodes.version": -1 }); const latestSecretSnapshot = await SecretSnapshot.findOne({ workspace: workspaceId, @@ -52,8 +85,11 @@ const takeSecretSnapshotHelper = async ({ const secretSnapshot = await new SecretSnapshot({ workspace: workspaceId, + environment, version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1, secretVersions: latestSecretVersions, + folderId, + folderVersion: latestFolderVersion, }).save(); return secretSnapshot; diff --git a/backend/src/ee/helpers/secretVersion.ts b/backend/src/ee/helpers/secretVersion.ts index c7190c3bc..4b173f9ef 100644 --- a/backend/src/ee/helpers/secretVersion.ts +++ b/backend/src/ee/helpers/secretVersion.ts @@ -1,92 +1,82 @@ -import { Types } from 'mongoose'; -import { SecretVersion } from '../models'; +import { Types } from "mongoose"; +import { SecretVersion } from "../models"; /** * Return latest secret versions for secrets with ids [secretIds] * @param {Object} obj * @param {Object} obj.secretIds = ids of secrets to get latest versions for - * @returns + * @returns */ const getLatestSecretVersionIds = async ({ - secretIds + secretIds, }: { - secretIds: Types.ObjectId[]; + secretIds: Types.ObjectId[]; }) => { - interface LatestSecretVersionId { - _id: Types.ObjectId; - version: number; - versionId: Types.ObjectId; - } - - const latestSecretVersionIds = (await SecretVersion.aggregate([ - { - $match: { - secret: { - $in: secretIds - } - } + const latestSecretVersionIds = await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds, }, - { - $group: { - _id: '$secret', - version: { $max: '$version' }, - versionId: { $max: '$_id' } // id of latest secret version - } - }, - { - $sort: { version: -1 } - } - ]) - .exec()); - - return latestSecretVersionIds; -} + }, + }, + { + $group: { + _id: "$secret", + version: { $max: "$version" }, + versionId: { $max: "$_id" }, // id of latest secret version + }, + }, + { + $sort: { version: -1 }, + }, + ]).exec(); + + return latestSecretVersionIds; +}; /** * Return latest [n] secret versions for secrets with ids [secretIds] * @param {Object} obj * @param {Object} obj.secretIds = ids of secrets to get latest versions for * @param {Number} obj.n - number of latest secret versions to return for each secret - * @returns + * @returns */ const getLatestNSecretSecretVersionIds = async ({ - secretIds, - n + secretIds, + n, }: { - secretIds: Types.ObjectId[]; - n: number; + secretIds: Types.ObjectId[]; + n: number; }) => { - // TODO: optimize query - const latestNSecretVersions = (await SecretVersion.aggregate([ - { - $match: { - secret: { - $in: secretIds, - }, - }, + // TODO: optimize query + const latestNSecretVersions = await SecretVersion.aggregate([ + { + $match: { + secret: { + $in: secretIds, }, - { - $sort: { version: -1 }, - }, - { - $group: { - _id: "$secret", - versions: { $push: "$$ROOT" }, - }, - }, - { - $project: { - _id: 0, - secret: "$_id", - versions: { $slice: ["$versions", n] }, - }, - } - ])); - - return latestNSecretVersions; -} + }, + }, + { + $sort: { version: -1 }, + }, + { + $group: { + _id: "$secret", + versions: { $push: "$$ROOT" }, + }, + }, + { + $project: { + _id: 0, + secret: "$_id", + versions: { $slice: ["$versions", n] }, + }, + }, + ]); -export { - getLatestSecretVersionIds, - getLatestNSecretSecretVersionIds -} + return latestNSecretVersions; +}; + +export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds }; diff --git a/backend/src/ee/models/folderVersion.ts b/backend/src/ee/models/folderVersion.ts new file mode 100644 index 000000000..f0fa5afbf --- /dev/null +++ b/backend/src/ee/models/folderVersion.ts @@ -0,0 +1,60 @@ +import { model, Schema, Types } from "mongoose"; + +export type TFolderRootVersionSchema = { + _id: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + nodes: TFolderVersionSchema; +}; + +export type TFolderVersionSchema = { + id: string; + name: string; + version: number; + children: TFolderVersionSchema[]; +}; + +const folderVersionSchema = new Schema({ + id: { + required: true, + type: String, + default: "root", + }, + name: { + required: true, + type: String, + default: "root", + }, + version: { + required: true, + type: Number, + default: 1, + }, +}); + +folderVersionSchema.add({ children: [folderVersionSchema] }); + +const folderRootVersionSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + }, + environment: { + type: String, + required: true, + }, + nodes: folderVersionSchema, + }, + { + timestamps: true, + } +); + +const FolderVersion = model( + "FolderVersion", + folderRootVersionSchema +); + +export default FolderVersion; diff --git a/backend/src/ee/models/index.ts b/backend/src/ee/models/index.ts index a6cee725e..7867674d6 100644 --- a/backend/src/ee/models/index.ts +++ b/backend/src/ee/models/index.ts @@ -1,15 +1,18 @@ -import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot'; -import SecretVersion, { ISecretVersion } from './secretVersion'; -import Log, { ILog } from './log'; -import Action, { IAction } from './action'; +import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot"; +import SecretVersion, { ISecretVersion } from "./secretVersion"; +import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion"; +import Log, { ILog } from "./log"; +import Action, { IAction } from "./action"; export { - SecretSnapshot, - ISecretSnapshot, - SecretVersion, - ISecretVersion, - Log, - ILog, - Action, - IAction -} \ No newline at end of file + SecretSnapshot, + ISecretSnapshot, + SecretVersion, + ISecretVersion, + FolderVersion, + TFolderRootVersionSchema, + Log, + ILog, + Action, + IAction, +}; diff --git a/backend/src/ee/models/secretSnapshot.ts b/backend/src/ee/models/secretSnapshot.ts index c646f353a..cfc5b03b7 100644 --- a/backend/src/ee/models/secretSnapshot.ts +++ b/backend/src/ee/models/secretSnapshot.ts @@ -1,33 +1,54 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; export interface ISecretSnapshot { - workspace: Types.ObjectId; - version: number; - secretVersions: Types.ObjectId[]; + workspace: Types.ObjectId; + environment: string; + folderId: string | "root"; + version: number; + secretVersions: Types.ObjectId[]; + folderVersion: Types.ObjectId; } const secretSnapshotSchema = new Schema( - { - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - version: { - type: Number, - required: true - }, - secretVersions: [{ - type: Schema.Types.ObjectId, - ref: 'SecretVersion', - required: true - }] + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, }, - { - timestamps: true - } + environment: { + type: String, + required: true, + }, + folderId: { + type: String, + default: "root", + }, + version: { + type: Number, + default: 1, + required: true, + }, + secretVersions: [ + { + type: Schema.Types.ObjectId, + ref: "SecretVersion", + required: true, + }, + ], + folderVersion: { + type: Schema.Types.ObjectId, + ref: "FolderVersion", + }, + }, + { + timestamps: true, + } ); -const SecretSnapshot = model('SecretSnapshot', secretSnapshotSchema); +const SecretSnapshot = model( + "SecretSnapshot", + secretSnapshotSchema +); -export default SecretSnapshot; \ No newline at end of file +export default SecretSnapshot; diff --git a/backend/src/ee/models/secretVersion.ts b/backend/src/ee/models/secretVersion.ts index 9aca6af1c..580b9ffcb 100644 --- a/backend/src/ee/models/secretVersion.ts +++ b/backend/src/ee/models/secretVersion.ts @@ -1,115 +1,123 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { - SECRET_SHARED, - SECRET_PERSONAL, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 -} from '../../variables'; + SECRET_SHARED, + SECRET_PERSONAL, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64, +} from "../../variables"; export interface ISecretVersion { - _id: Types.ObjectId; - secret: Types.ObjectId; - version: number; - workspace: Types.ObjectId; // new - type: string; // new - user?: Types.ObjectId; // new - environment: string; // new - isDeleted: boolean; - secretBlindIndex?: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - algorithm: 'aes-256-gcm'; - keyEncoding: 'utf8' | 'base64'; + _id: Types.ObjectId; + secret: Types.ObjectId; + version: number; + workspace: Types.ObjectId; // new + type: string; // new + user?: Types.ObjectId; // new + environment: string; // new + isDeleted: boolean; + secretBlindIndex?: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + algorithm: "aes-256-gcm"; + keyEncoding: "utf8" | "base64"; + folder?: string; } const secretVersionSchema = new Schema( - { - secret: { // could be deleted - type: Schema.Types.ObjectId, - ref: 'Secret', - required: true - }, - version: { - type: Number, - default: 1, - required: true - }, - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - type: { - type: String, - enum: [SECRET_SHARED, SECRET_PERSONAL], - required: true - }, - user: { - // user associated with the personal secret - type: Schema.Types.ObjectId, - ref: 'User' - }, - environment: { - type: String, - required: true - }, - isDeleted: { // consider removing field - type: Boolean, - default: false, - required: true - }, - secretBlindIndex: { - type: String, - select: false - }, - secretKeyCiphertext: { - type: String, - required: true - }, - secretKeyIV: { - type: String, // symmetric - required: true - }, - secretKeyTag: { - type: String, // symmetric - required: true - }, - secretValueCiphertext: { - type: String, - required: true - }, - secretValueIV: { - type: String, // symmetric - required: true - }, - secretValueTag: { - type: String, // symmetric - required: true - }, - algorithm: { // the encryption algorithm used - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true - }, - keyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 - ], - required: true - }, - }, - { - timestamps: true - } + { + secret: { + // could be deleted + type: Schema.Types.ObjectId, + ref: "Secret", + required: true, + }, + version: { + type: Number, + default: 1, + required: true, + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true, + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: "User", + }, + environment: { + type: String, + required: true, + }, + isDeleted: { + // consider removing field + type: Boolean, + default: false, + required: true, + }, + secretBlindIndex: { + type: String, + select: false, + }, + secretKeyCiphertext: { + type: String, + required: true, + }, + secretKeyIV: { + type: String, // symmetric + required: true, + }, + secretKeyTag: { + type: String, // symmetric + required: true, + }, + secretValueCiphertext: { + type: String, + required: true, + }, + secretValueIV: { + type: String, // symmetric + required: true, + }, + secretValueTag: { + type: String, // symmetric + required: true, + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true, + }, + folder: { + type: String, + required: false, + }, + }, + { + timestamps: true, + } ); -const SecretVersion = model('SecretVersion', secretVersionSchema); +const SecretVersion = model( + "SecretVersion", + secretVersionSchema +); -export default SecretVersion; \ No newline at end of file +export default SecretVersion; diff --git a/backend/src/ee/routes/v1/workspace.ts b/backend/src/ee/routes/v1/workspace.ts index 722bfb4a7..2b840892f 100644 --- a/backend/src/ee/routes/v1/workspace.ts +++ b/backend/src/ee/routes/v1/workspace.ts @@ -1,76 +1,82 @@ -import express from 'express'; +import express from "express"; const router = express.Router(); import { - requireAuth, - requireWorkspaceAuth, - validateRequest -} from '../../../middleware'; -import { param, query, body } from 'express-validator'; -import { ADMIN, MEMBER } from '../../../variables'; -import { workspaceController } from '../../controllers/v1'; + requireAuth, + requireWorkspaceAuth, + validateRequest, +} from "../../../middleware"; +import { param, query, body } from "express-validator"; +import { ADMIN, MEMBER } from "../../../variables"; +import { workspaceController } from "../../controllers/v1"; router.get( - '/:workspaceId/secret-snapshots', - requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' - }), - param('workspaceId').exists().trim(), - query('offset').exists().isInt(), - query('limit').exists().isInt(), - validateRequest, - workspaceController.getWorkspaceSecretSnapshots + "/:workspaceId/secret-snapshots", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params", + }), + param("workspaceId").exists().trim(), + query("environment").isString().exists().trim(), + query("folderId").default("root").isString().trim(), + query("offset").exists().isInt(), + query("limit").exists().isInt(), + validateRequest, + workspaceController.getWorkspaceSecretSnapshots ); router.get( - '/:workspaceId/secret-snapshots/count', - requireAuth({ - acceptedAuthModes: ['jwt'] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' - }), - param('workspaceId').exists().trim(), - validateRequest, - workspaceController.getWorkspaceSecretSnapshotsCount + "/:workspaceId/secret-snapshots/count", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params", + }), + param("workspaceId").exists().trim(), + query("environment").isString().exists().trim(), + query("folderId").default("root").isString().trim(), + validateRequest, + workspaceController.getWorkspaceSecretSnapshotsCount ); router.post( - '/:workspaceId/secret-snapshots/rollback', - requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' - }), - param('workspaceId').exists().trim(), - body('version').exists().isInt(), - validateRequest, - workspaceController.rollbackWorkspaceSecretSnapshot + "/:workspaceId/secret-snapshots/rollback", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params", + }), + param("workspaceId").exists().trim(), + body("environment").isString().exists().trim(), + query("folderId").default("root").isString().exists().trim(), + body("version").exists().isInt(), + validateRequest, + workspaceController.rollbackWorkspaceSecretSnapshot ); router.get( - '/:workspaceId/logs', - requireAuth({ - acceptedAuthModes: ['jwt', 'apiKey'] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'params' - }), - param('workspaceId').exists().trim(), - query('offset').exists().isInt(), - query('limit').exists().isInt(), - query('sortBy'), - query('userId'), - query('actionNames'), - validateRequest, - workspaceController.getWorkspaceLogs + "/:workspaceId/logs", + requireAuth({ + acceptedAuthModes: ["jwt", "apiKey"], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: "params", + }), + param("workspaceId").exists().trim(), + query("offset").exists().isInt(), + query("limit").exists().isInt(), + query("sortBy"), + query("userId"), + query("actionNames"), + validateRequest, + workspaceController.getWorkspaceLogs ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/ee/services/EESecretService.ts b/backend/src/ee/services/EESecretService.ts index e84e35816..a0803dd36 100644 --- a/backend/src/ee/services/EESecretService.ts +++ b/backend/src/ee/services/EESecretService.ts @@ -1,9 +1,9 @@ import { Types } from 'mongoose'; import { ISecretVersion } from '../models'; -import { - takeSecretSnapshotHelper, - addSecretVersionsHelper, - markDeletedSecretVersionsHelper +import { + takeSecretSnapshotHelper, + addSecretVersionsHelper, + markDeletedSecretVersionsHelper, } from '../helpers/secret'; import EELicenseService from './EELicenseService'; @@ -11,58 +11,65 @@ import EELicenseService from './EELicenseService'; * Class to handle Enterprise Edition secret actions */ class EESecretService { - - /** - * Save a secret snapshot that is a copy of the current state of secrets in workspace with id - * [workspaceId] under a new snapshot with incremented version under the - * SecretSnapshot collection. - * Requires a valid license key [licenseKey] - * @param {Object} obj - * @param {String} obj.workspaceId - * @returns {SecretSnapshot} secretSnapshot - new secret snpashot - */ - static async takeSecretSnapshot({ - workspaceId - }: { - workspaceId: Types.ObjectId; - }) { - if (!EELicenseService.isLicenseValid) return; - return await takeSecretSnapshotHelper({ workspaceId }); - } - - /** - * Add secret versions [secretVersions] to the SecretVersion collection. - * @param {Object} obj - * @param {Object[]} obj.secretVersions - * @returns {SecretVersion[]} newSecretVersions - new secret versions - */ - static async addSecretVersions({ - secretVersions - }: { - secretVersions: ISecretVersion[]; - }) { - if (!EELicenseService.isLicenseValid) return; - return await addSecretVersionsHelper({ - secretVersions - }); - } + /** + * Save a secret snapshot that is a copy of the current state of secrets in workspace with id + * [workspaceId] under a new snapshot with incremented version under the + * SecretSnapshot collection. + * Requires a valid license key [licenseKey] + * @param {Object} obj + * @param {String} obj.workspaceId + * @returns {SecretSnapshot} secretSnapshot - new secret snpashot + */ + static async takeSecretSnapshot({ + workspaceId, + environment, + folderId, + }: { + workspaceId: Types.ObjectId; + environment: string; + folderId?: string; + }) { + if (!EELicenseService.isLicenseValid) return; + return await takeSecretSnapshotHelper({ + workspaceId, + environment, + folderId, + }); + } - /** - * Mark secret versions associated with secrets with ids [secretIds] - * as deleted. - * @param {Object} obj - * @param {ObjectId[]} obj.secretIds - secret ids - */ - static async markDeletedSecretVersions({ - secretIds - }: { - secretIds: Types.ObjectId[]; - }) { - if (!EELicenseService.isLicenseValid) return; - await markDeletedSecretVersionsHelper({ - secretIds - }); - } + /** + * Add secret versions [secretVersions] to the SecretVersion collection. + * @param {Object} obj + * @param {Object[]} obj.secretVersions + * @returns {SecretVersion[]} newSecretVersions - new secret versions + */ + static async addSecretVersions({ + secretVersions, + }: { + secretVersions: ISecretVersion[]; + }) { + if (!EELicenseService.isLicenseValid) return; + return await addSecretVersionsHelper({ + secretVersions, + }); + } + + /** + * Mark secret versions associated with secrets with ids [secretIds] + * as deleted. + * @param {Object} obj + * @param {ObjectId[]} obj.secretIds - secret ids + */ + static async markDeletedSecretVersions({ + secretIds, + }: { + secretIds: Types.ObjectId[]; + }) { + if (!EELicenseService.isLicenseValid) return; + await markDeletedSecretVersionsHelper({ + secretIds, + }); + } } -export default EESecretService; \ No newline at end of file +export default EESecretService; diff --git a/backend/src/helpers/auth.ts b/backend/src/helpers/auth.ts index b175ab514..177df8ad8 100644 --- a/backend/src/helpers/auth.ts +++ b/backend/src/helpers/auth.ts @@ -44,7 +44,7 @@ const validateAuthMode = ({ }) => { const apiKey = headers['x-api-key']; const authHeader = headers['authorization']; - + let authMode, authTokenValue; if (apiKey === undefined && authHeader === undefined) { // case: no auth or X-API-KEY header present diff --git a/backend/src/helpers/membership.ts b/backend/src/helpers/membership.ts index a78100248..ee85fbb38 100644 --- a/backend/src/helpers/membership.ts +++ b/backend/src/helpers/membership.ts @@ -1,13 +1,7 @@ import * as Sentry from '@sentry/node'; import { Types } from 'mongoose'; -import { - Membership, - Key -} from '../models'; -import { - MembershipNotFoundError, - BadRequestError -} from '../utils/errors'; +import { Membership, Key } from '../models'; +import { MembershipNotFoundError, BadRequestError } from '../utils/errors'; /** * Validate that user with id [userId] is a member of workspace with id [workspaceId] @@ -18,32 +12,35 @@ import { * @returns {Membership} membership - membership of user with id [userId] for workspace with id [workspaceId] */ const validateMembership = async ({ - userId, - workspaceId, - acceptedRoles, + userId, + workspaceId, + acceptedRoles, }: { - userId: Types.ObjectId; - workspaceId: Types.ObjectId; - acceptedRoles?: Array<'admin' | 'member'>; + userId: Types.ObjectId | string; + workspaceId: Types.ObjectId | string; + acceptedRoles?: Array<'admin' | 'member'>; }) => { - - const membership = await Membership.findOne({ - user: userId, - workspace: workspaceId - }).populate("workspace"); - - if (!membership) { - throw MembershipNotFoundError({ message: 'Failed to find workspace membership' }); - } - - if (acceptedRoles) { - if (!acceptedRoles.includes(membership.role)) { - throw BadRequestError({ message: 'Failed authorization for membership role' }); - } - } - - return membership; -} + const membership = await Membership.findOne({ + user: userId, + workspace: workspaceId, + }).populate('workspace'); + + if (!membership) { + throw MembershipNotFoundError({ + message: 'Failed to find workspace membership', + }); + } + + if (acceptedRoles) { + if (!acceptedRoles.includes(membership.role)) { + throw BadRequestError({ + message: 'Failed authorization for membership role', + }); + } + } + + return membership; +}; /** * Return membership matching criteria specified in query [queryObj] @@ -51,16 +48,16 @@ const validateMembership = async ({ * @return {Object} membership - membership */ const findMembership = async (queryObj: any) => { - let membership; - try { - membership = await Membership.findOne(queryObj); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to find membership'); - } + let membership; + try { + membership = await Membership.findOne(queryObj); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to find membership'); + } - return membership; + return membership; }; /** @@ -72,39 +69,39 @@ const findMembership = async (queryObj: any) => { * @param {String[]} obj.roles - roles of users. */ const addMemberships = async ({ - userIds, - workspaceId, - roles + userIds, + workspaceId, + roles, }: { - userIds: string[]; - workspaceId: string; - roles: string[]; + userIds: string[]; + workspaceId: string; + roles: string[]; }): Promise => { - try { - const operations = userIds.map((userId, idx) => { - return { - updateOne: { - filter: { - user: userId, - workspace: workspaceId, - role: roles[idx] - }, - update: { - user: userId, - workspace: workspaceId, - role: roles[idx] - }, - upsert: true - } - }; - }); + try { + const operations = userIds.map((userId, idx) => { + return { + updateOne: { + filter: { + user: userId, + workspace: workspaceId, + role: roles[idx], + }, + update: { + user: userId, + workspace: workspaceId, + role: roles[idx], + }, + upsert: true, + }, + }; + }); - await Membership.bulkWrite(operations as any); - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to add users to workspace'); - } + await Membership.bulkWrite(operations as any); + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to add users to workspace'); + } }; /** @@ -113,32 +110,27 @@ const addMemberships = async ({ * @param {String} obj.membershipId - id of membership to delete */ const deleteMembership = async ({ membershipId }: { membershipId: string }) => { - let deletedMembership; - try { - deletedMembership = await Membership.findOneAndDelete({ - _id: membershipId - }); + let deletedMembership; + try { + deletedMembership = await Membership.findOneAndDelete({ + _id: membershipId, + }); - // delete keys associated with the membership - if (deletedMembership?.user) { - // case: membership had a registered user - await Key.deleteMany({ - receiver: deletedMembership.user, - workspace: deletedMembership.workspace - }); - } - } catch (err) { - Sentry.setUser(null); - Sentry.captureException(err); - throw new Error('Failed to delete membership'); - } + // delete keys associated with the membership + if (deletedMembership?.user) { + // case: membership had a registered user + await Key.deleteMany({ + receiver: deletedMembership.user, + workspace: deletedMembership.workspace, + }); + } + } catch (err) { + Sentry.setUser(null); + Sentry.captureException(err); + throw new Error('Failed to delete membership'); + } - return deletedMembership; + return deletedMembership; }; -export { - validateMembership, - addMemberships, - findMembership, - deleteMembership -}; +export { validateMembership, addMemberships, findMembership, deleteMembership }; diff --git a/backend/src/helpers/secret.ts b/backend/src/helpers/secret.ts index 9b81d79c9..22b362e79 100644 --- a/backend/src/helpers/secret.ts +++ b/backend/src/helpers/secret.ts @@ -1,5 +1,5 @@ import { Types } from "mongoose"; -import { Secret, ISecret, Membership } from "../models"; +import { Secret, ISecret } from "../models"; import { EESecretService, EELogService } from "../ee/services"; import { IAction, SecretVersion } from "../ee/models"; import { @@ -12,8 +12,6 @@ import { ALGORITHM_AES_256_GCM, ENCODING_SCHEME_UTF8, } from "../variables"; -import _ from "lodash"; -import { BadRequestError, UnauthorizedRequestError } from "../utils/errors"; interface V1PushSecret { ciphertextKey: string; @@ -197,7 +195,7 @@ const v1PushSecrets = async ({ secretValueTag: newSecret.tagValue, secretValueHash: newSecret.hashValue, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, }); }), }); @@ -230,7 +228,7 @@ const v1PushSecrets = async ({ secretCommentTag: s.tagComment, secretCommentHash: s.hashComment, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, }; if (toAdd[idx].type === "personal") { @@ -261,7 +259,7 @@ const v1PushSecrets = async ({ secretValueTag, secretValueHash, algorithm, - keyEncoding + keyEncoding, }) => new SecretVersion({ secret: _id, @@ -280,7 +278,7 @@ const v1PushSecrets = async ({ secretValueTag, secretValueHash, algorithm, - keyEncoding + keyEncoding, }) ), }); @@ -289,6 +287,7 @@ const v1PushSecrets = async ({ // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId: new Types.ObjectId(workspaceId), + environment, }); }; @@ -491,7 +490,7 @@ const v2PushSecrets = async ({ secret: secretDocument._id, isDeleted: false, algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 + keyEncoding: ENCODING_SCHEME_UTF8, }); }), }); @@ -508,6 +507,7 @@ const v2PushSecrets = async ({ // (EE) take a secret snapshot await EESecretService.takeSecretSnapshot({ workspaceId: new Types.ObjectId(workspaceId), + environment, }); // (EE) create (audit) log diff --git a/backend/src/helpers/secrets.ts b/backend/src/helpers/secrets.ts index 562c173fa..193c82698 100644 --- a/backend/src/helpers/secrets.ts +++ b/backend/src/helpers/secrets.ts @@ -1,49 +1,42 @@ import { Types } from 'mongoose'; import { - CreateSecretParams, - GetSecretsParams, - GetSecretParams, - UpdateSecretParams, - DeleteSecretParams + CreateSecretParams, + GetSecretsParams, + GetSecretParams, + UpdateSecretParams, + DeleteSecretParams, } from '../interfaces/services/SecretService'; -import { - Secret, - ISecret, - SecretBlindIndexData, -} from '../models'; +import { Secret, ISecret, SecretBlindIndexData } from '../models'; import { SecretVersion } from '../ee/models'; -import { - BadRequestError, - SecretNotFoundError, - SecretBlindIndexDataNotFoundError, - InternalServerError +import { + BadRequestError, + SecretNotFoundError, + SecretBlindIndexDataNotFoundError, + InternalServerError, } from '../utils/errors'; import { - SECRET_PERSONAL, - SECRET_SHARED, - ACTION_ADD_SECRETS, - ACTION_READ_SECRETS, - ACTION_UPDATE_SECRETS, - ACTION_DELETE_SECRETS, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 + SECRET_PERSONAL, + SECRET_SHARED, + ACTION_ADD_SECRETS, + ACTION_READ_SECRETS, + ACTION_UPDATE_SECRETS, + ACTION_DELETE_SECRETS, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64, } from '../variables'; import crypto from 'crypto'; import * as argon2 from 'argon2'; import { - encryptSymmetric128BitHexKeyUTF8, - decryptSymmetric128BitHexKeyUTF8 + encryptSymmetric128BitHexKeyUTF8, + decryptSymmetric128BitHexKeyUTF8, } from '../utils/crypto'; import { getEncryptionKey, client, getRootEncryptionKey } from '../config'; import { TelemetryService } from '../services'; +import { EESecretService, EELogService } from '../ee/services'; import { - EESecretService, - EELogService -} from '../ee/services'; -import { - getAuthDataPayloadIdObj, - getAuthDataPayloadUserObj + getAuthDataPayloadIdObj, + getAuthDataPayloadUserObj, } from '../utils/auth'; /** @@ -53,95 +46,99 @@ import { * @param {Types.ObjectId} obj.workspaceId */ const createSecretBlindIndexDataHelper = async ({ - workspaceId + workspaceId, }: { - workspaceId: Types.ObjectId; + workspaceId: Types.ObjectId; }) => { + // initialize random blind index salt for workspace + const salt = crypto.randomBytes(16).toString('base64'); - // initialize random blind index salt for workspace - const salt = crypto.randomBytes(16).toString('base64'); + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); - const encryptionKey = await getEncryptionKey(); - const rootEncryptionKey = await getRootEncryptionKey(); + if (rootEncryptionKey) { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag, + } = client.encryptSymmetric(salt, rootEncryptionKey); - if (rootEncryptionKey) { - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = client.encryptSymmetric(salt, rootEncryptionKey); - - return await new SecretBlindIndexData({ - workspace: workspaceId, - encryptedSaltCiphertext, - saltIV, - saltTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 - }).save(); - } else { - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = encryptSymmetric128BitHexKeyUTF8({ - plaintext: salt, - key: encryptionKey - }); - - return await new SecretBlindIndexData({ - workspace: workspaceId, - encryptedSaltCiphertext, - saltIV, - saltTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }).save(); - } -} + return await new SecretBlindIndexData({ + workspace: workspaceId, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64, + }).save(); + } else { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag, + } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: salt, + key: encryptionKey, + }); + + return await new SecretBlindIndexData({ + workspace: workspaceId, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }).save(); + } +}; /** * Get secret blind index salt for workspace with id [workspaceId] * @param {Object} obj * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for - * @returns + * @returns */ const getSecretBlindIndexSaltHelper = async ({ - workspaceId + workspaceId, }: { - workspaceId: Types.ObjectId; + workspaceId: Types.ObjectId; }) => { - - const encryptionKey = await getEncryptionKey(); - const rootEncryptionKey = await getRootEncryptionKey(); + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); - const secretBlindIndexData = await SecretBlindIndexData.findOne({ - workspace: workspaceId - }).select('+algorithm +keyEncoding'); - - if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); + const secretBlindIndexData = await SecretBlindIndexData.findOne({ + workspace: workspaceId, + }).select('+algorithm +keyEncoding'); - if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) { - return client.decryptSymmetric( - secretBlindIndexData.encryptedSaltCiphertext, - rootEncryptionKey, - secretBlindIndexData.saltIV, - secretBlindIndexData.saltTag - ); - } else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) { - // decrypt workspace salt - return decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secretBlindIndexData.encryptedSaltCiphertext, - iv: secretBlindIndexData.saltIV, - tag: secretBlindIndexData.saltTag, - key: encryptionKey - }); - } + if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); - throw InternalServerError({ - message: 'Failed to obtain workspace salt needed for secret blind indexing' + if ( + rootEncryptionKey && + secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64 + ) { + return client.decryptSymmetric( + secretBlindIndexData.encryptedSaltCiphertext, + rootEncryptionKey, + secretBlindIndexData.saltIV, + secretBlindIndexData.saltTag + ); + } else if ( + encryptionKey && + secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8 + ) { + // decrypt workspace salt + return decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secretBlindIndexData.encryptedSaltCiphertext, + iv: secretBlindIndexData.saltIV, + tag: secretBlindIndexData.saltTag, + key: encryptionKey, }); -} + } + + throw InternalServerError({ + message: 'Failed to obtain workspace salt needed for secret blind indexing', + }); +}; /** * Generate blind index for secret with name [secretName] @@ -150,65 +147,65 @@ const getSecretBlindIndexSaltHelper = async ({ * @param {String} obj.secretName - name of secret to generate blind index for * @param {String} obj.salt - base64-salt */ - const generateSecretBlindIndexWithSaltHelper = async ({ - secretName, - salt +const generateSecretBlindIndexWithSaltHelper = async ({ + secretName, + salt, }: { - secretName: string; - salt: string; + secretName: string; + salt: string; }) => { + // generate secret blind index + const secretBlindIndex = ( + await argon2.hash(secretName, { + type: argon2.argon2id, + salt: Buffer.from(salt, 'base64'), + saltLength: 16, // default 16 bytes + memoryCost: 65536, // default pool of 64 MiB per thread. + hashLength: 32, + parallelism: 1, + raw: true, + }) + ).toString('base64'); - // generate secret blind index - const secretBlindIndex = (await argon2.hash(secretName, { - type: argon2.argon2id, - salt: Buffer.from(salt, 'base64'), - saltLength: 16, // default 16 bytes - memoryCost: 65536, // default pool of 64 MiB per thread. - hashLength: 32, - parallelism: 1, - raw: true - })).toString('base64'); - - return secretBlindIndex; -} + return secretBlindIndex; +}; /** - * Generate blind index for secret with name [secretName] + * Generate blind index for secret with name [secretName] * for workspace with id [workspaceId] * @param {Object} obj * @param {Stringj} obj.secretName - name of secret to generate blind index for * @param {Types.ObjectId} obj.workspaceId - id of workspace that secret belongs to */ const generateSecretBlindIndexHelper = async ({ - secretName, - workspaceId + secretName, + workspaceId, }: { - secretName: string; - workspaceId: Types.ObjectId; + secretName: string; + workspaceId: Types.ObjectId; }) => { + // check if workspace blind index data exists + const secretBlindIndexData = await SecretBlindIndexData.findOne({ + workspace: workspaceId, + }); - // check if workspace blind index data exists - const secretBlindIndexData = await SecretBlindIndexData.findOne({ - workspace: workspaceId - }); - - if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); - - // decrypt workspace salt - const salt = decryptSymmetric128BitHexKeyUTF8({ - ciphertext: secretBlindIndexData.encryptedSaltCiphertext, - iv: secretBlindIndexData.saltIV, - tag: secretBlindIndexData.saltTag, - key: await getEncryptionKey() - }); + if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError(); - const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ - secretName, - salt - }); + // decrypt workspace salt + const salt = decryptSymmetric128BitHexKeyUTF8({ + ciphertext: secretBlindIndexData.encryptedSaltCiphertext, + iv: secretBlindIndexData.saltIV, + tag: secretBlindIndexData.saltTag, + key: await getEncryptionKey(), + }); - return secretBlindIndex; -} + const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({ + secretName, + salt, + }); + + return secretBlindIndex; +}; /** * Create secret with name [secretName] @@ -218,14 +215,67 @@ const generateSecretBlindIndexHelper = async ({ * @param {String} obj.environment - environment in workspace to create secret for * @param {'shared' | 'personal'} obj.type - type of secret * @param {AuthData} obj.authData - authentication data on request - * @returns + * @returns */ const createSecretHelper = async ({ + secretName, + workspaceId, + environment, + type, + authData, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + secretCommentCiphertext, + secretCommentIV, + secretCommentTag, + folderId, +}: CreateSecretParams) => { + const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, - workspaceId, + workspaceId: new Types.ObjectId(workspaceId), + }); + + const exists = await Secret.exists({ + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + type, + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), + }); + + if (exists) + throw BadRequestError({ + message: 'Failed to create secret that already exists', + }); + + if (type === SECRET_PERSONAL) { + // case: secret type is personal -> check if a corresponding shared secret + // with the same blind index [secretBlindIndex] exists + + const exists = await Secret.exists({ + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + type: SECRET_SHARED, + }); + + if (!exists) + throw BadRequestError({ + message: + 'Failed to create personal secret override for no corresponding shared secret', + }); + } + + // create secret + const secret = await new Secret({ + version: 1, + workspace: new Types.ObjectId(workspaceId), environment, type, - authData, + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), + secretBlindIndex, secretKeyCiphertext, secretKeyIV, secretKeyTag, @@ -234,125 +284,80 @@ const createSecretHelper = async ({ secretValueTag, secretCommentCiphertext, secretCommentIV, - secretCommentTag -}: CreateSecretParams) => { - const secretBlindIndex = await generateSecretBlindIndexHelper({ - secretName, - workspaceId: new Types.ObjectId(workspaceId) - }); + secretCommentTag, + folder: folderId, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }).save(); - const exists = await Secret.exists({ - secretBlindIndex, - workspace: new Types.ObjectId(workspaceId), - type, - ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}) - }); - - if (exists) throw BadRequestError({ - message: 'Failed to create secret that already exists' - }); - - if (type === SECRET_PERSONAL) { - // case: secret type is personal -> check if a corresponding shared secret - // with the same blind index [secretBlindIndex] exists + const secretVersion = new SecretVersion({ + secret: secret._id, + version: secret.version, + workspace: secret.workspace, + type, + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), + environment: secret.environment, + isDeleted: false, + secretBlindIndex, + secretKeyCiphertext, + secretKeyIV, + secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }); - const exists = await Secret.exists({ - secretBlindIndex, - workspace: new Types.ObjectId(workspaceId), - type: SECRET_SHARED - }); - - if (!exists) throw BadRequestError({ - message: 'Failed to create personal secret override for no corresponding shared secret' - }); - } - - // create secret - const secret = await new Secret({ - version: 1, - workspace: new Types.ObjectId(workspaceId), + // (EE) add version for new secret + await EESecretService.addSecretVersions({ + secretVersions: [secretVersion], + }); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_ADD_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: [secret._id], + }); + + action && + (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP, + })); + + // (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, - type, - ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - secretCommentCiphertext, - secretCommentIV, - secretCommentTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }).save(); - - const secretVersion = new SecretVersion({ - secret: secret._id, - version: secret.version, - workspace: secret.workspace, - type, - ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), - environment: secret.environment, - isDeleted: false, - secretBlindIndex, - secretKeyCiphertext, - secretKeyIV, - secretKeyTag, - secretValueCiphertext, - secretValueIV, - secretValueTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }); - - // // (EE) add version for new secret - await EESecretService.addSecretVersions({ - secretVersions: [secretVersion] - }); - - // (EE) create (audit) log - const action = await EELogService.createAction({ - name: ACTION_ADD_SECRETS, - ...getAuthDataPayloadIdObj(authData), workspaceId, - secretIds: [secret._id] - }); - - action && await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP - }); - - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId + userAgent: authData.authUserAgent, + }, }); + } - const postHogClient = await TelemetryService.getPostHogClient(); - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets added', - distinctId: await TelemetryService.getDistinctId({ - authData - }), - properties: { - numberOfSecrets: 1, - environment, - workspaceId, - channel: authData.authChannel, - userAgent: authData.authUserAgent - } - }); - } - - return secret; -} + return secret; +}; /** * Get secrets for workspace with id [workspaceId] and environment [environment] @@ -360,69 +365,72 @@ const createSecretHelper = async ({ * @param {Types.ObjectId} obj.workspaceId - id of workspace * @param {String} obj.environment - environment in workspace * @param {AuthData} obj.authData - authentication data on request - * @returns + * @returns */ const getSecretsHelper = async ({ - workspaceId, - environment, - authData + workspaceId, + environment, + authData, }: GetSecretsParams) => { - let secrets: ISecret[] = []; + let secrets: ISecret[] = []; - // get personal secrets first - secrets = await Secret.find({ - workspace: new Types.ObjectId(workspaceId), - environment, - type: SECRET_PERSONAL, - ...getAuthDataPayloadUserObj(authData) - }); + // get personal secrets first + secrets = await Secret.find({ + workspace: new Types.ObjectId(workspaceId), + environment, + type: SECRET_PERSONAL, + ...getAuthDataPayloadUserObj(authData), + }); - // concat with shared secrets - secrets = secrets.concat(await Secret.find({ - workspace: new Types.ObjectId(workspaceId), - environment, - type: SECRET_SHARED, - secretBlindIndex: { - $nin: secrets.map((secret) => secret.secretBlindIndex) - } + // concat with shared secrets + secrets = secrets.concat( + await Secret.find({ + workspace: new Types.ObjectId(workspaceId), + environment, + type: SECRET_SHARED, + secretBlindIndex: { + $nin: secrets.map((secret) => secret.secretBlindIndex), + }, + }) + ); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_READ_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: secrets.map((secret) => secret._id), + }); + + action && + (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP, })); - // (EE) create (audit) log - const action = await EELogService.createAction({ - name: ACTION_READ_SECRETS, - ...getAuthDataPayloadIdObj(authData), + const postHogClient = await TelemetryService.getPostHogClient(); + + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pulled', + distinctId: await TelemetryService.getDistinctId({ + authData, + }), + properties: { + numberOfSecrets: secrets.length, + environment, workspaceId, - secretIds: secrets.map((secret) => secret._id) - }); - - action && await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP + userAgent: authData.authUserAgent, + }, }); + } - const postHogClient = await TelemetryService.getPostHogClient(); - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets pulled', - distinctId: await TelemetryService.getDistinctId({ - authData - }), - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: authData.authChannel, - userAgent: authData.authUserAgent - } - }); - } - - return secrets; -} + return secrets; +}; /** * Get secret with name [secretName] @@ -432,79 +440,80 @@ const getSecretsHelper = async ({ * @param {String} obj.environment - environment in workspace that secret belongs to * @param {'shared' | 'personal'} obj.type - type of secret * @param {AuthData} obj.authData - authentication data on request - * @returns + * @returns */ const getSecretHelper = async ({ - secretName, - workspaceId, - environment, - type, - authData + secretName, + workspaceId, + environment, + type, + authData, }: GetSecretParams) => { - const secretBlindIndex = await generateSecretBlindIndexHelper({ - secretName, - workspaceId: new Types.ObjectId(workspaceId) - }); - let secret: ISecret | null = null; - - // try getting personal secret first (if exists) + const secretBlindIndex = await generateSecretBlindIndexHelper({ + secretName, + workspaceId: new Types.ObjectId(workspaceId), + }); + let secret: ISecret | null = null; + + // try getting personal secret first (if exists) + secret = await Secret.findOne({ + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + environment, + type: type ?? SECRET_PERSONAL, + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), + }); + + if (!secret) { + // case: failed to find personal secret matching criteria + // -> find shared secret matching criteria secret = await Secret.findOne({ - secretBlindIndex, - workspace: new Types.ObjectId(workspaceId), + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + environment, + type: SECRET_SHARED, + }); + } + + if (!secret) throw SecretNotFoundError(); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_READ_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: [secret._id], + }); + + action && + (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP, + })); + + const postHogClient = await TelemetryService.getPostHogClient(); + + if (postHogClient) { + postHogClient.capture({ + event: 'secrets pull', + distinctId: await TelemetryService.getDistinctId({ + authData, + }), + properties: { + numberOfSecrets: 1, environment, - type: type ?? SECRET_PERSONAL, - ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}) - }); - - if (!secret) { - // case: failed to find personal secret matching criteria - // -> find shared secret matching criteria - secret = await Secret.findOne({ - secretBlindIndex, - workspace: new Types.ObjectId(workspaceId), - environment, - type: SECRET_SHARED - }); - } - - if (!secret) throw SecretNotFoundError(); - - // (EE) create (audit) log - const action = await EELogService.createAction({ - name: ACTION_READ_SECRETS, - ...getAuthDataPayloadIdObj(authData), workspaceId, - secretIds: [secret._id] - }); - - action && await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP + userAgent: authData.authUserAgent, + }, }); + } - const postHogClient = await TelemetryService.getPostHogClient(); - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets pull', - distinctId: await TelemetryService.getDistinctId({ - authData - }), - properties: { - numberOfSecrets: 1, - environment, - workspaceId, - channel: authData.authChannel, - userAgent: authData.authUserAgent - } - }); - } - - return secret; -} + return secret; +}; /** * Update secret with name [secretName] @@ -517,134 +526,137 @@ const getSecretHelper = async ({ * @param {String} obj.secretValueIV - IV of secret value * @param {String} obj.secretValueTag - tag of secret value * @param {AuthData} obj.authData - authentication data on request - * @returns + * @returns */ const updateSecretHelper = async ({ - secretName, - workspaceId, - environment, - type, - authData, - secretValueCiphertext, - secretValueIV, - secretValueTag + secretName, + workspaceId, + environment, + type, + authData, + secretValueCiphertext, + secretValueIV, + secretValueTag, }: UpdateSecretParams) => { - const secretBlindIndex = await generateSecretBlindIndexHelper({ - secretName, - workspaceId: new Types.ObjectId(workspaceId) - }); + const secretBlindIndex = await generateSecretBlindIndexHelper({ + secretName, + workspaceId: new Types.ObjectId(workspaceId), + }); - let secret: ISecret | null = null; - - if (type === SECRET_SHARED) { - // case: update shared secret - secret = await Secret.findOneAndUpdate( - { - secretBlindIndex, - workspace: new Types.ObjectId(workspaceId), - environment, - type - }, - { - secretValueCiphertext, - secretValueIV, - secretValueTag, - $inc: { version: 1 } - }, - { - new: true - } - ); - } else { - // case: update personal secret - - secret = await Secret.findOneAndUpdate( - { - secretBlindIndex, - workspace: new Types.ObjectId(workspaceId), - environment, - type, - ...getAuthDataPayloadUserObj(authData) - }, - { - secretValueCiphertext, - secretValueIV, - secretValueTag, - $inc: { version: 1 } - }, - { - new: true - } - ); - } + let secret: ISecret | null = null; - if (!secret) throw SecretNotFoundError(); - - const secretVersion = new SecretVersion({ - secret: secret._id, - version: secret.version, - workspace: secret.workspace, - type, - ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), - environment: secret.environment, - isDeleted: false, + if (type === SECRET_SHARED) { + // case: update shared secret + secret = await Secret.findOneAndUpdate( + { secretBlindIndex, - secretKeyCiphertext: secret.secretKeyCiphertext, - secretKeyIV: secret.secretKeyIV, - secretKeyTag: secret.secretKeyTag, + workspace: new Types.ObjectId(workspaceId), + environment, + type, + }, + { secretValueCiphertext, secretValueIV, secretValueTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }); + $inc: { version: 1 }, + }, + { + new: true, + } + ); + } else { + // case: update personal secret - // (EE) add version for new secret - await EESecretService.addSecretVersions({ - secretVersions: [secretVersion] - }); + secret = await Secret.findOneAndUpdate( + { + secretBlindIndex, + workspace: new Types.ObjectId(workspaceId), + environment, + type, + ...getAuthDataPayloadUserObj(authData), + }, + { + secretValueCiphertext, + secretValueIV, + secretValueTag, + $inc: { version: 1 }, + }, + { + new: true, + } + ); + } - // (EE) create (audit) log - const action = await EELogService.createAction({ - name: ACTION_UPDATE_SECRETS, - ...getAuthDataPayloadIdObj(authData), + if (!secret) throw SecretNotFoundError(); + + const secretVersion = new SecretVersion({ + secret: secret._id, + version: secret.version, + workspace: secret.workspace, + type, + ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), + environment: secret.environment, + isDeleted: false, + secretBlindIndex, + secretKeyCiphertext: secret.secretKeyCiphertext, + secretKeyIV: secret.secretKeyIV, + secretKeyTag: secret.secretKeyTag, + secretValueCiphertext, + secretValueIV, + secretValueTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }); + + // (EE) add version for new secret + await EESecretService.addSecretVersions({ + secretVersions: [secretVersion], + }); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_UPDATE_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: [secret._id], + }); + + action && + (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP, + })); + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ + workspaceId, + environment, + folderId: secret?.folder, + }); + + const postHogClient = await TelemetryService.getPostHogClient(); + + if (postHogClient) { + postHogClient.capture({ + event: 'secrets modified', + distinctId: await TelemetryService.getDistinctId({ + authData, + }), + properties: { + numberOfSecrets: 1, + environment, workspaceId, - secretIds: [secret._id] - }); - - action && await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP - }); - - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId + userAgent: authData.authUserAgent, + }, }); + } - const postHogClient = await TelemetryService.getPostHogClient(); - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets modified', - distinctId: await TelemetryService.getDistinctId({ - authData - }), - properties: { - numberOfSecrets: 1, - environment, - workspaceId, - channel: authData.authChannel, - userAgent: authData.authUserAgent - } - }); - } - - return secret; -} + return secret; +}; /** * Delete secret with name [secretName] @@ -654,116 +666,119 @@ const updateSecretHelper = async ({ * @param {String} obj.environment - environment in workspace that secret belongs to * @param {'shared' | 'personal'} obj.type - type of secret * @param {AuthData} obj.authData - authentication data on request - * @returns + * @returns */ const deleteSecretHelper = async ({ + secretName, + workspaceId, + environment, + type, + authData, +}: DeleteSecretParams) => { + const secretBlindIndex = await generateSecretBlindIndexHelper({ secretName, + workspaceId: new Types.ObjectId(workspaceId), + }); + + let secrets: ISecret[] = []; + let secret: ISecret | null = null; + + if (type === SECRET_SHARED) { + secrets = await Secret.find({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment, + }); + + secret = await Secret.findOneAndDelete({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + }); + + await Secret.deleteMany({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment, + }); + } else { + secret = await Secret.findOneAndDelete({ + secretBlindIndex, + workspaceId: new Types.ObjectId(workspaceId), + environment, + type, + ...getAuthDataPayloadUserObj(authData), + }); + + if (secret) { + secrets = [secret]; + } + } + + if (!secret) throw SecretNotFoundError(); + + await EESecretService.markDeletedSecretVersions({ + secretIds: secrets.map((secret) => secret._id), + }); + + // (EE) create (audit) log + const action = await EELogService.createAction({ + name: ACTION_DELETE_SECRETS, + ...getAuthDataPayloadIdObj(authData), + workspaceId, + secretIds: secrets.map((secret) => secret._id), + }); + + // (EE) take a secret snapshot + action && + (await EELogService.createLog({ + ...getAuthDataPayloadIdObj(authData), + workspaceId, + actions: [action], + channel: authData.authChannel, + ipAddress: authData.authIP, + })); + + // (EE) take a secret snapshot + await EESecretService.takeSecretSnapshot({ workspaceId, environment, - type, - authData -}: DeleteSecretParams) => { - const secretBlindIndex = await generateSecretBlindIndexHelper({ - secretName, - workspaceId: new Types.ObjectId(workspaceId) - }); + folderId: secret?.folder, + }); - let secrets: ISecret[] = []; - let secret: ISecret | null = null; - - if (type === SECRET_SHARED) { - secrets = await Secret.find({ - secretBlindIndex, - workspaceId: new Types.ObjectId(workspaceId), - environment - }); - - secret = await Secret.findOneAndDelete({ - secretBlindIndex, - workspaceId: new Types.ObjectId(workspaceId), - environment, - type - }); - - await Secret.deleteMany({ - secretBlindIndex, - workspaceId: new Types.ObjectId(workspaceId), - environment - }); - } else { - secret = await Secret.findOneAndDelete({ - secretBlindIndex, - workspaceId: new Types.ObjectId(workspaceId), - environment, - type, - ...getAuthDataPayloadUserObj(authData) - }); - - if (secret) { - secrets = [secret]; - } - } - - if (!secret) throw SecretNotFoundError(); - - await EESecretService.markDeletedSecretVersions({ - secretIds: secrets.map((secret) => secret._id) - }); + const postHogClient = await TelemetryService.getPostHogClient(); - // (EE) create (audit) log - const action = await EELogService.createAction({ - name: ACTION_DELETE_SECRETS, - ...getAuthDataPayloadIdObj(authData), + if (postHogClient) { + postHogClient.capture({ + event: 'secrets deleted', + distinctId: await TelemetryService.getDistinctId({ + authData, + }), + properties: { + numberOfSecrets: secrets.length, + environment, workspaceId, - secretIds: secrets.map((secret) => secret._id) - }); - - // (EE) take a secret snapshot - action && await EELogService.createLog({ - ...getAuthDataPayloadIdObj(authData), - workspaceId, - actions: [action], channel: authData.authChannel, - ipAddress: authData.authIP + userAgent: authData.authUserAgent, + }, }); + } - // (EE) take a secret snapshot - await EESecretService.takeSecretSnapshot({ - workspaceId - }); - - const postHogClient = await TelemetryService.getPostHogClient(); - - if (postHogClient) { - postHogClient.capture({ - event: 'secrets deleted', - distinctId: await TelemetryService.getDistinctId({ - authData - }), - properties: { - numberOfSecrets: secrets.length, - environment, - workspaceId, - channel: authData.authChannel, - userAgent: authData.authUserAgent - } - }); - } - - return ({ - secrets, - secret - }); -} + return { + secrets, + secret, + }; +}; export { - createSecretBlindIndexDataHelper, - getSecretBlindIndexSaltHelper, - generateSecretBlindIndexWithSaltHelper, - generateSecretBlindIndexHelper, - createSecretHelper, - getSecretsHelper, - getSecretHelper, - updateSecretHelper, - deleteSecretHelper -} \ No newline at end of file + createSecretBlindIndexDataHelper, + getSecretBlindIndexSaltHelper, + generateSecretBlindIndexWithSaltHelper, + generateSecretBlindIndexHelper, + createSecretHelper, + getSecretsHelper, + getSecretHelper, + updateSecretHelper, + deleteSecretHelper, +}; diff --git a/backend/src/index.ts b/backend/src/index.ts index 8d5107ce9..1fbc6e1fa 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -1,8 +1,6 @@ -import dotenv from "dotenv"; +import dotenv from 'dotenv'; dotenv.config(); import express from 'express'; -// eslint-disable-next-line @typescript-eslint/no-var-requires -require('express-async-errors'); import helmet from 'helmet'; import cors from 'cors'; import { DatabaseService } from './services'; @@ -21,7 +19,7 @@ import { secretSnapshot as eeSecretSnapshotRouter, action as eeActionRouter, organizations as eeOrganizationsRouter, - cloudProducts as eeCloudProductsRouter + cloudProducts as eeCloudProductsRouter, } from './ee/routes/v1'; import { signup as v1SignupRouter, @@ -41,7 +39,7 @@ import { stripe as v1StripeRouter, integration as v1IntegrationRouter, integrationAuth as v1IntegrationAuthRouter, - secretsFolder as v1SecretsFolder + secretsFolder as v1SecretsFolder, } from './routes/v1'; import { signup as v2SignupRouter, @@ -61,17 +59,13 @@ import { auth as v3AuthRouter, secrets as v3SecretsRouter, signup as v3SignupRouter, - workspaces as v3WorkspacesRouter + workspaces as v3WorkspacesRouter, } from './routes/v3'; import { healthCheck } from './routes/status'; import { getLogger } from './utils/logger'; import { RouteNotFoundError } from './utils/errors'; import { requestErrorHandler } from './middleware/requestErrorHandler'; -import { - getNodeEnv, - getPort, - getSiteURL -} from './config'; +import { getNodeEnv, getPort, getSiteURL } from './config'; import { setup } from './utils/setup'; const main = async () => { @@ -86,7 +80,7 @@ const main = async () => { app.use( cors({ credentials: true, - origin: await getSiteURL() + origin: await getSiteURL(), }) ); @@ -126,7 +120,7 @@ const main = async () => { app.use('/api/v1/stripe', v1StripeRouter); app.use('/api/v1/integration', v1IntegrationRouter); app.use('/api/v1/integration-auth', v1IntegrationAuthRouter); - app.use('/api/v1/folder', v1SecretsFolder) + app.use('/api/v1/folders', v1SecretsFolder); // v2 routes (improvements) app.use('/api/v2/signup', v2SignupRouter); @@ -143,34 +137,37 @@ const main = async () => { app.use('/api/v2/api-key', v2APIKeyDataRouter); // v3 routes (experimental) - app.use("/api/v3/auth", v3AuthRouter); app.use('/api/v3/secrets', v3SecretsRouter); app.use('/api/v3/workspaces', v3WorkspacesRouter); - app.use("/api/v3/signup", v3SignupRouter); - // api docs - app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)) + // api docs + app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile)); // server status - app.use('/api', healthCheck) + app.use('/api', healthCheck); //* Handle unrouted requests and respond with proper error message as well as status code app.use((req, res, next) => { if (res.headersSent) return next(); - next(RouteNotFoundError({ message: `The requested source '(${req.method})${req.url}' was not found` })) - }) + next( + RouteNotFoundError({ + message: `The requested source '(${req.method})${req.url}' was not found`, + }) + ); + }); app.use(requestErrorHandler); const server = app.listen(await getPort(), async () => { - (await getLogger("backend-main")).info( + (await getLogger('backend-main')).info( `Server started listening at port ${await getPort()}` ); }); + // await createTestUserForDevelopment(); setUpHealthEndpoint(server); - server.on("close", async () => { + server.on('close', async () => { await DatabaseService.closeDatabase(); }); diff --git a/backend/src/interfaces/services/SecretService/index.ts b/backend/src/interfaces/services/SecretService/index.ts index b72068b90..2efd53f4e 100644 --- a/backend/src/interfaces/services/SecretService/index.ts +++ b/backend/src/interfaces/services/SecretService/index.ts @@ -1,52 +1,54 @@ -import { Types } from 'mongoose'; -import { AuthData } from '../../middleware'; +import { Types } from "mongoose"; +import { AuthData } from "../../middleware"; export interface CreateSecretParams { - secretName: string; - workspaceId: Types.ObjectId; - environment: string; - type: 'shared' | 'personal'; - authData: AuthData; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretCommentCiphertext?: string; - secretCommentIV?: string; - secretCommentTag?: string; + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + folderId?: string; + type: "shared" | "personal"; + authData: AuthData; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; } export interface GetSecretsParams { - workspaceId: Types.ObjectId; - environment: string; - authData: AuthData; + workspaceId: Types.ObjectId; + environment: string; + authData: AuthData; } export interface GetSecretParams { - secretName: string; - workspaceId: Types.ObjectId; - environment: string; - type?: 'shared' | 'personal'; - authData: AuthData; + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + type?: "shared" | "personal"; + authData: AuthData; } export interface UpdateSecretParams { - secretName: string; - workspaceId: Types.ObjectId; - environment: string; - type: 'shared' | 'personal', - authData: AuthData - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + type: "shared" | "personal"; + authData: AuthData; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + folderId?: string; } export interface DeleteSecretParams { - secretName: string; - workspaceId: Types.ObjectId; - environment: string; - type: 'shared' | 'personal'; - authData: AuthData; -} \ No newline at end of file + secretName: string; + workspaceId: Types.ObjectId; + environment: string; + type: "shared" | "personal"; + authData: AuthData; +} diff --git a/backend/src/middleware/requestErrorHandler.ts b/backend/src/middleware/requestErrorHandler.ts index 046540c0e..1f1e9f9b2 100644 --- a/backend/src/middleware/requestErrorHandler.ts +++ b/backend/src/middleware/requestErrorHandler.ts @@ -7,6 +7,11 @@ import { getNodeEnv } from '../config'; export const requestErrorHandler: ErrorRequestHandler = async (error: RequestError | Error, req, res, next) => { if (res.headersSent) return next(); + if ((await getNodeEnv()) !== "production") { + /* eslint-disable no-console */ + console.log(error) + /* eslint-enable no-console */ + } //TODO: Find better way to type check for error. In current setting you need to cast type to get the functions and variables from RequestError if (!(error instanceof RequestError)) { diff --git a/backend/src/models/folder.ts b/backend/src/models/folder.ts index 885e320a8..ac0358287 100644 --- a/backend/src/models/folder.ts +++ b/backend/src/models/folder.ts @@ -1,36 +1,56 @@ -import { Schema, Types, model } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; -const folderSchema = new Schema({ +export type TFolderRootSchema = { + _id: Types.ObjectId; + workspace: Types.ObjectId; + environment: string; + nodes: TFolderSchema; +}; + +export type TFolderSchema = { + id: string; + name: string; + version: number; + children: TFolderSchema[]; +}; + +const folderSchema = new Schema({ + id: { + required: true, + type: String, + }, + version: { + required: true, + type: Number, + default: 1, + }, name: { - type: String, required: true, - }, - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true, - }, - environment: { type: String, - required: true, + default: "root", }, - parent: { - type: Schema.Types.ObjectId, - ref: 'Folder', - required: false, // optional for root folders - }, - path: { - type: String, - required: true - }, - parentPath: { - type: String, - required: true, - }, -}, { - timestamps: true }); -const Folder = model('Folder', folderSchema); +folderSchema.add({ children: [folderSchema] }); -export default Folder; \ No newline at end of file +const folderRootSchema = new Schema( + { + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + }, + environment: { + type: String, + required: true, + }, + nodes: folderSchema, + }, + { + timestamps: true, + } +); + +const Folder = model("Folder", folderRootSchema); + +export default Folder; diff --git a/backend/src/models/secret.ts b/backend/src/models/secret.ts index 39e362bf7..75163ccd1 100644 --- a/backend/src/models/secret.ts +++ b/backend/src/models/secret.ts @@ -1,154 +1,145 @@ -import { Schema, model, Types } from 'mongoose'; +import { Schema, model, Types } from "mongoose"; import { - SECRET_SHARED, - SECRET_PERSONAL, - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 -} from '../variables'; -import { ROOT_FOLDER_PATH } from '../utils/folder'; + SECRET_SHARED, + SECRET_PERSONAL, + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64, +} from "../variables"; export interface ISecret { - _id: Types.ObjectId; - version: number; - workspace: Types.ObjectId; - type: string; - user: Types.ObjectId; - environment: string; - secretBlindIndex?: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretKeyHash: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretValueHash: string; - secretCommentCiphertext?: string; - secretCommentIV?: string; - secretCommentTag?: string; - secretCommentHash?: string; - algorithm: 'aes-256-gcm'; - keyEncoding: 'utf8' | 'base64'; - tags?: string[]; - path?: string; - folder?: Types.ObjectId; + _id: Types.ObjectId; + version: number; + workspace: Types.ObjectId; + type: string; + user: Types.ObjectId; + environment: string; + secretBlindIndex?: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretKeyHash: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretValueHash: string; + secretCommentCiphertext?: string; + secretCommentIV?: string; + secretCommentTag?: string; + secretCommentHash?: string; + algorithm: "aes-256-gcm"; + keyEncoding: "utf8" | "base64"; + tags?: string[]; + folder?: string; } const secretSchema = new Schema( - { - version: { - type: Number, - required: true, - default: 1 - }, - workspace: { - type: Schema.Types.ObjectId, - ref: 'Workspace', - required: true - }, - type: { - type: String, - enum: [SECRET_SHARED, SECRET_PERSONAL], - required: true - }, - user: { - // user associated with the personal secret - type: Schema.Types.ObjectId, - ref: 'User' - }, - tags: { - ref: 'Tag', - type: [Schema.Types.ObjectId], - default: [] - }, - environment: { - type: String, - required: true - }, - secretBlindIndex: { - type: String, - select: false - }, - secretKeyCiphertext: { - type: String, - required: true - }, - secretKeyIV: { - type: String, // symmetric - required: true - }, - secretKeyTag: { - type: String, // symmetric - required: true - }, - secretKeyHash: { - type: String - }, - secretValueCiphertext: { - type: String, - required: true - }, - secretValueIV: { - type: String, // symmetric - required: true - }, - secretValueTag: { - type: String, // symmetric - required: true - }, - secretValueHash: { - type: String - }, - secretCommentCiphertext: { - type: String, - required: false - }, - secretCommentIV: { - type: String, // symmetric - required: false - }, - secretCommentTag: { - type: String, // symmetric - required: false - }, - secretCommentHash: { - type: String, - required: false - }, - algorithm: { // the encryption algorithm used - type: String, - enum: [ALGORITHM_AES_256_GCM], - required: true - }, - keyEncoding: { - type: String, - enum: [ - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 - ], - required: true - }, - // the full path to the secret in relation to folders - path: { - type: String, - required: false, - default: ROOT_FOLDER_PATH - }, - folder: { - type: Schema.Types.ObjectId, - ref: 'Folder', - required: false, - }, - }, - { - timestamps: true - } + { + version: { + type: Number, + required: true, + default: 1, + }, + workspace: { + type: Schema.Types.ObjectId, + ref: "Workspace", + required: true, + }, + type: { + type: String, + enum: [SECRET_SHARED, SECRET_PERSONAL], + required: true, + }, + user: { + // user associated with the personal secret + type: Schema.Types.ObjectId, + ref: "User", + }, + tags: { + ref: "Tag", + type: [Schema.Types.ObjectId], + default: [], + }, + environment: { + type: String, + required: true, + }, + secretBlindIndex: { + type: String, + select: false, + }, + secretKeyCiphertext: { + type: String, + required: true, + }, + secretKeyIV: { + type: String, // symmetric + required: true, + }, + secretKeyTag: { + type: String, // symmetric + required: true, + }, + secretKeyHash: { + type: String, + }, + secretValueCiphertext: { + type: String, + required: true, + }, + secretValueIV: { + type: String, // symmetric + required: true, + }, + secretValueTag: { + type: String, // symmetric + required: true, + }, + secretValueHash: { + type: String, + }, + secretCommentCiphertext: { + type: String, + required: false, + }, + secretCommentIV: { + type: String, // symmetric + required: false, + }, + secretCommentTag: { + type: String, // symmetric + required: false, + }, + secretCommentHash: { + type: String, + required: false, + }, + algorithm: { + // the encryption algorithm used + type: String, + enum: [ALGORITHM_AES_256_GCM], + required: true, + default: ALGORITHM_AES_256_GCM, + }, + keyEncoding: { + type: String, + enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], + required: true, + default: ENCODING_SCHEME_UTF8, + }, + // the full path to the secret in relation to folders + folder: { + type: String, + default: "root", + }, + }, + { + timestamps: true, + } ); +secretSchema.index({ tags: 1 }, { background: true }); -secretSchema.index({ tags: 1 }, { background: true }) - -const Secret = model('Secret', secretSchema); +const Secret = model("Secret", secretSchema); export default Secret; diff --git a/backend/src/routes/v1/secretsFolder.ts b/backend/src/routes/v1/secretsFolder.ts index e6319b752..2644a835b 100644 --- a/backend/src/routes/v1/secretsFolder.ts +++ b/backend/src/routes/v1/secretsFolder.ts @@ -1,50 +1,70 @@ -import express, { Request, Response } from 'express'; +import express from "express"; const router = express.Router(); import { requireAuth, requireWorkspaceAuth, - validateRequest -} from '../../middleware'; -import { body, param } from 'express-validator'; -import { createFolder, deleteFolder, getFolderById } from '../../controllers/v1/secretsFolderController'; -import { ADMIN, MEMBER } from '../../variables'; + validateRequest, +} from "../../middleware"; +import { body, param, query } from "express-validator"; +import { + createFolder, + deleteFolder, + getFolders, + updateFolderById, +} from "../../controllers/v1/secretsFolderController"; +import { ADMIN, MEMBER } from "../../variables"; router.post( - '/', + "/", requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ["jwt"], }), requireWorkspaceAuth({ acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' + locationWorkspaceId: "body", }), - body('workspaceId').exists(), - body('environment').exists(), - body('folderName').exists(), - body('parentFolderId'), + body("workspaceId").exists(), + body("environment").exists(), + body("folderName").exists(), + body("parentFolderId"), validateRequest, createFolder ); -router.delete( - '/:folderId', +router.patch( + "/:folderId", requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ["jwt"], }), - param('folderId').exists(), + body("workspaceId").exists(), + body("environment").exists(), + param("folderId").not().isIn(["root"]).exists(), + validateRequest, + updateFolderById +); + +router.delete( + "/:folderId", + requireAuth({ + acceptedAuthModes: ["jwt"], + }), + body("workspaceId").exists(), + body("environment").exists(), + param("folderId").not().isIn(["root"]).exists(), validateRequest, deleteFolder ); router.get( - '/:folderId', + "/", requireAuth({ - acceptedAuthModes: ['jwt'] + acceptedAuthModes: ["jwt"], }), - param('folderId').exists(), + query("workspaceId").exists().isString().trim(), + query("environment").exists().isString().trim(), + query("parentFolderId").optional().isString().trim(), validateRequest, - getFolderById + getFolders ); - -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/routes/v2/secrets.ts b/backend/src/routes/v2/secrets.ts index 0104505a9..cef804882 100644 --- a/backend/src/routes/v2/secrets.ts +++ b/backend/src/routes/v2/secrets.ts @@ -2,204 +2,233 @@ import express from 'express'; const router = express.Router(); import { Types } from 'mongoose'; import { - requireAuth, - requireWorkspaceAuth, - requireSecretsAuth, - validateRequest + requireAuth, + requireWorkspaceAuth, + requireSecretsAuth, + validateRequest, } from '../../middleware'; import { validateClientForSecrets } from '../../validation'; import { query, body } from 'express-validator'; import { secretsController } from '../../controllers/v2'; import { - ADMIN, - MEMBER, - SECRET_PERSONAL, - SECRET_SHARED, - PERMISSION_READ_SECRETS, - PERMISSION_WRITE_SECRETS, - AUTH_MODE_JWT, - AUTH_MODE_SERVICE_ACCOUNT, - AUTH_MODE_SERVICE_TOKEN, - AUTH_MODE_API_KEY + ADMIN, + MEMBER, + SECRET_PERSONAL, + SECRET_SHARED, + PERMISSION_READ_SECRETS, + PERMISSION_WRITE_SECRETS, + AUTH_MODE_JWT, + AUTH_MODE_SERVICE_ACCOUNT, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_API_KEY, } from '../../variables'; -import { - BatchSecretRequest -} from '../../types/secret'; +import { BatchSecretRequest } from '../../types/secret'; router.post( - '/batch', - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body' - }), - body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim(), - body('requests') - .exists() - .custom(async (requests: BatchSecretRequest[], { req }) => { - if (Array.isArray(requests)) { - const secretIds = requests - .map((request) => request.secret._id) - .filter((secretId) => secretId !== undefined) + '/batch', + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body', + }), + body('workspaceId').exists().isString().trim(), + body('folderId').default('root').isString().trim(), + body('environment').exists().isString().trim(), + body('requests') + .exists() + .custom(async (requests: BatchSecretRequest[], { req }) => { + if (Array.isArray(requests)) { + const secretIds = requests + .map((request) => request.secret._id) + .filter((secretId) => secretId !== undefined); - if (secretIds.length > 0) { - req.secrets = await validateClientForSecrets({ - authData: req.authData, - secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)), - requiredPermissions: [] - }); - } - } - return true; - }), - validateRequest, - secretsController.batchSecrets + if (secretIds.length > 0) { + req.secrets = await validateClientForSecrets({ + authData: req.authData, + secretIds: secretIds.map( + (secretId: string) => new Types.ObjectId(secretId) + ), + requiredPermissions: [], + }); + } + } + return true; + }), + validateRequest, + secretsController.batchSecrets ); router.post( - '/', - body('workspaceId').exists().isString().trim(), - body('environment').exists().isString().trim(), - body('secrets') - .exists() - .custom((value) => { - if (Array.isArray(value)) { - // case: create multiple secrets - if (value.length === 0) throw new Error('secrets cannot be an empty array') - for (const secret of value) { - if ( - !secret.type || - !(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) || - !secret.secretKeyCiphertext || - !secret.secretKeyIV || - !secret.secretKeyTag || - (typeof secret.secretValueCiphertext !== 'string') || - !secret.secretValueIV || - !secret.secretValueTag - ) { - throw new Error('secrets array must contain objects that have required secret properties'); - } - } - } else if (typeof value === 'object') { - // case: update 1 secret - if ( - !value.type || - !(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) || - !value.secretKeyCiphertext || - !value.secretKeyIV || - !value.secretKeyTag || - !value.secretValueCiphertext || - !value.secretValueIV || - !value.secretValueTag - ) { - throw new Error('secrets object is missing required secret properties'); - } - } else { - throw new Error('secrets must be an object or an array of objects') - } + '/', + body('workspaceId').exists().isString().trim(), + body('environment').exists().isString().trim(), + body('folderId').default('root').isString().trim(), + body('secrets') + .exists() + .custom((value) => { + if (Array.isArray(value)) { + // case: create multiple secrets + if (value.length === 0) + throw new Error('secrets cannot be an empty array'); + for (const secret of value) { + if ( + !secret.type || + !( + secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED + ) || + !secret.secretKeyCiphertext || + !secret.secretKeyIV || + !secret.secretKeyTag || + typeof secret.secretValueCiphertext !== 'string' || + !secret.secretValueIV || + !secret.secretValueTag + ) { + throw new Error( + 'secrets array must contain objects that have required secret properties' + ); + } + } + } else if (typeof value === 'object') { + // case: update 1 secret + if ( + !value.type || + !(value.type === SECRET_PERSONAL || value.type === SECRET_SHARED) || + !value.secretKeyCiphertext || + !value.secretKeyIV || + !value.secretKeyTag || + !value.secretValueCiphertext || + !value.secretValueIV || + !value.secretValueTag + ) { + throw new Error( + 'secrets object is missing required secret properties' + ); + } + } else { + throw new Error('secrets must be an object or an array of objects'); + } - return true; - }), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] + return true; }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'body', - locationEnvironment: 'body', - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }), - secretsController.createSecrets + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'body', + locationEnvironment: 'body', + requiredPermissions: [PERMISSION_WRITE_SECRETS], + }), + secretsController.createSecrets ); router.get( - '/', - query('workspaceId').exists().trim(), - query('environment').exists().trim(), - query('tagSlugs'), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_ACCOUNT] - }), - requireWorkspaceAuth({ - acceptedRoles: [ADMIN, MEMBER], - locationWorkspaceId: 'query', - locationEnvironment: 'query', - requiredPermissions: [PERMISSION_READ_SECRETS] - }), - secretsController.getSecrets + '/', + query('workspaceId').exists().trim(), + query('environment').exists().trim(), + query('tagSlugs'), + query('folderId').default('root').isString().trim(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + AUTH_MODE_SERVICE_ACCOUNT, + ], + }), + requireWorkspaceAuth({ + acceptedRoles: [ADMIN, MEMBER], + locationWorkspaceId: 'query', + locationEnvironment: 'query', + requiredPermissions: [PERMISSION_READ_SECRETS], + }), + secretsController.getSecrets ); router.patch( - '/', - body('secrets') - .exists() - .custom((value) => { - if (Array.isArray(value)) { - // case: update multiple secrets - if (value.length === 0) throw new Error('secrets cannot be an empty array') - for (const secret of value) { - if ( - !secret.id - ) { - throw new Error('Each secret must contain a ID property'); - } - } - } else if (typeof value === 'object') { - // case: update 1 secret - if ( - !value.id - ) { - throw new Error('secret must contain a ID property'); - } - } else { - throw new Error('secrets must be an object or an array of objects') - } + '/', + body('secrets') + .exists() + .custom((value) => { + if (Array.isArray(value)) { + // case: update multiple secrets + if (value.length === 0) + throw new Error('secrets cannot be an empty array'); + for (const secret of value) { + if (!secret.id) { + throw new Error('Each secret must contain a ID property'); + } + } + } else if (typeof value === 'object') { + // case: update 1 secret + if (!value.id) { + throw new Error('secret must contain a ID property'); + } + } else { + throw new Error('secrets must be an object or an array of objects'); + } - return true; - }), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] + return true; }), - requireSecretsAuth({ - acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }), - secretsController.updateSecrets + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + ], + }), + requireSecretsAuth({ + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_WRITE_SECRETS], + }), + secretsController.updateSecrets ); router.delete( - '/', - body('secretIds') - .exists() - .custom((value) => { - // case: delete 1 secret - if (typeof value === 'string') return true; + '/', + body('secretIds') + .exists() + .custom((value) => { + // case: delete 1 secret + if (typeof value === 'string') return true; - if (Array.isArray(value)) { - // case: delete multiple secrets - if (value.length === 0) throw new Error('secrets cannot be an empty array'); - return value.every((id: string) => typeof id === 'string') - } + if (Array.isArray(value)) { + // case: delete multiple secrets + if (value.length === 0) + throw new Error('secrets cannot be an empty array'); + return value.every((id: string) => typeof id === 'string'); + } - throw new Error('secretIds must be a string or an array of strings'); - }) - .not() - .isEmpty(), - validateRequest, - requireAuth({ - acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN] - }), - requireSecretsAuth({ - acceptedRoles: [ADMIN, MEMBER], - requiredPermissions: [PERMISSION_WRITE_SECRETS] - }), - secretsController.deleteSecrets + throw new Error('secretIds must be a string or an array of strings'); + }) + .not() + .isEmpty(), + validateRequest, + requireAuth({ + acceptedAuthModes: [ + AUTH_MODE_JWT, + AUTH_MODE_API_KEY, + AUTH_MODE_SERVICE_TOKEN, + ], + }), + requireSecretsAuth({ + acceptedRoles: [ADMIN, MEMBER], + requiredPermissions: [PERMISSION_WRITE_SECRETS], + }), + secretsController.deleteSecrets ); -export default router; \ No newline at end of file +export default router; diff --git a/backend/src/services/FolderService.ts b/backend/src/services/FolderService.ts new file mode 100644 index 000000000..d1869f47e --- /dev/null +++ b/backend/src/services/FolderService.ts @@ -0,0 +1,178 @@ +import { nanoid } from "nanoid"; +import { TFolderSchema } from "../models/folder"; + +type TAppendFolderDTO = { + folderName: string; + parentFolderId?: string; +}; + +type TRenameFolderDTO = { + folderName: string; + folderId: string; +}; + +export const validateFolderName = (folderName: string) => { + const validNameRegex = /^[a-zA-Z0-9-_]+$/; + return validNameRegex.test(folderName); +}; + +export const generateFolderId = (): string => nanoid(12); + +// simple bfs search +export const searchByFolderId = ( + root: TFolderSchema, + folderId: string +): TFolderSchema | undefined => { + const queue = [root]; + while (queue.length) { + const folder = queue.pop() as TFolderSchema; + if (folder.id === folderId) { + return 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); + if (folder) { + throw new Error("Folder already exists"); + } + const id = generateFolderId(); + folders.version += 1; + folders.children.push({ + id, + name: folderName, + children: [], + version: 1, + }); + return { id, name: folderName }; +}; + +// root of append child wrapper +export const appendFolder = ( + folders: TFolderSchema, + { folderName, parentFolderId }: TAppendFolderDTO +) => { + const isRoot = !parentFolderId; + + if (isRoot) { + return appendChild(folders, folderName); + } + const folder = searchByFolderId(folders, parentFolderId); + if (!folder) { + throw new Error("Parent Folder not found"); + } + return appendChild(folder, folderName); +}; + +export const renameFolder = ( + folders: TFolderSchema, + { folderName, folderId }: TRenameFolderDTO +) => { + const folder = searchByFolderId(folders, folderId); + if (!folder) { + throw new Error("Folder doesn't exist"); + } + + folder.name = folderName; +}; + +// bfs but stops on parent folder +// Then unmount the required child and then return both +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); + return { deletedNode: deletedFolder[0], parent: folder }; + } + queue.push(...folder.children); + } +}; + +// bfs but return parent of the folderID +export const getParentFromFolderId = ( + folders: TFolderSchema, + folderId: string +) => { + const queue = [folders]; + while (queue.length) { + const folder = queue.pop() as TFolderSchema; + const index = folder.children.findIndex(({ id }) => folderId === id); + if (index !== -1) return folder; + + queue.push(...folder.children); + } +}; + +// to get all folders ids from everything from below nodes +export const getAllFolderIds = (folders: TFolderSchema) => { + const folderIds: Array<{ id: string; name: string }> = []; + const queue = [folders]; + while (queue.length) { + const folder = queue.pop() as TFolderSchema; + folderIds.push({ id: folder.id, name: folder.name }); + queue.push(...folder.children); + } + return folderIds; +}; + +// To get the path of a folder from the root. Used for breadcrumbs +// LOGIC: We do dfs instead if bfs +// Each time we go down we record the current node +// We then record the number of childs of each root node +// When we reach leaf node or when all childs of a root node are visited +// We remove it from path recorded by using the total child record +export const searchByFolderIdWithDir = ( + folders: TFolderSchema, + folderId: string +) => { + const stack = [folders]; + const dir: Array<{ id: string; name: string }> = []; + const hits: Record = {}; + + while (stack.length) { + const folder = stack.shift() as TFolderSchema; + // score the hit + hits[folder.id] = folder.children.length; + const parent = dir[dir.length - 1]; + if (parent) hits[parent.id] -= 1; + + if (folder.id === folderId) { + dir.push({ name: folder.name, id: folder.id }); + return { folder, dir }; + } + + if (folder.children.length) { + dir.push({ name: folder.name, id: folder.id }); + stack.unshift(...folder.children); + } else { + if (!hits[parent.id]) { + dir.pop(); + } + } + } + return; +}; + +// to get folder of a path given +// Like /frontend/folder#1 +export const getFolderByPath = (folders: TFolderSchema, searchPath: string) => { + const path = searchPath.split("/").filter(Boolean); + const queue = [folders]; + let segment: TFolderSchema | undefined; + while (queue.length && path.length) { + const folder = queue.pop(); + const segmentPath = path.shift(); + segment = folder?.children.find(({ name }) => name === segmentPath); + if (!segment) return; + + queue.push(segment); + } + return segment; +}; diff --git a/backend/src/types/secret/index.d.ts b/backend/src/types/secret/index.d.ts index 7a6ec7a5a..bddd07e64 100644 --- a/backend/src/types/secret/index.d.ts +++ b/backend/src/types/secret/index.d.ts @@ -1,53 +1,46 @@ -import { Types } from 'mongoose'; -import { Assign, Omit } from 'utility-types'; -import { ISecret } from '../../models'; -import { mongo } from 'mongoose'; +import { Types } from "mongoose"; +import { Assign, Omit } from "utility-types"; +import { ISecret } from "../../models"; +import { mongo } from "mongoose"; // Everything is required, except the omitted types -export type CreateSecretRequestBody = Omit; +export type CreateSecretRequestBody = Omit< + ISecret, + "user" | "version" | "environment" | "workspace" +>; -// Omit the listed properties, then make everything optional and then make _id required -export type ModifySecretRequestBody = Assign>, { _id: string }>; +// Omit the listed properties, then make everything optional and then make _id required +export type ModifySecretRequestBody = Assign< + Partial>, + { _id: string } +>; // Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading -export type SanitizedSecretModify = Partial>; +export type SanitizedSecretModify = Partial< + Omit +>; // Everything is required, except the omitted types export type SanitizedSecretForCreate = Omit; export interface BatchSecretRequest { - id: string; - method: 'POST' | 'PATCH' | 'DELETE'; - secret: Secret; + id: string; + method: "POST" | "PATCH" | "DELETE"; + secret: Secret; } export interface BatchSecret { - _id: string; - type: 'shared' | 'personal', - secretBlindIndex: string; - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - tags: string[]; + _id: string; + type: "shared" | "personal"; + secretBlindIndex: string; + secretKeyCiphertext: string; + secretKeyIV: string; + secretKeyTag: string; + secretValueCiphertext: string; + secretValueIV: string; + secretValueTag: string; + secretCommentCiphertext: string; + secretCommentIV: string; + secretCommentTag: string; + tags: string[]; } - -export interface BatchSecret { - _id: string; - type: 'shared' | 'personal', - secretKeyCiphertext: string; - secretKeyIV: string; - secretKeyTag: string; - secretValueCiphertext: string; - secretValueIV: string; - secretValueTag: string; - secretCommentCiphertext: string; - secretCommentIV: string; - secretCommentTag: string; - tags: string[]; -} \ No newline at end of file diff --git a/backend/src/utils/folder.ts b/backend/src/utils/folder.ts index f12845339..45a8b94d6 100644 --- a/backend/src/utils/folder.ts +++ b/backend/src/utils/folder.ts @@ -1,87 +1,87 @@ -import Folder from "../models/folder"; +// import Folder from "../models/folder"; -export const ROOT_FOLDER_PATH = "/" +// export const ROOT_FOLDER_PATH = "/" -export const getFolderPath = async (folderId: string) => { - let currentFolder = await Folder.findById(folderId); - const pathSegments = []; +// export const getFolderPath = async (folderId: string) => { +// let currentFolder = await Folder.findById(folderId); +// const pathSegments = []; - while (currentFolder) { - pathSegments.unshift(currentFolder.name); - currentFolder = currentFolder.parent ? await Folder.findById(currentFolder.parent) : null; - } +// while (currentFolder) { +// pathSegments.unshift(currentFolder.name); +// currentFolder = currentFolder.parent ? await Folder.findById(currentFolder.parent) : null; +// } - return '/' + pathSegments.join('/'); -}; +// return '/' + pathSegments.join('/'); +// }; -/** - Returns the folder ID associated with the specified secret path in the given workspace and environment. - @param workspaceId - The ID of the workspace to search in. - @param environment - The environment to search in. - @param secretPath - The secret path to search for. - @returns The folder ID associated with the specified secret path, or undefined if the path is at the root folder level. - @throws Error if the specified secret path is not found. -*/ -export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => { - const secretPathParts = secretPath.split("/").filter(path => path != "") - if (secretPathParts.length <= 1) { - return undefined // root folder, so no folder id - } +// /** +// Returns the folder ID associated with the specified secret path in the given workspace and environment. +// @param workspaceId - The ID of the workspace to search in. +// @param environment - The environment to search in. +// @param secretPath - The secret path to search for. +// @returns The folder ID associated with the specified secret path, or undefined if the path is at the root folder level. +// @throws Error if the specified secret path is not found. +// */ +// export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => { +// const secretPathParts = secretPath.split("/").filter(path => path != "") +// if (secretPathParts.length <= 1) { +// return undefined // root folder, so no folder id +// } - const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment }) - if (!folderId) { - throw Error("Secret path not found") - } +// const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment }) +// if (!folderId) { +// throw Error("Secret path not found") +// } - return folderId -} +// return folderId +// } -/** - * Cleans up a path by removing empty parts, duplicate slashes, - * and ensuring it starts with ROOT_FOLDER_PATH. - * @param path - The input path to clean up. - * @returns The cleaned-up path string. - */ -export const normalizePath = (path: string) => { - if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) { - return ROOT_FOLDER_PATH - } +// /** +// * Cleans up a path by removing empty parts, duplicate slashes, +// * and ensuring it starts with ROOT_FOLDER_PATH. +// * @param path - The input path to clean up. +// * @returns The cleaned-up path string. +// */ +// export const normalizePath = (path: string) => { +// if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) { +// return ROOT_FOLDER_PATH +// } - const pathParts = path.split("/").filter(part => part != "") - const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/") +// const pathParts = path.split("/").filter(part => part != "") +// const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/") - return cleanPathString -} +// return cleanPathString +// } -export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => { - const normalizedPath = normalizePath(pathString) - const foldersInDirectory = await Folder.find({ - workspace: workspaceId, - environment: environment, - parentPath: normalizedPath, - }); +// export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => { +// const normalizedPath = normalizePath(pathString) +// const foldersInDirectory = await Folder.find({ +// workspace: workspaceId, +// environment: environment, +// parentPath: normalizedPath, +// }); - return foldersInDirectory; -} +// return foldersInDirectory; +// } -/** - * Returns the parent path of the given path. - * @param path - The input path. - * @returns The parent path string. - */ -export const getParentPath = (path: string) => { - const normalizedPath = normalizePath(path); - const folderParts = normalizedPath.split('/').filter(part => part !== ''); +// /** +// * Returns the parent path of the given path. +// * @param path - The input path. +// * @returns The parent path string. +// */ +// export const getParentPath = (path: string) => { +// const normalizedPath = normalizePath(path); +// const folderParts = normalizedPath.split('/').filter(part => part !== ''); - let folderParent = ROOT_FOLDER_PATH; - if (folderParts.length > 1) { - folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/'); - } +// let folderParent = ROOT_FOLDER_PATH; +// if (folderParts.length > 1) { +// folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/'); +// } - return folderParent; -} +// return folderParent; +// } -export const validateFolderName = (folderName: string) => { - const validNameRegex = /^[a-zA-Z0-9-_]+$/; - return validNameRegex.test(folderName); -} \ No newline at end of file +// export const validateFolderName = (folderName: string) => { +// const validNameRegex = /^[a-zA-Z0-9-_]+$/; +// return validNameRegex.test(folderName); +// } diff --git a/backend/src/utils/patchAsyncRoutes.js b/backend/src/utils/patchAsyncRoutes.js new file mode 100644 index 000000000..24fe007f9 --- /dev/null +++ b/backend/src/utils/patchAsyncRoutes.js @@ -0,0 +1,69 @@ +/* +Original work Copyright (c) 2016, Nikolay Nemshilov +Modified work Copyright (c) 2016, David Banham + +Permission to use, copy, modify, and/or distribute this software for any purpose +with or without fee is hereby granted, provided that the above copyright notice +and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND +FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. + +*/ + +/* eslint-disable @typescript-eslint/no-var-requires */ +/* eslint-env node */ +const Layer = require('express/lib/router/layer'); +const Router = require('express/lib/router'); + +const last = (arr = []) => arr[arr.length - 1]; +const noop = Function.prototype; + +function copyFnProps(oldFn, newFn) { + Object.keys(oldFn).forEach((key) => { + newFn[key] = oldFn[key]; + }); + return newFn; +} + +function wrap(fn) { + const newFn = function newFn(...args) { + const ret = fn.apply(this, args); + const next = (args.length === 5 ? args[2] : last(args)) || noop; + if (ret && ret.catch) ret.catch(err => next(err)); + return ret; + }; + Object.defineProperty(newFn, 'length', { + value: fn.length, + writable: false, + }); + return copyFnProps(fn, newFn); +} + +function patchRouterParam() { + const originalParam = Router.prototype.constructor.param; + Router.prototype.constructor.param = function param(name, fn) { + fn = wrap(fn); + return originalParam.call(this, name, fn); + }; +} + +Object.defineProperty(Layer.prototype, 'handle', { + enumerable: true, + get() { + return this.__handle; + }, + set(fn) { + fn = wrap(fn); + this.__handle = fn; + }, +}); + +module.exports = { + patchRouterParam +}; diff --git a/backend/src/utils/setup/backfillData.ts b/backend/src/utils/setup/backfillData.ts index cffb16476..aff5cf7d9 100644 --- a/backend/src/utils/setup/backfillData.ts +++ b/backend/src/utils/setup/backfillData.ts @@ -1,211 +1,209 @@ -import crypto from 'crypto'; -import { encryptSymmetric128BitHexKeyUTF8 } from '../crypto'; -import { EESecretService } from '../../ee/services'; -import { SecretVersion } from '../../ee/models'; +import crypto from "crypto"; +import { Types } from "mongoose"; +import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto"; +import { EESecretService } from "../../ee/services"; +import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models"; import { - Secret, - ISecret, - SecretBlindIndexData, - Workspace, - Bot, - BackupPrivateKey, - IntegrationAuth, -} from '../../models'; + Secret, + ISecret, + SecretBlindIndexData, + Workspace, + Bot, + BackupPrivateKey, + IntegrationAuth, +} from "../../models"; +import { generateKeyPair } from "../../utils/crypto"; +import { client, getEncryptionKey, getRootEncryptionKey } from "../../config"; import { - generateKeyPair -} from '../../utils/crypto'; -import { - client, - getEncryptionKey, - getRootEncryptionKey -} from '../../config'; -import { - ALGORITHM_AES_256_GCM, - ENCODING_SCHEME_UTF8, - ENCODING_SCHEME_BASE64 -} from '../../variables'; -import { InternalServerError } from '../errors'; + ALGORITHM_AES_256_GCM, + ENCODING_SCHEME_UTF8, + ENCODING_SCHEME_BASE64, +} from "../../variables"; +import { InternalServerError } from "../errors"; /** * Backfill secrets to ensure that they're all versioned and have * corresponding secret versions */ export const backfillSecretVersions = async () => { - await Secret.updateMany( - { version: { $exists: false } }, - { $set: { version: 1 } } - ); - - const unversionedSecrets: ISecret[] = await Secret.aggregate([ - { - $lookup: { - from: "secretversions", - localField: "_id", - foreignField: "secret", - as: "versions", - }, - }, - { - $match: { - versions: { $size: 0 }, - }, - }, - ]); + await Secret.updateMany( + { version: { $exists: false } }, + { $set: { version: 1 } } + ); - if (unversionedSecrets.length > 0) { - await EESecretService.addSecretVersions({ - secretVersions: unversionedSecrets.map( - (s, idx) => - new SecretVersion({ - ...s, - secret: s._id, - version: s.version ? s.version : 1, - isDeleted: false, - workspace: s.workspace, - environment: s.environment, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }) - ), - }); - } -} + const unversionedSecrets: ISecret[] = await Secret.aggregate([ + { + $lookup: { + from: "secretversions", + localField: "_id", + foreignField: "secret", + as: "versions", + }, + }, + { + $match: { + versions: { $size: 0 }, + }, + }, + ]); + + if (unversionedSecrets.length > 0) { + await EESecretService.addSecretVersions({ + secretVersions: unversionedSecrets.map( + (s, idx) => + new SecretVersion({ + ...s, + secret: s._id, + version: s.version ? s.version : 1, + isDeleted: false, + workspace: s.workspace, + environment: s.environment, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }) + ), + }); + } +}; /** * Backfill workspace bots to ensure that every workspace has a bot */ export const backfillBots = async () => { - const encryptionKey = await getEncryptionKey(); - const rootEncryptionKey = await getRootEncryptionKey(); + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); - const workspaceIdsWithBot = await Bot.distinct('workspace'); - const workspaceIdsToAddBot = await Workspace.distinct('_id', { - _id: { - $nin: workspaceIdsWithBot - } - }); + const workspaceIdsWithBot = await Bot.distinct("workspace"); + const workspaceIdsToAddBot = await Workspace.distinct("_id", { + _id: { + $nin: workspaceIdsWithBot, + }, + }); - if (workspaceIdsToAddBot.length === 0) return; + if (workspaceIdsToAddBot.length === 0) return; - const botsToInsert = await Promise.all( - workspaceIdsToAddBot.map(async (workspaceToAddBot) => { - const { publicKey, privateKey } = generateKeyPair(); - - if (rootEncryptionKey) { - const { - ciphertext: encryptedPrivateKey, - iv, - tag - } = client.encryptSymmetric(privateKey, rootEncryptionKey); + const botsToInsert = await Promise.all( + workspaceIdsToAddBot.map(async (workspaceToAddBot) => { + const { publicKey, privateKey } = generateKeyPair(); - return new Bot({ - name: 'Infisical Bot', - workspace: workspaceToAddBot, - isActive: false, - publicKey, - encryptedPrivateKey, - iv, - tag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 - }); - } else if (encryptionKey) { - const { - ciphertext: encryptedPrivateKey, - iv, - tag - } = encryptSymmetric128BitHexKeyUTF8({ - plaintext: privateKey, - key: encryptionKey - }); + if (rootEncryptionKey) { + const { + ciphertext: encryptedPrivateKey, + iv, + tag, + } = client.encryptSymmetric(privateKey, rootEncryptionKey); - return new Bot({ - name: 'Infisical Bot', - workspace: workspaceToAddBot, - isActive: false, - publicKey, - encryptedPrivateKey, - iv, - tag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }); - } + return new Bot({ + name: "Infisical Bot", + workspace: workspaceToAddBot, + isActive: false, + publicKey, + encryptedPrivateKey, + iv, + tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64, + }); + } else if (encryptionKey) { + const { + ciphertext: encryptedPrivateKey, + iv, + tag, + } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: privateKey, + key: encryptionKey, + }); - throw InternalServerError({ - message: 'Failed to backfill workspace bots due to missing encryption key' - }); - }) - ); - - await Bot.insertMany(botsToInsert); -} + return new Bot({ + name: "Infisical Bot", + workspace: workspaceToAddBot, + isActive: false, + publicKey, + encryptedPrivateKey, + iv, + tag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }); + } + + throw InternalServerError({ + message: + "Failed to backfill workspace bots due to missing encryption key", + }); + }) + ); + + await Bot.insertMany(botsToInsert); +}; /** * Backfill secret blind index data to ensure that every workspace * has a secret blind index data */ export const backfillSecretBlindIndexData = async () => { + const encryptionKey = await getEncryptionKey(); + const rootEncryptionKey = await getRootEncryptionKey(); - const encryptionKey = await getEncryptionKey(); - const rootEncryptionKey = await getRootEncryptionKey(); - - const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace'); - const workspaceIdsToBlindIndex = await Workspace.distinct('_id', { - _id: { - $nin: workspaceIdsBlindIndexed - } - }); + const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct( + "workspace" + ); + const workspaceIdsToBlindIndex = await Workspace.distinct("_id", { + _id: { + $nin: workspaceIdsBlindIndexed, + }, + }); - if (workspaceIdsToBlindIndex.length === 0) return; - - const secretBlindIndexDataToInsert = await Promise.all( - workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => { - const salt = crypto.randomBytes(16).toString('base64'); - - if (rootEncryptionKey) { - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = client.encryptSymmetric(salt, rootEncryptionKey) + if (workspaceIdsToBlindIndex.length === 0) return; - return new SecretBlindIndexData({ - workspace: workspaceToBlindIndex, - encryptedSaltCiphertext, - saltIV, - saltTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_BASE64 - }); - } else if (encryptionKey) { - const { - ciphertext: encryptedSaltCiphertext, - iv: saltIV, - tag: saltTag - } = encryptSymmetric128BitHexKeyUTF8({ - plaintext: salt, - key: encryptionKey - }); + const secretBlindIndexDataToInsert = await Promise.all( + workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => { + const salt = crypto.randomBytes(16).toString("base64"); - return new SecretBlindIndexData({ - workspace: workspaceToBlindIndex, - encryptedSaltCiphertext, - saltIV, - saltTag, - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - }); - } - - throw InternalServerError({ - message: 'Failed to backfill secret blind index data due to missing encryption key' - }); - }) - ); - - SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert); -} + if (rootEncryptionKey) { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag, + } = client.encryptSymmetric(salt, rootEncryptionKey); + + return new SecretBlindIndexData({ + workspace: workspaceToBlindIndex, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_BASE64, + }); + } else if (encryptionKey) { + const { + ciphertext: encryptedSaltCiphertext, + iv: saltIV, + tag: saltTag, + } = encryptSymmetric128BitHexKeyUTF8({ + plaintext: salt, + key: encryptionKey, + }); + + return new SecretBlindIndexData({ + workspace: workspaceToBlindIndex, + encryptedSaltCiphertext, + saltIV, + saltTag, + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }); + } + + throw InternalServerError({ + message: + "Failed to backfill secret blind index data due to missing encryption key", + }); + }) + ); + + SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert); +}; /** * Backfill Secret, SecretVersion, SecretBlindIndexData, Bot, @@ -213,112 +211,163 @@ export const backfillSecretBlindIndexData = async () => { * they all have encryption metadata documented */ export const backfillEncryptionMetadata = async () => { + // backfill secret encryption metadata + await Secret.updateMany( + { + algorithm: { + $exists: false, + }, + keyEncoding: { + $exists: false, + }, + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + } + ); - // backfill secret encryption metadata - await Secret.updateMany( - { - algorithm: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); + // backfill secret version encryption metadata + await SecretVersion.updateMany( + { + algorithm: { + $exists: false, + }, + keyEncoding: { + $exists: false, + }, + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + } + ); - // backfill secret version encryption metadata - await SecretVersion.updateMany( - { - algorithm: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); + // backfill secret blind index encryption metadata + await SecretBlindIndexData.updateMany( + { + algorithm: { + $exists: false, + }, + keyEncoding: { + $exists: false, + }, + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + } + ); - // backfill secret blind index encryption metadata - await SecretBlindIndexData.updateMany( - { - algorithm: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); + // backfill bot encryption metadata + await Bot.updateMany( + { + algorithm: { + $exists: false, + }, + keyEncoding: { + $exists: false, + }, + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + } + ); - // backfill bot encryption metadata - await Bot.updateMany( - { - algorithm: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); + // backfill backup private key encryption metadata + await BackupPrivateKey.updateMany( + { + algorithm: { + $exists: false, + }, + keyEncoding: { + $exists: false, + }, + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + } + ); - // backfill backup private key encryption metadata - await BackupPrivateKey.updateMany( - { - algorithm: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); + // backfill integration auth encryption metadata + await IntegrationAuth.updateMany( + { + algorithm: { + $exists: false, + }, + keyEncoding: { + $exists: false, + }, + }, + { + $set: { + algorithm: ALGORITHM_AES_256_GCM, + keyEncoding: ENCODING_SCHEME_UTF8, + }, + } + ); +}; - // backfill integration auth encryption metadata - await IntegrationAuth.updateMany( - { - algorithm: { - $exists: false - }, - keyEncoding: { - $exists: false - } - }, - { - $set: { - algorithm: ALGORITHM_AES_256_GCM, - keyEncoding: ENCODING_SCHEME_UTF8 - } - } - ); -} \ No newline at end of file +export const backfillSecretFolders = async () => { + await Secret.updateMany( + { + folder: { + $exists: false, + }, + }, + { + $set: { + folder: "root", + }, + } + ); + + let secretSnapshots = await SecretSnapshot.find({ + environment: { + $exists: false, + }, + }) + .populate<{ secretVersions: ISecretVersion[] }>("secretVersions") + .limit(50); + + while (secretSnapshots.length > 0) { + for (const secSnapshot of secretSnapshots) { + const groupSnapByEnv: Record> = {}; + secSnapshot.secretVersions.forEach((secVer) => { + if (!groupSnapByEnv?.[secVer.environment]) + groupSnapByEnv[secVer.environment] = []; + groupSnapByEnv[secVer.environment].push(secVer); + }); + + const newSnapshots = Object.keys(groupSnapByEnv).map((snapEnv) => ({ + ...secSnapshot.toObject({ virtuals: false }), + _id: new Types.ObjectId(), + environment: snapEnv, + secretVersions: groupSnapByEnv[snapEnv], + })); + + await SecretSnapshot.insertMany(newSnapshots); + await secSnapshot.delete(); + } + + secretSnapshots = await SecretSnapshot.find({ + environment: { + $exists: false, + }, + }) + .populate<{ secretVersions: ISecretVersion[] }>("secretVersions") + .limit(50); + } +}; diff --git a/backend/src/utils/setup/index.ts b/backend/src/utils/setup/index.ts index ba6c4b618..0ef14bfe6 100644 --- a/backend/src/utils/setup/index.ts +++ b/backend/src/utils/setup/index.ts @@ -3,26 +3,27 @@ import { DatabaseService, TelemetryService } from '../../services'; import { setTransporter } from '../../helpers/nodemailer'; import { EELicenseService } from '../../ee/services'; import { initSmtp } from '../../services/smtp'; -import { createTestUserForDevelopment } from '../addDevelopmentUser' +import { createTestUserForDevelopment } from '../addDevelopmentUser'; +// eslint-disable-next-line @typescript-eslint/no-var-requires +const { patchRouterParam } = require('../patchAsyncRoutes'); +import { validateEncryptionKeysConfig } from './validateConfig'; import { - validateEncryptionKeysConfig -} from './validateConfig'; -import { - backfillSecretVersions, - backfillBots, - backfillSecretBlindIndexData, - backfillEncryptionMetadata + backfillSecretVersions, + backfillBots, + backfillSecretBlindIndexData, + backfillEncryptionMetadata, + backfillSecretFolders, } from './backfillData'; import { - reencryptBotPrivateKeys, - reencryptSecretBlindIndexDataSalts + reencryptBotPrivateKeys, + reencryptSecretBlindIndexDataSalts, } from './reencryptData'; import { - getNodeEnv, - getMongoURL, - getSentryDSN, - getClientSecretGoogle, - getClientIdGoogle + getNodeEnv, + getMongoURL, + getSentryDSN, + getClientSecretGoogle, + getClientIdGoogle, } from '../../config'; import { initializePassport } from '../auth'; @@ -37,49 +38,58 @@ import { initializePassport } from '../auth'; * - Re-encrypting data */ export const setup = async () => { - await validateEncryptionKeysConfig(); - await TelemetryService.logTelemetryMessage(); + patchRouterParam(); + await validateEncryptionKeysConfig(); + await TelemetryService.logTelemetryMessage(); - // initializing SMTP configuration - setTransporter(await initSmtp()); + // initializing SMTP configuration + setTransporter(await initSmtp()); - // initializing global feature set - await EELicenseService.initGlobalFeatureSet(); + // initializing global feature set + await EELicenseService.initGlobalFeatureSet(); - // initializing the database connection - await DatabaseService.initDatabase(await getMongoURL()); + // initializing the database connection + await DatabaseService.initDatabase(await getMongoURL()); - const googleClientSecret: string = await getClientSecretGoogle(); - const googleClientId: string = await getClientIdGoogle(); + const googleClientSecret: string = await getClientSecretGoogle(); + const googleClientId: string = await getClientIdGoogle(); - if (googleClientId && googleClientSecret) { - await initializePassport(); - } + if (googleClientId && googleClientSecret) { + await initializePassport(); + } - /** - * NOTE: the order in this setup function is critical. - * It is important to backfill data before performing any re-encryption functionality. - */ + // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY + // to base64 256-bit ROOT_ENCRYPTION_KEY + // await reencryptBotPrivateKeys(); + // await reencryptSecretBlindIndexDataSalts(); - // backfilling data to catch up with new collections and updated fields - await backfillSecretVersions(); - await backfillBots(); - await backfillSecretBlindIndexData(); - await backfillEncryptionMetadata(); + // initializing the database connection + await DatabaseService.initDatabase(await getMongoURL()); - // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY - // to base64 256-bit ROOT_ENCRYPTION_KEY - // await reencryptBotPrivateKeys(); - // await reencryptSecretBlindIndexDataSalts(); + /** + * NOTE: the order in this setup function is critical. + * It is important to backfill data before performing any re-encryption functionality. + */ - // initializing Sentry - Sentry.init({ - dsn: await getSentryDSN(), - tracesSampleRate: 1.0, - debug: (await getNodeEnv()) === 'production' ? false : true, - environment: (await getNodeEnv()) - }); + // backfilling data to catch up with new collections and updated fields + await backfillSecretVersions(); + await backfillBots(); + await backfillSecretBlindIndexData(); + await backfillEncryptionMetadata(); + await backfillSecretFolders(); - await createTestUserForDevelopment(); -} + // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY + // to base64 256-bit ROOT_ENCRYPTION_KEY + await reencryptBotPrivateKeys(); + await reencryptSecretBlindIndexDataSalts(); + // initializing Sentry + Sentry.init({ + dsn: await getSentryDSN(), + tracesSampleRate: 1.0, + debug: (await getNodeEnv()) === 'production' ? false : true, + environment: await getNodeEnv(), + }); + + await createTestUserForDevelopment(); +};