mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-10 06:28:50 +00:00
Generated
+23
-15
@@ -29,7 +29,6 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"express-async-errors": "^3.1.1",
|
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
@@ -42,6 +41,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mongoose": "^6.10.5",
|
"mongoose": "^6.10.5",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
|
"nanoid": "^3.3.6",
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.8.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
@@ -5190,14 +5190,6 @@
|
|||||||
"node": ">= 0.10.0"
|
"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": {
|
"node_modules/express-rate-limit": {
|
||||||
"version": "6.7.0",
|
"version": "6.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
|
||||||
@@ -7432,6 +7424,23 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"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": {
|
"node_modules/natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
@@ -16406,12 +16415,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": {
|
"express-rate-limit": {
|
||||||
"version": "6.7.0",
|
"version": "6.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.7.0.tgz",
|
||||||
@@ -18085,6 +18088,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"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": {
|
"natural-compare": {
|
||||||
"version": "1.4.0",
|
"version": "1.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||||
|
|||||||
@@ -20,7 +20,6 @@
|
|||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"dotenv": "^16.0.1",
|
"dotenv": "^16.0.1",
|
||||||
"express": "^4.18.1",
|
"express": "^4.18.1",
|
||||||
"express-async-errors": "^3.1.1",
|
|
||||||
"express-rate-limit": "^6.7.0",
|
"express-rate-limit": "^6.7.0",
|
||||||
"express-validator": "^6.14.2",
|
"express-validator": "^6.14.2",
|
||||||
"handlebars": "^4.7.7",
|
"handlebars": "^4.7.7",
|
||||||
@@ -33,6 +32,7 @@
|
|||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"mongoose": "^6.10.5",
|
"mongoose": "^6.10.5",
|
||||||
"node-cache": "^5.1.2",
|
"node-cache": "^5.1.2",
|
||||||
|
"nanoid": "^3.3.6",
|
||||||
"nodemailer": "^6.8.0",
|
"nodemailer": "^6.8.0",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
"passport-google-oauth20": "^2.0.0",
|
"passport-google-oauth20": "^2.0.0",
|
||||||
|
|||||||
@@ -1,107 +1,218 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import { Secret } from '../../models';
|
import { Secret } from "../../models";
|
||||||
import Folder from '../../models/folder';
|
import Folder from "../../models/folder";
|
||||||
import { BadRequestError } from '../../utils/errors';
|
import { BadRequestError } from "../../utils/errors";
|
||||||
import { ROOT_FOLDER_PATH, getFolderPath, getParentPath, normalizePath, validateFolderName } from '../../utils/folder';
|
import {
|
||||||
import { ADMIN, MEMBER } from '../../variables';
|
appendFolder,
|
||||||
import { validateMembership } from '../../helpers/membership';
|
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
|
// TODO
|
||||||
// verify workspace id/environment
|
// verify workspace id/environment
|
||||||
export const createFolder = async (req: Request, res: Response) => {
|
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)) {
|
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 folders = await Folder.findOne({
|
||||||
const parentFolder = await Folder.find({ environment: environment, workspace: workspaceId, id: parentFolderId });
|
workspace: workspaceId,
|
||||||
|
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();
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
});
|
||||||
|
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,
|
||||||
|
nodes: parentFolder,
|
||||||
|
});
|
||||||
|
await folderVersion.save();
|
||||||
|
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: parentFolderId,
|
||||||
|
});
|
||||||
|
|
||||||
|
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],
|
||||||
|
});
|
||||||
|
|
||||||
|
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
|
||||||
if (!parentFolder) {
|
if (!parentFolder) {
|
||||||
throw BadRequestError({ message: "The parent folder doesn't exist" })
|
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" });
|
||||||
}
|
}
|
||||||
|
|
||||||
let completePath = await getFolderPath(parentFolderId)
|
parentFolder.version += 1;
|
||||||
if (completePath == ROOT_FOLDER_PATH) {
|
folder.name = name;
|
||||||
completePath = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentFolderPath = completePath + "/" + folderName // construct new path with current folder to be created
|
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||||
const normalizedCurrentPath = normalizePath(currentFolderPath)
|
const folderVersion = new FolderVersion({
|
||||||
const normalizedParentPath = getParentPath(normalizedCurrentPath)
|
|
||||||
|
|
||||||
const existingFolder = await Folder.findOne({
|
|
||||||
name: folderName,
|
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
environment: environment,
|
environment,
|
||||||
parent: parentFolderId,
|
nodes: parentFolder,
|
||||||
path: normalizedCurrentPath
|
});
|
||||||
|
await folderVersion.save();
|
||||||
|
|
||||||
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: parentFolder.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingFolder) {
|
return res.json({
|
||||||
return res.json(existingFolder)
|
message: "Successfully updated folder",
|
||||||
}
|
folder: { name: folder.name, id: folder.id },
|
||||||
|
|
||||||
const newFolder = new Folder({
|
|
||||||
name: folderName,
|
|
||||||
workspace: workspaceId,
|
|
||||||
environment: environment,
|
|
||||||
parent: parentFolderId,
|
|
||||||
path: normalizedCurrentPath,
|
|
||||||
parentPath: normalizedParentPath
|
|
||||||
});
|
});
|
||||||
|
};
|
||||||
await newFolder.save();
|
|
||||||
|
|
||||||
return res.json(newFolder)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteFolder = async (req: Request, res: Response) => {
|
export const deleteFolder = async (req: Request, res: Response) => {
|
||||||
const { folderId } = req.params
|
const { folderId } = req.params;
|
||||||
const queue: any[] = [folderId];
|
const { workspaceId, environment } = req.body;
|
||||||
|
|
||||||
const folder = await Folder.findById(folderId);
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
if (!folder) {
|
if (!folders) {
|
||||||
throw BadRequestError({ message: "The folder doesn't exist" })
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that user is a member of the workspace
|
// check that user is a member of the workspace
|
||||||
await validateMembership({
|
await validateMembership({
|
||||||
userId: req.user._id.toString(),
|
userId: req.user._id.toString(),
|
||||||
workspaceId: folder.workspace as any,
|
workspaceId,
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
});
|
});
|
||||||
|
|
||||||
while (queue.length > 0) {
|
const delOp = deleteFolderById(folders.nodes, folderId);
|
||||||
const currentFolderId = queue.shift();
|
if (!delOp) {
|
||||||
|
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||||
|
}
|
||||||
|
const { deletedNode: delFolder, parent: parentFolder } = delOp;
|
||||||
|
|
||||||
const childFolders = await Folder.find({ parent: currentFolderId });
|
parentFolder.version += 1;
|
||||||
for (const childFolder of childFolders) {
|
const delFolderIds = getAllFolderIds(delFolder);
|
||||||
queue.push(childFolder._id);
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await Secret.deleteMany({ folder: currentFolderId });
|
await EESecretService.takeSecretSnapshot({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: parentFolder.id,
|
||||||
|
});
|
||||||
|
|
||||||
await Folder.deleteOne({ _id: currentFolderId });
|
res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||||
}
|
};
|
||||||
|
|
||||||
res.send()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: validate workspace
|
// TODO: validate workspace
|
||||||
export const getFolderById = async (req: Request, res: Response) => {
|
export const getFolders = async (req: Request, res: Response) => {
|
||||||
const { folderId } = req.params
|
const { workspaceId, environment, parentFolderId } = req.query as {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
parentFolderId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
const folder = await Folder.findById(folderId);
|
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||||
if (!folder) {
|
if (!folders) {
|
||||||
throw BadRequestError({ message: "The folder doesn't exist" })
|
res.send({ folders: [], dir: [] });
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// check that user is a member of the workspace
|
// check that user is a member of the workspace
|
||||||
await validateMembership({
|
await validateMembership({
|
||||||
userId: req.user._id.toString(),
|
userId: req.user._id.toString(),
|
||||||
workspaceId: folder.workspace as any,
|
workspaceId,
|
||||||
acceptedRoles: [ADMIN, MEMBER]
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
});
|
});
|
||||||
|
|
||||||
res.send({ folder })
|
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,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from "@sentry/node";
|
||||||
import { Secret } from '../../../models';
|
import { Secret } from "../../../models";
|
||||||
import { SecretVersion } from '../../models';
|
import { SecretVersion } from "../../models";
|
||||||
import { EESecretService } from '../../services';
|
import { EESecretService } from "../../services";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret versions for secret with id [secretId]
|
* Return secret versions for secret with id [secretId]
|
||||||
@@ -57,30 +57,32 @@ import { EESecretService } from '../../services';
|
|||||||
*/
|
*/
|
||||||
let secretVersions;
|
let secretVersions;
|
||||||
try {
|
try {
|
||||||
const { secretId } = req.params;
|
const { secretId, workspaceId, environment, folderId } = req.params;
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
secretVersions = await SecretVersion.find({
|
secretVersions = await SecretVersion.find({
|
||||||
secret: secretId
|
secret: secretId,
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
folder: folderId,
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get secret versions'
|
message: "Failed to get secret versions",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secretVersions
|
secretVersions,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Roll back secret with id [secretId] to version [version]
|
* Roll back secret with id [secretId] to version [version]
|
||||||
@@ -145,10 +147,10 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
// validate secret version
|
// validate secret version
|
||||||
const oldSecretVersion = await SecretVersion.findOne({
|
const oldSecretVersion = await SecretVersion.findOne({
|
||||||
secret: secretId,
|
secret: secretId,
|
||||||
version
|
version,
|
||||||
}).select('+secretBlindIndex')
|
}).select("+secretBlindIndex");
|
||||||
|
|
||||||
if (!oldSecretVersion) throw new Error('Failed to find secret version');
|
if (!oldSecretVersion) throw new Error("Failed to find secret version");
|
||||||
|
|
||||||
const {
|
const {
|
||||||
workspace,
|
workspace,
|
||||||
@@ -162,8 +164,9 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
|
folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding
|
keyEncoding,
|
||||||
} = oldSecretVersion;
|
} = oldSecretVersion;
|
||||||
|
|
||||||
// update secret
|
// update secret
|
||||||
@@ -171,7 +174,7 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretId,
|
secretId,
|
||||||
{
|
{
|
||||||
$inc: {
|
$inc: {
|
||||||
version: 1
|
version: 1,
|
||||||
},
|
},
|
||||||
workspace,
|
workspace,
|
||||||
type,
|
type,
|
||||||
@@ -184,15 +187,16 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
|
folderId: folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding
|
keyEncoding,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!secret) throw new Error('Failed to find and update secret');
|
if (!secret) throw new Error("Failed to find and update secret");
|
||||||
|
|
||||||
// add new secret version
|
// add new secret version
|
||||||
await new SecretVersion({
|
await new SecretVersion({
|
||||||
@@ -210,24 +214,26 @@ export const rollbackSecretVersion = async (req: Request, res: Response) => {
|
|||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
|
folder,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding
|
keyEncoding,
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
// take secret snapshot
|
// take secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: secret.workspace
|
workspaceId: secret.workspace,
|
||||||
|
environment,
|
||||||
|
folderId: folder,
|
||||||
});
|
});
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to roll back secret version'
|
message: "Failed to roll back secret version",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secret
|
secret,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from "@sentry/node";
|
||||||
import { SecretSnapshot } from '../../models';
|
import {
|
||||||
|
ISecretVersion,
|
||||||
|
SecretSnapshot,
|
||||||
|
TFolderRootVersionSchema,
|
||||||
|
} from "../../models";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret snapshot with id [secretSnapshotId]
|
* Return secret snapshot with id [secretSnapshotId]
|
||||||
@@ -13,21 +17,32 @@ export const getSecretSnapshot = async (req: Request, res: Response) => {
|
|||||||
try {
|
try {
|
||||||
const { secretSnapshotId } = req.params;
|
const { secretSnapshotId } = req.params;
|
||||||
|
|
||||||
secretSnapshot = await SecretSnapshot
|
secretSnapshot = await SecretSnapshot.findById(secretSnapshotId)
|
||||||
.findById(secretSnapshotId)
|
.lean()
|
||||||
.populate('secretVersions');
|
.populate<{ secretVersions: ISecretVersion[] }>("secretVersions")
|
||||||
|
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
|
||||||
|
|
||||||
|
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get secret snapshot'
|
message: "Failed to get secret snapshot",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
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({
|
return res.status(200).send({
|
||||||
secretSnapshot
|
secretSnapshot,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from "express";
|
||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from "@sentry/node";
|
||||||
import { Types } from 'mongoose';
|
import { PipelineStage, Types } from "mongoose";
|
||||||
import {
|
import { Secret } from "../../../models";
|
||||||
Secret
|
|
||||||
} from '../../../models';
|
|
||||||
import {
|
import {
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
Log,
|
Log,
|
||||||
SecretVersion,
|
SecretVersion,
|
||||||
ISecretVersion
|
ISecretVersion,
|
||||||
} from '../../models';
|
FolderVersion,
|
||||||
import { EESecretService } from '../../services';
|
TFolderRootVersionSchema,
|
||||||
import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
} from "../../models";
|
||||||
|
import { EESecretService } from "../../services";
|
||||||
|
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
|
||||||
|
import Folder, { TFolderSchema } from "../../../models/folder";
|
||||||
|
import { searchByFolderId } from "../../../services/FolderService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return secret snapshots for workspace with id [workspaceId]
|
* Return secret snapshots for workspace with id [workspaceId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @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.summary = 'Return project secret snapshot ids'
|
||||||
#swagger.description = 'Return project secret snapshots ids'
|
#swagger.description = 'Return project secret snapshots ids'
|
||||||
@@ -67,54 +72,63 @@ import { getLatestSecretVersionIds } from '../../helpers/secretVersion';
|
|||||||
let secretSnapshots;
|
let secretSnapshots;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
const { environment, folderId } = req.query;
|
||||||
|
|
||||||
const offset: number = parseInt(req.query.offset as string);
|
const offset: number = parseInt(req.query.offset as string);
|
||||||
const limit: number = parseInt(req.query.limit as string);
|
const limit: number = parseInt(req.query.limit as string);
|
||||||
|
|
||||||
secretSnapshots = await SecretSnapshot.find({
|
secretSnapshots = await SecretSnapshot.find({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: folderId || "root",
|
||||||
})
|
})
|
||||||
.sort({ createdAt: -1 })
|
.sort({ createdAt: -1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit);
|
.limit(limit);
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get secret snapshots'
|
message: "Failed to get secret snapshots",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secretSnapshots
|
secretSnapshots,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return count of secret snapshots for workspace with id [workspaceId]
|
* Return count of secret snapshots for workspace with id [workspaceId]
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
*/
|
*/
|
||||||
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
|
export const getWorkspaceSecretSnapshotsCount = async (
|
||||||
|
req: Request,
|
||||||
|
res: Response
|
||||||
|
) => {
|
||||||
let count;
|
let count;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
const { environment, folderId } = req.query;
|
||||||
|
|
||||||
count = await SecretSnapshot.countDocuments({
|
count = await SecretSnapshot.countDocuments({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: folderId || "root",
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to count number of secret snapshots'
|
message: "Failed to count number of secret snapshots",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
count
|
count,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
|
* Rollback secret snapshot with id [secretSnapshotId] to version [version]
|
||||||
@@ -122,7 +136,10 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
|
|||||||
* @param res
|
* @param res
|
||||||
* @returns
|
* @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.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.'
|
#swagger.description = 'Roll back project secrets to those captured in a secret snapshot version.'
|
||||||
@@ -177,51 +194,192 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
let secrets;
|
let secrets;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
const { version } = req.body;
|
const { version, environment, folderId = "root" } = req.body;
|
||||||
|
|
||||||
// validate secret snapshot
|
// validate secret snapshot
|
||||||
const secretSnapshot = await SecretSnapshot.findOne({
|
const secretSnapshot = await SecretSnapshot.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
version
|
version,
|
||||||
}).populate<{ secretVersions: ISecretVersion[]}>({
|
environment,
|
||||||
path: 'secretVersions',
|
folderId: folderId,
|
||||||
select: '+secretBlindIndex'
|
})
|
||||||
|
.populate<{ secretVersions: ISecretVersion[] }>({
|
||||||
|
path: "secretVersions",
|
||||||
|
select: "+secretBlindIndex",
|
||||||
|
})
|
||||||
|
.populate<{ folderVersion: TFolderRootVersionSchema }>("folderVersion");
|
||||||
|
|
||||||
|
if (!secretSnapshot) throw new Error("Failed to find secret snapshot");
|
||||||
|
|
||||||
|
const snapshotFolderTree = secretSnapshot.folderVersion;
|
||||||
|
const latestFolderTree = await Folder.findOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!secretSnapshot) throw new Error('Failed to find secret snapshot');
|
const latestFolderVersion = await FolderVersion.findOne({
|
||||||
|
environment,
|
||||||
|
workspace: workspaceId,
|
||||||
|
"nodes.id": folderId,
|
||||||
|
}).sort({ "nodes.version": -1 });
|
||||||
|
|
||||||
// TODO: fix any
|
const oldSecretVersionsObj: Record<string, ISecretVersion> = {};
|
||||||
const oldSecretVersionsObj: any = secretSnapshot.secretVersions
|
const secretIds: Types.ObjectId[] = [];
|
||||||
.reduce((accumulator, s) => ({
|
const folderIds: string[] = [folderId];
|
||||||
...accumulator,
|
|
||||||
[`${s.secret.toString()}`]: s
|
|
||||||
}), {});
|
|
||||||
|
|
||||||
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
secretSnapshot.secretVersions.forEach((snapSecVer) => {
|
||||||
secretIds: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
||||||
|
secretIds.push(snapSecVer.secret);
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: fix any
|
// the parent node from current latest one
|
||||||
const latestSecretVersions: any = (await SecretVersion.find({
|
// this will be modified according to the snapshot and latest snapshots
|
||||||
_id: {
|
const newFolderTree =
|
||||||
$in: latestSecretVersionIds.map((s) => s.versionId)
|
latestFolderTree && searchByFolderId(latestFolderTree.nodes, folderId);
|
||||||
|
|
||||||
|
if (newFolderTree) {
|
||||||
|
newFolderTree.children = snapshotFolderTree?.nodes?.children || [];
|
||||||
|
const queue = [newFolderTree];
|
||||||
|
// a bfs algorithm in which we take the latest snapshots of all the folders in a level
|
||||||
|
while (queue.length) {
|
||||||
|
const groupByFolderId: Record<string, TFolderSchema> = {};
|
||||||
|
// the original queue is popped out completely to get what ever in a level
|
||||||
|
// subqueue is filled with all the children thus next level folders
|
||||||
|
// subQueue will then be transfered to the oriinal queue
|
||||||
|
const subQueue: TFolderSchema[] = [];
|
||||||
|
// get everything inside a level
|
||||||
|
while (queue.length) {
|
||||||
|
const folder = queue.pop() as TFolderSchema;
|
||||||
|
folder.children.forEach((el) => {
|
||||||
|
folderIds.push(el.id); // push ids and data into queu
|
||||||
|
subQueue.push(el);
|
||||||
|
// to modify the original tree very fast we keep a reference object
|
||||||
|
// key with folder id and pointing to the various nodes
|
||||||
|
groupByFolderId[el.id] = el;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, 'secret version'))
|
// get latest snapshots of all the folder
|
||||||
.reduce((accumulator, s) => ({
|
const matchWsFoldersPipeline = {
|
||||||
|
$match: {
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
folderId: {
|
||||||
|
$in: Object.keys(groupByFolderId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const sortByFolderIdAndVersion: PipelineStage = {
|
||||||
|
$sort: { folderId: 1, version: -1 },
|
||||||
|
};
|
||||||
|
const pickLatestVersionOfEachFolder = {
|
||||||
|
$group: {
|
||||||
|
_id: "$folderId",
|
||||||
|
latestVersion: { $first: "$version" },
|
||||||
|
doc: {
|
||||||
|
$first: "$$ROOT",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const populateSecVersion = {
|
||||||
|
$lookup: {
|
||||||
|
from: SecretVersion.collection.name,
|
||||||
|
localField: "doc.secretVersions",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "doc.secretVersions",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const populateFolderVersion = {
|
||||||
|
$lookup: {
|
||||||
|
from: FolderVersion.collection.name,
|
||||||
|
localField: "doc.folderVersion",
|
||||||
|
foreignField: "_id",
|
||||||
|
as: "doc.folderVersion",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const unwindFolderVerField = {
|
||||||
|
$unwind: {
|
||||||
|
path: "$doc.folderVersion",
|
||||||
|
preserveNullAndEmptyArrays: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const latestSnapshotsByFolders: Array<{ doc: typeof secretSnapshot }> =
|
||||||
|
await SecretSnapshot.aggregate([
|
||||||
|
matchWsFoldersPipeline,
|
||||||
|
sortByFolderIdAndVersion,
|
||||||
|
pickLatestVersionOfEachFolder,
|
||||||
|
populateSecVersion,
|
||||||
|
populateFolderVersion,
|
||||||
|
unwindFolderVerField,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// recursive snapshotting each level
|
||||||
|
latestSnapshotsByFolders.forEach((snap) => {
|
||||||
|
// mutate the folder tree to update the nodes to the latest version tree
|
||||||
|
// we are reconstructing the folder tree by latest snapshots here
|
||||||
|
if (groupByFolderId[snap.doc.folderId]) {
|
||||||
|
groupByFolderId[snap.doc.folderId].children =
|
||||||
|
snap.doc?.folderVersion?.nodes?.children || [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// push all children of next level snapshots
|
||||||
|
if (snap.doc.folderVersion?.nodes?.children) {
|
||||||
|
queue.push(...snap.doc.folderVersion.nodes.children);
|
||||||
|
}
|
||||||
|
|
||||||
|
snap.doc.secretVersions.forEach((snapSecVer) => {
|
||||||
|
// record all the secrets
|
||||||
|
oldSecretVersionsObj[snapSecVer.secret.toString()] = snapSecVer;
|
||||||
|
secretIds.push(snapSecVer.secret);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
queue.push(...subQueue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: fix any
|
||||||
|
const latestSecretVersionIds = await getLatestSecretVersionIds({
|
||||||
|
secretIds,
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: fix any
|
||||||
|
const latestSecretVersions: any = (
|
||||||
|
await SecretVersion.find(
|
||||||
|
{
|
||||||
|
_id: {
|
||||||
|
$in: latestSecretVersionIds.map((s) => s.versionId),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"secret version"
|
||||||
|
)
|
||||||
|
).reduce(
|
||||||
|
(accumulator, s) => ({
|
||||||
...accumulator,
|
...accumulator,
|
||||||
[`${s.secret.toString()}`]: s
|
[`${s.secret.toString()}`]: s,
|
||||||
}), {});
|
}),
|
||||||
|
{}
|
||||||
|
);
|
||||||
|
|
||||||
|
const secDelQuery: Record<string, unknown> = {
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
// undefined means root thus collect all secrets
|
||||||
|
};
|
||||||
|
if (folderId !== "root" && folderIds.length)
|
||||||
|
secDelQuery.folder = { $in: folderIds };
|
||||||
|
|
||||||
// delete existing secrets
|
// delete existing secrets
|
||||||
await Secret.deleteMany({
|
await Secret.deleteMany(secDelQuery);
|
||||||
workspace: workspaceId
|
await Folder.deleteOne({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
});
|
});
|
||||||
|
|
||||||
// add secrets
|
// add secrets
|
||||||
secrets = await Secret.insertMany(
|
secrets = await Secret.insertMany(
|
||||||
secretSnapshot.secretVersions.map((sv) => {
|
Object.keys(oldSecretVersionsObj).map((sv) => {
|
||||||
const secretId = sv.secret;
|
|
||||||
const {
|
const {
|
||||||
|
secret: secretId,
|
||||||
workspace,
|
workspace,
|
||||||
type,
|
type,
|
||||||
user,
|
user,
|
||||||
@@ -230,15 +388,16 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
secretKeyCiphertext,
|
secretKeyCiphertext,
|
||||||
secretKeyIV,
|
secretKeyIV,
|
||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretKeyHash,
|
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash,
|
createdAt,
|
||||||
createdAt
|
algorithm,
|
||||||
} = oldSecretVersionsObj[secretId.toString()];
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
} = oldSecretVersionsObj[sv];
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
_id: secretId,
|
_id: secretId,
|
||||||
version: latestSecretVersions[secretId.toString()].version + 1,
|
version: latestSecretVersions[secretId.toString()].version + 1,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -249,22 +408,24 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
secretKeyCiphertext,
|
secretKeyCiphertext,
|
||||||
secretKeyIV,
|
secretKeyIV,
|
||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretKeyHash,
|
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash,
|
secretCommentCiphertext: "",
|
||||||
secretCommentCiphertext: '',
|
secretCommentIV: "",
|
||||||
secretCommentIV: '',
|
secretCommentTag: "",
|
||||||
secretCommentTag: '',
|
createdAt,
|
||||||
createdAt
|
algorithm,
|
||||||
});
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
};
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// add secret versions
|
// add secret versions
|
||||||
const secretV = await SecretVersion.insertMany(
|
const secretV = await SecretVersion.insertMany(
|
||||||
secrets.map(({
|
secrets.map(
|
||||||
|
({
|
||||||
_id,
|
_id,
|
||||||
version,
|
version,
|
||||||
workspace,
|
workspace,
|
||||||
@@ -275,11 +436,12 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
secretKeyCiphertext,
|
secretKeyCiphertext,
|
||||||
secretKeyIV,
|
secretKeyIV,
|
||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretKeyHash,
|
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash
|
algorithm,
|
||||||
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
}) => ({
|
}) => ({
|
||||||
_id: new Types.ObjectId(),
|
_id: new Types.ObjectId(),
|
||||||
secret: _id,
|
secret: _id,
|
||||||
@@ -293,39 +455,64 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
|||||||
secretKeyCiphertext,
|
secretKeyCiphertext,
|
||||||
secretKeyIV,
|
secretKeyIV,
|
||||||
secretKeyTag,
|
secretKeyTag,
|
||||||
secretKeyHash,
|
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash
|
algorithm,
|
||||||
}))
|
keyEncoding,
|
||||||
|
folder: secFolderId,
|
||||||
|
})
|
||||||
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
// update secret versions of restored secrets as not deleted
|
if (newFolderTree && latestFolderTree) {
|
||||||
await SecretVersion.updateMany({
|
// save the updated folder tree to the present one
|
||||||
secret: {
|
newFolderTree.version = (latestFolderVersion?.nodes?.version || 0) + 1;
|
||||||
$in: secretSnapshot.secretVersions.map((sv) => sv.secret)
|
latestFolderTree._id = new Types.ObjectId();
|
||||||
}
|
latestFolderTree.isNew = true;
|
||||||
}, {
|
await latestFolderTree.save();
|
||||||
isDeleted: false
|
|
||||||
|
// create new folder version
|
||||||
|
const newFolderVersion = new FolderVersion({
|
||||||
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
nodes: newFolderTree,
|
||||||
});
|
});
|
||||||
|
await newFolderVersion.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// update secret versions of restored secrets as not deleted
|
||||||
|
await SecretVersion.updateMany(
|
||||||
|
{
|
||||||
|
secret: {
|
||||||
|
$in: Object.keys(oldSecretVersionsObj).map(
|
||||||
|
(sv) => oldSecretVersionsObj[sv].secret
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
isDeleted: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// take secret snapshot
|
// take secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
|
folderId,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to roll back secret snapshot'
|
message: "Failed to roll back secret snapshot",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
secrets
|
secrets,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return (audit) logs for workspace with id [workspaceId]
|
* Return (audit) logs for workspace with id [workspaceId]
|
||||||
@@ -400,7 +587,7 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
let logs
|
let logs;
|
||||||
try {
|
try {
|
||||||
const { workspaceId } = req.params;
|
const { workspaceId } = req.params;
|
||||||
|
|
||||||
@@ -413,30 +600,28 @@ export const getWorkspaceLogs = async (req: Request, res: Response) => {
|
|||||||
logs = await Log.find({
|
logs = await Log.find({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
...(userId ? { user: userId } : {}),
|
...(userId ? { user: userId } : {}),
|
||||||
...(
|
...(actionNames
|
||||||
actionNames
|
|
||||||
? {
|
? {
|
||||||
actionNames: {
|
actionNames: {
|
||||||
$in: actionNames.split(',')
|
$in: actionNames.split(","),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
} : {}
|
: {}),
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.sort({ createdAt: sortBy === 'recent' ? -1 : 1 })
|
.sort({ createdAt: sortBy === "recent" ? -1 : 1 })
|
||||||
.skip(offset)
|
.skip(offset)
|
||||||
.limit(limit)
|
.limit(limit)
|
||||||
.populate('actions')
|
.populate("actions")
|
||||||
.populate('user serviceAccount serviceTokenData');
|
.populate("user serviceAccount serviceTokenData");
|
||||||
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Sentry.setUser({ email: req.user.email });
|
Sentry.setUser({ email: req.user.email });
|
||||||
Sentry.captureException(err);
|
Sentry.captureException(err);
|
||||||
return res.status(400).send({
|
return res.status(400).send({
|
||||||
message: 'Failed to get workspace logs'
|
message: "Failed to get workspace logs",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).send({
|
return res.status(200).send({
|
||||||
logs
|
logs,
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,6 +1,11 @@
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { Secret, ISecret } from "../../models";
|
import { Secret, ISecret } from "../../models";
|
||||||
import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
|
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
|
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
||||||
@@ -12,22 +17,31 @@ import { SecretSnapshot, SecretVersion, ISecretVersion } from "../models";
|
|||||||
*/
|
*/
|
||||||
const takeSecretSnapshotHelper = async ({
|
const takeSecretSnapshotHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId = "root",
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
folderId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
// get all folder ids
|
||||||
const secretIds = (
|
const secretIds = (
|
||||||
await Secret.find(
|
await Secret.find(
|
||||||
{
|
{
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
|
folder: folderId,
|
||||||
},
|
},
|
||||||
"_id"
|
"_id"
|
||||||
)
|
).lean()
|
||||||
).map((s) => s._id);
|
).map((s) => s._id);
|
||||||
|
|
||||||
const latestSecretVersions = (
|
const latestSecretVersions = (
|
||||||
await SecretVersion.aggregate([
|
await SecretVersion.aggregate([
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
|
environment,
|
||||||
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
secret: {
|
secret: {
|
||||||
$in: secretIds,
|
$in: secretIds,
|
||||||
},
|
},
|
||||||
@@ -45,6 +59,11 @@ const takeSecretSnapshotHelper = async ({
|
|||||||
},
|
},
|
||||||
]).exec()
|
]).exec()
|
||||||
).map((s) => s.versionId);
|
).map((s) => s.versionId);
|
||||||
|
const latestFolderVersion = await FolderVersion.findOne({
|
||||||
|
environment,
|
||||||
|
workspace: workspaceId,
|
||||||
|
"nodes.id": folderId,
|
||||||
|
}).sort({ "nodes.version": -1 });
|
||||||
|
|
||||||
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
const latestSecretSnapshot = await SecretSnapshot.findOne({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
@@ -52,8 +71,11 @@ const takeSecretSnapshotHelper = async ({
|
|||||||
|
|
||||||
const secretSnapshot = await new SecretSnapshot({
|
const secretSnapshot = await new SecretSnapshot({
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
|
environment,
|
||||||
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
version: latestSecretSnapshot ? latestSecretSnapshot.version + 1 : 1,
|
||||||
secretVersions: latestSecretVersions,
|
secretVersions: latestSecretVersions,
|
||||||
|
folderId,
|
||||||
|
folderVersion: latestFolderVersion,
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
return secretSnapshot;
|
return secretSnapshot;
|
||||||
@@ -96,5 +118,5 @@ const markDeletedSecretVersionsHelper = async ({
|
|||||||
export {
|
export {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper
|
markDeletedSecretVersionsHelper,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import { SecretVersion } from '../models';
|
import { SecretVersion } from "../models";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return latest secret versions for secrets with ids [secretIds]
|
* Return latest secret versions for secrets with ids [secretIds]
|
||||||
@@ -8,39 +8,32 @@ import { SecretVersion } from '../models';
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getLatestSecretVersionIds = async ({
|
const getLatestSecretVersionIds = async ({
|
||||||
secretIds
|
secretIds,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) => {
|
}) => {
|
||||||
interface LatestSecretVersionId {
|
const latestSecretVersionIds = await SecretVersion.aggregate([
|
||||||
_id: Types.ObjectId;
|
|
||||||
version: number;
|
|
||||||
versionId: Types.ObjectId;
|
|
||||||
}
|
|
||||||
|
|
||||||
const latestSecretVersionIds = (await SecretVersion.aggregate([
|
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
secret: {
|
secret: {
|
||||||
$in: secretIds
|
$in: secretIds,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$group: {
|
$group: {
|
||||||
_id: '$secret',
|
_id: "$secret",
|
||||||
version: { $max: '$version' },
|
version: { $max: "$version" },
|
||||||
versionId: { $max: '$_id' } // id of latest secret version
|
versionId: { $max: "$_id" }, // id of latest secret version
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$sort: { version: -1 }
|
$sort: { version: -1 },
|
||||||
}
|
},
|
||||||
])
|
]).exec();
|
||||||
.exec());
|
|
||||||
|
|
||||||
return latestSecretVersionIds;
|
return latestSecretVersionIds;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return latest [n] secret versions for secrets with ids [secretIds]
|
* Return latest [n] secret versions for secrets with ids [secretIds]
|
||||||
@@ -51,13 +44,13 @@ const getLatestSecretVersionIds = async ({
|
|||||||
*/
|
*/
|
||||||
const getLatestNSecretSecretVersionIds = async ({
|
const getLatestNSecretSecretVersionIds = async ({
|
||||||
secretIds,
|
secretIds,
|
||||||
n
|
n,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
n: number;
|
n: number;
|
||||||
}) => {
|
}) => {
|
||||||
// TODO: optimize query
|
// TODO: optimize query
|
||||||
const latestNSecretVersions = (await SecretVersion.aggregate([
|
const latestNSecretVersions = await SecretVersion.aggregate([
|
||||||
{
|
{
|
||||||
$match: {
|
$match: {
|
||||||
secret: {
|
secret: {
|
||||||
@@ -80,13 +73,10 @@ const getLatestNSecretSecretVersionIds = async ({
|
|||||||
secret: "$_id",
|
secret: "$_id",
|
||||||
versions: { $slice: ["$versions", n] },
|
versions: { $slice: ["$versions", n] },
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
]));
|
]);
|
||||||
|
|
||||||
return latestNSecretVersions;
|
return latestNSecretVersions;
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export { getLatestSecretVersionIds, getLatestNSecretSecretVersionIds };
|
||||||
getLatestSecretVersionIds,
|
|
||||||
getLatestNSecretSecretVersionIds
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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<TFolderVersionSchema>({
|
||||||
|
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<TFolderRootVersionSchema>(
|
||||||
|
{
|
||||||
|
workspace: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "Workspace",
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
nodes: folderVersionSchema,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
timestamps: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const FolderVersion = model<TFolderRootVersionSchema>(
|
||||||
|
"FolderVersion",
|
||||||
|
folderRootVersionSchema
|
||||||
|
);
|
||||||
|
|
||||||
|
export default FolderVersion;
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
import SecretSnapshot, { ISecretSnapshot } from './secretSnapshot';
|
import SecretSnapshot, { ISecretSnapshot } from "./secretSnapshot";
|
||||||
import SecretVersion, { ISecretVersion } from './secretVersion';
|
import SecretVersion, { ISecretVersion } from "./secretVersion";
|
||||||
import Log, { ILog } from './log';
|
import FolderVersion, { TFolderRootVersionSchema } from "./folderVersion";
|
||||||
import Action, { IAction } from './action';
|
import Log, { ILog } from "./log";
|
||||||
|
import Action, { IAction } from "./action";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
SecretSnapshot,
|
SecretSnapshot,
|
||||||
ISecretSnapshot,
|
ISecretSnapshot,
|
||||||
SecretVersion,
|
SecretVersion,
|
||||||
ISecretVersion,
|
ISecretVersion,
|
||||||
|
FolderVersion,
|
||||||
|
TFolderRootVersionSchema,
|
||||||
Log,
|
Log,
|
||||||
ILog,
|
ILog,
|
||||||
Action,
|
Action,
|
||||||
IAction
|
IAction,
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,33 +1,54 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
|
|
||||||
export interface ISecretSnapshot {
|
export interface ISecretSnapshot {
|
||||||
workspace: Types.ObjectId;
|
workspace: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
folderId: string | "root";
|
||||||
version: number;
|
version: number;
|
||||||
secretVersions: Types.ObjectId[];
|
secretVersions: Types.ObjectId[];
|
||||||
|
folderVersion: Types.ObjectId;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
const secretSnapshotSchema = new Schema<ISecretSnapshot>(
|
||||||
{
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
|
},
|
||||||
|
environment: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
folderId: {
|
||||||
|
type: String,
|
||||||
|
default: "root",
|
||||||
},
|
},
|
||||||
version: {
|
version: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true
|
default: 1,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
secretVersions: [{
|
secretVersions: [
|
||||||
|
{
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'SecretVersion',
|
ref: "SecretVersion",
|
||||||
required: true
|
required: true,
|
||||||
}]
|
},
|
||||||
|
],
|
||||||
|
folderVersion: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "FolderVersion",
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretSnapshot = model<ISecretSnapshot>('SecretSnapshot', secretSnapshotSchema);
|
const SecretSnapshot = model<ISecretSnapshot>(
|
||||||
|
"SecretSnapshot",
|
||||||
|
secretSnapshotSchema
|
||||||
|
);
|
||||||
|
|
||||||
export default SecretSnapshot;
|
export default SecretSnapshot;
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8,
|
||||||
ENCODING_SCHEME_BASE64
|
ENCODING_SCHEME_BASE64,
|
||||||
} from '../../variables';
|
} from "../../variables";
|
||||||
|
|
||||||
export interface ISecretVersion {
|
export interface ISecretVersion {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -23,93 +23,104 @@ export interface ISecretVersion {
|
|||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
algorithm: 'aes-256-gcm';
|
algorithm: "aes-256-gcm";
|
||||||
keyEncoding: 'utf8' | 'base64';
|
keyEncoding: "utf8" | "base64";
|
||||||
|
createdAt: string;
|
||||||
|
folder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretVersionSchema = new Schema<ISecretVersion>(
|
const secretVersionSchema = new Schema<ISecretVersion>(
|
||||||
{
|
{
|
||||||
secret: { // could be deleted
|
secret: {
|
||||||
|
// could be deleted
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Secret',
|
ref: "Secret",
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
version: {
|
version: {
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 1,
|
default: 1,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
// user associated with the personal secret
|
// user associated with the personal secret
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'User'
|
ref: "User",
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
isDeleted: { // consider removing field
|
isDeleted: {
|
||||||
|
// consider removing field
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretBlindIndex: {
|
secretBlindIndex: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
secretKeyCiphertext: {
|
secretKeyCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretKeyIV: {
|
secretKeyIV: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretKeyTag: {
|
secretKeyTag: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretValueCiphertext: {
|
secretValueCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretValueIV: {
|
secretValueIV: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretValueTag: {
|
secretValueTag: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
algorithm: { // the encryption algorithm used
|
algorithm: {
|
||||||
|
// the encryption algorithm used
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ALGORITHM_AES_256_GCM],
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
required: true
|
required: true,
|
||||||
|
default: ALGORITHM_AES_256_GCM,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [
|
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||||
ENCODING_SCHEME_UTF8,
|
required: true,
|
||||||
ENCODING_SCHEME_BASE64
|
default: ENCODING_SCHEME_UTF8,
|
||||||
],
|
},
|
||||||
required: true
|
folder: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const SecretVersion = model<ISecretVersion>('SecretVersion', secretVersionSchema);
|
const SecretVersion = model<ISecretVersion>(
|
||||||
|
"SecretVersion",
|
||||||
|
secretVersionSchema
|
||||||
|
);
|
||||||
|
|
||||||
export default SecretVersion;
|
export default SecretVersion;
|
||||||
@@ -1,74 +1,80 @@
|
|||||||
import express from 'express';
|
import express from "express";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
validateRequest
|
validateRequest,
|
||||||
} from '../../../middleware';
|
} from "../../../middleware";
|
||||||
import { param, query, body } from 'express-validator';
|
import { param, query, body } from "express-validator";
|
||||||
import { ADMIN, MEMBER } from '../../../variables';
|
import { ADMIN, MEMBER } from "../../../variables";
|
||||||
import { workspaceController } from '../../controllers/v1';
|
import { workspaceController } from "../../controllers/v1";
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/secret-snapshots',
|
"/:workspaceId/secret-snapshots",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ["jwt", "apiKey"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: "params",
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().trim(),
|
param("workspaceId").exists().trim(),
|
||||||
query('offset').exists().isInt(),
|
query("environment").isString().exists().trim(),
|
||||||
query('limit').exists().isInt(),
|
query("folderId").default("root").isString().trim(),
|
||||||
|
query("offset").exists().isInt(),
|
||||||
|
query("limit").exists().isInt(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
workspaceController.getWorkspaceSecretSnapshots
|
workspaceController.getWorkspaceSecretSnapshots
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/secret-snapshots/count',
|
"/:workspaceId/secret-snapshots/count",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: "params",
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().trim(),
|
param("workspaceId").exists().trim(),
|
||||||
|
query("environment").isString().exists().trim(),
|
||||||
|
query("folderId").default("root").isString().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
workspaceController.getWorkspaceSecretSnapshotsCount
|
workspaceController.getWorkspaceSecretSnapshotsCount
|
||||||
);
|
);
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/:workspaceId/secret-snapshots/rollback',
|
"/:workspaceId/secret-snapshots/rollback",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ["jwt", "apiKey"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: "params",
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().trim(),
|
param("workspaceId").exists().trim(),
|
||||||
body('version').exists().isInt(),
|
body("environment").isString().exists().trim(),
|
||||||
|
query("folderId").default("root").isString().exists().trim(),
|
||||||
|
body("version").exists().isInt(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
workspaceController.rollbackWorkspaceSecretSnapshot
|
workspaceController.rollbackWorkspaceSecretSnapshot
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:workspaceId/logs',
|
"/:workspaceId/logs",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt', 'apiKey']
|
acceptedAuthModes: ["jwt", "apiKey"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'params'
|
locationWorkspaceId: "params",
|
||||||
}),
|
}),
|
||||||
param('workspaceId').exists().trim(),
|
param("workspaceId").exists().trim(),
|
||||||
query('offset').exists().isInt(),
|
query("offset").exists().isInt(),
|
||||||
query('limit').exists().isInt(),
|
query("limit").exists().isInt(),
|
||||||
query('sortBy'),
|
query("sortBy"),
|
||||||
query('userId'),
|
query("userId"),
|
||||||
query('actionNames'),
|
query("actionNames"),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
workspaceController.getWorkspaceLogs
|
workspaceController.getWorkspaceLogs
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ISecretVersion } from '../models';
|
|||||||
import {
|
import {
|
||||||
takeSecretSnapshotHelper,
|
takeSecretSnapshotHelper,
|
||||||
addSecretVersionsHelper,
|
addSecretVersionsHelper,
|
||||||
markDeletedSecretVersionsHelper
|
markDeletedSecretVersionsHelper,
|
||||||
} from '../helpers/secret';
|
} from '../helpers/secret';
|
||||||
import EELicenseService from './EELicenseService';
|
import EELicenseService from './EELicenseService';
|
||||||
|
|
||||||
@@ -11,7 +11,6 @@ import EELicenseService from './EELicenseService';
|
|||||||
* Class to handle Enterprise Edition secret actions
|
* Class to handle Enterprise Edition secret actions
|
||||||
*/
|
*/
|
||||||
class EESecretService {
|
class EESecretService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save a secret snapshot that is a copy of the current state of secrets in workspace with id
|
* 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
|
* [workspaceId] under a new snapshot with incremented version under the
|
||||||
@@ -22,12 +21,20 @@ class EESecretService {
|
|||||||
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
|
* @returns {SecretSnapshot} secretSnapshot - new secret snpashot
|
||||||
*/
|
*/
|
||||||
static async takeSecretSnapshot({
|
static async takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
folderId?: string;
|
||||||
}) {
|
}) {
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
return await takeSecretSnapshotHelper({ workspaceId });
|
return await takeSecretSnapshotHelper({
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,13 +44,13 @@ class EESecretService {
|
|||||||
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
* @returns {SecretVersion[]} newSecretVersions - new secret versions
|
||||||
*/
|
*/
|
||||||
static async addSecretVersions({
|
static async addSecretVersions({
|
||||||
secretVersions
|
secretVersions,
|
||||||
}: {
|
}: {
|
||||||
secretVersions: ISecretVersion[];
|
secretVersions: ISecretVersion[];
|
||||||
}) {
|
}) {
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
return await addSecretVersionsHelper({
|
return await addSecretVersionsHelper({
|
||||||
secretVersions
|
secretVersions,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,13 +61,13 @@ class EESecretService {
|
|||||||
* @param {ObjectId[]} obj.secretIds - secret ids
|
* @param {ObjectId[]} obj.secretIds - secret ids
|
||||||
*/
|
*/
|
||||||
static async markDeletedSecretVersions({
|
static async markDeletedSecretVersions({
|
||||||
secretIds
|
secretIds,
|
||||||
}: {
|
}: {
|
||||||
secretIds: Types.ObjectId[];
|
secretIds: Types.ObjectId[];
|
||||||
}) {
|
}) {
|
||||||
if (!EELicenseService.isLicenseValid) return;
|
if (!EELicenseService.isLicenseValid) return;
|
||||||
await markDeletedSecretVersionsHelper({
|
await markDeletedSecretVersionsHelper({
|
||||||
secretIds
|
secretIds,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,7 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { Types } from 'mongoose';
|
import { Types } from 'mongoose';
|
||||||
import {
|
import { Membership, Key } from '../models';
|
||||||
Membership,
|
import { MembershipNotFoundError, BadRequestError } from '../utils/errors';
|
||||||
Key
|
|
||||||
} from '../models';
|
|
||||||
import {
|
|
||||||
MembershipNotFoundError,
|
|
||||||
BadRequestError
|
|
||||||
} from '../utils/errors';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
* Validate that user with id [userId] is a member of workspace with id [workspaceId]
|
||||||
@@ -22,28 +16,31 @@ const validateMembership = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
acceptedRoles,
|
acceptedRoles,
|
||||||
}: {
|
}: {
|
||||||
userId: Types.ObjectId;
|
userId: Types.ObjectId | string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId | string;
|
||||||
acceptedRoles?: Array<'admin' | 'member'>;
|
acceptedRoles?: Array<'admin' | 'member'>;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const membership = await Membership.findOne({
|
const membership = await Membership.findOne({
|
||||||
user: userId,
|
user: userId,
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).populate("workspace");
|
}).populate('workspace');
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
throw MembershipNotFoundError({ message: 'Failed to find workspace membership' });
|
throw MembershipNotFoundError({
|
||||||
|
message: 'Failed to find workspace membership',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (acceptedRoles) {
|
if (acceptedRoles) {
|
||||||
if (!acceptedRoles.includes(membership.role)) {
|
if (!acceptedRoles.includes(membership.role)) {
|
||||||
throw BadRequestError({ message: 'Failed authorization for membership role' });
|
throw BadRequestError({
|
||||||
|
message: 'Failed authorization for membership role',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return membership;
|
return membership;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return membership matching criteria specified in query [queryObj]
|
* Return membership matching criteria specified in query [queryObj]
|
||||||
@@ -74,7 +71,7 @@ const findMembership = async (queryObj: any) => {
|
|||||||
const addMemberships = async ({
|
const addMemberships = async ({
|
||||||
userIds,
|
userIds,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
roles
|
roles,
|
||||||
}: {
|
}: {
|
||||||
userIds: string[];
|
userIds: string[];
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
@@ -87,15 +84,15 @@ const addMemberships = async ({
|
|||||||
filter: {
|
filter: {
|
||||||
user: userId,
|
user: userId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
role: roles[idx]
|
role: roles[idx],
|
||||||
},
|
},
|
||||||
update: {
|
update: {
|
||||||
user: userId,
|
user: userId,
|
||||||
workspace: workspaceId,
|
workspace: workspaceId,
|
||||||
role: roles[idx]
|
role: roles[idx],
|
||||||
|
},
|
||||||
|
upsert: true,
|
||||||
},
|
},
|
||||||
upsert: true
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -116,7 +113,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
let deletedMembership;
|
let deletedMembership;
|
||||||
try {
|
try {
|
||||||
deletedMembership = await Membership.findOneAndDelete({
|
deletedMembership = await Membership.findOneAndDelete({
|
||||||
_id: membershipId
|
_id: membershipId,
|
||||||
});
|
});
|
||||||
|
|
||||||
// delete keys associated with the membership
|
// delete keys associated with the membership
|
||||||
@@ -124,7 +121,7 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
// case: membership had a registered user
|
// case: membership had a registered user
|
||||||
await Key.deleteMany({
|
await Key.deleteMany({
|
||||||
receiver: deletedMembership.user,
|
receiver: deletedMembership.user,
|
||||||
workspace: deletedMembership.workspace
|
workspace: deletedMembership.workspace,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -136,9 +133,4 @@ const deleteMembership = async ({ membershipId }: { membershipId: string }) => {
|
|||||||
return deletedMembership;
|
return deletedMembership;
|
||||||
};
|
};
|
||||||
|
|
||||||
export {
|
export { validateMembership, addMemberships, findMembership, deleteMembership };
|
||||||
validateMembership,
|
|
||||||
addMemberships,
|
|
||||||
findMembership,
|
|
||||||
deleteMembership
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Types } from "mongoose";
|
import { Types } from "mongoose";
|
||||||
import { Secret, ISecret, Membership } from "../models";
|
import { Secret, ISecret } from "../models";
|
||||||
import { EESecretService, EELogService } from "../ee/services";
|
import { EESecretService, EELogService } from "../ee/services";
|
||||||
import { IAction, SecretVersion } from "../ee/models";
|
import { IAction, SecretVersion } from "../ee/models";
|
||||||
import {
|
import {
|
||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8,
|
||||||
} from "../variables";
|
} from "../variables";
|
||||||
import _ from "lodash";
|
|
||||||
import { BadRequestError, UnauthorizedRequestError } from "../utils/errors";
|
|
||||||
|
|
||||||
interface V1PushSecret {
|
interface V1PushSecret {
|
||||||
ciphertextKey: string;
|
ciphertextKey: string;
|
||||||
@@ -197,7 +195,7 @@ const v1PushSecrets = async ({
|
|||||||
secretValueTag: newSecret.tagValue,
|
secretValueTag: newSecret.tagValue,
|
||||||
secretValueHash: newSecret.hashValue,
|
secretValueHash: newSecret.hashValue,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
@@ -230,7 +228,7 @@ const v1PushSecrets = async ({
|
|||||||
secretCommentTag: s.tagComment,
|
secretCommentTag: s.tagComment,
|
||||||
secretCommentHash: s.hashComment,
|
secretCommentHash: s.hashComment,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (toAdd[idx].type === "personal") {
|
if (toAdd[idx].type === "personal") {
|
||||||
@@ -261,7 +259,7 @@ const v1PushSecrets = async ({
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash,
|
secretValueHash,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding
|
keyEncoding,
|
||||||
}) =>
|
}) =>
|
||||||
new SecretVersion({
|
new SecretVersion({
|
||||||
secret: _id,
|
secret: _id,
|
||||||
@@ -280,7 +278,7 @@ const v1PushSecrets = async ({
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretValueHash,
|
secretValueHash,
|
||||||
algorithm,
|
algorithm,
|
||||||
keyEncoding
|
keyEncoding,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
@@ -289,6 +287,7 @@ const v1PushSecrets = async ({
|
|||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -491,7 +490,7 @@ const v2PushSecrets = async ({
|
|||||||
secret: secretDocument._id,
|
secret: secretDocument._id,
|
||||||
isDeleted: false,
|
isDeleted: false,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
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
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
|
environment,
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
|
|||||||
+144
-129
@@ -4,19 +4,15 @@ import {
|
|||||||
GetSecretsParams,
|
GetSecretsParams,
|
||||||
GetSecretParams,
|
GetSecretParams,
|
||||||
UpdateSecretParams,
|
UpdateSecretParams,
|
||||||
DeleteSecretParams
|
DeleteSecretParams,
|
||||||
} from '../interfaces/services/SecretService';
|
} from '../interfaces/services/SecretService';
|
||||||
import {
|
import { Secret, ISecret, SecretBlindIndexData } from '../models';
|
||||||
Secret,
|
|
||||||
ISecret,
|
|
||||||
SecretBlindIndexData,
|
|
||||||
} from '../models';
|
|
||||||
import { SecretVersion } from '../ee/models';
|
import { SecretVersion } from '../ee/models';
|
||||||
import {
|
import {
|
||||||
BadRequestError,
|
BadRequestError,
|
||||||
SecretNotFoundError,
|
SecretNotFoundError,
|
||||||
SecretBlindIndexDataNotFoundError,
|
SecretBlindIndexDataNotFoundError,
|
||||||
InternalServerError
|
InternalServerError,
|
||||||
} from '../utils/errors';
|
} from '../utils/errors';
|
||||||
import {
|
import {
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
@@ -27,23 +23,20 @@ import {
|
|||||||
ACTION_DELETE_SECRETS,
|
ACTION_DELETE_SECRETS,
|
||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8,
|
||||||
ENCODING_SCHEME_BASE64
|
ENCODING_SCHEME_BASE64,
|
||||||
} from '../variables';
|
} from '../variables';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
import * as argon2 from 'argon2';
|
import * as argon2 from 'argon2';
|
||||||
import {
|
import {
|
||||||
encryptSymmetric128BitHexKeyUTF8,
|
encryptSymmetric128BitHexKeyUTF8,
|
||||||
decryptSymmetric128BitHexKeyUTF8
|
decryptSymmetric128BitHexKeyUTF8,
|
||||||
} from '../utils/crypto';
|
} from '../utils/crypto';
|
||||||
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
|
import { getEncryptionKey, client, getRootEncryptionKey } from '../config';
|
||||||
import { TelemetryService } from '../services';
|
import { TelemetryService } from '../services';
|
||||||
import {
|
import { EESecretService, EELogService } from '../ee/services';
|
||||||
EESecretService,
|
|
||||||
EELogService
|
|
||||||
} from '../ee/services';
|
|
||||||
import {
|
import {
|
||||||
getAuthDataPayloadIdObj,
|
getAuthDataPayloadIdObj,
|
||||||
getAuthDataPayloadUserObj
|
getAuthDataPayloadUserObj,
|
||||||
} from '../utils/auth';
|
} from '../utils/auth';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -53,11 +46,10 @@ import {
|
|||||||
* @param {Types.ObjectId} obj.workspaceId
|
* @param {Types.ObjectId} obj.workspaceId
|
||||||
*/
|
*/
|
||||||
const createSecretBlindIndexDataHelper = async ({
|
const createSecretBlindIndexDataHelper = async ({
|
||||||
workspaceId
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// initialize random blind index salt for workspace
|
// initialize random blind index salt for workspace
|
||||||
const salt = crypto.randomBytes(16).toString('base64');
|
const salt = crypto.randomBytes(16).toString('base64');
|
||||||
|
|
||||||
@@ -68,7 +60,7 @@ const createSecretBlindIndexDataHelper = async ({
|
|||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag
|
tag: saltTag,
|
||||||
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||||
|
|
||||||
return await new SecretBlindIndexData({
|
return await new SecretBlindIndexData({
|
||||||
@@ -77,16 +69,16 @@ const createSecretBlindIndexDataHelper = async ({
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_BASE64
|
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||||
}).save();
|
}).save();
|
||||||
} else {
|
} else {
|
||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag
|
tag: saltTag,
|
||||||
} = encryptSymmetric128BitHexKeyUTF8({
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: salt,
|
plaintext: salt,
|
||||||
key: encryptionKey
|
key: encryptionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
return await new SecretBlindIndexData({
|
return await new SecretBlindIndexData({
|
||||||
@@ -95,10 +87,10 @@ const createSecretBlindIndexDataHelper = async ({
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}).save();
|
}).save();
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get secret blind index salt for workspace with id [workspaceId]
|
* Get secret blind index salt for workspace with id [workspaceId]
|
||||||
@@ -107,41 +99,46 @@ const createSecretBlindIndexDataHelper = async ({
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
const getSecretBlindIndexSaltHelper = async ({
|
const getSecretBlindIndexSaltHelper = async ({
|
||||||
workspaceId
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
const encryptionKey = await getEncryptionKey();
|
const encryptionKey = await getEncryptionKey();
|
||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
}).select('+algorithm +keyEncoding');
|
}).select('+algorithm +keyEncoding');
|
||||||
|
|
||||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||||
|
|
||||||
if (rootEncryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64) {
|
if (
|
||||||
|
rootEncryptionKey &&
|
||||||
|
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_BASE64
|
||||||
|
) {
|
||||||
return client.decryptSymmetric(
|
return client.decryptSymmetric(
|
||||||
secretBlindIndexData.encryptedSaltCiphertext,
|
secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
rootEncryptionKey,
|
rootEncryptionKey,
|
||||||
secretBlindIndexData.saltIV,
|
secretBlindIndexData.saltIV,
|
||||||
secretBlindIndexData.saltTag
|
secretBlindIndexData.saltTag
|
||||||
);
|
);
|
||||||
} else if (encryptionKey && secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8) {
|
} else if (
|
||||||
|
encryptionKey &&
|
||||||
|
secretBlindIndexData.keyEncoding === ENCODING_SCHEME_UTF8
|
||||||
|
) {
|
||||||
// decrypt workspace salt
|
// decrypt workspace salt
|
||||||
return decryptSymmetric128BitHexKeyUTF8({
|
return decryptSymmetric128BitHexKeyUTF8({
|
||||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
iv: secretBlindIndexData.saltIV,
|
iv: secretBlindIndexData.saltIV,
|
||||||
tag: secretBlindIndexData.saltTag,
|
tag: secretBlindIndexData.saltTag,
|
||||||
key: encryptionKey
|
key: encryptionKey,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw InternalServerError({
|
throw InternalServerError({
|
||||||
message: 'Failed to obtain workspace salt needed for secret blind indexing'
|
message: 'Failed to obtain workspace salt needed for secret blind indexing',
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate blind index for secret with name [secretName]
|
* Generate blind index for secret with name [secretName]
|
||||||
@@ -152,25 +149,26 @@ const getSecretBlindIndexSaltHelper = async ({
|
|||||||
*/
|
*/
|
||||||
const generateSecretBlindIndexWithSaltHelper = async ({
|
const generateSecretBlindIndexWithSaltHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
salt
|
salt,
|
||||||
}: {
|
}: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
salt: string;
|
salt: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// generate secret blind index
|
// generate secret blind index
|
||||||
const secretBlindIndex = (await argon2.hash(secretName, {
|
const secretBlindIndex = (
|
||||||
|
await argon2.hash(secretName, {
|
||||||
type: argon2.argon2id,
|
type: argon2.argon2id,
|
||||||
salt: Buffer.from(salt, 'base64'),
|
salt: Buffer.from(salt, 'base64'),
|
||||||
saltLength: 16, // default 16 bytes
|
saltLength: 16, // default 16 bytes
|
||||||
memoryCost: 65536, // default pool of 64 MiB per thread.
|
memoryCost: 65536, // default pool of 64 MiB per thread.
|
||||||
hashLength: 32,
|
hashLength: 32,
|
||||||
parallelism: 1,
|
parallelism: 1,
|
||||||
raw: true
|
raw: true,
|
||||||
})).toString('base64');
|
})
|
||||||
|
).toString('base64');
|
||||||
|
|
||||||
return secretBlindIndex;
|
return secretBlindIndex;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate blind index for secret with name [secretName]
|
* Generate blind index for secret with name [secretName]
|
||||||
@@ -181,15 +179,14 @@ const getSecretBlindIndexSaltHelper = async ({
|
|||||||
*/
|
*/
|
||||||
const generateSecretBlindIndexHelper = async ({
|
const generateSecretBlindIndexHelper = async ({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId
|
workspaceId,
|
||||||
}: {
|
}: {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
}) => {
|
}) => {
|
||||||
|
|
||||||
// check if workspace blind index data exists
|
// check if workspace blind index data exists
|
||||||
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
const secretBlindIndexData = await SecretBlindIndexData.findOne({
|
||||||
workspace: workspaceId
|
workspace: workspaceId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
if (!secretBlindIndexData) throw SecretBlindIndexDataNotFoundError();
|
||||||
@@ -199,16 +196,16 @@ const generateSecretBlindIndexHelper = async ({
|
|||||||
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
ciphertext: secretBlindIndexData.encryptedSaltCiphertext,
|
||||||
iv: secretBlindIndexData.saltIV,
|
iv: secretBlindIndexData.saltIV,
|
||||||
tag: secretBlindIndexData.saltTag,
|
tag: secretBlindIndexData.saltTag,
|
||||||
key: await getEncryptionKey()
|
key: await getEncryptionKey(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
const secretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||||
secretName,
|
secretName,
|
||||||
salt
|
salt,
|
||||||
});
|
});
|
||||||
|
|
||||||
return secretBlindIndex;
|
return secretBlindIndex;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create secret with name [secretName]
|
* Create secret with name [secretName]
|
||||||
@@ -234,22 +231,24 @@ const createSecretHelper = async ({
|
|||||||
secretValueTag,
|
secretValueTag,
|
||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag
|
secretCommentTag,
|
||||||
|
folderId,
|
||||||
}: CreateSecretParams) => {
|
}: CreateSecretParams) => {
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
});
|
});
|
||||||
|
|
||||||
const exists = await Secret.exists({
|
const exists = await Secret.exists({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
type,
|
type,
|
||||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (exists) throw BadRequestError({
|
if (exists)
|
||||||
message: 'Failed to create secret that already exists'
|
throw BadRequestError({
|
||||||
|
message: 'Failed to create secret that already exists',
|
||||||
});
|
});
|
||||||
|
|
||||||
if (type === SECRET_PERSONAL) {
|
if (type === SECRET_PERSONAL) {
|
||||||
@@ -259,11 +258,13 @@ const createSecretHelper = async ({
|
|||||||
const exists = await Secret.exists({
|
const exists = await Secret.exists({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
type: SECRET_SHARED
|
type: SECRET_SHARED,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!exists) throw BadRequestError({
|
if (!exists)
|
||||||
message: 'Failed to create personal secret override for no corresponding shared secret'
|
throw BadRequestError({
|
||||||
|
message:
|
||||||
|
'Failed to create personal secret override for no corresponding shared secret',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,8 +285,9 @@ const createSecretHelper = async ({
|
|||||||
secretCommentCiphertext,
|
secretCommentCiphertext,
|
||||||
secretCommentIV,
|
secretCommentIV,
|
||||||
secretCommentTag,
|
secretCommentTag,
|
||||||
|
folder: folderId,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}).save();
|
}).save();
|
||||||
|
|
||||||
const secretVersion = new SecretVersion({
|
const secretVersion = new SecretVersion({
|
||||||
@@ -304,12 +306,12 @@ const createSecretHelper = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
});
|
});
|
||||||
|
|
||||||
// // (EE) add version for new secret
|
// (EE) add version for new secret
|
||||||
await EESecretService.addSecretVersions({
|
await EESecretService.addSecretVersions({
|
||||||
secretVersions: [secretVersion]
|
secretVersions: [secretVersion],
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -317,20 +319,23 @@ const createSecretHelper = async ({
|
|||||||
name: ACTION_ADD_SECRETS,
|
name: ACTION_ADD_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: [secret._id]
|
secretIds: [secret._id],
|
||||||
});
|
});
|
||||||
|
|
||||||
action && await EELogService.createLog({
|
action &&
|
||||||
|
(await EELogService.createLog({
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP
|
ipAddress: authData.authIP,
|
||||||
});
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -339,20 +344,20 @@ const createSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets added',
|
event: 'secrets added',
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData
|
authData,
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent
|
userAgent: authData.authUserAgent,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get secrets for workspace with id [workspaceId] and environment [environment]
|
* Get secrets for workspace with id [workspaceId] and environment [environment]
|
||||||
@@ -365,7 +370,7 @@ const createSecretHelper = async ({
|
|||||||
const getSecretsHelper = async ({
|
const getSecretsHelper = async ({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
authData
|
authData,
|
||||||
}: GetSecretsParams) => {
|
}: GetSecretsParams) => {
|
||||||
let secrets: ISecret[] = [];
|
let secrets: ISecret[] = [];
|
||||||
|
|
||||||
@@ -374,34 +379,37 @@ const getSecretsHelper = async ({
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: SECRET_PERSONAL,
|
type: SECRET_PERSONAL,
|
||||||
...getAuthDataPayloadUserObj(authData)
|
...getAuthDataPayloadUserObj(authData),
|
||||||
});
|
});
|
||||||
|
|
||||||
// concat with shared secrets
|
// concat with shared secrets
|
||||||
secrets = secrets.concat(await Secret.find({
|
secrets = secrets.concat(
|
||||||
|
await Secret.find({
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: SECRET_SHARED,
|
type: SECRET_SHARED,
|
||||||
secretBlindIndex: {
|
secretBlindIndex: {
|
||||||
$nin: secrets.map((secret) => secret.secretBlindIndex)
|
$nin: secrets.map((secret) => secret.secretBlindIndex),
|
||||||
}
|
},
|
||||||
}));
|
})
|
||||||
|
);
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
const action = await EELogService.createAction({
|
const action = await EELogService.createAction({
|
||||||
name: ACTION_READ_SECRETS,
|
name: ACTION_READ_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: secrets.map((secret) => secret._id)
|
secretIds: secrets.map((secret) => secret._id),
|
||||||
});
|
});
|
||||||
|
|
||||||
action && await EELogService.createLog({
|
action &&
|
||||||
|
(await EELogService.createLog({
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP
|
ipAddress: authData.authIP,
|
||||||
});
|
}));
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
|
|
||||||
@@ -409,20 +417,20 @@ const getSecretsHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pulled',
|
event: 'secrets pulled',
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData
|
authData,
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent
|
userAgent: authData.authUserAgent,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return secrets;
|
return secrets;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get secret with name [secretName]
|
* Get secret with name [secretName]
|
||||||
@@ -439,11 +447,11 @@ const getSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
authData
|
authData,
|
||||||
}: GetSecretParams) => {
|
}: GetSecretParams) => {
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
});
|
});
|
||||||
let secret: ISecret | null = null;
|
let secret: ISecret | null = null;
|
||||||
|
|
||||||
@@ -453,7 +461,7 @@ const getSecretHelper = async ({
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: type ?? SECRET_PERSONAL,
|
type: type ?? SECRET_PERSONAL,
|
||||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!secret) {
|
if (!secret) {
|
||||||
@@ -463,7 +471,7 @@ const getSecretHelper = async ({
|
|||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type: SECRET_SHARED
|
type: SECRET_SHARED,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -474,16 +482,17 @@ const getSecretHelper = async ({
|
|||||||
name: ACTION_READ_SECRETS,
|
name: ACTION_READ_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: [secret._id]
|
secretIds: [secret._id],
|
||||||
});
|
});
|
||||||
|
|
||||||
action && await EELogService.createLog({
|
action &&
|
||||||
|
(await EELogService.createLog({
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP
|
ipAddress: authData.authIP,
|
||||||
});
|
}));
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
|
|
||||||
@@ -491,20 +500,20 @@ const getSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets pull',
|
event: 'secrets pull',
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData
|
authData,
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent
|
userAgent: authData.authUserAgent,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update secret with name [secretName]
|
* Update secret with name [secretName]
|
||||||
@@ -527,11 +536,11 @@ const updateSecretHelper = async ({
|
|||||||
authData,
|
authData,
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag
|
secretValueTag,
|
||||||
}: UpdateSecretParams) => {
|
}: UpdateSecretParams) => {
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
});
|
});
|
||||||
|
|
||||||
let secret: ISecret | null = null;
|
let secret: ISecret | null = null;
|
||||||
@@ -543,16 +552,16 @@ const updateSecretHelper = async ({
|
|||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type
|
type,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
$inc: { version: 1 }
|
$inc: { version: 1 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -564,16 +573,16 @@ const updateSecretHelper = async ({
|
|||||||
workspace: new Types.ObjectId(workspaceId),
|
workspace: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
...getAuthDataPayloadUserObj(authData)
|
...getAuthDataPayloadUserObj(authData),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
secretValueCiphertext,
|
secretValueCiphertext,
|
||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
$inc: { version: 1 }
|
$inc: { version: 1 },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
new: true
|
new: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -596,12 +605,12 @@ const updateSecretHelper = async ({
|
|||||||
secretValueIV,
|
secretValueIV,
|
||||||
secretValueTag,
|
secretValueTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) add version for new secret
|
// (EE) add version for new secret
|
||||||
await EESecretService.addSecretVersions({
|
await EESecretService.addSecretVersions({
|
||||||
secretVersions: [secretVersion]
|
secretVersions: [secretVersion],
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -609,20 +618,23 @@ const updateSecretHelper = async ({
|
|||||||
name: ACTION_UPDATE_SECRETS,
|
name: ACTION_UPDATE_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: [secret._id]
|
secretIds: [secret._id],
|
||||||
});
|
});
|
||||||
|
|
||||||
action && await EELogService.createLog({
|
action &&
|
||||||
|
(await EELogService.createLog({
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP
|
ipAddress: authData.authIP,
|
||||||
});
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: secret?.folder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -631,20 +643,20 @@ const updateSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets modified',
|
event: 'secrets modified',
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData
|
authData,
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: 1,
|
numberOfSecrets: 1,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent
|
userAgent: authData.authUserAgent,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return secret;
|
return secret;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete secret with name [secretName]
|
* Delete secret with name [secretName]
|
||||||
@@ -661,11 +673,11 @@ const deleteSecretHelper = async ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
authData
|
authData,
|
||||||
}: DeleteSecretParams) => {
|
}: DeleteSecretParams) => {
|
||||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||||
secretName,
|
secretName,
|
||||||
workspaceId: new Types.ObjectId(workspaceId)
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
});
|
});
|
||||||
|
|
||||||
let secrets: ISecret[] = [];
|
let secrets: ISecret[] = [];
|
||||||
@@ -675,20 +687,20 @@ const deleteSecretHelper = async ({
|
|||||||
secrets = await Secret.find({
|
secrets = await Secret.find({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment
|
environment,
|
||||||
});
|
});
|
||||||
|
|
||||||
secret = await Secret.findOneAndDelete({
|
secret = await Secret.findOneAndDelete({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type
|
type,
|
||||||
});
|
});
|
||||||
|
|
||||||
await Secret.deleteMany({
|
await Secret.deleteMany({
|
||||||
secretBlindIndex,
|
secretBlindIndex,
|
||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment
|
environment,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
secret = await Secret.findOneAndDelete({
|
secret = await Secret.findOneAndDelete({
|
||||||
@@ -696,7 +708,7 @@ const deleteSecretHelper = async ({
|
|||||||
workspaceId: new Types.ObjectId(workspaceId),
|
workspaceId: new Types.ObjectId(workspaceId),
|
||||||
environment,
|
environment,
|
||||||
type,
|
type,
|
||||||
...getAuthDataPayloadUserObj(authData)
|
...getAuthDataPayloadUserObj(authData),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (secret) {
|
if (secret) {
|
||||||
@@ -707,7 +719,7 @@ const deleteSecretHelper = async ({
|
|||||||
if (!secret) throw SecretNotFoundError();
|
if (!secret) throw SecretNotFoundError();
|
||||||
|
|
||||||
await EESecretService.markDeletedSecretVersions({
|
await EESecretService.markDeletedSecretVersions({
|
||||||
secretIds: secrets.map((secret) => secret._id)
|
secretIds: secrets.map((secret) => secret._id),
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) create (audit) log
|
// (EE) create (audit) log
|
||||||
@@ -715,21 +727,24 @@ const deleteSecretHelper = async ({
|
|||||||
name: ACTION_DELETE_SECRETS,
|
name: ACTION_DELETE_SECRETS,
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
secretIds: secrets.map((secret) => secret._id)
|
secretIds: secrets.map((secret) => secret._id),
|
||||||
});
|
});
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
action && await EELogService.createLog({
|
action &&
|
||||||
|
(await EELogService.createLog({
|
||||||
...getAuthDataPayloadIdObj(authData),
|
...getAuthDataPayloadIdObj(authData),
|
||||||
workspaceId,
|
workspaceId,
|
||||||
actions: [action],
|
actions: [action],
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
ipAddress: authData.authIP
|
ipAddress: authData.authIP,
|
||||||
});
|
}));
|
||||||
|
|
||||||
// (EE) take a secret snapshot
|
// (EE) take a secret snapshot
|
||||||
await EESecretService.takeSecretSnapshot({
|
await EESecretService.takeSecretSnapshot({
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
folderId: secret?.folder,
|
||||||
});
|
});
|
||||||
|
|
||||||
const postHogClient = await TelemetryService.getPostHogClient();
|
const postHogClient = await TelemetryService.getPostHogClient();
|
||||||
@@ -738,23 +753,23 @@ const deleteSecretHelper = async ({
|
|||||||
postHogClient.capture({
|
postHogClient.capture({
|
||||||
event: 'secrets deleted',
|
event: 'secrets deleted',
|
||||||
distinctId: await TelemetryService.getDistinctId({
|
distinctId: await TelemetryService.getDistinctId({
|
||||||
authData
|
authData,
|
||||||
}),
|
}),
|
||||||
properties: {
|
properties: {
|
||||||
numberOfSecrets: secrets.length,
|
numberOfSecrets: secrets.length,
|
||||||
environment,
|
environment,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
channel: authData.authChannel,
|
channel: authData.authChannel,
|
||||||
userAgent: authData.authUserAgent
|
userAgent: authData.authUserAgent,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return ({
|
return {
|
||||||
secrets,
|
secrets,
|
||||||
secret
|
secret,
|
||||||
});
|
};
|
||||||
}
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
createSecretBlindIndexDataHelper,
|
createSecretBlindIndexDataHelper,
|
||||||
@@ -765,5 +780,5 @@ export {
|
|||||||
getSecretsHelper,
|
getSecretsHelper,
|
||||||
getSecretHelper,
|
getSecretHelper,
|
||||||
updateSecretHelper,
|
updateSecretHelper,
|
||||||
deleteSecretHelper
|
deleteSecretHelper,
|
||||||
}
|
};
|
||||||
|
|||||||
+17
-20
@@ -1,8 +1,6 @@
|
|||||||
import dotenv from "dotenv";
|
import dotenv from 'dotenv';
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
||||||
require('express-async-errors');
|
|
||||||
import helmet from 'helmet';
|
import helmet from 'helmet';
|
||||||
import cors from 'cors';
|
import cors from 'cors';
|
||||||
import { DatabaseService } from './services';
|
import { DatabaseService } from './services';
|
||||||
@@ -21,7 +19,7 @@ import {
|
|||||||
secretSnapshot as eeSecretSnapshotRouter,
|
secretSnapshot as eeSecretSnapshotRouter,
|
||||||
action as eeActionRouter,
|
action as eeActionRouter,
|
||||||
organizations as eeOrganizationsRouter,
|
organizations as eeOrganizationsRouter,
|
||||||
cloudProducts as eeCloudProductsRouter
|
cloudProducts as eeCloudProductsRouter,
|
||||||
} from './ee/routes/v1';
|
} from './ee/routes/v1';
|
||||||
import {
|
import {
|
||||||
signup as v1SignupRouter,
|
signup as v1SignupRouter,
|
||||||
@@ -41,7 +39,7 @@ import {
|
|||||||
stripe as v1StripeRouter,
|
stripe as v1StripeRouter,
|
||||||
integration as v1IntegrationRouter,
|
integration as v1IntegrationRouter,
|
||||||
integrationAuth as v1IntegrationAuthRouter,
|
integrationAuth as v1IntegrationAuthRouter,
|
||||||
secretsFolder as v1SecretsFolder
|
secretsFolder as v1SecretsFolder,
|
||||||
} from './routes/v1';
|
} from './routes/v1';
|
||||||
import {
|
import {
|
||||||
signup as v2SignupRouter,
|
signup as v2SignupRouter,
|
||||||
@@ -61,17 +59,13 @@ import {
|
|||||||
auth as v3AuthRouter,
|
auth as v3AuthRouter,
|
||||||
secrets as v3SecretsRouter,
|
secrets as v3SecretsRouter,
|
||||||
signup as v3SignupRouter,
|
signup as v3SignupRouter,
|
||||||
workspaces as v3WorkspacesRouter
|
workspaces as v3WorkspacesRouter,
|
||||||
} from './routes/v3';
|
} from './routes/v3';
|
||||||
import { healthCheck } from './routes/status';
|
import { healthCheck } from './routes/status';
|
||||||
import { getLogger } from './utils/logger';
|
import { getLogger } from './utils/logger';
|
||||||
import { RouteNotFoundError } from './utils/errors';
|
import { RouteNotFoundError } from './utils/errors';
|
||||||
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
import { requestErrorHandler } from './middleware/requestErrorHandler';
|
||||||
import {
|
import { getNodeEnv, getPort, getSiteURL } from './config';
|
||||||
getNodeEnv,
|
|
||||||
getPort,
|
|
||||||
getSiteURL
|
|
||||||
} from './config';
|
|
||||||
import { setup } from './utils/setup';
|
import { setup } from './utils/setup';
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
@@ -86,7 +80,7 @@ const main = async () => {
|
|||||||
app.use(
|
app.use(
|
||||||
cors({
|
cors({
|
||||||
credentials: true,
|
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/stripe', v1StripeRouter);
|
||||||
app.use('/api/v1/integration', v1IntegrationRouter);
|
app.use('/api/v1/integration', v1IntegrationRouter);
|
||||||
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
app.use('/api/v1/integration-auth', v1IntegrationAuthRouter);
|
||||||
app.use('/api/v1/folder', v1SecretsFolder)
|
app.use('/api/v1/folders', v1SecretsFolder);
|
||||||
|
|
||||||
// v2 routes (improvements)
|
// v2 routes (improvements)
|
||||||
app.use('/api/v2/signup', v2SignupRouter);
|
app.use('/api/v2/signup', v2SignupRouter);
|
||||||
@@ -143,34 +137,37 @@ const main = async () => {
|
|||||||
app.use('/api/v2/api-key', v2APIKeyDataRouter);
|
app.use('/api/v2/api-key', v2APIKeyDataRouter);
|
||||||
|
|
||||||
// v3 routes (experimental)
|
// v3 routes (experimental)
|
||||||
app.use("/api/v3/auth", v3AuthRouter);
|
|
||||||
app.use('/api/v3/secrets', v3SecretsRouter);
|
app.use('/api/v3/secrets', v3SecretsRouter);
|
||||||
app.use('/api/v3/workspaces', v3WorkspacesRouter);
|
app.use('/api/v3/workspaces', v3WorkspacesRouter);
|
||||||
app.use("/api/v3/signup", v3SignupRouter);
|
|
||||||
|
|
||||||
// api docs
|
// api docs
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile))
|
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerFile));
|
||||||
|
|
||||||
// server status
|
// server status
|
||||||
app.use('/api', healthCheck)
|
app.use('/api', healthCheck);
|
||||||
|
|
||||||
//* Handle unrouted requests and respond with proper error message as well as status code
|
//* Handle unrouted requests and respond with proper error message as well as status code
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
if (res.headersSent) return 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);
|
app.use(requestErrorHandler);
|
||||||
|
|
||||||
const server = app.listen(await getPort(), async () => {
|
const server = app.listen(await getPort(), async () => {
|
||||||
(await getLogger("backend-main")).info(
|
(await getLogger('backend-main')).info(
|
||||||
`Server started listening at port ${await getPort()}`
|
`Server started listening at port ${await getPort()}`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// await createTestUserForDevelopment();
|
||||||
setUpHealthEndpoint(server);
|
setUpHealthEndpoint(server);
|
||||||
|
|
||||||
server.on("close", async () => {
|
server.on('close', async () => {
|
||||||
await DatabaseService.closeDatabase();
|
await DatabaseService.closeDatabase();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import { AuthData } from '../../middleware';
|
import { AuthData } from "../../middleware";
|
||||||
|
|
||||||
export interface CreateSecretParams {
|
export interface CreateSecretParams {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment: string;
|
environment: string;
|
||||||
type: 'shared' | 'personal';
|
folderId?: string;
|
||||||
|
type: "shared" | "personal";
|
||||||
authData: AuthData;
|
authData: AuthData;
|
||||||
secretKeyCiphertext: string;
|
secretKeyCiphertext: string;
|
||||||
secretKeyIV: string;
|
secretKeyIV: string;
|
||||||
@@ -28,7 +29,7 @@ export interface GetSecretParams {
|
|||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment: string;
|
environment: string;
|
||||||
type?: 'shared' | 'personal';
|
type?: "shared" | "personal";
|
||||||
authData: AuthData;
|
authData: AuthData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,17 +37,18 @@ export interface UpdateSecretParams {
|
|||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment: string;
|
environment: string;
|
||||||
type: 'shared' | 'personal',
|
type: "shared" | "personal";
|
||||||
authData: AuthData
|
authData: AuthData;
|
||||||
secretValueCiphertext: string;
|
secretValueCiphertext: string;
|
||||||
secretValueIV: string;
|
secretValueIV: string;
|
||||||
secretValueTag: string;
|
secretValueTag: string;
|
||||||
|
folderId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeleteSecretParams {
|
export interface DeleteSecretParams {
|
||||||
secretName: string;
|
secretName: string;
|
||||||
workspaceId: Types.ObjectId;
|
workspaceId: Types.ObjectId;
|
||||||
environment: string;
|
environment: string;
|
||||||
type: 'shared' | 'personal';
|
type: "shared" | "personal";
|
||||||
authData: AuthData;
|
authData: AuthData;
|
||||||
}
|
}
|
||||||
@@ -1,29 +1,45 @@
|
|||||||
import * as Sentry from '@sentry/node';
|
import * as Sentry from '@sentry/node';
|
||||||
import { ErrorRequestHandler } from "express";
|
import { ErrorRequestHandler } from 'express';
|
||||||
import { InternalServerError } from "../utils/errors";
|
import { InternalServerError } from '../utils/errors';
|
||||||
import { getLogger } from "../utils/logger";
|
import { getLogger } from '../utils/logger';
|
||||||
import RequestError, { LogLevel } from "../utils/requestError";
|
import RequestError, { LogLevel } from '../utils/requestError';
|
||||||
import { getNodeEnv } from '../config';
|
|
||||||
|
|
||||||
export const requestErrorHandler: ErrorRequestHandler = async (error: RequestError | Error, req, res, next) => {
|
export const requestErrorHandler: ErrorRequestHandler = async (
|
||||||
|
error: RequestError | Error,
|
||||||
|
req,
|
||||||
|
res,
|
||||||
|
next
|
||||||
|
) => {
|
||||||
if (res.headersSent) return next();
|
if (res.headersSent) return next();
|
||||||
|
|
||||||
//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
|
//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)) {
|
if (!(error instanceof RequestError)) {
|
||||||
error = InternalServerError({ context: { exception: error.message }, stack: error.stack });
|
error = InternalServerError({
|
||||||
(await getLogger('backend-main')).log((<RequestError>error).levelName.toLowerCase(), (<RequestError>error).message)
|
context: { exception: error.message },
|
||||||
|
stack: error.stack,
|
||||||
|
});
|
||||||
|
(await getLogger('backend-main')).log(
|
||||||
|
(<RequestError>error).levelName.toLowerCase(),
|
||||||
|
(<RequestError>error).message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//* Set Sentry user identification if req.user is populated
|
//* Set Sentry user identification if req.user is populated
|
||||||
if (req.user !== undefined && req.user !== null) {
|
if (req.user !== undefined && req.user !== null) {
|
||||||
Sentry.setUser({ email: (req.user as any).email })
|
Sentry.setUser({ email: (req.user as any).email });
|
||||||
}
|
}
|
||||||
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
|
//* Only sent error to Sentry if LogLevel is one of the following level 'ERROR', 'EMERGENCY' or 'CRITICAL'
|
||||||
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
|
//* with this we will eliminate false-positive errors like 'BadRequestError', 'UnauthorizedRequestError' and so on
|
||||||
if ([LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes((<RequestError>error).level)) {
|
if (
|
||||||
Sentry.captureException(error)
|
[LogLevel.ERROR, LogLevel.EMERGENCY, LogLevel.CRITICAL].includes(
|
||||||
|
(<RequestError>error).level
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Sentry.captureException(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status((<RequestError>error).statusCode).json((<RequestError>error).format(req))
|
res
|
||||||
next()
|
.status((<RequestError>error).statusCode)
|
||||||
}
|
.json((<RequestError>error).format(req));
|
||||||
|
next();
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,36 +1,56 @@
|
|||||||
import { Schema, Types, model } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
|
|
||||||
const folderSchema = new Schema({
|
export type TFolderRootSchema = {
|
||||||
name: {
|
_id: Types.ObjectId;
|
||||||
type: String,
|
workspace: Types.ObjectId;
|
||||||
|
environment: string;
|
||||||
|
nodes: TFolderSchema;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TFolderSchema = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
version: number;
|
||||||
|
children: TFolderSchema[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const folderSchema = new Schema<TFolderSchema>({
|
||||||
|
id: {
|
||||||
required: true,
|
required: true,
|
||||||
|
type: String,
|
||||||
},
|
},
|
||||||
|
version: {
|
||||||
|
required: true,
|
||||||
|
type: Number,
|
||||||
|
default: 1,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
required: true,
|
||||||
|
type: String,
|
||||||
|
default: "root",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
folderSchema.add({ children: [folderSchema] });
|
||||||
|
|
||||||
|
const folderRootSchema = new Schema<TFolderRootSchema>(
|
||||||
|
{
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
parent: {
|
nodes: folderSchema,
|
||||||
type: Schema.Types.ObjectId,
|
|
||||||
ref: 'Folder',
|
|
||||||
required: false, // optional for root folders
|
|
||||||
},
|
},
|
||||||
path: {
|
{
|
||||||
type: String,
|
timestamps: true,
|
||||||
required: true
|
}
|
||||||
},
|
);
|
||||||
parentPath: {
|
|
||||||
type: String,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
}, {
|
|
||||||
timestamps: true
|
|
||||||
});
|
|
||||||
|
|
||||||
const Folder = model('Folder', folderSchema);
|
const Folder = model<TFolderRootSchema>("Folder", folderRootSchema);
|
||||||
|
|
||||||
export default Folder;
|
export default Folder;
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
import { Schema, model, Types } from 'mongoose';
|
import { Schema, model, Types } from "mongoose";
|
||||||
import {
|
import {
|
||||||
SECRET_SHARED,
|
SECRET_SHARED,
|
||||||
SECRET_PERSONAL,
|
SECRET_PERSONAL,
|
||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8,
|
||||||
ENCODING_SCHEME_BASE64
|
ENCODING_SCHEME_BASE64,
|
||||||
} from '../variables';
|
} from "../variables";
|
||||||
import { ROOT_FOLDER_PATH } from '../utils/folder';
|
|
||||||
|
|
||||||
export interface ISecret {
|
export interface ISecret {
|
||||||
_id: Types.ObjectId;
|
_id: Types.ObjectId;
|
||||||
@@ -28,11 +27,10 @@ export interface ISecret {
|
|||||||
secretCommentIV?: string;
|
secretCommentIV?: string;
|
||||||
secretCommentTag?: string;
|
secretCommentTag?: string;
|
||||||
secretCommentHash?: string;
|
secretCommentHash?: string;
|
||||||
algorithm: 'aes-256-gcm';
|
algorithm: "aes-256-gcm";
|
||||||
keyEncoding: 'utf8' | 'base64';
|
keyEncoding: "utf8" | "base64";
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
path?: string;
|
folder?: string;
|
||||||
folder?: Types.ObjectId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const secretSchema = new Schema<ISecret>(
|
const secretSchema = new Schema<ISecret>(
|
||||||
@@ -40,115 +38,107 @@ const secretSchema = new Schema<ISecret>(
|
|||||||
version: {
|
version: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: true,
|
required: true,
|
||||||
default: 1
|
default: 1,
|
||||||
},
|
},
|
||||||
workspace: {
|
workspace: {
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'Workspace',
|
ref: "Workspace",
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
user: {
|
user: {
|
||||||
// user associated with the personal secret
|
// user associated with the personal secret
|
||||||
type: Schema.Types.ObjectId,
|
type: Schema.Types.ObjectId,
|
||||||
ref: 'User'
|
ref: "User",
|
||||||
},
|
},
|
||||||
tags: {
|
tags: {
|
||||||
ref: 'Tag',
|
ref: "Tag",
|
||||||
type: [Schema.Types.ObjectId],
|
type: [Schema.Types.ObjectId],
|
||||||
default: []
|
default: [],
|
||||||
},
|
},
|
||||||
environment: {
|
environment: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretBlindIndex: {
|
secretBlindIndex: {
|
||||||
type: String,
|
type: String,
|
||||||
select: false
|
select: false,
|
||||||
},
|
},
|
||||||
secretKeyCiphertext: {
|
secretKeyCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretKeyIV: {
|
secretKeyIV: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretKeyTag: {
|
secretKeyTag: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretKeyHash: {
|
secretKeyHash: {
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
secretValueCiphertext: {
|
secretValueCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretValueIV: {
|
secretValueIV: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretValueTag: {
|
secretValueTag: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: true
|
required: true,
|
||||||
},
|
},
|
||||||
secretValueHash: {
|
secretValueHash: {
|
||||||
type: String
|
type: String,
|
||||||
},
|
},
|
||||||
secretCommentCiphertext: {
|
secretCommentCiphertext: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
secretCommentIV: {
|
secretCommentIV: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
secretCommentTag: {
|
secretCommentTag: {
|
||||||
type: String, // symmetric
|
type: String, // symmetric
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
secretCommentHash: {
|
secretCommentHash: {
|
||||||
type: String,
|
type: String,
|
||||||
required: false
|
required: false,
|
||||||
},
|
},
|
||||||
algorithm: { // the encryption algorithm used
|
algorithm: {
|
||||||
|
// the encryption algorithm used
|
||||||
type: String,
|
type: String,
|
||||||
enum: [ALGORITHM_AES_256_GCM],
|
enum: [ALGORITHM_AES_256_GCM],
|
||||||
required: true
|
required: true,
|
||||||
|
default: ALGORITHM_AES_256_GCM,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: [
|
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||||
ENCODING_SCHEME_UTF8,
|
required: true,
|
||||||
ENCODING_SCHEME_BASE64
|
default: ENCODING_SCHEME_UTF8,
|
||||||
],
|
|
||||||
required: true
|
|
||||||
},
|
|
||||||
// the full path to the secret in relation to folders
|
|
||||||
path: {
|
|
||||||
type: String,
|
|
||||||
required: false,
|
|
||||||
default: ROOT_FOLDER_PATH
|
|
||||||
},
|
},
|
||||||
folder: {
|
folder: {
|
||||||
type: Schema.Types.ObjectId,
|
type: String,
|
||||||
ref: 'Folder',
|
default: "root",
|
||||||
required: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
timestamps: true
|
timestamps: true,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
secretSchema.index({ tags: 1 }, { background: true });
|
||||||
|
|
||||||
secretSchema.index({ tags: 1 }, { background: true })
|
const Secret = model<ISecret>("Secret", secretSchema);
|
||||||
|
|
||||||
const Secret = model<ISecret>('Secret', secretSchema);
|
|
||||||
|
|
||||||
export default Secret;
|
export default Secret;
|
||||||
|
|||||||
@@ -1,50 +1,70 @@
|
|||||||
import express, { Request, Response } from 'express';
|
import express from "express";
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
import {
|
import {
|
||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
validateRequest
|
validateRequest,
|
||||||
} from '../../middleware';
|
} from "../../middleware";
|
||||||
import { body, param } from 'express-validator';
|
import { body, param, query } from "express-validator";
|
||||||
import { createFolder, deleteFolder, getFolderById } from '../../controllers/v1/secretsFolderController';
|
import {
|
||||||
import { ADMIN, MEMBER } from '../../variables';
|
createFolder,
|
||||||
|
deleteFolder,
|
||||||
|
getFolders,
|
||||||
|
updateFolderById,
|
||||||
|
} from "../../controllers/v1/secretsFolderController";
|
||||||
|
import { ADMIN, MEMBER } from "../../variables";
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/',
|
"/",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'body'
|
locationWorkspaceId: "body",
|
||||||
}),
|
}),
|
||||||
body('workspaceId').exists(),
|
body("workspaceId").exists(),
|
||||||
body('environment').exists(),
|
body("environment").exists(),
|
||||||
body('folderName').exists(),
|
body("folderName").exists(),
|
||||||
body('parentFolderId'),
|
body("parentFolderId"),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
createFolder
|
createFolder
|
||||||
);
|
);
|
||||||
|
|
||||||
router.delete(
|
router.patch(
|
||||||
'/:folderId',
|
"/:folderId",
|
||||||
requireAuth({
|
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,
|
validateRequest,
|
||||||
deleteFolder
|
deleteFolder
|
||||||
);
|
);
|
||||||
|
|
||||||
router.get(
|
router.get(
|
||||||
'/:folderId',
|
"/",
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: ['jwt']
|
acceptedAuthModes: ["jwt"],
|
||||||
}),
|
}),
|
||||||
param('folderId').exists(),
|
query("workspaceId").exists().isString().trim(),
|
||||||
|
query("environment").exists().isString().trim(),
|
||||||
|
query("parentFolderId").optional().isString().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
getFolderById
|
getFolders
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
export default router;
|
export default router;
|
||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
requireAuth,
|
requireAuth,
|
||||||
requireWorkspaceAuth,
|
requireWorkspaceAuth,
|
||||||
requireSecretsAuth,
|
requireSecretsAuth,
|
||||||
validateRequest
|
validateRequest,
|
||||||
} from '../../middleware';
|
} from '../../middleware';
|
||||||
import { validateClientForSecrets } from '../../validation';
|
import { validateClientForSecrets } from '../../validation';
|
||||||
import { query, body } from 'express-validator';
|
import { query, body } from 'express-validator';
|
||||||
@@ -20,22 +20,25 @@ import {
|
|||||||
AUTH_MODE_JWT,
|
AUTH_MODE_JWT,
|
||||||
AUTH_MODE_SERVICE_ACCOUNT,
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
AUTH_MODE_SERVICE_TOKEN,
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
AUTH_MODE_API_KEY
|
AUTH_MODE_API_KEY,
|
||||||
} from '../../variables';
|
} from '../../variables';
|
||||||
import {
|
import { BatchSecretRequest } from '../../types/secret';
|
||||||
BatchSecretRequest
|
|
||||||
} from '../../types/secret';
|
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/batch',
|
'/batch',
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
acceptedAuthModes: [
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'body'
|
locationWorkspaceId: 'body',
|
||||||
}),
|
}),
|
||||||
body('workspaceId').exists().isString().trim(),
|
body('workspaceId').exists().isString().trim(),
|
||||||
|
body('folderId').default('root').isString().trim(),
|
||||||
body('environment').exists().isString().trim(),
|
body('environment').exists().isString().trim(),
|
||||||
body('requests')
|
body('requests')
|
||||||
.exists()
|
.exists()
|
||||||
@@ -43,13 +46,15 @@ router.post(
|
|||||||
if (Array.isArray(requests)) {
|
if (Array.isArray(requests)) {
|
||||||
const secretIds = requests
|
const secretIds = requests
|
||||||
.map((request) => request.secret._id)
|
.map((request) => request.secret._id)
|
||||||
.filter((secretId) => secretId !== undefined)
|
.filter((secretId) => secretId !== undefined);
|
||||||
|
|
||||||
if (secretIds.length > 0) {
|
if (secretIds.length > 0) {
|
||||||
req.secrets = await validateClientForSecrets({
|
req.secrets = await validateClientForSecrets({
|
||||||
authData: req.authData,
|
authData: req.authData,
|
||||||
secretIds: secretIds.map((secretId: string) => new Types.ObjectId(secretId)),
|
secretIds: secretIds.map(
|
||||||
requiredPermissions: []
|
(secretId: string) => new Types.ObjectId(secretId)
|
||||||
|
),
|
||||||
|
requiredPermissions: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,24 +68,30 @@ router.post(
|
|||||||
'/',
|
'/',
|
||||||
body('workspaceId').exists().isString().trim(),
|
body('workspaceId').exists().isString().trim(),
|
||||||
body('environment').exists().isString().trim(),
|
body('environment').exists().isString().trim(),
|
||||||
|
body('folderId').default('root').isString().trim(),
|
||||||
body('secrets')
|
body('secrets')
|
||||||
.exists()
|
.exists()
|
||||||
.custom((value) => {
|
.custom((value) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// case: create multiple secrets
|
// case: create multiple secrets
|
||||||
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
if (value.length === 0)
|
||||||
|
throw new Error('secrets cannot be an empty array');
|
||||||
for (const secret of value) {
|
for (const secret of value) {
|
||||||
if (
|
if (
|
||||||
!secret.type ||
|
!secret.type ||
|
||||||
!(secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED) ||
|
!(
|
||||||
|
secret.type === SECRET_PERSONAL || secret.type === SECRET_SHARED
|
||||||
|
) ||
|
||||||
!secret.secretKeyCiphertext ||
|
!secret.secretKeyCiphertext ||
|
||||||
!secret.secretKeyIV ||
|
!secret.secretKeyIV ||
|
||||||
!secret.secretKeyTag ||
|
!secret.secretKeyTag ||
|
||||||
(typeof secret.secretValueCiphertext !== 'string') ||
|
typeof secret.secretValueCiphertext !== 'string' ||
|
||||||
!secret.secretValueIV ||
|
!secret.secretValueIV ||
|
||||||
!secret.secretValueTag
|
!secret.secretValueTag
|
||||||
) {
|
) {
|
||||||
throw new Error('secrets array must contain objects that have required secret properties');
|
throw new Error(
|
||||||
|
'secrets array must contain objects that have required secret properties'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === 'object') {
|
||||||
@@ -95,23 +106,29 @@ router.post(
|
|||||||
!value.secretValueIV ||
|
!value.secretValueIV ||
|
||||||
!value.secretValueTag
|
!value.secretValueTag
|
||||||
) {
|
) {
|
||||||
throw new Error('secrets object is missing required secret properties');
|
throw new Error(
|
||||||
|
'secrets object is missing required secret properties'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('secrets must be an object or an array of objects')
|
throw new Error('secrets must be an object or an array of objects');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
acceptedAuthModes: [
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'body',
|
locationWorkspaceId: 'body',
|
||||||
locationEnvironment: 'body',
|
locationEnvironment: 'body',
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
}),
|
}),
|
||||||
secretsController.createSecrets
|
secretsController.createSecrets
|
||||||
);
|
);
|
||||||
@@ -121,15 +138,21 @@ router.get(
|
|||||||
query('workspaceId').exists().trim(),
|
query('workspaceId').exists().trim(),
|
||||||
query('environment').exists().trim(),
|
query('environment').exists().trim(),
|
||||||
query('tagSlugs'),
|
query('tagSlugs'),
|
||||||
|
query('folderId').default('root').isString().trim(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN, AUTH_MODE_SERVICE_ACCOUNT]
|
acceptedAuthModes: [
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
AUTH_MODE_SERVICE_ACCOUNT,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
requireWorkspaceAuth({
|
requireWorkspaceAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
locationWorkspaceId: 'query',
|
locationWorkspaceId: 'query',
|
||||||
locationEnvironment: 'query',
|
locationEnvironment: 'query',
|
||||||
requiredPermissions: [PERMISSION_READ_SECRETS]
|
requiredPermissions: [PERMISSION_READ_SECRETS],
|
||||||
}),
|
}),
|
||||||
secretsController.getSecrets
|
secretsController.getSecrets
|
||||||
);
|
);
|
||||||
@@ -141,34 +164,35 @@ router.patch(
|
|||||||
.custom((value) => {
|
.custom((value) => {
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// case: update multiple secrets
|
// case: update multiple secrets
|
||||||
if (value.length === 0) throw new Error('secrets cannot be an empty array')
|
if (value.length === 0)
|
||||||
|
throw new Error('secrets cannot be an empty array');
|
||||||
for (const secret of value) {
|
for (const secret of value) {
|
||||||
if (
|
if (!secret.id) {
|
||||||
!secret.id
|
|
||||||
) {
|
|
||||||
throw new Error('Each secret must contain a ID property');
|
throw new Error('Each secret must contain a ID property');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (typeof value === 'object') {
|
} else if (typeof value === 'object') {
|
||||||
// case: update 1 secret
|
// case: update 1 secret
|
||||||
if (
|
if (!value.id) {
|
||||||
!value.id
|
|
||||||
) {
|
|
||||||
throw new Error('secret must contain a ID property');
|
throw new Error('secret must contain a ID property');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
throw new Error('secrets must be an object or an array of objects')
|
throw new Error('secrets must be an object or an array of objects');
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
acceptedAuthModes: [
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
requireSecretsAuth({
|
requireSecretsAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
}),
|
}),
|
||||||
secretsController.updateSecrets
|
secretsController.updateSecrets
|
||||||
);
|
);
|
||||||
@@ -183,8 +207,9 @@ router.delete(
|
|||||||
|
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
// case: delete multiple secrets
|
// case: delete multiple secrets
|
||||||
if (value.length === 0) throw new Error('secrets cannot be an empty array');
|
if (value.length === 0)
|
||||||
return value.every((id: string) => typeof id === 'string')
|
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');
|
throw new Error('secretIds must be a string or an array of strings');
|
||||||
@@ -193,11 +218,15 @@ router.delete(
|
|||||||
.isEmpty(),
|
.isEmpty(),
|
||||||
validateRequest,
|
validateRequest,
|
||||||
requireAuth({
|
requireAuth({
|
||||||
acceptedAuthModes: [AUTH_MODE_JWT, AUTH_MODE_API_KEY, AUTH_MODE_SERVICE_TOKEN]
|
acceptedAuthModes: [
|
||||||
|
AUTH_MODE_JWT,
|
||||||
|
AUTH_MODE_API_KEY,
|
||||||
|
AUTH_MODE_SERVICE_TOKEN,
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
requireSecretsAuth({
|
requireSecretsAuth({
|
||||||
acceptedRoles: [ADMIN, MEMBER],
|
acceptedRoles: [ADMIN, MEMBER],
|
||||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
requiredPermissions: [PERMISSION_WRITE_SECRETS],
|
||||||
}),
|
}),
|
||||||
secretsController.deleteSecrets
|
secretsController.deleteSecrets
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,189 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const folderBfsTraversal = async (
|
||||||
|
root: TFolderSchema,
|
||||||
|
callback: (data: TFolderSchema) => void | Promise<void>
|
||||||
|
) => {
|
||||||
|
const queue = [root];
|
||||||
|
while (queue.length) {
|
||||||
|
const folder = queue.pop() as TFolderSchema;
|
||||||
|
await callback(folder);
|
||||||
|
queue.push(...folder.children);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// bfs and then append to the folder
|
||||||
|
const appendChild = (folders: TFolderSchema, folderName: string) => {
|
||||||
|
const folder = folders.children.find(({ name }) => name === folderName);
|
||||||
|
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;
|
||||||
|
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<string, number> = {};
|
||||||
|
|
||||||
|
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;
|
||||||
|
};
|
||||||
Vendored
+17
-24
@@ -1,29 +1,37 @@
|
|||||||
import { Types } from 'mongoose';
|
import { Types } from "mongoose";
|
||||||
import { Assign, Omit } from 'utility-types';
|
import { Assign, Omit } from "utility-types";
|
||||||
import { ISecret } from '../../models';
|
import { ISecret } from "../../models";
|
||||||
import { mongo } from 'mongoose';
|
import { mongo } from "mongoose";
|
||||||
|
|
||||||
// Everything is required, except the omitted types
|
// Everything is required, except the omitted types
|
||||||
export type CreateSecretRequestBody = Omit<ISecret, "user" | "version" | "environment" | "workspace">;
|
export type CreateSecretRequestBody = Omit<
|
||||||
|
ISecret,
|
||||||
|
"user" | "version" | "environment" | "workspace"
|
||||||
|
>;
|
||||||
|
|
||||||
// Omit the listed properties, then make everything optional and then make _id required
|
// Omit the listed properties, then make everything optional and then make _id required
|
||||||
export type ModifySecretRequestBody = Assign<Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>, { _id: string }>;
|
export type ModifySecretRequestBody = Assign<
|
||||||
|
Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>,
|
||||||
|
{ _id: string }
|
||||||
|
>;
|
||||||
|
|
||||||
// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading
|
// Used for modeling sanitized secrets before uplaod. To be used for converting user input for uploading
|
||||||
export type SanitizedSecretModify = Partial<Omit<ISecret, "user" | "version" | "environment" | "workspace">>;
|
export type SanitizedSecretModify = Partial<
|
||||||
|
Omit<ISecret, "user" | "version" | "environment" | "workspace">
|
||||||
|
>;
|
||||||
|
|
||||||
// Everything is required, except the omitted types
|
// Everything is required, except the omitted types
|
||||||
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;
|
export type SanitizedSecretForCreate = Omit<ISecret, "version" | "_id">;
|
||||||
|
|
||||||
export interface BatchSecretRequest {
|
export interface BatchSecretRequest {
|
||||||
id: string;
|
id: string;
|
||||||
method: 'POST' | 'PATCH' | 'DELETE';
|
method: "POST" | "PATCH" | "DELETE";
|
||||||
secret: Secret;
|
secret: Secret;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BatchSecret {
|
export interface BatchSecret {
|
||||||
_id: string;
|
_id: string;
|
||||||
type: 'shared' | 'personal',
|
type: "shared" | "personal";
|
||||||
secretBlindIndex: string;
|
secretBlindIndex: string;
|
||||||
secretKeyCiphertext: string;
|
secretKeyCiphertext: string;
|
||||||
secretKeyIV: string;
|
secretKeyIV: string;
|
||||||
@@ -36,18 +44,3 @@ export interface BatchSecret {
|
|||||||
secretCommentTag: string;
|
secretCommentTag: string;
|
||||||
tags: 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[];
|
|
||||||
}
|
|
||||||
+71
-71
@@ -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) => {
|
// export const getFolderPath = async (folderId: string) => {
|
||||||
let currentFolder = await Folder.findById(folderId);
|
// let currentFolder = await Folder.findById(folderId);
|
||||||
const pathSegments = [];
|
// const pathSegments = [];
|
||||||
|
|
||||||
while (currentFolder) {
|
// while (currentFolder) {
|
||||||
pathSegments.unshift(currentFolder.name);
|
// pathSegments.unshift(currentFolder.name);
|
||||||
currentFolder = currentFolder.parent ? await Folder.findById(currentFolder.parent) : null;
|
// 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.
|
// 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 workspaceId - The ID of the workspace to search in.
|
||||||
@param environment - The environment to search in.
|
// @param environment - The environment to search in.
|
||||||
@param secretPath - The secret path to search for.
|
// @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.
|
// @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.
|
// @throws Error if the specified secret path is not found.
|
||||||
*/
|
// */
|
||||||
export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => {
|
// export const getFolderIdFromPath = async (workspaceId: string, environment: string, secretPath: string) => {
|
||||||
const secretPathParts = secretPath.split("/").filter(path => path != "")
|
// const secretPathParts = secretPath.split("/").filter(path => path != "")
|
||||||
if (secretPathParts.length <= 1) {
|
// if (secretPathParts.length <= 1) {
|
||||||
return undefined // root folder, so no folder id
|
// return undefined // root folder, so no folder id
|
||||||
}
|
// }
|
||||||
|
|
||||||
const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment })
|
// const folderId = await Folder.find({ path: secretPath, workspace: workspaceId, environment: environment })
|
||||||
if (!folderId) {
|
// if (!folderId) {
|
||||||
throw Error("Secret path not found")
|
// throw Error("Secret path not found")
|
||||||
}
|
// }
|
||||||
|
|
||||||
return folderId
|
// return folderId
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Cleans up a path by removing empty parts, duplicate slashes,
|
// * Cleans up a path by removing empty parts, duplicate slashes,
|
||||||
* and ensuring it starts with ROOT_FOLDER_PATH.
|
// * and ensuring it starts with ROOT_FOLDER_PATH.
|
||||||
* @param path - The input path to clean up.
|
// * @param path - The input path to clean up.
|
||||||
* @returns The cleaned-up path string.
|
// * @returns The cleaned-up path string.
|
||||||
*/
|
// */
|
||||||
export const normalizePath = (path: string) => {
|
// export const normalizePath = (path: string) => {
|
||||||
if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) {
|
// if (path == undefined || path == "" || path == ROOT_FOLDER_PATH) {
|
||||||
return ROOT_FOLDER_PATH
|
// return ROOT_FOLDER_PATH
|
||||||
}
|
// }
|
||||||
|
|
||||||
const pathParts = path.split("/").filter(part => part != "")
|
// const pathParts = path.split("/").filter(part => part != "")
|
||||||
const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/")
|
// const cleanPathString = ROOT_FOLDER_PATH + pathParts.join("/")
|
||||||
|
|
||||||
return cleanPathString
|
// return cleanPathString
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => {
|
// export const getFoldersInDirectory = async (workspaceId: string, environment: string, pathString: string) => {
|
||||||
const normalizedPath = normalizePath(pathString)
|
// const normalizedPath = normalizePath(pathString)
|
||||||
const foldersInDirectory = await Folder.find({
|
// const foldersInDirectory = await Folder.find({
|
||||||
workspace: workspaceId,
|
// workspace: workspaceId,
|
||||||
environment: environment,
|
// environment: environment,
|
||||||
parentPath: normalizedPath,
|
// parentPath: normalizedPath,
|
||||||
});
|
// });
|
||||||
|
|
||||||
return foldersInDirectory;
|
// return foldersInDirectory;
|
||||||
}
|
// }
|
||||||
|
|
||||||
/**
|
// /**
|
||||||
* Returns the parent path of the given path.
|
// * Returns the parent path of the given path.
|
||||||
* @param path - The input path.
|
// * @param path - The input path.
|
||||||
* @returns The parent path string.
|
// * @returns The parent path string.
|
||||||
*/
|
// */
|
||||||
export const getParentPath = (path: string) => {
|
// export const getParentPath = (path: string) => {
|
||||||
const normalizedPath = normalizePath(path);
|
// const normalizedPath = normalizePath(path);
|
||||||
const folderParts = normalizedPath.split('/').filter(part => part !== '');
|
// const folderParts = normalizedPath.split('/').filter(part => part !== '');
|
||||||
|
|
||||||
let folderParent = ROOT_FOLDER_PATH;
|
// let folderParent = ROOT_FOLDER_PATH;
|
||||||
if (folderParts.length > 1) {
|
// if (folderParts.length > 1) {
|
||||||
folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/');
|
// folderParent = ROOT_FOLDER_PATH + folderParts.slice(0, folderParts.length - 1).join('/');
|
||||||
}
|
// }
|
||||||
|
|
||||||
return folderParent;
|
// return folderParent;
|
||||||
}
|
// }
|
||||||
|
|
||||||
export const validateFolderName = (folderName: string) => {
|
// export const validateFolderName = (folderName: string) => {
|
||||||
const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
// const validNameRegex = /^[a-zA-Z0-9-_]+$/;
|
||||||
return validNameRegex.test(folderName);
|
// return validNameRegex.test(folderName);
|
||||||
}
|
// }
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
Original work Copyright (c) 2016, Nikolay Nemshilov <[email protected]>
|
||||||
|
Modified work Copyright (c) 2016, David Banham <[email protected]>
|
||||||
|
|
||||||
|
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
|
||||||
|
};
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import crypto from 'crypto';
|
import crypto from "crypto";
|
||||||
import { encryptSymmetric128BitHexKeyUTF8 } from '../crypto';
|
import { Types } from "mongoose";
|
||||||
import { EESecretService } from '../../ee/services';
|
import { encryptSymmetric128BitHexKeyUTF8 } from "../crypto";
|
||||||
import { SecretVersion } from '../../ee/models';
|
import { EESecretService } from "../../ee/services";
|
||||||
|
import { ISecretVersion, SecretSnapshot, SecretVersion } from "../../ee/models";
|
||||||
import {
|
import {
|
||||||
Secret,
|
Secret,
|
||||||
ISecret,
|
ISecret,
|
||||||
@@ -10,21 +11,15 @@ import {
|
|||||||
Bot,
|
Bot,
|
||||||
BackupPrivateKey,
|
BackupPrivateKey,
|
||||||
IntegrationAuth,
|
IntegrationAuth,
|
||||||
} from '../../models';
|
} from "../../models";
|
||||||
import {
|
import { generateKeyPair } from "../../utils/crypto";
|
||||||
generateKeyPair
|
import { client, getEncryptionKey, getRootEncryptionKey } from "../../config";
|
||||||
} from '../../utils/crypto';
|
|
||||||
import {
|
|
||||||
client,
|
|
||||||
getEncryptionKey,
|
|
||||||
getRootEncryptionKey
|
|
||||||
} from '../../config';
|
|
||||||
import {
|
import {
|
||||||
ALGORITHM_AES_256_GCM,
|
ALGORITHM_AES_256_GCM,
|
||||||
ENCODING_SCHEME_UTF8,
|
ENCODING_SCHEME_UTF8,
|
||||||
ENCODING_SCHEME_BASE64
|
ENCODING_SCHEME_BASE64,
|
||||||
} from '../../variables';
|
} from "../../variables";
|
||||||
import { InternalServerError } from '../errors';
|
import { InternalServerError } from "../errors";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backfill secrets to ensure that they're all versioned and have
|
* Backfill secrets to ensure that they're all versioned and have
|
||||||
@@ -64,12 +59,12 @@ export const backfillSecretVersions = async () => {
|
|||||||
workspace: s.workspace,
|
workspace: s.workspace,
|
||||||
environment: s.environment,
|
environment: s.environment,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backfill workspace bots to ensure that every workspace has a bot
|
* Backfill workspace bots to ensure that every workspace has a bot
|
||||||
@@ -78,11 +73,11 @@ export const backfillBots = async () => {
|
|||||||
const encryptionKey = await getEncryptionKey();
|
const encryptionKey = await getEncryptionKey();
|
||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const workspaceIdsWithBot = await Bot.distinct('workspace');
|
const workspaceIdsWithBot = await Bot.distinct("workspace");
|
||||||
const workspaceIdsToAddBot = await Workspace.distinct('_id', {
|
const workspaceIdsToAddBot = await Workspace.distinct("_id", {
|
||||||
_id: {
|
_id: {
|
||||||
$nin: workspaceIdsWithBot
|
$nin: workspaceIdsWithBot,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workspaceIdsToAddBot.length === 0) return;
|
if (workspaceIdsToAddBot.length === 0) return;
|
||||||
@@ -95,11 +90,11 @@ export const backfillBots = async () => {
|
|||||||
const {
|
const {
|
||||||
ciphertext: encryptedPrivateKey,
|
ciphertext: encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag
|
tag,
|
||||||
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
} = client.encryptSymmetric(privateKey, rootEncryptionKey);
|
||||||
|
|
||||||
return new Bot({
|
return new Bot({
|
||||||
name: 'Infisical Bot',
|
name: "Infisical Bot",
|
||||||
workspace: workspaceToAddBot,
|
workspace: workspaceToAddBot,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
publicKey,
|
publicKey,
|
||||||
@@ -107,20 +102,20 @@ export const backfillBots = async () => {
|
|||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_BASE64
|
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||||
});
|
});
|
||||||
} else if (encryptionKey) {
|
} else if (encryptionKey) {
|
||||||
const {
|
const {
|
||||||
ciphertext: encryptedPrivateKey,
|
ciphertext: encryptedPrivateKey,
|
||||||
iv,
|
iv,
|
||||||
tag
|
tag,
|
||||||
} = encryptSymmetric128BitHexKeyUTF8({
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: privateKey,
|
plaintext: privateKey,
|
||||||
key: encryptionKey
|
key: encryptionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Bot({
|
return new Bot({
|
||||||
name: 'Infisical Bot',
|
name: "Infisical Bot",
|
||||||
workspace: workspaceToAddBot,
|
workspace: workspaceToAddBot,
|
||||||
isActive: false,
|
isActive: false,
|
||||||
publicKey,
|
publicKey,
|
||||||
@@ -128,47 +123,49 @@ export const backfillBots = async () => {
|
|||||||
iv,
|
iv,
|
||||||
tag,
|
tag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw InternalServerError({
|
throw InternalServerError({
|
||||||
message: 'Failed to backfill workspace bots due to missing encryption key'
|
message:
|
||||||
|
"Failed to backfill workspace bots due to missing encryption key",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
await Bot.insertMany(botsToInsert);
|
await Bot.insertMany(botsToInsert);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backfill secret blind index data to ensure that every workspace
|
* Backfill secret blind index data to ensure that every workspace
|
||||||
* has a secret blind index data
|
* has a secret blind index data
|
||||||
*/
|
*/
|
||||||
export const backfillSecretBlindIndexData = async () => {
|
export const backfillSecretBlindIndexData = async () => {
|
||||||
|
|
||||||
const encryptionKey = await getEncryptionKey();
|
const encryptionKey = await getEncryptionKey();
|
||||||
const rootEncryptionKey = await getRootEncryptionKey();
|
const rootEncryptionKey = await getRootEncryptionKey();
|
||||||
|
|
||||||
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct('workspace');
|
const workspaceIdsBlindIndexed = await SecretBlindIndexData.distinct(
|
||||||
const workspaceIdsToBlindIndex = await Workspace.distinct('_id', {
|
"workspace"
|
||||||
|
);
|
||||||
|
const workspaceIdsToBlindIndex = await Workspace.distinct("_id", {
|
||||||
_id: {
|
_id: {
|
||||||
$nin: workspaceIdsBlindIndexed
|
$nin: workspaceIdsBlindIndexed,
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (workspaceIdsToBlindIndex.length === 0) return;
|
if (workspaceIdsToBlindIndex.length === 0) return;
|
||||||
|
|
||||||
const secretBlindIndexDataToInsert = await Promise.all(
|
const secretBlindIndexDataToInsert = await Promise.all(
|
||||||
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
|
workspaceIdsToBlindIndex.map(async (workspaceToBlindIndex) => {
|
||||||
const salt = crypto.randomBytes(16).toString('base64');
|
const salt = crypto.randomBytes(16).toString("base64");
|
||||||
|
|
||||||
if (rootEncryptionKey) {
|
if (rootEncryptionKey) {
|
||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag
|
tag: saltTag,
|
||||||
} = client.encryptSymmetric(salt, rootEncryptionKey)
|
} = client.encryptSymmetric(salt, rootEncryptionKey);
|
||||||
|
|
||||||
return new SecretBlindIndexData({
|
return new SecretBlindIndexData({
|
||||||
workspace: workspaceToBlindIndex,
|
workspace: workspaceToBlindIndex,
|
||||||
@@ -176,16 +173,16 @@ export const backfillSecretBlindIndexData = async () => {
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_BASE64
|
keyEncoding: ENCODING_SCHEME_BASE64,
|
||||||
});
|
});
|
||||||
} else if (encryptionKey) {
|
} else if (encryptionKey) {
|
||||||
const {
|
const {
|
||||||
ciphertext: encryptedSaltCiphertext,
|
ciphertext: encryptedSaltCiphertext,
|
||||||
iv: saltIV,
|
iv: saltIV,
|
||||||
tag: saltTag
|
tag: saltTag,
|
||||||
} = encryptSymmetric128BitHexKeyUTF8({
|
} = encryptSymmetric128BitHexKeyUTF8({
|
||||||
plaintext: salt,
|
plaintext: salt,
|
||||||
key: encryptionKey
|
key: encryptionKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
return new SecretBlindIndexData({
|
return new SecretBlindIndexData({
|
||||||
@@ -194,18 +191,19 @@ export const backfillSecretBlindIndexData = async () => {
|
|||||||
saltIV,
|
saltIV,
|
||||||
saltTag,
|
saltTag,
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
throw InternalServerError({
|
throw InternalServerError({
|
||||||
message: 'Failed to backfill secret blind index data due to missing encryption key'
|
message:
|
||||||
|
"Failed to backfill secret blind index data due to missing encryption key",
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
|
SecretBlindIndexData.insertMany(secretBlindIndexDataToInsert);
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Backfill Secret, SecretVersion, SecretBlindIndexData, Bot,
|
* Backfill Secret, SecretVersion, SecretBlindIndexData, Bot,
|
||||||
@@ -213,22 +211,21 @@ export const backfillSecretBlindIndexData = async () => {
|
|||||||
* they all have encryption metadata documented
|
* they all have encryption metadata documented
|
||||||
*/
|
*/
|
||||||
export const backfillEncryptionMetadata = async () => {
|
export const backfillEncryptionMetadata = async () => {
|
||||||
|
|
||||||
// backfill secret encryption metadata
|
// backfill secret encryption metadata
|
||||||
await Secret.updateMany(
|
await Secret.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -236,17 +233,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await SecretVersion.updateMany(
|
await SecretVersion.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -254,17 +251,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await SecretBlindIndexData.updateMany(
|
await SecretBlindIndexData.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -272,17 +269,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await Bot.updateMany(
|
await Bot.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -290,17 +287,17 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await BackupPrivateKey.updateMany(
|
await BackupPrivateKey.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -308,17 +305,69 @@ export const backfillEncryptionMetadata = async () => {
|
|||||||
await IntegrationAuth.updateMany(
|
await IntegrationAuth.updateMany(
|
||||||
{
|
{
|
||||||
algorithm: {
|
algorithm: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
},
|
},
|
||||||
keyEncoding: {
|
keyEncoding: {
|
||||||
$exists: false
|
$exists: false,
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
$set: {
|
$set: {
|
||||||
algorithm: ALGORITHM_AES_256_GCM,
|
algorithm: ALGORITHM_AES_256_GCM,
|
||||||
keyEncoding: ENCODING_SCHEME_UTF8
|
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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<string, Array<ISecretVersion>> = {};
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,26 +3,27 @@ import { DatabaseService, TelemetryService } from '../../services';
|
|||||||
import { setTransporter } from '../../helpers/nodemailer';
|
import { setTransporter } from '../../helpers/nodemailer';
|
||||||
import { EELicenseService } from '../../ee/services';
|
import { EELicenseService } from '../../ee/services';
|
||||||
import { initSmtp } from '../../services/smtp';
|
import { initSmtp } from '../../services/smtp';
|
||||||
import { createTestUserForDevelopment } from '../addDevelopmentUser'
|
import { createTestUserForDevelopment } from '../addDevelopmentUser';
|
||||||
import {
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
validateEncryptionKeysConfig
|
const { patchRouterParam } = require('../patchAsyncRoutes');
|
||||||
} from './validateConfig';
|
import { validateEncryptionKeysConfig } from './validateConfig';
|
||||||
import {
|
import {
|
||||||
backfillSecretVersions,
|
backfillSecretVersions,
|
||||||
backfillBots,
|
backfillBots,
|
||||||
backfillSecretBlindIndexData,
|
backfillSecretBlindIndexData,
|
||||||
backfillEncryptionMetadata
|
backfillEncryptionMetadata,
|
||||||
|
backfillSecretFolders,
|
||||||
} from './backfillData';
|
} from './backfillData';
|
||||||
import {
|
import {
|
||||||
reencryptBotPrivateKeys,
|
reencryptBotPrivateKeys,
|
||||||
reencryptSecretBlindIndexDataSalts
|
reencryptSecretBlindIndexDataSalts,
|
||||||
} from './reencryptData';
|
} from './reencryptData';
|
||||||
import {
|
import {
|
||||||
getNodeEnv,
|
getNodeEnv,
|
||||||
getMongoURL,
|
getMongoURL,
|
||||||
getSentryDSN,
|
getSentryDSN,
|
||||||
getClientSecretGoogle,
|
getClientSecretGoogle,
|
||||||
getClientIdGoogle
|
getClientIdGoogle,
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
import { initializePassport } from '../auth';
|
import { initializePassport } from '../auth';
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ import { initializePassport } from '../auth';
|
|||||||
* - Re-encrypting data
|
* - Re-encrypting data
|
||||||
*/
|
*/
|
||||||
export const setup = async () => {
|
export const setup = async () => {
|
||||||
|
patchRouterParam();
|
||||||
await validateEncryptionKeysConfig();
|
await validateEncryptionKeysConfig();
|
||||||
await TelemetryService.logTelemetryMessage();
|
await TelemetryService.logTelemetryMessage();
|
||||||
|
|
||||||
@@ -56,6 +58,14 @@ export const setup = async () => {
|
|||||||
await initializePassport();
|
await initializePassport();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 the database connection
|
||||||
|
await DatabaseService.initDatabase(await getMongoURL());
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* NOTE: the order in this setup function is critical.
|
* NOTE: the order in this setup function is critical.
|
||||||
* It is important to backfill data before performing any re-encryption functionality.
|
* It is important to backfill data before performing any re-encryption functionality.
|
||||||
@@ -66,20 +76,20 @@ export const setup = async () => {
|
|||||||
await backfillBots();
|
await backfillBots();
|
||||||
await backfillSecretBlindIndexData();
|
await backfillSecretBlindIndexData();
|
||||||
await backfillEncryptionMetadata();
|
await backfillEncryptionMetadata();
|
||||||
|
await backfillSecretFolders();
|
||||||
|
|
||||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||||
// await reencryptBotPrivateKeys();
|
await reencryptBotPrivateKeys();
|
||||||
// await reencryptSecretBlindIndexDataSalts();
|
await reencryptSecretBlindIndexDataSalts();
|
||||||
|
|
||||||
// initializing Sentry
|
// initializing Sentry
|
||||||
Sentry.init({
|
Sentry.init({
|
||||||
dsn: await getSentryDSN(),
|
dsn: await getSentryDSN(),
|
||||||
tracesSampleRate: 1.0,
|
tracesSampleRate: 1.0,
|
||||||
debug: (await getNodeEnv()) === 'production' ? false : true,
|
debug: (await getNodeEnv()) === 'production' ? false : true,
|
||||||
environment: (await getNodeEnv())
|
environment: await getNodeEnv(),
|
||||||
});
|
});
|
||||||
|
|
||||||
await createTestUserForDevelopment();
|
await createTestUserForDevelopment();
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,17 @@ import { useOrganization, useWorkspace } from '@app/context';
|
|||||||
|
|
||||||
import { Select, SelectItem, Tooltip } from '../v2';
|
import { Select, SelectItem, Tooltip } from '../v2';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
pageName: string;
|
||||||
|
isProjectRelated?: boolean;
|
||||||
|
isOrganizationRelated?: boolean;
|
||||||
|
currentEnv?: string;
|
||||||
|
userAvailableEnvs?: any[];
|
||||||
|
onEnvChange?: (slug: string) => void;
|
||||||
|
folders?: Array<{ id: string; name: string }>;
|
||||||
|
isFolderMode?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: make links clickable and clean up
|
// TODO: make links clickable and clean up
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -29,15 +40,10 @@ export default function NavHeader({
|
|||||||
isOrganizationRelated,
|
isOrganizationRelated,
|
||||||
currentEnv,
|
currentEnv,
|
||||||
userAvailableEnvs,
|
userAvailableEnvs,
|
||||||
onEnvChange
|
onEnvChange,
|
||||||
}: {
|
folders,
|
||||||
pageName: string;
|
isFolderMode
|
||||||
isProjectRelated?: boolean;
|
}: Props): JSX.Element {
|
||||||
isOrganizationRelated?: boolean;
|
|
||||||
currentEnv?: string;
|
|
||||||
userAvailableEnvs?: any[];
|
|
||||||
onEnvChange?: (slug: string) => void;
|
|
||||||
}): JSX.Element {
|
|
||||||
const { currentWorkspace } = useWorkspace();
|
const { currentWorkspace } = useWorkspace();
|
||||||
const { currentOrg } = useOrganization();
|
const { currentOrg } = useOrganization();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -95,6 +101,26 @@ export default function NavHeader({
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{isFolderMode &&
|
||||||
|
folders?.map(({ id, name }, index) => {
|
||||||
|
const query = { ...router.query };
|
||||||
|
if (name !== 'root') query.folderId = id;
|
||||||
|
else delete query.folderId;
|
||||||
|
return (
|
||||||
|
<div className="flex items-center space-x-3" key={`breadcrumb-folder-${id}`}>
|
||||||
|
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
|
||||||
|
{index + 1 === folders?.length ? (
|
||||||
|
<span className="text-sm font-semibold text-bunker-300">{name}</span>
|
||||||
|
) : (
|
||||||
|
<Link passHref legacyBehavior href={{ pathname: '/dashboard/[id]', query }}>
|
||||||
|
<a className="text-sm font-semibold capitalize text-primary/80 hover:text-primary">
|
||||||
|
{name}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
|
import { SizeProp } from '@fortawesome/fontawesome-svg-core';
|
||||||
import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
import { faCubesStacked, IconDefinition } from '@fortawesome/free-solid-svg-icons';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import { twMerge } from 'tailwind-merge';
|
import { twMerge } from 'tailwind-merge';
|
||||||
@@ -8,13 +9,25 @@ type Props = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
children?: ReactNode;
|
children?: ReactNode;
|
||||||
icon?: IconDefinition;
|
icon?: IconDefinition;
|
||||||
|
iconSize?: SizeProp;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EmptyState = ({ title, className, children, icon = faCubesStacked }: Props) => (
|
export const EmptyState = ({
|
||||||
<div className={twMerge('flex w-full bg-bunker-700 flex-col items-center px-2 pt-6 text-bunker-300', className)}>
|
title,
|
||||||
<FontAwesomeIcon icon={icon} size="2x" className='mr-4' />
|
className,
|
||||||
<div className='flex flex-row items-center py-4'>
|
children,
|
||||||
<div className="text-bunker-300 text-sm">{title}</div>
|
icon = faCubesStacked,
|
||||||
|
iconSize = '2x'
|
||||||
|
}: Props) => (
|
||||||
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
'flex w-full flex-col items-center bg-bunker-700 px-2 pt-6 text-bunker-300',
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={icon} size={iconSize} className="mr-4" />
|
||||||
|
<div className="flex flex-row items-center py-4">
|
||||||
|
<div className="text-sm text-bunker-300">{title}</div>
|
||||||
<div>{children}</div>
|
<div>{children}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export const Tooltip = ({
|
|||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
{...props}
|
{...props}
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
`z-20 select-none max-w-[15rem] rounded-md bg-mineshaft-800 border border-mineshaft-600 py-2 px-4 font-light text-sm text-bunker-200 shadow-md
|
`z-50 max-w-[15rem] select-none rounded-md border border-mineshaft-600 bg-mineshaft-800 py-2 px-4 text-sm font-light text-bunker-200 shadow-md
|
||||||
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
|
data-[state=delayed-open]:data-[side=top]:animate-slideDownAndFade
|
||||||
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
data-[state=delayed-open]:data-[side=right]:animate-slideLeftAndFade
|
||||||
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
data-[state=delayed-open]:data-[side=left]:animate-slideRightAndFade
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ export * from './auth';
|
|||||||
export * from './incidentContacts';
|
export * from './incidentContacts';
|
||||||
export * from './keys';
|
export * from './keys';
|
||||||
export * from './organization';
|
export * from './organization';
|
||||||
|
export * from './secretFolders';
|
||||||
export * from './secrets';
|
export * from './secrets';
|
||||||
export * from './secretSnapshots';
|
export * from './secretSnapshots';
|
||||||
export * from './serviceAccounts';
|
export * from './serviceAccounts';
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export { useCreateFolder, useDeleteFolder, useGetProjectFolders, useUpdateFolder } from './queries';
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import { useCallback } from 'react';
|
||||||
|
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { apiRequest } from '@app/config/request';
|
||||||
|
|
||||||
|
import { secretSnapshotKeys } from '../secretSnapshots/queries';
|
||||||
|
import {
|
||||||
|
CreateFolderDTO,
|
||||||
|
DeleteFolderDTO,
|
||||||
|
GetProjectFoldersDTO,
|
||||||
|
TSecretFolder,
|
||||||
|
UpdateFolderDTO
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const queryKeys = {
|
||||||
|
getSecretFolders: (workspaceId: string, environment: string, parentFolderId?: string) =>
|
||||||
|
['secret-folders', { workspaceId, environment, parentFolderId }] as const
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useGetProjectFolders = ({
|
||||||
|
workspaceId,
|
||||||
|
parentFolderId,
|
||||||
|
environment,
|
||||||
|
isPaused,
|
||||||
|
sortDir
|
||||||
|
}: GetProjectFoldersDTO) =>
|
||||||
|
useQuery({
|
||||||
|
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId),
|
||||||
|
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
|
||||||
|
'/api/v1/folders',
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
workspaceId,
|
||||||
|
environment,
|
||||||
|
parentFolderId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
select: useCallback(
|
||||||
|
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
|
||||||
|
dir,
|
||||||
|
folders: folders.sort((a, b) =>
|
||||||
|
sortDir === 'asc'
|
||||||
|
? a?.name?.localeCompare(b?.name || '')
|
||||||
|
: b?.name?.localeCompare(a?.name || '')
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
[sortDir]
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
export const useCreateFolder = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, CreateFolderDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.post('/api/v1/folders', dto);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment, parentFolderId }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useUpdateFolder = (parentFolderId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, UpdateFolderDTO>({
|
||||||
|
mutationFn: async ({ folderId, name, environment, workspaceId }) => {
|
||||||
|
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, {
|
||||||
|
name,
|
||||||
|
environment,
|
||||||
|
workspaceId
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useDeleteFolder = (parentFolderId: string) => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{}, {}, DeleteFolderDTO>({
|
||||||
|
mutationFn: async ({ folderId, environment, workspaceId }) => {
|
||||||
|
const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, {
|
||||||
|
data: {
|
||||||
|
environment,
|
||||||
|
workspaceId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { workspaceId, environment }) => {
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.count(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.list(workspaceId, environment, parentFolderId)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
export type TSecretFolder = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetProjectFoldersDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
parentFolderId?: string;
|
||||||
|
isPaused?: boolean;
|
||||||
|
sortDir?: 'asc' | 'desc';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CreateFolderDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
folderName: string;
|
||||||
|
parentFolderId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UpdateFolderDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
name: string;
|
||||||
|
folderId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeleteFolderDTO = {
|
||||||
|
workspaceId: string;
|
||||||
|
environment: string;
|
||||||
|
folderId: string;
|
||||||
|
};
|
||||||
@@ -17,18 +17,31 @@ import {
|
|||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const secretSnapshotKeys = {
|
export const secretSnapshotKeys = {
|
||||||
list: (workspaceId: string) => [{ workspaceId }, 'secret-snapshot'] as const,
|
list: (workspaceId: string, env: string, folderId?: string) =>
|
||||||
|
[{ workspaceId, env, folderId }, 'secret-snapshot'] as const,
|
||||||
snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const,
|
snapshotSecrets: (snapshotId: string) => [{ snapshotId }, 'secret-snapshot'] as const,
|
||||||
count: (workspaceId: string) => [{ workspaceId }, 'count', 'secret-snapshot']
|
count: (workspaceId: string, env: string, folderId?: string) => [
|
||||||
|
{ workspaceId, env, folderId },
|
||||||
|
'count',
|
||||||
|
'secret-snapshot'
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, offset = 0) => {
|
const fetchWorkspaceSecretSnaphots = async (
|
||||||
|
workspaceId: string,
|
||||||
|
environment: string,
|
||||||
|
folderId?: string,
|
||||||
|
limit = 10,
|
||||||
|
offset = 0
|
||||||
|
) => {
|
||||||
const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>(
|
const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>(
|
||||||
`/api/v1/workspace/${workspaceId}/secret-snapshots`,
|
`/api/v1/workspace/${workspaceId}/secret-snapshots`,
|
||||||
{
|
{
|
||||||
params: {
|
params: {
|
||||||
limit,
|
limit,
|
||||||
offset
|
offset,
|
||||||
|
environment,
|
||||||
|
folderId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -38,9 +51,16 @@ const fetchWorkspaceSecretSnaphots = async (workspaceId: string, limit = 10, off
|
|||||||
|
|
||||||
export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) =>
|
export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) =>
|
||||||
useInfiniteQuery({
|
useInfiniteQuery({
|
||||||
enabled: Boolean(dto.workspaceId),
|
enabled: Boolean(dto.workspaceId && dto.environment),
|
||||||
queryKey: secretSnapshotKeys.list(dto.workspaceId),
|
queryKey: secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folder),
|
||||||
queryFn: ({ pageParam }) => fetchWorkspaceSecretSnaphots(dto.workspaceId, dto.limit, pageParam),
|
queryFn: ({ pageParam }) =>
|
||||||
|
fetchWorkspaceSecretSnaphots(
|
||||||
|
dto.workspaceId,
|
||||||
|
dto.environment,
|
||||||
|
dto?.folder,
|
||||||
|
dto.limit,
|
||||||
|
pageParam
|
||||||
|
),
|
||||||
getNextPageParam: (lastPage, pages) =>
|
getNextPageParam: (lastPage, pages) =>
|
||||||
lastPage.length !== 0 ? pages.length * dto.limit : undefined
|
lastPage.length !== 0 ? pages.length * dto.limit : undefined
|
||||||
});
|
});
|
||||||
@@ -115,40 +135,54 @@ export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnap
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return { version: data.version, secrets: sharedSecrets, createdAt: data.createdAt };
|
return {
|
||||||
|
version: data.version,
|
||||||
|
secrets: sharedSecrets,
|
||||||
|
createdAt: data.createdAt,
|
||||||
|
folders: data.folderVersion
|
||||||
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const fetchWorkspaceSecretSnaphotCount = async (workspaceId: string) => {
|
const fetchWorkspaceSecretSnaphotCount = async (
|
||||||
|
workspaceId: string,
|
||||||
|
environment: string,
|
||||||
|
folderId?: string
|
||||||
|
) => {
|
||||||
const res = await apiRequest.get<{ count: number }>(
|
const res = await apiRequest.get<{ count: number }>(
|
||||||
`/api/v1/workspace/${workspaceId}/secret-snapshots/count`
|
`/api/v1/workspace/${workspaceId}/secret-snapshots/count`,
|
||||||
|
{
|
||||||
|
params: {
|
||||||
|
environment,
|
||||||
|
folderId
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
return res.data.count;
|
return res.data.count;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useGetWsSnapshotCount = (workspaceId: string) =>
|
export const useGetWsSnapshotCount = (workspaceId: string, env: string, folderId?: string) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
enabled: Boolean(workspaceId),
|
enabled: Boolean(workspaceId && env),
|
||||||
queryKey: secretSnapshotKeys.count(workspaceId),
|
queryKey: secretSnapshotKeys.count(workspaceId, env, folderId),
|
||||||
queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId)
|
queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId, env, folderId)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const usePerformSecretRollback = () => {
|
export const usePerformSecretRollback = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
return useMutation<{}, {}, TSecretRollbackDTO>({
|
return useMutation<{}, {}, TSecretRollbackDTO>({
|
||||||
mutationFn: async (dto) => {
|
mutationFn: async ({ workspaceId, ...dto }) => {
|
||||||
const { data } = await apiRequest.post(
|
const { data } = await apiRequest.post(
|
||||||
`/api/v1/workspace/${dto.workspaceId}/secret-snapshots/rollback`,
|
`/api/v1/workspace/${workspaceId}/secret-snapshots/rollback`,
|
||||||
{
|
dto
|
||||||
version: dto.version
|
|
||||||
}
|
|
||||||
);
|
);
|
||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, dto) => {
|
onSuccess: (_, { workspaceId, environment, folderId }) => {
|
||||||
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
|
queryClient.invalidateQueries([{ workspaceId, environment }, 'secrets']);
|
||||||
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
|
queryClient.invalidateQueries(secretSnapshotKeys.list(workspaceId, environment, folderId));
|
||||||
|
queryClient.invalidateQueries(secretSnapshotKeys.count(workspaceId, environment, folderId));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export type TWorkspaceSecretSnapshot = {
|
|||||||
|
|
||||||
export type TSnapshotSecret = Omit<TWorkspaceSecretSnapshot, 'secretVersions'> & {
|
export type TSnapshotSecret = Omit<TWorkspaceSecretSnapshot, 'secretVersions'> & {
|
||||||
secretVersions: EncryptedSecretVersion[];
|
secretVersions: EncryptedSecretVersion[];
|
||||||
|
folderVersion: Array<{ name: string; id: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSnapshotSecretProps = {
|
export type TSnapshotSecretProps = {
|
||||||
@@ -24,9 +25,13 @@ export type TSnapshotSecretProps = {
|
|||||||
export type GetWorkspaceSecretSnapshotsDTO = {
|
export type GetWorkspaceSecretSnapshotsDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
limit: number;
|
limit: number;
|
||||||
|
environment: string;
|
||||||
|
folder?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TSecretRollbackDTO = {
|
export type TSecretRollbackDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
version: number;
|
version: number;
|
||||||
|
environment: string;
|
||||||
|
folderId?: string;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -19,19 +19,24 @@ import {
|
|||||||
|
|
||||||
export const secretKeys = {
|
export const secretKeys = {
|
||||||
// this is also used in secretSnapshot part
|
// this is also used in secretSnapshot part
|
||||||
getProjectSecret: (workspaceId: string, env: string | string[]) => [
|
getProjectSecret: (workspaceId: string, env: string | string[], folderId?: string) => [
|
||||||
{ workspaceId, env },
|
{ workspaceId, env, folderId },
|
||||||
'secrets'
|
'secrets'
|
||||||
],
|
],
|
||||||
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
|
getSecretVersion: (secretId: string) => [{ secretId }, 'secret-versions']
|
||||||
};
|
};
|
||||||
|
|
||||||
const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | string[]) => {
|
const fetchProjectEncryptedSecrets = async (
|
||||||
|
workspaceId: string,
|
||||||
|
env: string | string[],
|
||||||
|
folderId?: string
|
||||||
|
) => {
|
||||||
if (typeof env === 'string') {
|
if (typeof env === 'string') {
|
||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
params: {
|
params: {
|
||||||
environment: env,
|
environment: env,
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
folderId: folderId || undefined
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return data.secrets;
|
return data.secrets;
|
||||||
@@ -46,7 +51,8 @@ const fetchProjectEncryptedSecrets = async (workspaceId: string, env: string | s
|
|||||||
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>('/api/v2/secrets', {
|
||||||
params: {
|
params: {
|
||||||
environment: envPoint,
|
environment: envPoint,
|
||||||
workspaceId
|
workspaceId,
|
||||||
|
folderId
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
allEnvData = allEnvData.concat(data.secrets);
|
allEnvData = allEnvData.concat(data.secrets);
|
||||||
@@ -63,13 +69,14 @@ export const useGetProjectSecrets = ({
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
env,
|
env,
|
||||||
decryptFileKey,
|
decryptFileKey,
|
||||||
isPaused
|
isPaused,
|
||||||
|
folderId
|
||||||
}: GetProjectSecretsDTO) =>
|
}: GetProjectSecretsDTO) =>
|
||||||
useQuery({
|
useQuery({
|
||||||
// wait for all values to be available
|
// wait for all values to be available
|
||||||
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused,
|
||||||
queryKey: secretKeys.getProjectSecret(workspaceId, env),
|
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId),
|
||||||
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env),
|
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId),
|
||||||
select: (data) => {
|
select: (data) => {
|
||||||
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
const PRIVATE_KEY = localStorage.getItem('PRIVATE_KEY') as string;
|
||||||
const latestKey = decryptFileKey;
|
const latestKey = decryptFileKey;
|
||||||
@@ -283,9 +290,15 @@ export const useBatchSecretsOp = () => {
|
|||||||
return data;
|
return data;
|
||||||
},
|
},
|
||||||
onSuccess: (_, dto) => {
|
onSuccess: (_, dto) => {
|
||||||
queryClient.invalidateQueries(secretKeys.getProjectSecret(dto.workspaceId, dto.environment));
|
queryClient.invalidateQueries(
|
||||||
queryClient.invalidateQueries(secretSnapshotKeys.list(dto.workspaceId));
|
secretKeys.getProjectSecret(dto.workspaceId, dto.environment, dto.folderId)
|
||||||
queryClient.invalidateQueries(secretSnapshotKeys.count(dto.workspaceId));
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folderId)
|
||||||
|
);
|
||||||
|
queryClient.invalidateQueries(
|
||||||
|
secretSnapshotKeys.count(dto.workspaceId, dto.environment, dto?.folderId)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ type SecretTagArg = { _id: string; name: string; slug: string };
|
|||||||
|
|
||||||
export type UpdateSecretArg = {
|
export type UpdateSecretArg = {
|
||||||
_id: string;
|
_id: string;
|
||||||
|
folderId?: string;
|
||||||
type: 'shared' | 'personal';
|
type: 'shared' | 'personal';
|
||||||
secretName: string;
|
secretName: string;
|
||||||
secretKeyCiphertext: string;
|
secretKeyCiphertext: string;
|
||||||
@@ -81,6 +82,7 @@ export type DeleteSecretArg = { _id: string };
|
|||||||
|
|
||||||
export type BatchSecretDTO = {
|
export type BatchSecretDTO = {
|
||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
|
folderId: string;
|
||||||
environment: string;
|
environment: string;
|
||||||
requests: Array<
|
requests: Array<
|
||||||
| { method: 'POST'; secret: CreateSecretArg }
|
| { method: 'POST'; secret: CreateSecretArg }
|
||||||
@@ -93,6 +95,7 @@ export type GetProjectSecretsDTO = {
|
|||||||
workspaceId: string;
|
workspaceId: string;
|
||||||
env: string | string[];
|
env: string | string[];
|
||||||
decryptFileKey: UserWsKeyPair;
|
decryptFileKey: UserWsKeyPair;
|
||||||
|
folderId?: string;
|
||||||
isPaused?: boolean;
|
isPaused?: boolean;
|
||||||
onSuccess?: (data: DecryptedSecret[]) => void;
|
onSuccess?: (data: DecryptedSecret[]) => void;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
faDownload,
|
faDownload,
|
||||||
faEye,
|
faEye,
|
||||||
faEyeSlash,
|
faEyeSlash,
|
||||||
|
faFolderPlus,
|
||||||
faMagnifyingGlass,
|
faMagnifyingGlass,
|
||||||
faPlus
|
faPlus
|
||||||
} from '@fortawesome/free-solid-svg-icons';
|
} from '@fortawesome/free-solid-svg-icons';
|
||||||
@@ -21,6 +22,7 @@ import { useNotificationContext } from '@app/components/context/Notifications/No
|
|||||||
import NavHeader from '@app/components/navigation/NavHeader';
|
import NavHeader from '@app/components/navigation/NavHeader';
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
DeleteActionModal,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
Input,
|
||||||
Modal,
|
Modal,
|
||||||
@@ -37,7 +39,10 @@ import { useWorkspace } from '@app/context';
|
|||||||
import { useLeaveConfirm, usePopUp, useToggle } from '@app/hooks';
|
import { useLeaveConfirm, usePopUp, useToggle } from '@app/hooks';
|
||||||
import {
|
import {
|
||||||
useBatchSecretsOp,
|
useBatchSecretsOp,
|
||||||
|
useCreateFolder,
|
||||||
useCreateWsTag,
|
useCreateWsTag,
|
||||||
|
useDeleteFolder,
|
||||||
|
useGetProjectFolders,
|
||||||
useGetProjectSecrets,
|
useGetProjectSecrets,
|
||||||
useGetSecretVersion,
|
useGetSecretVersion,
|
||||||
useGetSnapshotSecrets,
|
useGetSnapshotSecrets,
|
||||||
@@ -48,13 +53,20 @@ import {
|
|||||||
useGetWsSnapshotCount,
|
useGetWsSnapshotCount,
|
||||||
useGetWsTags,
|
useGetWsTags,
|
||||||
usePerformSecretRollback,
|
usePerformSecretRollback,
|
||||||
useRegisterUserAction
|
useRegisterUserAction,
|
||||||
|
useUpdateFolder
|
||||||
} from '@app/hooks/api';
|
} from '@app/hooks/api';
|
||||||
import { secretKeys } from '@app/hooks/api/secrets/queries';
|
import { secretKeys } from '@app/hooks/api/secrets/queries';
|
||||||
import { WorkspaceEnv } from '@app/hooks/api/types';
|
import { WorkspaceEnv } from '@app/hooks/api/types';
|
||||||
|
|
||||||
import { CompareSecret } from './components/CompareSecret';
|
import { CompareSecret } from './components/CompareSecret';
|
||||||
import { CreateTagModal } from './components/CreateTagModal';
|
import { CreateTagModal } from './components/CreateTagModal';
|
||||||
|
import {
|
||||||
|
FolderForm,
|
||||||
|
FolderSection,
|
||||||
|
TDeleteFolderForm,
|
||||||
|
TEditFolderForm
|
||||||
|
} from './components/FolderSection';
|
||||||
import { PitDrawer } from './components/PitDrawer';
|
import { PitDrawer } from './components/PitDrawer';
|
||||||
import { SecretDetailDrawer } from './components/SecretDetailDrawer';
|
import { SecretDetailDrawer } from './components/SecretDetailDrawer';
|
||||||
import { SecretDropzone } from './components/SecretDropzone';
|
import { SecretDropzone } from './components/SecretDropzone';
|
||||||
@@ -96,7 +108,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
'addTag',
|
'addTag',
|
||||||
'secretSnapshots',
|
'secretSnapshots',
|
||||||
'uploadedSecOpts',
|
'uploadedSecOpts',
|
||||||
'compareSecrets'
|
'compareSecrets',
|
||||||
|
'folderForm',
|
||||||
|
'deleteFolder'
|
||||||
] as const);
|
] as const);
|
||||||
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
|
const [isSecretValueHidden, setIsSecretValueHidden] = useToggle(true);
|
||||||
const [searchFilter, setSearchFilter] = useState('');
|
const [searchFilter, setSearchFilter] = useState('');
|
||||||
@@ -106,6 +120,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
const deletedSecretIds = useRef<string[]>([]);
|
const deletedSecretIds = useRef<string[]>([]);
|
||||||
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
|
const { hasUnsavedChanges, setHasUnsavedChanges } = useLeaveConfirm({ initialValue: false });
|
||||||
|
|
||||||
|
const folderId = router.query.folderId as string;
|
||||||
|
const isRollbackMode = Boolean(snapshotId);
|
||||||
|
|
||||||
const { currentWorkspace, isLoading } = useWorkspace();
|
const { currentWorkspace, isLoading } = useWorkspace();
|
||||||
const workspaceId = currentWorkspace?._id as string;
|
const workspaceId = currentWorkspace?._id as string;
|
||||||
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
|
||||||
@@ -142,7 +159,16 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
workspaceId,
|
workspaceId,
|
||||||
env: selectedEnv?.slug || '',
|
env: selectedEnv?.slug || '',
|
||||||
decryptFileKey: latestFileKey!,
|
decryptFileKey: latestFileKey!,
|
||||||
isPaused: Boolean(snapshotId)
|
isPaused: Boolean(snapshotId),
|
||||||
|
folderId
|
||||||
|
});
|
||||||
|
|
||||||
|
const { data: folderData, isLoading: isFoldersLoading } = useGetProjectFolders({
|
||||||
|
workspaceId: workspaceId || '',
|
||||||
|
environment: selectedEnv?.slug || '',
|
||||||
|
parentFolderId: folderId,
|
||||||
|
isPaused: isRollbackMode,
|
||||||
|
sortDir
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -152,6 +178,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
isFetchingNextPage
|
isFetchingNextPage
|
||||||
} = useGetWorkspaceSecretSnapshots({
|
} = useGetWorkspaceSecretSnapshots({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
environment: selectedEnv?.slug || '',
|
||||||
|
folder: folderId,
|
||||||
limit: 10
|
limit: 10
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -165,8 +193,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
decryptFileKey: latestFileKey!
|
decryptFileKey: latestFileKey!
|
||||||
});
|
});
|
||||||
|
|
||||||
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } =
|
const { data: snapshotCount, isLoading: isLoadingSnapshotCount } = useGetWsSnapshotCount(
|
||||||
useGetWsSnapshotCount(workspaceId);
|
workspaceId,
|
||||||
|
selectedEnv?.slug || '',
|
||||||
|
folderId
|
||||||
|
);
|
||||||
|
|
||||||
const { data: wsTags } = useGetWsTags(workspaceId);
|
const { data: wsTags } = useGetWsTags(workspaceId);
|
||||||
// mutation calls
|
// mutation calls
|
||||||
@@ -174,6 +205,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
|
const { mutateAsync: performSecretRollback } = usePerformSecretRollback();
|
||||||
const { mutateAsync: registerUserAction } = useRegisterUserAction();
|
const { mutateAsync: registerUserAction } = useRegisterUserAction();
|
||||||
const { mutateAsync: createWsTag } = useCreateWsTag();
|
const { mutateAsync: createWsTag } = useCreateWsTag();
|
||||||
|
const { mutateAsync: createFolder } = useCreateFolder();
|
||||||
|
const { mutateAsync: updateFolder } = useUpdateFolder(folderId);
|
||||||
|
const { mutateAsync: deleteFolder } = useDeleteFolder(folderId);
|
||||||
|
|
||||||
const method = useForm<FormData>({
|
const method = useForm<FormData>({
|
||||||
// why any: well yup inferred ts expects other keys to defined as undefined
|
// why any: well yup inferred ts expects other keys to defined as undefined
|
||||||
@@ -192,7 +226,6 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
reset
|
reset
|
||||||
} = method;
|
} = method;
|
||||||
const { fields, prepend, append, remove } = useFieldArray({ control, name: 'secrets' });
|
const { fields, prepend, append, remove } = useFieldArray({ control, name: 'secrets' });
|
||||||
const isRollbackMode = Boolean(snapshotId);
|
|
||||||
const isReadOnly = selectedEnv?.isWriteDenied;
|
const isReadOnly = selectedEnv?.isWriteDenied;
|
||||||
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
|
const isAddOnly = selectedEnv?.isReadDenied && !selectedEnv?.isWriteDenied;
|
||||||
const canDoRollback = !isReadOnly && !isAddOnly;
|
const canDoRollback = !isReadOnly && !isAddOnly;
|
||||||
@@ -281,7 +314,9 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
try {
|
try {
|
||||||
await performSecretRollback({
|
await performSecretRollback({
|
||||||
workspaceId,
|
workspaceId,
|
||||||
version: snapshotSecret.version
|
version: snapshotSecret.version,
|
||||||
|
environment: selectedEnv?.slug || '',
|
||||||
|
folderId
|
||||||
});
|
});
|
||||||
setValue('isSnapshotMode', false);
|
setValue('isSnapshotMode', false);
|
||||||
setSnaphotId(null);
|
setSnaphotId(null);
|
||||||
@@ -326,6 +361,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
await batchSecretOp({
|
await batchSecretOp({
|
||||||
requests: batchedSecret,
|
requests: batchedSecret,
|
||||||
workspaceId,
|
workspaceId,
|
||||||
|
folderId,
|
||||||
environment: selectedEnv?.slug
|
environment: selectedEnv?.slug
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
@@ -356,9 +392,11 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
const env = wsEnv?.find((el) => el.slug === slug);
|
const env = wsEnv?.find((el) => el.slug === slug);
|
||||||
if (env) setSelectedEnv(env);
|
if (env) setSelectedEnv(env);
|
||||||
|
const query: Record<string, string> = { ...router.query, env: slug };
|
||||||
|
delete query.folderId;
|
||||||
router.push({
|
router.push({
|
||||||
pathname: router.pathname,
|
pathname: router.pathname,
|
||||||
query: { ...router.query, env: slug }
|
query
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -393,6 +431,100 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleFolderOpen = (id: string) => {
|
||||||
|
setSearchFilter('');
|
||||||
|
router.push({
|
||||||
|
pathname: router.pathname,
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
folderId: id
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isEditFolder = Boolean(popUp?.folderForm?.data);
|
||||||
|
|
||||||
|
const handleFolderCreate = async (name: string) => {
|
||||||
|
try {
|
||||||
|
await createFolder({
|
||||||
|
workspaceId,
|
||||||
|
environment: selectedEnv?.slug || '',
|
||||||
|
folderName: name,
|
||||||
|
parentFolderId: folderId
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Successfully created folder'
|
||||||
|
});
|
||||||
|
handlePopUpClose('folderForm');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to create folder',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFolderUpdate = async (name: string) => {
|
||||||
|
const { id } = popUp?.folderForm?.data as TDeleteFolderForm;
|
||||||
|
try {
|
||||||
|
await updateFolder({
|
||||||
|
folderId: id,
|
||||||
|
workspaceId,
|
||||||
|
environment: selectedEnv?.slug || '',
|
||||||
|
name
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Successfully updated folder'
|
||||||
|
});
|
||||||
|
handlePopUpClose('folderForm');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to update folder',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFolderDelete = async () => {
|
||||||
|
const { id } = popUp?.deleteFolder?.data as TDeleteFolderForm;
|
||||||
|
try {
|
||||||
|
deleteFolder({
|
||||||
|
workspaceId,
|
||||||
|
environment: selectedEnv?.slug || '',
|
||||||
|
folderId: id
|
||||||
|
});
|
||||||
|
createNotification({
|
||||||
|
type: 'success',
|
||||||
|
text: 'Successfully removed folder'
|
||||||
|
});
|
||||||
|
handlePopUpClose('deleteFolder');
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove folder',
|
||||||
|
type: 'error'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// when secrets is not loading and secrets list is empty
|
||||||
|
const isDashboardSecretEmpty = !isSecretsLoading && !fields?.length;
|
||||||
|
|
||||||
|
// folder list checks
|
||||||
|
const isFolderListLoading = isRollbackMode ? isSnapshotSecretsLoading : isFoldersLoading;
|
||||||
|
const folderList = isRollbackMode ? snapshotSecret?.folders : folderData?.folders;
|
||||||
|
|
||||||
|
// when using snapshot mode and snapshot is loading and snapshot list is empty
|
||||||
|
const isFoldersEmpty = !isFolderListLoading && !folderList?.length;
|
||||||
|
const isSnapshotSecretEmtpy =
|
||||||
|
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
|
||||||
|
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
|
||||||
|
const isEmptyPage = isFoldersEmpty && isSecretEmpty;
|
||||||
|
|
||||||
if (isSecretsLoading || isEnvListLoading) {
|
if (isSecretsLoading || isEnvListLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="container mx-auto flex h-1/2 w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
<div className="container mx-auto flex h-1/2 w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
@@ -401,64 +533,42 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// when secrets is not loading and secrets list is empty
|
|
||||||
const isDashboardSecretEmpty = !isSecretsLoading && false;
|
|
||||||
// when using snapshot mode and snapshot is loading and snapshot list is empty
|
|
||||||
const isSnapshotSecretEmtpy =
|
|
||||||
isRollbackMode && !isSnapshotSecretsLoading && !snapshotSecret?.secrets?.length;
|
|
||||||
const isSecretEmpty = (!isRollbackMode && isDashboardSecretEmpty) || isSnapshotSecretEmtpy;
|
|
||||||
|
|
||||||
const userAvailableEnvs = wsEnv?.filter(
|
const userAvailableEnvs = wsEnv?.filter(
|
||||||
({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied
|
({ isReadDenied, isWriteDenied }) => !isReadDenied || !isWriteDenied
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mr-auto container px-6 text-mineshaft-50 dark:[color-scheme:dark] h-full">
|
<div className="container mx-auto h-full px-6 text-mineshaft-50 dark:[color-scheme:dark]">
|
||||||
<FormProvider {...method}>
|
<FormProvider {...method}>
|
||||||
<form autoComplete="off">
|
<form autoComplete="off" className="h-full">
|
||||||
{/* breadcrumb row */}
|
{/* breadcrumb row */}
|
||||||
<div className="relative right-6 mb-6 -top-2">
|
<div className="relative right-6 -top-2 mb-2">
|
||||||
<NavHeader
|
<NavHeader
|
||||||
pageName={t('dashboard.title')}
|
pageName={t('dashboard.title')}
|
||||||
currentEnv={
|
currentEnv={
|
||||||
userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || ''
|
userAvailableEnvs?.filter((envir) => envir.slug === envFromTop)[0].name || ''
|
||||||
}
|
}
|
||||||
|
isFolderMode
|
||||||
|
folders={folderData?.dir}
|
||||||
isProjectRelated
|
isProjectRelated
|
||||||
userAvailableEnvs={userAvailableEnvs}
|
userAvailableEnvs={userAvailableEnvs}
|
||||||
onEnvChange={onEnvChange}
|
onEnvChange={onEnvChange}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* This is only for rollbacks */}
|
<div className="mb-4">
|
||||||
{isRollbackMode &&
|
<h6 className="text-2xl">{isRollbackMode ? 'Secret Snapshot' : ''}</h6>
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center space-x-4">
|
|
||||||
<h1 className="text-3xl font-semibold">Secret Snapshot</h1>
|
|
||||||
{isRollbackMode && Boolean(snapshotSecret) && (
|
{isRollbackMode && Boolean(snapshotSecret) && (
|
||||||
<Tag colorSchema="green">
|
<Tag colorSchema="green">
|
||||||
{new Date(snapshotSecret?.createdAt || '').toLocaleString()}
|
{new Date(snapshotSecret?.createdAt || '').toLocaleString()}
|
||||||
</Tag>
|
</Tag>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Button
|
|
||||||
variant="star"
|
|
||||||
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
|
||||||
onClick={() => {
|
|
||||||
setSnaphotId(null);
|
|
||||||
reset({ ...secrets, isSnapshotMode: false });
|
|
||||||
}}
|
|
||||||
className="h-10"
|
|
||||||
>
|
|
||||||
Go back
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>}
|
|
||||||
{/* Environment, search and other action row */}
|
{/* Environment, search and other action row */}
|
||||||
<div className="mt-2 flex items-center space-x-2 justify-between">
|
<div className="flex items-center justify-between space-x-2">
|
||||||
<div className="flex-grow max-w-sm">
|
<div className="flex max-w-lg flex-grow space-x-2">
|
||||||
<Input
|
<Input
|
||||||
className="h-[2.3rem] bg-mineshaft-800 focus:bg-mineshaft-700/80 duration-200 placeholder-mineshaft-50"
|
className="h-[2.3rem] bg-mineshaft-800 placeholder-mineshaft-50 duration-200 focus:bg-mineshaft-700/80"
|
||||||
placeholder="Search keys..."
|
placeholder="Search by folder name, key name, comment..."
|
||||||
value={searchFilter}
|
value={searchFilter}
|
||||||
onChange={(e) => setSearchFilter(e.target.value)}
|
onChange={(e) => setSearchFilter(e.target.value)}
|
||||||
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
leftIcon={<FontAwesomeIcon icon={faMagnifyingGlass} />}
|
||||||
@@ -500,8 +610,8 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className='block xl:hidden'>
|
<div className="block xl:hidden">
|
||||||
<Tooltip content='Point-in-time Recovery'>
|
<Tooltip content="Point-in-time Recovery">
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="recovery"
|
ariaLabel="recovery"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
@@ -511,7 +621,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className='hidden xl:block'>
|
<div className="hidden xl:block">
|
||||||
<Button
|
<Button
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
onClick={() => handlePopUpOpen('secretSnapshots')}
|
onClick={() => handlePopUpOpen('secretSnapshots')}
|
||||||
@@ -525,8 +635,20 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
</div>
|
</div>
|
||||||
{!isReadOnly && !isRollbackMode && (
|
{!isReadOnly && !isRollbackMode && (
|
||||||
<>
|
<>
|
||||||
<div className='block lg:hidden'>
|
<div className="block lg:hidden">
|
||||||
<Tooltip content='Point-in-time Recovery'>
|
<Tooltip content="Add Folder">
|
||||||
|
<IconButton
|
||||||
|
ariaLabel="recovery"
|
||||||
|
variant="outline_bg"
|
||||||
|
onClick={() => handlePopUpOpen('folderForm')}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faFolderPlus} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="block lg:hidden">
|
||||||
|
<Tooltip content="Point-in-time Recovery">
|
||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="recovery"
|
ariaLabel="recovery"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
@@ -544,7 +666,19 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className='hidden lg:block'>
|
<div className="hidden lg:block">
|
||||||
|
<Button
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faFolderPlus} />}
|
||||||
|
onClick={() => handlePopUpOpen('folderForm')}
|
||||||
|
isDisabled={isReadOnly || isRollbackMode}
|
||||||
|
variant="outline_bg"
|
||||||
|
className="h-10"
|
||||||
|
>
|
||||||
|
Add Folder
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="hidden lg:block">
|
||||||
<Button
|
<Button
|
||||||
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
leftIcon={<FontAwesomeIcon icon={faPlus} />}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -565,6 +699,19 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
{isRollbackMode && (
|
||||||
|
<Button
|
||||||
|
variant="star"
|
||||||
|
leftIcon={<FontAwesomeIcon icon={faArrowLeft} />}
|
||||||
|
onClick={() => {
|
||||||
|
setSnaphotId(null);
|
||||||
|
reset({ ...secrets, isSnapshotMode: false });
|
||||||
|
}}
|
||||||
|
className="h-10"
|
||||||
|
>
|
||||||
|
Go back
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
<Button
|
<Button
|
||||||
isDisabled={isSubmitDisabled}
|
isDisabled={isSubmitDisabled}
|
||||||
isLoading={isSubmitting}
|
isLoading={isSubmitting}
|
||||||
@@ -580,15 +727,22 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isSecretEmpty ? 'flex flex-col items-center justify-center' : ''
|
isEmptyPage ? 'flex flex-col items-center justify-center' : ''
|
||||||
} no-scrollbar::-webkit-scrollbar mt-3 h-[calc(100vh-220px)] overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
} no-scrollbar::-webkit-scrollbar mt-3 h-3/4 overflow-x-hidden overflow-y-scroll no-scrollbar`}
|
||||||
ref={secretContainer}
|
ref={secretContainer}
|
||||||
>
|
>
|
||||||
{!isSecretEmpty && (
|
{!isEmptyPage && (
|
||||||
<TableContainer className="max-h-[calc(100%-40px)] no-scrollbar no-scrollbar::-webkit-scrollbar">
|
<TableContainer className="no-scrollbar::-webkit-scrollbar max-h-[calc(100%-120px)] no-scrollbar">
|
||||||
<table className="secret-table relative">
|
<table className="secret-table relative">
|
||||||
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
<SecretTableHeader sortDir={sortDir} onSort={onSortSecrets} />
|
||||||
<tbody className="max-h-96 overflow-y-auto">
|
<tbody className="max-h-96 overflow-y-auto">
|
||||||
|
<FolderSection
|
||||||
|
onFolderOpen={handleFolderOpen}
|
||||||
|
onFolderUpdate={(id, name) => handlePopUpOpen('folderForm', { id, name })}
|
||||||
|
onFolderDelete={(id, name) => handlePopUpOpen('deleteFolder', { id, name })}
|
||||||
|
folders={folderList}
|
||||||
|
search={searchFilter}
|
||||||
|
/>
|
||||||
{fields.map(({ id, _id }, index) => (
|
{fields.map(({ id, _id }, index) => (
|
||||||
<SecretInputRow
|
<SecretInputRow
|
||||||
key={id}
|
key={id}
|
||||||
@@ -609,7 +763,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
<td colSpan={3} className="hover:bg-mineshaft-700">
|
<td colSpan={3} className="hover:bg-mineshaft-700">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="pl-12 cursor-default w-full flex h-8 items-center justify-start font-normal text-bunker-300"
|
className="flex h-8 w-full cursor-default items-center justify-start pl-12 font-normal text-bunker-300"
|
||||||
onClick={onAppendSecret}
|
onClick={onAppendSecret}
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faPlus} />
|
<FontAwesomeIcon icon={faPlus} />
|
||||||
@@ -643,7 +797,7 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
onEnvCompare={(key) => handlePopUpOpen('compareSecrets', key)}
|
onEnvCompare={(key) => handlePopUpOpen('compareSecrets', key)}
|
||||||
/>
|
/>
|
||||||
<SecretDropzone
|
<SecretDropzone
|
||||||
isSmaller={!isSecretEmpty}
|
isSmaller={!isEmptyPage}
|
||||||
onParsedEnv={handleUploadedEnv}
|
onParsedEnv={handleUploadedEnv}
|
||||||
onAddNewSecret={onAppendSecret}
|
onAddNewSecret={onAppendSecret}
|
||||||
/>
|
/>
|
||||||
@@ -695,6 +849,26 @@ export const DashboardPage = ({ envFromTop }: { envFromTop: string }) => {
|
|||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<Modal
|
||||||
|
isOpen={popUp?.folderForm?.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle('folderForm', isOpen)}
|
||||||
|
>
|
||||||
|
<ModalContent title={isEditFolder ? 'Edit Folder' : 'Create Folder'}>
|
||||||
|
<FolderForm
|
||||||
|
isEdit={isEditFolder}
|
||||||
|
onUpdateFolder={handleFolderUpdate}
|
||||||
|
onCreateFolder={handleFolderCreate}
|
||||||
|
defaultFolderName={(popUp?.folderForm?.data as TEditFolderForm)?.name}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
<DeleteActionModal
|
||||||
|
isOpen={popUp.deleteFolder.isOpen}
|
||||||
|
deleteKey={(popUp.deleteFolder?.data as TDeleteFolderForm)?.name}
|
||||||
|
title="Do you want to delete this folder?"
|
||||||
|
onChange={(isOpen) => handlePopUpToggle('deleteFolder', isOpen)}
|
||||||
|
onDeleteApproved={handleFolderDelete}
|
||||||
|
/>
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={popUp?.compareSecrets?.isOpen}
|
isOpen={popUp?.compareSecrets?.isOpen}
|
||||||
onOpenChange={(open) => handlePopUpToggle('compareSecrets', open)}
|
onOpenChange={(open) => handlePopUpToggle('compareSecrets', open)}
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
|
import { yupResolver } from '@hookform/resolvers/yup';
|
||||||
|
import * as yup from 'yup';
|
||||||
|
|
||||||
|
import { Button, FormControl, Input, ModalClose } from '@app/components/v2';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
onCreateFolder: (folderName: string) => Promise<void>;
|
||||||
|
onUpdateFolder: (folderName: string) => Promise<void>;
|
||||||
|
isEdit?: boolean;
|
||||||
|
defaultFolderName?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formSchema = yup.object({
|
||||||
|
name: yup
|
||||||
|
.string()
|
||||||
|
.required()
|
||||||
|
.trim()
|
||||||
|
.matches(/^[a-zA-Z0-9-_]+$/, 'Folder name cannot contain spaces. Only underscore and dashes')
|
||||||
|
.label('Tag Name')
|
||||||
|
});
|
||||||
|
type TFormData = yup.InferType<typeof formSchema>;
|
||||||
|
|
||||||
|
export const FolderForm = ({
|
||||||
|
isEdit,
|
||||||
|
onCreateFolder,
|
||||||
|
defaultFolderName,
|
||||||
|
onUpdateFolder
|
||||||
|
}: Props): JSX.Element => {
|
||||||
|
const {
|
||||||
|
control,
|
||||||
|
reset,
|
||||||
|
formState: { isSubmitting },
|
||||||
|
handleSubmit
|
||||||
|
} = useForm<TFormData>({
|
||||||
|
resolver: yupResolver(formSchema),
|
||||||
|
defaultValues: {
|
||||||
|
name: defaultFolderName
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async ({ name }: TFormData) => {
|
||||||
|
if (isEdit) {
|
||||||
|
await onUpdateFolder(name);
|
||||||
|
} else {
|
||||||
|
await onCreateFolder(name);
|
||||||
|
}
|
||||||
|
reset();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)}>
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="name"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field, fieldState: { error } }) => (
|
||||||
|
<FormControl label="Folder Name" isError={Boolean(error)} errorText={error?.message}>
|
||||||
|
<Input {...field} placeholder="Type your folder name" />
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<div className="mt-8 flex items-center">
|
||||||
|
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
|
||||||
|
{isEdit ? 'Save' : 'Create'}
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button variant="plain" colorSchema="secondary">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { faEdit, faFolder, faXmark } from '@fortawesome/free-solid-svg-icons';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
|
|
||||||
|
import { IconButton, Tooltip } from '@app/components/v2';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
folders?: Array<{ id: string; name: string }>;
|
||||||
|
search?: string;
|
||||||
|
onFolderUpdate: (folderId: string, name: string) => void;
|
||||||
|
onFolderDelete: (folderId: string, name: string) => void;
|
||||||
|
onFolderOpen: (folderId: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FolderSection = ({
|
||||||
|
onFolderUpdate: handleFolderUpdate,
|
||||||
|
onFolderDelete: handleFolderDelete,
|
||||||
|
onFolderOpen: handleFolderOpen,
|
||||||
|
search = '',
|
||||||
|
folders = []
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{folders
|
||||||
|
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
|
||||||
|
.map(({ id, name }) => (
|
||||||
|
<tr key={id} className="group flex flex-row items-center hover:bg-mineshaft-700 cursor-default">
|
||||||
|
<td className="flex h-10 w-10 items-center justify-center border-none px-4 ml-0.5">
|
||||||
|
<FontAwesomeIcon icon={faFolder} className="text-primary-700" />
|
||||||
|
</td>
|
||||||
|
<td
|
||||||
|
colSpan={2}
|
||||||
|
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis uppercase lg:min-w-[240px] xl:min-w-[280px]"
|
||||||
|
style={{ paddingTop: '0', paddingBottom: '0' }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="flex-grow p-2 cursor-default"
|
||||||
|
onKeyDown={() => null}
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
|
onClick={() => handleFolderOpen(id)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</div>
|
||||||
|
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Settings" className="capitalize">
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
colorSchema="primary"
|
||||||
|
variant="plain"
|
||||||
|
onClick={() => handleFolderUpdate(id, name)}
|
||||||
|
ariaLabel="expand"
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faEdit} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
|
<Tooltip content="Delete" className="capitalize">
|
||||||
|
<IconButton
|
||||||
|
size="md"
|
||||||
|
variant="plain"
|
||||||
|
colorSchema="danger"
|
||||||
|
ariaLabel="delete"
|
||||||
|
onClick={() => handleFolderDelete(id, name)}
|
||||||
|
>
|
||||||
|
<FontAwesomeIcon icon={faXmark} />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export { FolderForm } from './FolderForm';
|
||||||
|
export { FolderSection } from './FolderSection';
|
||||||
|
export * from './types';
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export type TEditFolderForm = { id: string; name: string };
|
||||||
|
export type TDeleteFolderForm = { id: string; name: string };
|
||||||
@@ -24,9 +24,9 @@ export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridd
|
|||||||
|
|
||||||
const syntaxHighlight = useCallback((val: string) => {
|
const syntaxHighlight = useCallback((val: string) => {
|
||||||
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>;
|
if (val?.length === 0) return <span className="font-sans text-bunker-400/80">EMPTY</span>;
|
||||||
return val?.split(REGEX).map((word) =>
|
return val?.split(REGEX).map((word, i) =>
|
||||||
word.match(REGEX) !== null ? (
|
word.match(REGEX) !== null ? (
|
||||||
<span className="ph-no-capture text-yellow" key={`${val}-${index + 1}`}>
|
<span className="ph-no-capture text-yellow" key={`${val}-${i + 1}`}>
|
||||||
{word.slice(0, 2)}
|
{word.slice(0, 2)}
|
||||||
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
<span className="ph-no-capture text-yellow-200/80">{word.slice(2, word.length - 1)}</span>
|
||||||
{word.slice(word.length - 1, word.length) === '}' ? (
|
{word.slice(word.length - 1, word.length) === '}' ? (
|
||||||
@@ -40,7 +40,7 @@ export const MaskedInput = ({ isReadOnly, isSecretValueHidden, index, isOverridd
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span key={`${word}_${index + 1}`} className="ph-no-capture">
|
<span key={`${word}_${i + 1}`} className="ph-no-capture">
|
||||||
{word}
|
{word}
|
||||||
</span>
|
</span>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
faComment,
|
faComment,
|
||||||
faEllipsis,
|
faEllipsis,
|
||||||
faInfoCircle,
|
faInfoCircle,
|
||||||
faKey,
|
|
||||||
faPlus,
|
faPlus,
|
||||||
faTags,
|
faTags,
|
||||||
faXmark
|
faXmark
|
||||||
@@ -159,9 +158,8 @@ export const SecretInputRow = memo(
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<tr className="group flex flex-row items-center" key={index}>
|
<tr className="group flex flex-row items-center" key={index}>
|
||||||
<td className="flex h-10 w-10 items-center justify-center px-4 border-none">
|
<td className="flex h-10 w-10 items-center justify-center border-none px-4">
|
||||||
{/* <div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div> */}
|
<div className="w-10 text-center text-xs text-bunker-400">{index + 1}</div>
|
||||||
<div className="w-10 text-center text-xs text-bunker-400"><FontAwesomeIcon icon={faKey} className="w-4 h-4 text-bunker-400/60 pl-2.5 pt-0.5" /></div>
|
|
||||||
</td>
|
</td>
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
@@ -200,7 +198,7 @@ export const SecretInputRow = memo(
|
|||||||
</HoverCard>
|
</HoverCard>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<td className="flex h-10 border-none w-full flex-grow flex-row items-center justify-center border-r border-red">
|
<td className="flex h-10 w-full flex-grow flex-row items-center justify-center border-r border-none border-red">
|
||||||
<MaskedInput
|
<MaskedInput
|
||||||
isReadOnly={
|
isReadOnly={
|
||||||
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
isReadOnly || isRollbackMode || (isOverridden ? isAddOnly : shouldBeBlockedInAddOnly)
|
||||||
@@ -226,10 +224,10 @@ export const SecretInputRow = memo(
|
|||||||
</Tag>
|
</Tag>
|
||||||
))}
|
))}
|
||||||
{!(isReadOnly || isAddOnly || isRollbackMode) && (
|
{!(isReadOnly || isAddOnly || isRollbackMode) && (
|
||||||
<div className="overflow-hidden duration-0 ml-1">
|
<div className="duration-0 ml-1 overflow-hidden">
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<div className="w-0 data-[state=open]:w-6 group-hover:w-6">
|
<div className="w-0 group-hover:w-6 data-[state=open]:w-6">
|
||||||
<Tooltip content="Add tags">
|
<Tooltip content="Add tags">
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="plain"
|
variant="plain"
|
||||||
@@ -291,7 +289,7 @@ export const SecretInputRow = memo(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row items-center h-full pr-2">
|
<div className="flex h-full flex-row items-center pr-2">
|
||||||
{!isAddOnly && (
|
{!isAddOnly && (
|
||||||
<div>
|
<div>
|
||||||
<Tooltip content="Override with a personal value">
|
<Tooltip content="Override with a personal value">
|
||||||
@@ -314,16 +312,14 @@ export const SecretInputRow = memo(
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Tooltip content="Comment">
|
<Tooltip content="Comment">
|
||||||
<div
|
<div className={`mt-0.5 overflow-hidden `}>
|
||||||
className={`mt-0.5 overflow-hidden `}
|
|
||||||
>
|
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
'overflow-hidden p-0 w-7',
|
'w-7 overflow-hidden p-0',
|
||||||
'data-[state=open]:w-7 group-hover:w-7 w-0',
|
'w-0 group-hover:w-7 data-[state=open]:w-7',
|
||||||
hasComment ? 'text-primary w-7' : 'group-hover:w-7'
|
hasComment ? 'w-7 text-primary' : 'group-hover:w-7'
|
||||||
)}
|
)}
|
||||||
variant="plain"
|
variant="plain"
|
||||||
size="md"
|
size="md"
|
||||||
@@ -332,12 +328,13 @@ export const SecretInputRow = memo(
|
|||||||
<FontAwesomeIcon icon={faComment} />
|
<FontAwesomeIcon icon={faComment} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl" sticky="always">
|
<PopoverContent
|
||||||
|
className="w-auto border border-mineshaft-600 bg-mineshaft-800 p-2 drop-shadow-2xl"
|
||||||
|
sticky="always"
|
||||||
|
>
|
||||||
<FormControl label="Comment" className="mb-0">
|
<FormControl label="Comment" className="mb-0">
|
||||||
<TextArea
|
<TextArea
|
||||||
isDisabled={
|
isDisabled={isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly}
|
||||||
isReadOnly || isRollbackMode || shouldBeBlockedInAddOnly
|
|
||||||
}
|
|
||||||
className="border border-mineshaft-600 text-sm"
|
className="border border-mineshaft-600 text-sm"
|
||||||
{...register(`secrets.${index}.comment`)}
|
{...register(`secrets.${index}.comment`)}
|
||||||
rows={8}
|
rows={8}
|
||||||
@@ -349,7 +346,7 @@ export const SecretInputRow = memo(
|
|||||||
</div>
|
</div>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
<div className="duration-0 w-0 flex items-center justify-end space-x-2.5 overflow-hidden transition-all w-16 border-l border-mineshaft-600 h-10">
|
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
|
||||||
{!isAddOnly && (
|
{!isAddOnly && (
|
||||||
<div className="opacity-0 group-hover:opacity-100">
|
<div className="opacity-0 group-hover:opacity-100">
|
||||||
<Tooltip content="Settings">
|
<Tooltip content="Settings">
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ type Props = {
|
|||||||
export const SecretTableHeader = ({ sortDir, onSort }: Props): JSX.Element => (
|
export const SecretTableHeader = ({ sortDir, onSort }: Props): JSX.Element => (
|
||||||
<thead className="sticky top-0 z-50 bg-mineshaft-800">
|
<thead className="sticky top-0 z-50 bg-mineshaft-800">
|
||||||
<tr className="top-0 flex flex-row">
|
<tr className="top-0 flex flex-row">
|
||||||
<td className="flex w-10 items-center justify-center px-4 border-none">
|
<td className="flex w-10 items-center justify-center border-none px-4">
|
||||||
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
<div className="w-10 text-center text-xs text-transparent">{0}</div>
|
||||||
</td>
|
</td>
|
||||||
<td className="flex items-center">
|
<td className="flex items-center">
|
||||||
|
|||||||
Reference in New Issue
Block a user