mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 22:26:07 +00:00
Fix merge conflicts
This commit is contained in:
@@ -16,6 +16,7 @@ import * as workspaceController from "./workspaceController";
|
||||
import * as secretScanningController from "./secretScanningController";
|
||||
import * as webhookController from "./webhookController";
|
||||
import * as secretImpsController from "./secretImpsController";
|
||||
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
|
||||
|
||||
export {
|
||||
authController,
|
||||
@@ -35,5 +36,6 @@ export {
|
||||
workspaceController,
|
||||
secretScanningController,
|
||||
webhookController,
|
||||
secretImpsController
|
||||
secretImpsController,
|
||||
secretApprovalPolicyController
|
||||
};
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { Request, Response } from "express";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
ProjectPermissionSub,
|
||||
getUserProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { validateRequest } from "../../helpers/validation";
|
||||
import { SecretApprovalPolicy } from "../../models/secretApprovalPolicy";
|
||||
import { BadRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/secretApproval";
|
||||
|
||||
const ERR_SECRET_APPROVAL_NOT_FOUND = BadRequestError({ message: "secret approval not found" });
|
||||
|
||||
export const createSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, secretPath, approvers, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.CreateSecretApprovalRule, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const secretApproval = new SecretApprovalPolicy({
|
||||
workspace: workspaceId,
|
||||
secretPath,
|
||||
environment,
|
||||
approvals,
|
||||
approvers
|
||||
});
|
||||
await secretApproval.save();
|
||||
|
||||
return res.send({
|
||||
approval: secretApproval
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { approvals, approvers, secretPath },
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.UpdateSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApproval.workspace.toString()
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const updatedDoc = await SecretApprovalPolicy.findByIdAndUpdate(id, {
|
||||
approvals,
|
||||
approvers,
|
||||
...(secretPath === null ? { $unset: { secretPath: 1 } } : { secretPath })
|
||||
});
|
||||
|
||||
return res.send({
|
||||
approval: updatedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { id }
|
||||
} = await validateRequest(reqValidator.DeleteSecretApprovalRule, req);
|
||||
|
||||
const secretApproval = await SecretApprovalPolicy.findById(id);
|
||||
if (!secretApproval) throw ERR_SECRET_APPROVAL_NOT_FOUND;
|
||||
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
secretApproval.workspace.toString()
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const deletedDoc = await SecretApprovalPolicy.findByIdAndDelete(id);
|
||||
|
||||
return res.send({
|
||||
approval: deletedDoc
|
||||
});
|
||||
};
|
||||
|
||||
export const getSecretApprovalPolicy = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId }
|
||||
} = await validateRequest(reqValidator.GetSecretApprovalRuleList, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
ProjectPermissionSub.SecretApproval
|
||||
);
|
||||
|
||||
const doc = await SecretApprovalPolicy.find({ workspace: workspaceId });
|
||||
|
||||
return res.send({
|
||||
approvals: doc
|
||||
});
|
||||
};
|
||||
@@ -2,7 +2,7 @@ import { Request, Response } from "express";
|
||||
import { isValidScope } from "../../helpers";
|
||||
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
|
||||
import { getAllImportedSecrets } from "../../services/SecretImportService";
|
||||
import { getFolderWithPathFromId } from "../../services/FolderService";
|
||||
import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
|
||||
import {
|
||||
BadRequestError,
|
||||
ResourceNotFoundError,
|
||||
@@ -95,37 +95,12 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
*/
|
||||
|
||||
const {
|
||||
body: { workspaceId, environment, folderId, secretImport }
|
||||
body: { workspaceId, environment, directory, secretImport }
|
||||
} = await validateRequest(reqValidator.CreateSecretImportV1, req);
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (!folders && folderId !== "root") {
|
||||
throw ResourceNotFoundError({
|
||||
message: "Failed to find folder"
|
||||
});
|
||||
}
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId);
|
||||
secretPath = folderPath;
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
let isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
secretImport.environment,
|
||||
secretImport.secretPath
|
||||
);
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
@@ -133,27 +108,31 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: secretImport.environment,
|
||||
secretPath: secretImport.secretPath
|
||||
})
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (!folders && directory !== "/")
|
||||
throw ResourceNotFoundError({ message: "Failed to find folder" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const importToSecretPath = folders
|
||||
? getFolderWithPathFromId(folders.nodes, folderId).folderPath
|
||||
: "/";
|
||||
|
||||
if (!importSecDoc) {
|
||||
const doc = new SecretImport({
|
||||
workspace: workspaceId,
|
||||
@@ -173,7 +152,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
importFromEnvironment: secretImport.environment,
|
||||
importFromSecretPath: secretImport.secretPath,
|
||||
importToEnvironment: environment,
|
||||
importToSecretPath
|
||||
importToSecretPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -206,7 +185,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
|
||||
importFromEnvironment: secretImport.environment,
|
||||
importFromSecretPath: secretImport.secretPath,
|
||||
importToEnvironment: environment,
|
||||
importToSecretPath
|
||||
importToSecretPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -563,8 +542,38 @@ export const getSecretImports = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
query: { workspaceId, environment, folderId }
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetSecretImportsV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@@ -575,41 +584,6 @@ export const getSecretImports = async (req: Request, res: Response) => {
|
||||
return res.status(200).json({ secretImport: {} });
|
||||
}
|
||||
|
||||
// check for service token validity
|
||||
const folders = await Folder.findOne({
|
||||
workspace: importSecDoc.workspace,
|
||||
environment: importSecDoc.environment
|
||||
}).lean();
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
||||
secretPath = folderPath;
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
importSecDoc.environment,
|
||||
secretPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(
|
||||
req.user._id,
|
||||
importSecDoc.workspace.toString()
|
||||
);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: importSecDoc.environment,
|
||||
secretPath
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return res.status(200).json({ secretImport: importSecDoc });
|
||||
};
|
||||
|
||||
@@ -621,9 +595,39 @@ export const getSecretImports = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { workspaceId, environment, folderId }
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// check for service token validity
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Read,
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment,
|
||||
secretPath: directory
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
let folderId = "root";
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Folder not found" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const importSecDoc = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@@ -634,11 +638,6 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
|
||||
return res.status(200).json({ secrets: [] });
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: importSecDoc.workspace,
|
||||
environment: importSecDoc.environment
|
||||
}).lean();
|
||||
|
||||
let secretPath = "/";
|
||||
if (folders) {
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
|
||||
|
||||
@@ -9,12 +9,10 @@ import { Secret, ServiceTokenData } from "../../models";
|
||||
import { Folder } from "../../models/folder";
|
||||
import {
|
||||
appendFolder,
|
||||
deleteFolderById,
|
||||
generateFolderId,
|
||||
getAllFolderIds,
|
||||
getFolderByPath,
|
||||
getFolderWithPathFromId,
|
||||
getParentFromFolderId,
|
||||
validateFolderName
|
||||
} from "../../services/FolderService";
|
||||
import {
|
||||
@@ -25,13 +23,9 @@ import {
|
||||
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
|
||||
import * as reqValidator from "../../validation/folders";
|
||||
|
||||
/**
|
||||
* Create folder with name [folderName] for workspace with id [workspaceId]
|
||||
* and environment [environment]
|
||||
* @param req
|
||||
* @param res
|
||||
* @returns
|
||||
*/
|
||||
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
|
||||
|
||||
// verify workspace id/environment
|
||||
export const createFolder = async (req: Request, res: Response) => {
|
||||
/*
|
||||
#swagger.summary = 'Create a folder'
|
||||
@@ -107,7 +101,7 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: { workspaceId, environment, folderName, parentFolderId }
|
||||
body: { workspaceId, environment, folderName, directory }
|
||||
} = await validateRequest(reqValidator.CreateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(folderName)) {
|
||||
@@ -116,33 +110,29 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// token check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// user check
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
}).lean();
|
||||
|
||||
if (req.user) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
const secretPath =
|
||||
folders && parentFolderId
|
||||
? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath
|
||||
: "/";
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
// space has no folders initialized
|
||||
|
||||
if (!folders) {
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const id = generateFolderId();
|
||||
const folder = new Folder({
|
||||
@@ -186,27 +176,10 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
return res.json({ folder: { id, name: folderName } });
|
||||
}
|
||||
|
||||
const folder = appendFolder(folders.nodes, { folderName, parentFolderId });
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId(
|
||||
folders.nodes,
|
||||
parentFolderId || "root"
|
||||
);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
environment,
|
||||
parentFolderPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const folder = appendFolder(folders.nodes, { folderName, parentFolderId: parentFolder.id });
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const folderVersion = new FolderVersion({
|
||||
@@ -219,11 +192,9 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: parentFolderId
|
||||
folderId: parentFolder.id
|
||||
});
|
||||
|
||||
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
req.authData,
|
||||
{
|
||||
@@ -232,7 +203,7 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
folderId: folder.id,
|
||||
folderName,
|
||||
folderPath
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -332,8 +303,8 @@ export const updateFolderById = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
body: { workspaceId, environment, name },
|
||||
params: { folderId }
|
||||
body: { workspaceId, environment, name, directory },
|
||||
params: { folderName }
|
||||
} = await validateRequest(reqValidator.UpdateFolderV1, req);
|
||||
|
||||
if (!validateFolderName(name)) {
|
||||
@@ -342,38 +313,31 @@ export const updateFolderById = async (req: Request, res: Response) => {
|
||||
});
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const parentFolder = getParentFromFolderId(folders.nodes, folderId);
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
if (req.user) {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath;
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
}
|
||||
|
||||
const folder = parentFolder.children.find(({ id }) => id === folderId);
|
||||
if (!folder) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
|
||||
// root check
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folder = parentFolder.children.find(({ name }) => name === folderName);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const oldFolderName = folder.name;
|
||||
parentFolder.version += 1;
|
||||
@@ -505,24 +469,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
params: { folderId },
|
||||
body: { environment, workspaceId }
|
||||
params: { folderName },
|
||||
body: { environment, workspaceId, directory }
|
||||
} = await validateRequest(reqValidator.DeleteFolderV1, req);
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
|
||||
const delOp = deleteFolderById(folders.nodes, folderId);
|
||||
if (!delOp) {
|
||||
throw BadRequestError({ message: "The folder doesn't exist" });
|
||||
}
|
||||
const { deletedNode: delFolder, parent: parentFolder } = delOp;
|
||||
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
|
||||
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
@@ -531,12 +483,23 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
|
||||
);
|
||||
}
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const parentFolder = getFolderByPath(folders.nodes, directory);
|
||||
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
|
||||
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const deletedFolder = parentFolder.children.splice(index, 1)[0];
|
||||
|
||||
parentFolder.version += 1;
|
||||
const delFolderIds = getAllFolderIds(delFolder);
|
||||
const delFolderIds = getAllFolderIds(deletedFolder);
|
||||
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
const folderVersion = new FolderVersion({
|
||||
@@ -565,9 +528,9 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
type: EventType.DELETE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId,
|
||||
folderName: delFolder.name,
|
||||
folderPath: secretPath
|
||||
folderId: deletedFolder.id,
|
||||
folderName: deletedFolder.name,
|
||||
folderPath: directory
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -575,7 +538,7 @@ export const deleteFolder = async (req: Request, res: Response) => {
|
||||
}
|
||||
);
|
||||
|
||||
res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||
return res.send({ message: "successfully deleted folders", folders: delFolderIds });
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -677,69 +640,27 @@ export const getFolders = async (req: Request, res: Response) => {
|
||||
}
|
||||
*/
|
||||
const {
|
||||
query: { workspaceId, environment, parentFolderId, parentFolderPath }
|
||||
query: { workspaceId, environment, directory }
|
||||
} = await validateRequest(reqValidator.GetFoldersV1, req);
|
||||
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
|
||||
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
|
||||
if (!folders) {
|
||||
res.send({ folders: [], dir: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
// if instead of parentFolderId given a path like /folder1/folder2
|
||||
if (parentFolderPath) {
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(
|
||||
req.authData.authPayload,
|
||||
environment,
|
||||
parentFolderPath
|
||||
);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
const folder = getFolderByPath(folders.nodes, parentFolderPath);
|
||||
|
||||
if (!folder) {
|
||||
res.send({ folders: [], dir: [] });
|
||||
return;
|
||||
}
|
||||
// dir is not needed at present as this is only used in overview section of secrets
|
||||
res.send({
|
||||
folders: folder.children.map(({ id, name }) => ({ id, name })),
|
||||
dir: [{ name: folder.name, id: folder.id }]
|
||||
});
|
||||
}
|
||||
|
||||
if (!parentFolderId) {
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
}
|
||||
|
||||
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
|
||||
id,
|
||||
name
|
||||
}));
|
||||
res.send({ folders: rootFolders });
|
||||
return;
|
||||
}
|
||||
|
||||
const { folder, folderPath, dir } = getFolderWithPathFromId(folders.nodes, parentFolderId);
|
||||
if (req.authData.authPayload instanceof ServiceTokenData) {
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath);
|
||||
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
|
||||
if (!isValidScopeAccess) {
|
||||
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
|
||||
}
|
||||
} else {
|
||||
// check that user is a member of the workspace
|
||||
await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
}
|
||||
|
||||
res.send({
|
||||
folders: folder.children.map(({ id, name }) => ({ id, name })),
|
||||
dir
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders) {
|
||||
return res.send({ folders: [], dir: [] });
|
||||
}
|
||||
|
||||
const folder = getFolderByPath(folders.nodes, directory);
|
||||
|
||||
return res.send({
|
||||
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Request, Response } from "express";
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
Folder,
|
||||
Integration,
|
||||
Membership,
|
||||
Secret,
|
||||
@@ -21,9 +22,12 @@ import {
|
||||
getUserProjectPermissions
|
||||
} from "../../ee/services/ProjectRoleService";
|
||||
import { ForbiddenError } from "@casl/ability";
|
||||
import { SecretImport } from "../../models";
|
||||
import { ServiceAccountWorkspacePermission } from "../../models";
|
||||
import { Webhook } from "../../models";
|
||||
|
||||
/**
|
||||
* Create new workspace environment named [environmentName]
|
||||
* Create new workspace environment named [environmentName]
|
||||
* with slug [environmentSlug] under workspace with id
|
||||
* @param req
|
||||
* @param res
|
||||
@@ -369,13 +373,38 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
await ServiceTokenData.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
{
|
||||
workspace: workspaceId,
|
||||
"scopes.environment": oldEnvironmentSlug
|
||||
},
|
||||
{ $set: { "scopes.$[element].environment": environmentSlug } },
|
||||
{ arrayFilters: [{ "element.environment": oldEnvironmentSlug }] }
|
||||
);
|
||||
await Integration.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Folder.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await SecretImport.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await ServiceAccountWorkspacePermission.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Webhook.updateMany(
|
||||
{ workspace: workspaceId, environment: oldEnvironmentSlug },
|
||||
{ environment: environmentSlug }
|
||||
);
|
||||
|
||||
await Membership.updateMany(
|
||||
{
|
||||
workspace: workspaceId,
|
||||
|
||||
@@ -184,7 +184,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { secretPath, environment, workspaceId, type },
|
||||
query: { secretPath, environment, workspaceId, type, include_imports },
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
|
||||
|
||||
@@ -225,7 +225,8 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData
|
||||
authData: req.authData,
|
||||
include_imports
|
||||
});
|
||||
|
||||
const key = await BotService.getWorkspaceKeyWithBot({
|
||||
@@ -248,7 +249,15 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { secretName },
|
||||
body: { secretPath, environment, workspaceId, type, secretValue, secretComment }
|
||||
body: {
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId,
|
||||
type,
|
||||
secretValue,
|
||||
secretComment,
|
||||
skipMultilineEncoding
|
||||
}
|
||||
} = await validateRequest(reqValidator.CreateSecretRawV3, req);
|
||||
|
||||
switch (req.authData.actor.type) {
|
||||
@@ -316,7 +325,8 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
secretPath,
|
||||
secretCommentCiphertext: secretCommentEncrypted.ciphertext,
|
||||
secretCommentIV: secretCommentEncrypted.iv,
|
||||
secretCommentTag: secretCommentEncrypted.tag
|
||||
secretCommentTag: secretCommentEncrypted.tag,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@@ -346,7 +356,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
|
||||
export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { secretName },
|
||||
body: { secretValue, environment, secretPath, type, workspaceId }
|
||||
body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
|
||||
|
||||
switch (req.authData.actor.type) {
|
||||
@@ -398,7 +408,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
|
||||
secretValueCiphertext: secretValueEncrypted.ciphertext,
|
||||
secretValueIV: secretValueEncrypted.iv,
|
||||
secretValueTag: secretValueEncrypted.tag,
|
||||
secretPath
|
||||
secretPath,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@@ -604,7 +615,7 @@ export const getSecrets = async (req: Request, res: Response) => {
|
||||
*/
|
||||
export const getSecretByName = async (req: Request, res: Response) => {
|
||||
const {
|
||||
query: { secretPath, environment, workspaceId, type },
|
||||
query: { secretPath, environment, workspaceId, type, include_imports },
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.GetSecretByNameV3, req);
|
||||
|
||||
@@ -645,7 +656,8 @@ export const getSecretByName = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
type,
|
||||
secretPath,
|
||||
authData: req.authData
|
||||
authData: req.authData,
|
||||
include_imports
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
@@ -674,7 +686,8 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding
|
||||
},
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.CreateSecretV3, req);
|
||||
@@ -726,7 +739,8 @@ export const createSecret = async (req: Request, res: Response) => {
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
metadata
|
||||
metadata,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@@ -759,10 +773,23 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
type,
|
||||
environment,
|
||||
secretPath,
|
||||
workspaceId
|
||||
workspaceId,
|
||||
tags,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
secretName: newSecretName,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
skipMultilineEncoding
|
||||
},
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
|
||||
|
||||
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) {
|
||||
throw BadRequestError({ message: "Missing encrypted key" });
|
||||
}
|
||||
|
||||
switch (req.authData.actor.type) {
|
||||
case ActorType.USER: {
|
||||
@@ -801,10 +828,19 @@ export const updateSecretByName = async (req: Request, res: Response) => {
|
||||
environment,
|
||||
type,
|
||||
authData: req.authData,
|
||||
newSecretName,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath
|
||||
secretPath,
|
||||
tags,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV
|
||||
});
|
||||
|
||||
await EventService.handleEvent({
|
||||
@@ -883,3 +919,150 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
secret
|
||||
});
|
||||
};
|
||||
|
||||
export const createSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
|
||||
|
||||
switch (req.authData.actor.type) {
|
||||
case ActorType.USER: {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ActorType.SERVICE: {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
acceptedPermissions: [Permission.READ_WRITE]
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const createdSecrets = await SecretService.createSecretBatch({
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secrets,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: createdSecrets
|
||||
});
|
||||
};
|
||||
|
||||
export const updateSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.UpdateSecretByNameBatchV3, req);
|
||||
|
||||
switch (req.authData.actor.type) {
|
||||
case ActorType.USER: {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ActorType.SERVICE: {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
acceptedPermissions: [Permission.READ_WRITE]
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const updatedSecrets = await SecretService.updateSecretBatch({
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secrets,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: updatedSecrets
|
||||
});
|
||||
};
|
||||
|
||||
export const deleteSecretByNameBatch = async (req: Request, res: Response) => {
|
||||
const {
|
||||
body: { secrets, secretPath, environment, workspaceId }
|
||||
} = await validateRequest(reqValidator.DeleteSecretByNameBatchV3, req);
|
||||
|
||||
switch (req.authData.actor.type) {
|
||||
case ActorType.USER: {
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
break;
|
||||
}
|
||||
case ActorType.SERVICE: {
|
||||
await validateServiceTokenDataClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenData,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
requiredPermissions: [PERMISSION_WRITE_SECRETS]
|
||||
});
|
||||
break;
|
||||
}
|
||||
case ActorType.SERVICE_V3: {
|
||||
await validateServiceTokenDataV3ClientForWorkspace({
|
||||
serviceTokenData: req.authData.authPayload as IServiceTokenDataV3,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
secretPath,
|
||||
acceptedPermissions: [Permission.READ_WRITE]
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const deletedSecrets = await SecretService.deleteSecretBatch({
|
||||
secretPath,
|
||||
environment,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
secrets,
|
||||
authData: req.authData
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
secrets: deletedSecrets
|
||||
});
|
||||
};
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
import { EESecretService } from "../../services";
|
||||
import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
|
||||
// import Folder, { TFolderSchema } from "../../../models/folder";
|
||||
import { searchByFolderId } from "../../../services/FolderService";
|
||||
import { getFolderByPath, searchByFolderId } from "../../../services/FolderService";
|
||||
import { EEAuditLogService, EELicenseService } from "../../services";
|
||||
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
|
||||
import { validateRequest } from "../../../helpers/validation";
|
||||
@@ -106,7 +106,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
|
||||
*/
|
||||
const {
|
||||
params: { workspaceId },
|
||||
query: { environment, folderId, offset, limit }
|
||||
query: { environment, directory, offset, limit }
|
||||
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
@@ -115,10 +115,20 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders?.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const secretSnapshots = await SecretSnapshot.find({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId: folderId || "root"
|
||||
folderId
|
||||
})
|
||||
.sort({ createdAt: -1 })
|
||||
.skip(offset)
|
||||
@@ -137,7 +147,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
|
||||
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
|
||||
const {
|
||||
params: { workspaceId },
|
||||
query: { environment, folderId }
|
||||
query: { environment, directory }
|
||||
} = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
@@ -146,10 +156,20 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders?.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
const count = await SecretSnapshot.countDocuments({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId: folderId || "root"
|
||||
folderId
|
||||
});
|
||||
|
||||
return res.status(200).send({
|
||||
@@ -217,7 +237,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
||||
|
||||
const {
|
||||
params: { workspaceId },
|
||||
body: { folderId, environment, version }
|
||||
body: { directory, environment, version }
|
||||
} = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req);
|
||||
|
||||
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
|
||||
@@ -226,6 +246,16 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
|
||||
ProjectPermissionSub.SecretRollback
|
||||
);
|
||||
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({ workspace: workspaceId, environment });
|
||||
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
|
||||
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders?.nodes, directory);
|
||||
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// validate secret snapshot
|
||||
const secretSnapshot = await SecretSnapshot.findOne({
|
||||
workspace: workspaceId,
|
||||
|
||||
@@ -5,10 +5,10 @@ export enum ActorType {
|
||||
}
|
||||
|
||||
export enum UserAgentType {
|
||||
WEB = "web",
|
||||
CLI = "cli",
|
||||
K8_OPERATOR = "k8-operator",
|
||||
OTHER = "other"
|
||||
WEB = "web",
|
||||
CLI = "cli",
|
||||
K8_OPERATOR = "k8-operator",
|
||||
OTHER = "other"
|
||||
}
|
||||
|
||||
export enum EventType {
|
||||
@@ -16,8 +16,11 @@ export enum EventType {
|
||||
GET_SECRET = "get-secret",
|
||||
REVEAL_SECRET = "reveal-secret",
|
||||
CREATE_SECRET = "create-secret",
|
||||
CREATE_SECRETS = "create-secrets",
|
||||
UPDATE_SECRET = "update-secret",
|
||||
UPDATE_SECRETS = "update-secrets",
|
||||
DELETE_SECRET = "delete-secret",
|
||||
DELETE_SECRETS = "delete-secrets",
|
||||
GET_WORKSPACE_KEY = "get-workspace-key",
|
||||
AUTHORIZE_INTEGRATION = "authorize-integration",
|
||||
UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
|
||||
|
||||
@@ -7,23 +7,23 @@ import {
|
||||
} from "../../../models/serviceTokenDataV3";
|
||||
|
||||
interface UserActorMetadata {
|
||||
userId: string;
|
||||
email: string;
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
interface ServiceActorMetadata {
|
||||
serviceId: string;
|
||||
name: string;
|
||||
serviceId: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface UserActor {
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
type: ActorType.USER;
|
||||
metadata: UserActorMetadata;
|
||||
}
|
||||
|
||||
export interface ServiceActor {
|
||||
type: ActorType.SERVICE;
|
||||
metadata: ServiceActorMetadata;
|
||||
type: ActorType.SERVICE;
|
||||
metadata: ServiceActorMetadata;
|
||||
}
|
||||
|
||||
export interface ServiceActorV3 {
|
||||
@@ -37,164 +37,191 @@ export type Actor =
|
||||
| ServiceActorV3;
|
||||
|
||||
interface GetSecretsEvent {
|
||||
type: EventType.GET_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
};
|
||||
type: EventType.GET_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
numberOfSecrets: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretEvent {
|
||||
type: EventType.GET_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
type: EventType.GET_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretEvent {
|
||||
type: EventType.CREATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
type: EventType.CREATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretBatchEvent {
|
||||
type: EventType.CREATE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretEvent {
|
||||
type: EventType.UPDATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
type: EventType.UPDATE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretBatchEvent {
|
||||
type: EventType.UPDATE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretEvent {
|
||||
type: EventType.DELETE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
}
|
||||
type: EventType.DELETE_SECRET;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secretId: string;
|
||||
secretKey: string;
|
||||
secretVersion: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretBatchEvent {
|
||||
type: EventType.DELETE_SECRETS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetWorkspaceKeyEvent {
|
||||
type: EventType.GET_WORKSPACE_KEY,
|
||||
metadata: {
|
||||
keyId: string;
|
||||
}
|
||||
type: EventType.GET_WORKSPACE_KEY;
|
||||
metadata: {
|
||||
keyId: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AuthorizeIntegrationEvent {
|
||||
type: EventType.AUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
}
|
||||
type: EventType.AUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UnauthorizeIntegrationEvent {
|
||||
type: EventType.UNAUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
}
|
||||
type: EventType.UNAUTHORIZE_INTEGRATION;
|
||||
metadata: {
|
||||
integration: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateIntegrationEvent {
|
||||
type: EventType.CREATE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
}
|
||||
type: EventType.CREATE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteIntegrationEvent {
|
||||
type: EventType.DELETE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
}
|
||||
type: EventType.DELETE_INTEGRATION;
|
||||
metadata: {
|
||||
integrationId: string;
|
||||
integration: string; // TODO: fix type
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
url?: string;
|
||||
app?: string;
|
||||
appId?: string;
|
||||
targetEnvironment?: string;
|
||||
targetEnvironmentId?: string;
|
||||
targetService?: string;
|
||||
targetServiceId?: string;
|
||||
path?: string;
|
||||
region?: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddTrustedIPEvent {
|
||||
type: EventType.ADD_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
type: EventType.ADD_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateTrustedIPEvent {
|
||||
type: EventType.UPDATE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
type: EventType.UPDATE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteTrustedIPEvent {
|
||||
type: EventType.DELETE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
}
|
||||
type: EventType.DELETE_TRUSTED_IP;
|
||||
metadata: {
|
||||
trustedIpId: string;
|
||||
ipAddress: string;
|
||||
prefix?: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateServiceTokenEvent {
|
||||
type: EventType.CREATE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
}
|
||||
type: EventType.CREATE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteServiceTokenEvent {
|
||||
type: EventType.DELETE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
}
|
||||
type: EventType.DELETE_SERVICE_TOKEN;
|
||||
metadata: {
|
||||
name: string;
|
||||
scopes: Array<{
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}>;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateServiceTokenV3Event {
|
||||
@@ -228,171 +255,171 @@ interface DeleteServiceTokenV3Event {
|
||||
}
|
||||
|
||||
interface CreateEnvironmentEvent {
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
type: EventType.CREATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateEnvironmentEvent {
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
oldName: string;
|
||||
newName: string;
|
||||
oldSlug: string;
|
||||
newSlug: string;
|
||||
}
|
||||
type: EventType.UPDATE_ENVIRONMENT;
|
||||
metadata: {
|
||||
oldName: string;
|
||||
newName: string;
|
||||
oldSlug: string;
|
||||
newSlug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteEnvironmentEvent {
|
||||
type: EventType.DELETE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
}
|
||||
type: EventType.DELETE_ENVIRONMENT;
|
||||
metadata: {
|
||||
name: string;
|
||||
slug: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface AddWorkspaceMemberEvent {
|
||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
type: EventType.ADD_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface RemoveWorkspaceMemberEvent {
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
}
|
||||
type: EventType.REMOVE_WORKSPACE_MEMBER;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateFolderEvent {
|
||||
type: EventType.CREATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
type: EventType.CREATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateFolderEvent {
|
||||
type: EventType.UPDATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
oldFolderName: string;
|
||||
newFolderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
type: EventType.UPDATE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
oldFolderName: string;
|
||||
newFolderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteFolderEvent {
|
||||
type: EventType.DELETE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
}
|
||||
type: EventType.DELETE_FOLDER;
|
||||
metadata: {
|
||||
environment: string;
|
||||
folderId: string;
|
||||
folderName: string;
|
||||
folderPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateWebhookEvent {
|
||||
type: EventType.CREATE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
type: EventType.CREATE_WEBHOOK;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateWebhookStatusEvent {
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
type: EventType.UPDATE_WEBHOOK_STATUS;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteWebhookEvent {
|
||||
type: EventType.DELETE_WEBHOOK,
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
}
|
||||
type: EventType.DELETE_WEBHOOK;
|
||||
metadata: {
|
||||
webhookId: string;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
webhookUrl: string;
|
||||
isDisabled: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
interface GetSecretImportsEvent {
|
||||
type: EventType.GET_SECRET_IMPORTS,
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
numberOfImports: number;
|
||||
}
|
||||
type: EventType.GET_SECRET_IMPORTS;
|
||||
metadata: {
|
||||
environment: string;
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
numberOfImports: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface CreateSecretImportEvent {
|
||||
type: EventType.CREATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
}
|
||||
type: EventType.CREATE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateSecretImportEvent {
|
||||
type: EventType.UPDATE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
orderBefore: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[],
|
||||
orderAfter: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[]
|
||||
}
|
||||
type: EventType.UPDATE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
orderBefore: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[];
|
||||
orderAfter: {
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}[];
|
||||
};
|
||||
}
|
||||
|
||||
interface DeleteSecretImportEvent {
|
||||
type: EventType.DELETE_SECRET_IMPORT,
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
}
|
||||
type: EventType.DELETE_SECRET_IMPORT;
|
||||
metadata: {
|
||||
secretImportId: string;
|
||||
folderId: string;
|
||||
importFromEnvironment: string;
|
||||
importFromSecretPath: string;
|
||||
importToEnvironment: string;
|
||||
importToSecretPath: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserRole {
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE,
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
oldRole: string;
|
||||
newRole: string;
|
||||
}
|
||||
type: EventType.UPDATE_USER_WORKSPACE_ROLE;
|
||||
metadata: {
|
||||
userId: string;
|
||||
email: string;
|
||||
oldRole: string;
|
||||
newRole: string;
|
||||
};
|
||||
}
|
||||
|
||||
interface UpdateUserDeniedPermissions {
|
||||
@@ -411,8 +438,11 @@ export type Event =
|
||||
| GetSecretsEvent
|
||||
| GetSecretEvent
|
||||
| CreateSecretEvent
|
||||
| CreateSecretBatchEvent
|
||||
| UpdateSecretEvent
|
||||
| UpdateSecretBatchEvent
|
||||
| DeleteSecretEvent
|
||||
| DeleteSecretBatchEvent
|
||||
| GetWorkspaceKeyEvent
|
||||
| AuthorizeIntegrationEvent
|
||||
| UnauthorizeIntegrationEvent
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../../variables";
|
||||
|
||||
export interface ISecretVersion {
|
||||
@@ -23,6 +23,7 @@ export interface ISecretVersion {
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
algorithm: "aes-256-gcm";
|
||||
keyEncoding: "utf8" | "base64";
|
||||
createdAt: string;
|
||||
@@ -36,95 +37,96 @@ const secretVersionSchema = new Schema<ISecretVersion>(
|
||||
// could be deleted
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Secret",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
version: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
ref: "User"
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
isDeleted: {
|
||||
// consider removing field
|
||||
type: Boolean,
|
||||
default: false,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretBlindIndex: {
|
||||
type: String,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
default: ALGORITHM_AES_256_GCM,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
default: ENCODING_SCHEME_UTF8,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
tags: {
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: [],
|
||||
default: []
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretVersion = model<ISecretVersion>(
|
||||
"SecretVersion",
|
||||
secretVersionSchema
|
||||
);
|
||||
export const SecretVersion = model<ISecretVersion>("SecretVersion", secretVersionSchema);
|
||||
|
||||
@@ -49,7 +49,8 @@ export enum ProjectPermissionSub {
|
||||
IpAllowList = "ip-allowlist",
|
||||
Workspace = "workspace",
|
||||
Secrets = "secrets",
|
||||
SecretRollback = "secret-rollback"
|
||||
SecretRollback = "secret-rollback",
|
||||
SecretApproval = "secret-approval"
|
||||
}
|
||||
|
||||
type SubjectFields = {
|
||||
@@ -72,6 +73,7 @@ export type ProjectPermissionSet =
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.Settings]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
|
||||
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
|
||||
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
|
||||
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
|
||||
@@ -85,6 +87,11 @@ const buildAdminPermission = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
@@ -154,6 +161,8 @@ const buildMemberPermission = () => {
|
||||
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
|
||||
|
||||
@@ -203,6 +212,7 @@ const buildViewerPermission = () => {
|
||||
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
|
||||
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
|
||||
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
|
||||
|
||||
@@ -103,7 +103,10 @@ export const getSecretsBotHelper = async ({
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
}) => {
|
||||
const content: Record<string, { value: string; comment?: string }> = {};
|
||||
const content: Record<
|
||||
string,
|
||||
{ value: string; comment?: string; skipMultilineEncoding?: boolean }
|
||||
> = {};
|
||||
const key = await getKey({ workspaceId: workspaceId });
|
||||
|
||||
let folderId = "root";
|
||||
@@ -165,6 +168,8 @@ export const getSecretsBotHelper = async ({
|
||||
});
|
||||
content[secretKey].comment = commentValue;
|
||||
}
|
||||
|
||||
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,6 +199,8 @@ export const getSecretsBotHelper = async ({
|
||||
});
|
||||
content[secretKey].comment = commentValue;
|
||||
}
|
||||
|
||||
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
|
||||
});
|
||||
|
||||
await expandSecrets(workspaceId.toString(), key, content);
|
||||
|
||||
+579
-15
@@ -1,9 +1,12 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretBatchParams,
|
||||
CreateSecretParams,
|
||||
DeleteSecretBatchParams,
|
||||
DeleteSecretParams,
|
||||
GetSecretParams,
|
||||
GetSecretsParams,
|
||||
UpdateSecretBatchParams,
|
||||
UpdateSecretParams
|
||||
} from "../interfaces/services/SecretService";
|
||||
import {
|
||||
@@ -53,6 +56,7 @@ import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/aut
|
||||
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
|
||||
import picomatch from "picomatch";
|
||||
import path from "path";
|
||||
import { getAnImportedSecret } from "../services/SecretImportService";
|
||||
|
||||
/**
|
||||
* Validate scope for service token v3
|
||||
@@ -115,6 +119,8 @@ export function containsGlobPatterns(secretPath: string) {
|
||||
return globChars.some((char) => normalizedPath.includes(char));
|
||||
}
|
||||
|
||||
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
|
||||
|
||||
/**
|
||||
* Returns an object containing secret [secret] but with its value, key, comment decrypted.
|
||||
*
|
||||
@@ -374,7 +380,8 @@ export const createSecretHelper = async ({
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretPath = "/",
|
||||
metadata
|
||||
metadata,
|
||||
skipMultilineEncoding
|
||||
}: CreateSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
@@ -438,6 +445,7 @@ export const createSecretHelper = async ({
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
skipMultilineEncoding,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
@@ -460,6 +468,7 @@ export const createSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
skipMultilineEncoding,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
@@ -667,13 +676,14 @@ export const getSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
secretPath = "/"
|
||||
secretPath = "/",
|
||||
include_imports = true
|
||||
}: GetSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
let secret: ISecret | null = null;
|
||||
let secret: ISecret | null | undefined = null;
|
||||
// if using service token filter towards the folderId by secretpath
|
||||
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
@@ -700,6 +710,11 @@ export const getSecretHelper = async ({
|
||||
}).lean();
|
||||
}
|
||||
|
||||
if (!secret && include_imports) {
|
||||
// if still no secret found search in imported secret and retreive
|
||||
secret = await getAnImportedSecret(secretName, workspaceId.toString(), environment, folderId);
|
||||
}
|
||||
|
||||
if (!secret) throw SecretNotFoundError();
|
||||
|
||||
// (EE) create (audit) log
|
||||
@@ -778,24 +793,57 @@ export const updateSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
newSecretName,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretPath
|
||||
secretPath,
|
||||
tags,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
skipMultilineEncoding
|
||||
}: UpdateSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
});
|
||||
|
||||
let secret: ISecret | null = null;
|
||||
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
|
||||
|
||||
let newSecretNameBlindIndex = undefined;
|
||||
if (newSecretName) {
|
||||
newSecretNameBlindIndex = await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName: newSecretName,
|
||||
salt
|
||||
});
|
||||
const doesSecretAlreadyExist = await Secret.exists({
|
||||
secretBlindIndex: newSecretNameBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
type
|
||||
});
|
||||
|
||||
if (doesSecretAlreadyExist) {
|
||||
throw BadRequestError({ message: "Secret with the provided name already exist" });
|
||||
}
|
||||
}
|
||||
|
||||
if (type === SECRET_SHARED) {
|
||||
// case: update shared secret
|
||||
secret = await Secret.findOneAndUpdate(
|
||||
{
|
||||
secretBlindIndex,
|
||||
secretBlindIndex: oldSecretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
@@ -805,6 +853,15 @@ export const updateSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex: newSecretNameBlindIndex,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
tags,
|
||||
$inc: { version: 1 }
|
||||
},
|
||||
{
|
||||
@@ -816,7 +873,7 @@ export const updateSecretHelper = async ({
|
||||
|
||||
secret = await Secret.findOneAndUpdate(
|
||||
{
|
||||
secretBlindIndex,
|
||||
secretBlindIndex: oldSecretBlindIndex,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
@@ -827,10 +884,13 @@ export const updateSecretHelper = async ({
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
tags,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex: newSecretNameBlindIndex,
|
||||
$inc: { version: 1 }
|
||||
},
|
||||
{
|
||||
new: true
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -843,16 +903,18 @@ export const updateSecretHelper = async ({
|
||||
workspace: secret.workspace,
|
||||
folder: folderId,
|
||||
type,
|
||||
tags,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment: secret.environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex,
|
||||
secretBlindIndex: newSecretName ? newSecretNameBlindIndex : oldSecretBlindIndex,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
skipMultilineEncoding,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
});
|
||||
@@ -1136,6 +1198,7 @@ const recursivelyExpandSecret = async (
|
||||
|
||||
let interpolatedValue = interpolatedSec[key];
|
||||
if (!interpolatedValue) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`Couldn't find referenced value - ${key}`);
|
||||
return "";
|
||||
}
|
||||
@@ -1185,7 +1248,7 @@ const formatMultiValueEnv = (val?: string) => {
|
||||
export const expandSecrets = async (
|
||||
workspaceId: string,
|
||||
rootEncKey: string,
|
||||
secrets: Record<string, { value: string; comment?: string }>
|
||||
secrets: Record<string, { value: string; comment?: string; skipMultilineEncoding?: boolean }>
|
||||
) => {
|
||||
const expandedSec: Record<string, string> = {};
|
||||
const interpolatedSec: Record<string, string> = {};
|
||||
@@ -1203,7 +1266,10 @@ export const expandSecrets = async (
|
||||
|
||||
for (const key of Object.keys(secrets)) {
|
||||
if (expandedSec?.[key]) {
|
||||
secrets[key].value = formatMultiValueEnv(expandedSec[key]);
|
||||
// should not do multi line encoding if user has set it to skip
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? expandedSec[key]
|
||||
: formatMultiValueEnv(expandedSec[key]);
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -1218,8 +1284,506 @@ export const expandSecrets = async (
|
||||
key
|
||||
);
|
||||
|
||||
secrets[key].value = formatMultiValueEnv(expandedVal);
|
||||
secrets[key].value = secrets[key].skipMultilineEncoding
|
||||
? expandedVal
|
||||
: formatMultiValueEnv(expandedVal);
|
||||
}
|
||||
|
||||
return secrets;
|
||||
};
|
||||
|
||||
export const createSecretBatchHelper = async ({
|
||||
secrets,
|
||||
workspaceId,
|
||||
authData,
|
||||
secretPath,
|
||||
environment
|
||||
}: CreateSecretBatchParams) => {
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
secrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[secrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = secrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const exists = await Secret.exists({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.exec();
|
||||
|
||||
if (exists)
|
||||
throw BadRequestError({
|
||||
message: "Failed to create secret that already exists"
|
||||
});
|
||||
|
||||
// create secret
|
||||
const newlyCreatedSecrets: ISecret[] = await Secret.insertMany(
|
||||
secrets.map(
|
||||
({
|
||||
type,
|
||||
secretName,
|
||||
secretKeyIV,
|
||||
metadata,
|
||||
secretKeyTag,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretKeyCiphertext,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding
|
||||
}) => ({
|
||||
version: 1,
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
secretKeyCiphertext,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
folder: folderId,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
metadata,
|
||||
skipMultilineEncoding,
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: newlyCreatedSecrets.map(
|
||||
(secret) =>
|
||||
new SecretVersion({
|
||||
secret: secret._id,
|
||||
version: secret.version,
|
||||
workspace: secret.workspace,
|
||||
type: secret.type,
|
||||
folder: folderId,
|
||||
skipMultilineEncoding: secret?.skipMultilineEncoding,
|
||||
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment: secret.environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex: secret.secretBlindIndex,
|
||||
secretKeyCiphertext: secret.secretKeyCiphertext,
|
||||
secretKeyIV: secret.secretKeyIV,
|
||||
secretKeyTag: secret.secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8
|
||||
})
|
||||
)
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.CREATE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secrets: newlyCreatedSecrets.map(({ secretBlindIndex, version, _id }) => ({
|
||||
secretId: _id.toString(),
|
||||
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
|
||||
secretVersion: version
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId
|
||||
}
|
||||
);
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets added",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return newlyCreatedSecrets;
|
||||
};
|
||||
|
||||
export const updateSecretBatchHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
authData,
|
||||
secretPath,
|
||||
secrets
|
||||
}: UpdateSecretBatchParams) => {
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
secrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[secrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = secrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const secretsToBeUpdated = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.select("+secretBlindIndex")
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.lean();
|
||||
|
||||
if (secretsToBeUpdated.length !== secrets.length)
|
||||
throw BadRequestError({ message: "Some secrets not found" });
|
||||
|
||||
await Secret.bulkWrite(
|
||||
secrets.map(
|
||||
({
|
||||
type,
|
||||
secretName,
|
||||
tags,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
secretValueCiphertext,
|
||||
secretCommentCiphertext,
|
||||
skipMultilineEncoding
|
||||
}) => ({
|
||||
updateOne: {
|
||||
filter: {
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folder: folderId,
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
},
|
||||
update: {
|
||||
$inc: {
|
||||
version: 1
|
||||
},
|
||||
secretValueCiphertext,
|
||||
secretValueIV,
|
||||
secretValueTag,
|
||||
secretCommentCiphertext,
|
||||
secretCommentIV,
|
||||
secretCommentTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
tags,
|
||||
skipMultilineEncoding
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const secretsGroupedByBlindIndex = secretsToBeUpdated.reduce<Record<string, ISecret>>(
|
||||
(prev, curr) => {
|
||||
if (curr.secretBlindIndex) prev[curr.secretBlindIndex] = curr;
|
||||
return prev;
|
||||
},
|
||||
{}
|
||||
);
|
||||
|
||||
await EESecretService.addSecretVersions({
|
||||
secretVersions: secrets.map((secret) => {
|
||||
const {
|
||||
_id,
|
||||
version,
|
||||
workspace,
|
||||
type,
|
||||
secretBlindIndex,
|
||||
secretKeyIV,
|
||||
secretKeyTag,
|
||||
secretKeyCiphertext,
|
||||
skipMultilineEncoding
|
||||
} = secretsGroupedByBlindIndex[secretBlindIndexes[secret.secretName]];
|
||||
|
||||
return new SecretVersion({
|
||||
secret: _id,
|
||||
version: version + 1,
|
||||
workspace: workspace,
|
||||
type,
|
||||
folder: folderId,
|
||||
...(secret.type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
|
||||
environment,
|
||||
isDeleted: false,
|
||||
secretBlindIndex: secretBlindIndex,
|
||||
secretKeyCiphertext: secretKeyCiphertext,
|
||||
secretKeyIV: secretKeyIV,
|
||||
secretKeyTag: secretKeyTag,
|
||||
secretValueCiphertext: secret.secretValueCiphertext,
|
||||
secretValueIV: secret.secretValueIV,
|
||||
secretValueTag: secret.secretValueTag,
|
||||
algorithm: ALGORITHM_AES_256_GCM,
|
||||
keyEncoding: ENCODING_SCHEME_UTF8,
|
||||
skipMultilineEncoding
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.UPDATE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secrets: secretsToBeUpdated.map(({ _id, version, secretBlindIndex }) => ({
|
||||
secretId: _id.toString(),
|
||||
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
|
||||
secretVersion: version + 1
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId
|
||||
}
|
||||
);
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets modified",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: 1,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
};
|
||||
|
||||
export const deleteSecretBatchHelper = async ({
|
||||
workspaceId,
|
||||
environment,
|
||||
authData,
|
||||
secretPath = "/",
|
||||
secrets
|
||||
}: DeleteSecretBatchParams) => {
|
||||
let folderId = "root";
|
||||
const folders = await Folder.findOne({
|
||||
workspace: workspaceId,
|
||||
environment
|
||||
});
|
||||
|
||||
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
if (folders) {
|
||||
const folder = getFolderByPath(folders.nodes, secretPath);
|
||||
if (!folder) throw ERR_FOLDER_NOT_FOUND;
|
||||
folderId = folder.id;
|
||||
}
|
||||
|
||||
// get secret blind index salt
|
||||
const salt = await getSecretBlindIndexSaltHelper({
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
|
||||
const secretBlindIndexes = await Promise.all(
|
||||
secrets.map(({ secretName }) =>
|
||||
generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt
|
||||
})
|
||||
)
|
||||
).then((blindIndexes) =>
|
||||
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
|
||||
prev[secrets[i].secretName] = curr;
|
||||
secretBlindIndexToKey[curr] = secrets[i].secretName;
|
||||
return prev;
|
||||
}, {})
|
||||
);
|
||||
|
||||
const deletedSecrets = await Secret.find({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.select({ secretBlindIndexes: 1 })
|
||||
.lean()
|
||||
.exec();
|
||||
|
||||
await Secret.deleteMany({
|
||||
workspace: new Types.ObjectId(workspaceId),
|
||||
folder: folderId,
|
||||
environment
|
||||
})
|
||||
.or(
|
||||
secrets.map(({ secretName, type }) => ({
|
||||
secretBlindIndex: secretBlindIndexes[secretName],
|
||||
type: type === "shared" ? { $in: ["shared", "personal"] } : type,
|
||||
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {})
|
||||
}))
|
||||
)
|
||||
.exec();
|
||||
|
||||
await EESecretService.markDeletedSecretVersions({
|
||||
secretIds: deletedSecrets.map((secret) => secret._id)
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
authData,
|
||||
{
|
||||
type: EventType.DELETE_SECRETS,
|
||||
metadata: {
|
||||
environment,
|
||||
secretPath,
|
||||
secrets: deletedSecrets.map(({ _id, version, secretBlindIndex }) => ({
|
||||
secretId: _id.toString(),
|
||||
secretKey: secretBlindIndexToKey[secretBlindIndex || ""],
|
||||
secretVersion: version
|
||||
}))
|
||||
}
|
||||
},
|
||||
{
|
||||
workspaceId
|
||||
}
|
||||
);
|
||||
|
||||
// (EE) take a secret snapshot
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
|
||||
const postHogClient = await TelemetryService.getPostHogClient();
|
||||
|
||||
if (postHogClient) {
|
||||
postHogClient.capture({
|
||||
event: "secrets deleted",
|
||||
distinctId: await TelemetryService.getDistinctId({
|
||||
authData
|
||||
}),
|
||||
properties: {
|
||||
numberOfSecrets: secrets.length,
|
||||
environment,
|
||||
workspaceId,
|
||||
folderId,
|
||||
channel: authData.userAgentType,
|
||||
userAgent: authData.userAgent
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
secrets: deletedSecrets
|
||||
};
|
||||
};
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
membership as v1MembershipRouter,
|
||||
organization as v1OrganizationRouter,
|
||||
password as v1PasswordRouter,
|
||||
secretApprovalPolicy as v1SecretApprovalPolicy,
|
||||
secretImps as v1SecretImpsRouter,
|
||||
secret as v1SecretRouter,
|
||||
secretsFolder as v1SecretsFolder,
|
||||
@@ -177,6 +178,7 @@ const main = async () => {
|
||||
app.use("/api/v1/webhooks", v1WebhooksRouter);
|
||||
app.use("/api/v1/secret-imports", v1SecretImpsRouter);
|
||||
app.use("/api/v1/roles", v1RoleRouter);
|
||||
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
|
||||
|
||||
// v2 routes (improvements)
|
||||
app.use("/api/v2/signup", v2SignupRouter);
|
||||
|
||||
@@ -65,10 +65,10 @@ import sodium from "libsodium-wrappers";
|
||||
import { standardRequest } from "../config/request";
|
||||
|
||||
const getSecretKeyValuePair = (
|
||||
secrets: Record<string, { value: string; comment?: string } | null>
|
||||
secrets: Record<string, { value: string | null; comment?: string } | null>
|
||||
) =>
|
||||
Object.keys(secrets).reduce<Record<string, string>>((prev, key) => {
|
||||
if (secrets[key]) prev[key] = secrets[key]?.value || "";
|
||||
Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
|
||||
prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value;
|
||||
return prev;
|
||||
}, {});
|
||||
|
||||
@@ -325,40 +325,42 @@ const syncSecretsGCPSecretManager = async ({
|
||||
name: string;
|
||||
createTime: string;
|
||||
}
|
||||
|
||||
|
||||
interface GCPSMListSecretsRes {
|
||||
secrets?: GCPSecret[];
|
||||
totalSize?: number;
|
||||
nextPageToken?: string;
|
||||
}
|
||||
|
||||
|
||||
let gcpSecrets: GCPSecret[] = [];
|
||||
|
||||
|
||||
const pageSize = 100;
|
||||
let pageToken: string | undefined;
|
||||
let hasMorePages = true;
|
||||
|
||||
const filterParam = integration.metadata.secretGCPLabel
|
||||
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
|
||||
const filterParam = integration.metadata.secretGCPLabel
|
||||
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
|
||||
: "";
|
||||
|
||||
|
||||
while (hasMorePages) {
|
||||
const params = new URLSearchParams({
|
||||
pageSize: String(pageSize),
|
||||
...(pageToken ? { pageToken } : {})
|
||||
});
|
||||
|
||||
const res: GCPSMListSecretsRes = (await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
"Authorization": `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
const res: GCPSMListSecretsRes = (
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
|
||||
{
|
||||
params,
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
|
||||
)
|
||||
).data;
|
||||
|
||||
if (res.secrets) {
|
||||
const filteredSecrets = res.secrets?.filter((gcpSecret) => {
|
||||
const arr = gcpSecret.name.split("/");
|
||||
@@ -366,54 +368,58 @@ const syncSecretsGCPSecretManager = async ({
|
||||
|
||||
let isValid = true;
|
||||
|
||||
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) {
|
||||
if (
|
||||
integration.metadata.secretPrefix &&
|
||||
!key.startsWith(integration.metadata.secretPrefix)
|
||||
) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
gcpSecrets = gcpSecrets.concat(filteredSecrets);
|
||||
}
|
||||
|
||||
|
||||
if (!res.nextPageToken) {
|
||||
hasMorePages = false;
|
||||
}
|
||||
|
||||
|
||||
pageToken = res.nextPageToken;
|
||||
}
|
||||
|
||||
const res: { [key: string]: string; } = {};
|
||||
|
||||
|
||||
const res: { [key: string]: string } = {};
|
||||
|
||||
interface GCPLatestSecretVersionAccess {
|
||||
name: string;
|
||||
payload: {
|
||||
data: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
for await (const gcpSecret of gcpSecrets) {
|
||||
const arr = gcpSecret.name.split("/");
|
||||
const key = arr[arr.length - 1];
|
||||
|
||||
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
const secretLatest: GCPLatestSecretVersionAccess = (
|
||||
await standardRequest.get(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
"Accept-Encoding": "application/json"
|
||||
}
|
||||
}
|
||||
}
|
||||
)).data;
|
||||
)
|
||||
).data;
|
||||
|
||||
|
||||
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
|
||||
}
|
||||
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in res)) {
|
||||
// case: create secret
|
||||
@@ -423,11 +429,14 @@ const syncSecretsGCPSecretManager = async ({
|
||||
replication: {
|
||||
automatic: {}
|
||||
},
|
||||
...(integration.metadata.secretGCPLabel ? {
|
||||
labels: {
|
||||
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue
|
||||
}
|
||||
} : {})
|
||||
...(integration.metadata.secretGCPLabel
|
||||
? {
|
||||
labels: {
|
||||
[integration.metadata.secretGCPLabel.labelName]:
|
||||
integration.metadata.secretGCPLabel.labelValue
|
||||
}
|
||||
}
|
||||
: {})
|
||||
},
|
||||
{
|
||||
params: {
|
||||
@@ -439,7 +448,7 @@ const syncSecretsGCPSecretManager = async ({
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
await standardRequest.post(
|
||||
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`,
|
||||
{
|
||||
@@ -456,7 +465,7 @@ const syncSecretsGCPSecretManager = async ({
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for await (const key of Object.keys(res)) {
|
||||
if (!(key in secrets)) {
|
||||
// case: delete secret
|
||||
@@ -489,7 +498,7 @@ const syncSecretsGCPSecretManager = async ({
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
|
||||
@@ -729,15 +738,12 @@ const syncSecretsAWSParameterStore = async ({
|
||||
} = {};
|
||||
|
||||
if (parameterList) {
|
||||
awsParameterStoreSecretsObj = parameterList.reduce(
|
||||
(obj: any, secret: any) => {
|
||||
return ({
|
||||
...obj,
|
||||
[secret.Name.substring(integration.path.length)]: secret
|
||||
});
|
||||
},
|
||||
{}
|
||||
);
|
||||
awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => {
|
||||
return {
|
||||
...obj,
|
||||
[secret.Name.substring(integration.path.length)]: secret
|
||||
};
|
||||
}, {});
|
||||
}
|
||||
|
||||
// Identify secrets to create
|
||||
@@ -1869,8 +1875,10 @@ const syncSecretsGitLab = async ({
|
||||
value: string;
|
||||
environment_scope: string;
|
||||
}
|
||||
|
||||
const gitLabApiUrl = integrationAuth.url ? `${integrationAuth.url}/api` : INTEGRATION_GITLAB_API_URL;
|
||||
|
||||
const gitLabApiUrl = integrationAuth.url
|
||||
? `${integrationAuth.url}/api`
|
||||
: INTEGRATION_GITLAB_API_URL;
|
||||
|
||||
const getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
|
||||
const headers = {
|
||||
@@ -1880,7 +1888,9 @@ const syncSecretsGitLab = async ({
|
||||
};
|
||||
|
||||
let allEnvVariables: GitLabSecret[] = [];
|
||||
let url: string | null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
|
||||
let url:
|
||||
| string
|
||||
| null = `${gitLabApiUrl}/v4/projects/${integrationAppId}/variables?per_page=100`;
|
||||
|
||||
while (url) {
|
||||
const response: any = await standardRequest.get(url, { headers });
|
||||
@@ -1901,23 +1911,27 @@ const syncSecretsGitLab = async ({
|
||||
|
||||
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
|
||||
const getSecretsRes: GitLabSecret[] = allEnvVariables
|
||||
.filter(
|
||||
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
|
||||
)
|
||||
.filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
|
||||
.filter((gitLabSecret) => {
|
||||
let isValid = true;
|
||||
|
||||
if (integration.metadata.secretPrefix && !gitLabSecret.key.startsWith(integration.metadata.secretPrefix)) {
|
||||
if (
|
||||
integration.metadata.secretPrefix &&
|
||||
!gitLabSecret.key.startsWith(integration.metadata.secretPrefix)
|
||||
) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) {
|
||||
if (
|
||||
integration.metadata.secretSuffix &&
|
||||
!gitLabSecret.key.endsWith(integration.metadata.secretSuffix)
|
||||
) {
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
|
||||
return isValid;
|
||||
});
|
||||
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
const existingSecret = getSecretsRes.find((s: any) => s.key == key);
|
||||
if (!existingSecret) {
|
||||
@@ -2371,41 +2385,43 @@ const syncSecretsTeamCity = async ({
|
||||
|
||||
if (integration.targetEnvironment && integration.targetEnvironmentId) {
|
||||
// case: sync to specific build-config in TeamCity project
|
||||
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
|
||||
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
))
|
||||
.data
|
||||
.property
|
||||
.filter((parameter) => !parameter.inherited)
|
||||
.reduce((obj: any, secret: TeamCitySecret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
};
|
||||
}, {});
|
||||
|
||||
const res = (
|
||||
await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
|
||||
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
)
|
||||
).data.property
|
||||
.filter((parameter) => !parameter.inherited)
|
||||
.reduce((obj: any, secret: TeamCitySecret) => {
|
||||
const secretName = secret.name.replace(/^env\./, "");
|
||||
return {
|
||||
...obj,
|
||||
[secretName]: secret.value
|
||||
};
|
||||
}, {});
|
||||
|
||||
for await (const key of Object.keys(secrets)) {
|
||||
if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
|
||||
// case: secret does not exist in TeamCity or secret value has changed
|
||||
// -> create/update secret
|
||||
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
name:`env.${key}`,
|
||||
value: secrets[key].value
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json",
|
||||
await standardRequest.post(
|
||||
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
|
||||
{
|
||||
name: `env.${key}`,
|
||||
value: secrets[key].value
|
||||
},
|
||||
});
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
Accept: "application/json"
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3034,4 +3050,4 @@ const syncSecretsNorthflank = async ({
|
||||
);
|
||||
};
|
||||
|
||||
export { syncSecrets };
|
||||
export { syncSecrets };
|
||||
|
||||
@@ -16,10 +16,11 @@ export interface CreateSecretParams {
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
secretPath: string;
|
||||
metadata?: {
|
||||
source?: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export interface GetSecretsParams {
|
||||
@@ -37,10 +38,15 @@ export interface GetSecretParams {
|
||||
environment: string;
|
||||
type?: "shared" | "personal";
|
||||
authData: AuthData;
|
||||
include_imports?: boolean;
|
||||
}
|
||||
|
||||
export interface UpdateSecretParams {
|
||||
secretName: string;
|
||||
newSecretName?: string;
|
||||
secretKeyCiphertext?: string;
|
||||
secretKeyIV?: string;
|
||||
secretKeyTag?: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
type: "shared" | "personal";
|
||||
@@ -49,6 +55,11 @@ export interface UpdateSecretParams {
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretPath: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
export interface DeleteSecretParams {
|
||||
@@ -59,3 +70,57 @@ export interface DeleteSecretParams {
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
}
|
||||
|
||||
export interface CreateSecretBatchParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
secrets: Array<{
|
||||
secretName: string;
|
||||
type: "shared" | "personal";
|
||||
secretKeyCiphertext: string;
|
||||
secretKeyIV: string;
|
||||
secretKeyTag: string;
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
metadata?: {
|
||||
source?: string;
|
||||
};
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface UpdateSecretBatchParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
secrets: Array<{
|
||||
secretName: string;
|
||||
type: "shared" | "personal";
|
||||
secretValueCiphertext: string;
|
||||
secretValueIV: string;
|
||||
secretValueTag: string;
|
||||
secretCommentCiphertext?: string;
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
tags?: string[];
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface DeleteSecretBatchParams {
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
authData: AuthData;
|
||||
secretPath: string;
|
||||
secrets: Array<{
|
||||
secretName: string;
|
||||
type: "shared" | "personal";
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
ENCODING_SCHEME_BASE64,
|
||||
ENCODING_SCHEME_UTF8,
|
||||
SECRET_PERSONAL,
|
||||
SECRET_SHARED,
|
||||
SECRET_SHARED
|
||||
} from "../variables";
|
||||
|
||||
export interface ISecret {
|
||||
@@ -12,7 +12,7 @@ export interface ISecret {
|
||||
version: number;
|
||||
workspace: Types.ObjectId;
|
||||
type: string;
|
||||
user: Types.ObjectId;
|
||||
user?: Types.ObjectId;
|
||||
environment: string;
|
||||
secretBlindIndex?: string;
|
||||
secretKeyCiphertext: string;
|
||||
@@ -27,13 +27,14 @@ export interface ISecret {
|
||||
secretCommentIV?: string;
|
||||
secretCommentTag?: string;
|
||||
secretCommentHash?: string;
|
||||
skipMultilineEncoding?: boolean;
|
||||
algorithm: "aes-256-gcm";
|
||||
keyEncoding: "utf8" | "base64";
|
||||
tags?: string[];
|
||||
folder?: string;
|
||||
metadata?: {
|
||||
[key: string]: string;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const secretSchema = new Schema<ISecret>(
|
||||
@@ -41,108 +42,112 @@ const secretSchema = new Schema<ISecret>(
|
||||
version: {
|
||||
type: Number,
|
||||
required: true,
|
||||
default: 1,
|
||||
default: 1
|
||||
},
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
enum: [SECRET_SHARED, SECRET_PERSONAL],
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
user: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
ref: "User"
|
||||
},
|
||||
tags: {
|
||||
ref: "Tag",
|
||||
type: [Schema.Types.ObjectId],
|
||||
default: [],
|
||||
default: []
|
||||
},
|
||||
environment: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretBlindIndex: {
|
||||
type: String,
|
||||
select: false,
|
||||
select: false
|
||||
},
|
||||
secretKeyCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretKeyHash: {
|
||||
type: String,
|
||||
type: String
|
||||
},
|
||||
secretValueCiphertext: {
|
||||
type: String,
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueIV: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueTag: {
|
||||
type: String, // symmetric
|
||||
required: true,
|
||||
required: true
|
||||
},
|
||||
secretValueHash: {
|
||||
type: String,
|
||||
type: String
|
||||
},
|
||||
secretCommentCiphertext: {
|
||||
type: String,
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
secretCommentIV: {
|
||||
type: String, // symmetric
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
secretCommentTag: {
|
||||
type: String, // symmetric
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
secretCommentHash: {
|
||||
type: String,
|
||||
required: false,
|
||||
required: false
|
||||
},
|
||||
skipMultilineEncoding: {
|
||||
type: Boolean,
|
||||
required: false
|
||||
},
|
||||
algorithm: {
|
||||
// the encryption algorithm used
|
||||
type: String,
|
||||
enum: [ALGORITHM_AES_256_GCM],
|
||||
required: true,
|
||||
default: ALGORITHM_AES_256_GCM,
|
||||
default: ALGORITHM_AES_256_GCM
|
||||
},
|
||||
keyEncoding: {
|
||||
type: String,
|
||||
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
|
||||
required: true,
|
||||
default: ENCODING_SCHEME_UTF8,
|
||||
default: ENCODING_SCHEME_UTF8
|
||||
},
|
||||
folder: {
|
||||
type: String,
|
||||
default: "root",
|
||||
default: "root"
|
||||
},
|
||||
metadata: {
|
||||
type: Schema.Types.Mixed
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
secretSchema.index({ tags: 1 }, { background: true });
|
||||
|
||||
export const Secret = model<ISecret>("Secret", secretSchema);
|
||||
export const Secret = model<ISecret>("Secret", secretSchema);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
|
||||
export interface ISecretApprovalPolicy {
|
||||
_id: Types.ObjectId;
|
||||
workspace: Types.ObjectId;
|
||||
environment: string;
|
||||
secretPath?: string;
|
||||
approvers: Types.ObjectId[];
|
||||
approvals: number;
|
||||
}
|
||||
|
||||
const secretApprovalPolicySchema = new Schema<ISecretApprovalPolicy>(
|
||||
{
|
||||
workspace: {
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Workspace",
|
||||
required: true
|
||||
},
|
||||
approvers: [
|
||||
{
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Membership"
|
||||
}
|
||||
],
|
||||
environment: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
secretPath: {
|
||||
type: String,
|
||||
required: false
|
||||
},
|
||||
approvals: {
|
||||
type: Number,
|
||||
default: 1
|
||||
}
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretApprovalPolicy = model<ISecretApprovalPolicy>(
|
||||
"SecretApprovalPolicy",
|
||||
secretApprovalPolicySchema
|
||||
);
|
||||
@@ -1,81 +1,68 @@
|
||||
import mongoose, { Schema, model } from "mongoose";
|
||||
import { ISecret, Secret } from "./secret";
|
||||
import { Schema, Types, model } from "mongoose";
|
||||
import { ISecretVersion, SecretVersion } from "../ee/models/secretVersion";
|
||||
|
||||
interface ISecretApprovalRequest {
|
||||
secret: mongoose.Types.ObjectId;
|
||||
requestedChanges: ISecret;
|
||||
requestedBy: mongoose.Types.ObjectId;
|
||||
approvers: IApprover[];
|
||||
status: ApprovalStatus;
|
||||
timestamp: Date;
|
||||
requestType: RequestType;
|
||||
requestId: string;
|
||||
enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
}
|
||||
|
||||
interface IApprover {
|
||||
userId: mongoose.Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
enum CommitType {
|
||||
DELETE = "delete",
|
||||
UPDATE = "update",
|
||||
CREATE = "create"
|
||||
}
|
||||
|
||||
export enum ApprovalStatus {
|
||||
PENDING = "pending",
|
||||
APPROVED = "approved",
|
||||
REJECTED = "rejected"
|
||||
export interface ISecretApprovalRequest {
|
||||
_id: Types.ObjectId;
|
||||
committer: Types.ObjectId;
|
||||
approvers: {
|
||||
member: Types.ObjectId;
|
||||
status: ApprovalStatus;
|
||||
}[];
|
||||
approvals: number;
|
||||
hasMerged: boolean;
|
||||
status: ApprovalStatus;
|
||||
commits: {
|
||||
secretVersion: Types.ObjectId;
|
||||
newVersion: ISecretVersion;
|
||||
op: CommitType;
|
||||
}[];
|
||||
}
|
||||
|
||||
export enum RequestType {
|
||||
UPDATE = "update",
|
||||
DELETE = "delete",
|
||||
CREATE = "create"
|
||||
}
|
||||
|
||||
const approverSchema = new mongoose.Schema({
|
||||
user: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
required: true,
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
enum: [ApprovalStatus],
|
||||
default: ApprovalStatus.PENDING,
|
||||
},
|
||||
});
|
||||
|
||||
const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
|
||||
{
|
||||
secret: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "Secret",
|
||||
},
|
||||
requestedChanges: Secret,
|
||||
requestedBy: {
|
||||
type: mongoose.Schema.Types.ObjectId,
|
||||
ref: "User",
|
||||
},
|
||||
approvers: [approverSchema],
|
||||
status: {
|
||||
type: String,
|
||||
enum: ApprovalStatus,
|
||||
default: ApprovalStatus.PENDING,
|
||||
},
|
||||
timestamp: {
|
||||
type: Date,
|
||||
default: Date.now,
|
||||
},
|
||||
requestType: {
|
||||
type: String,
|
||||
enum: RequestType,
|
||||
required: true,
|
||||
},
|
||||
requestId: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
timestamps: true,
|
||||
}
|
||||
{
|
||||
approvers: [
|
||||
{
|
||||
member: {
|
||||
// user associated with the personal secret
|
||||
type: Schema.Types.ObjectId,
|
||||
ref: "Membership"
|
||||
},
|
||||
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
|
||||
}
|
||||
],
|
||||
approvals: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
hasMerged: { type: Boolean, default: false },
|
||||
status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING },
|
||||
committer: { type: Schema.Types.ObjectId, ref: "Membership" },
|
||||
commits: [
|
||||
{
|
||||
secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
|
||||
newVersion: SecretVersion,
|
||||
op: { type: String, enum: [CommitType], required: true }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
timestamps: true
|
||||
}
|
||||
);
|
||||
|
||||
export const SecretApprovalRequest = model<ISecretApprovalRequest>("SecretApprovalRequest", secretApprovalRequestSchema);
|
||||
export const SecretApprovalRequest = model<ISecretApprovalRequest>(
|
||||
"SecretApprovalRequest",
|
||||
secretApprovalRequestSchema
|
||||
);
|
||||
|
||||
@@ -13,7 +13,7 @@ export const githubFullRepositorySecretScan = new Queue("github-full-repository-
|
||||
|
||||
type TScanPushEventQueueDetails = {
|
||||
organizationId: string,
|
||||
installationId: number,
|
||||
installationId: string,
|
||||
repository: {
|
||||
id: number,
|
||||
fullName: string,
|
||||
@@ -30,7 +30,8 @@ githubFullRepositorySecretScan.process(async (job: Job, done: Queue.DoneCallback
|
||||
installationId: installationId
|
||||
},
|
||||
});
|
||||
const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId, repository.fullName)
|
||||
|
||||
const findings: SecretMatch[] = await scanFullRepoContentAndGetFindings(octokit, installationId as any, repository.fullName)
|
||||
for (const finding of findings) {
|
||||
await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint },
|
||||
{
|
||||
|
||||
@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
|
||||
import secretsFolder from "./secretsFolder";
|
||||
import webhooks from "./webhook";
|
||||
import secretImps from "./secretImps";
|
||||
import secretApprovalPolicy from "./secretApprovalPolicy";
|
||||
|
||||
export {
|
||||
signup,
|
||||
@@ -37,5 +38,6 @@ export {
|
||||
integrationAuth,
|
||||
secretsFolder,
|
||||
webhooks,
|
||||
secretImps
|
||||
secretImps,
|
||||
secretApprovalPolicy
|
||||
};
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import { requireAuth } from "../../middleware";
|
||||
import { secretApprovalPolicyController } from "../../controllers/v1";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
router.get(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.getSecretApprovalPolicy
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.createSecretApprovalPolicy
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.updateSecretApprovalPolicy
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:id",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT]
|
||||
}),
|
||||
secretApprovalPolicyController.deleteSecretApprovalPolicy
|
||||
);
|
||||
|
||||
export default router;
|
||||
@@ -18,7 +18,7 @@ router.post(
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/:folderId",
|
||||
"/:folderName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
@@ -26,7 +26,7 @@ router.patch(
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/:folderId",
|
||||
"/:folderName",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
|
||||
@@ -1,14 +1,8 @@
|
||||
import express from "express";
|
||||
const router = express.Router();
|
||||
import {
|
||||
requireAuth,
|
||||
requireBlindIndicesEnabled,
|
||||
requireE2EEOff
|
||||
} from "../../middleware";
|
||||
import { requireAuth, requireBlindIndicesEnabled, requireE2EEOff } from "../../middleware";
|
||||
import { secretsController } from "../../controllers/v3";
|
||||
import {
|
||||
AuthMode
|
||||
} from "../../variables";
|
||||
import { AuthMode } from "../../variables";
|
||||
|
||||
router.get(
|
||||
"/raw",
|
||||
@@ -85,6 +79,40 @@ router.get(
|
||||
secretsController.getSecrets
|
||||
);
|
||||
|
||||
// akhilmhdh: dont put batch router below the individual operation as those have arbitory name as params
|
||||
router.post(
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
}),
|
||||
secretsController.createSecretByNameBatch
|
||||
);
|
||||
|
||||
router.patch(
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
}),
|
||||
secretsController.updateSecretByNameBatch
|
||||
);
|
||||
|
||||
router.delete(
|
||||
"/batch",
|
||||
requireAuth({
|
||||
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
|
||||
}),
|
||||
requireBlindIndicesEnabled({
|
||||
locationWorkspaceId: "body"
|
||||
}),
|
||||
secretsController.deleteSecretByNameBatch
|
||||
);
|
||||
|
||||
router.post(
|
||||
"/:secretName",
|
||||
requireAuth({
|
||||
|
||||
@@ -1,9 +1,61 @@
|
||||
import { Types } from "mongoose";
|
||||
import { generateSecretBlindIndexHelper } from "../helpers";
|
||||
import { Folder, ISecret, Secret, SecretImport } from "../models";
|
||||
import { getFolderByPath } from "./FolderService";
|
||||
|
||||
type TSecretImportFid = { environment: string; folderId: string; secretPath: string };
|
||||
|
||||
export const getAnImportedSecret = async (
|
||||
secretName: string,
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
folderId = "root"
|
||||
) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId: new Types.ObjectId(workspaceId)
|
||||
});
|
||||
|
||||
const secImports = await SecretImport.findOne({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
folderId
|
||||
});
|
||||
if (!secImports) return;
|
||||
if (secImports.imports.length === 0) return;
|
||||
const folders = await Folder.find({
|
||||
workspace: workspaceId,
|
||||
environment: { $in: secImports.imports.map((el) => el.environment) }
|
||||
});
|
||||
|
||||
const importedSecByFid: TSecretImportFid[] = [];
|
||||
secImports.imports.forEach((el) => {
|
||||
const folder = folders.find((fl) => fl.environment === el.environment);
|
||||
if (folder) {
|
||||
const secPathFolder = getFolderByPath(folder.nodes, el.secretPath);
|
||||
if (secPathFolder)
|
||||
importedSecByFid.push({
|
||||
environment: el.environment,
|
||||
folderId: secPathFolder.id,
|
||||
secretPath: el.secretPath
|
||||
});
|
||||
} else {
|
||||
if (el.secretPath === "/") {
|
||||
// this happens when importing with a fresh env without any folders
|
||||
importedSecByFid.push({ environment: el.environment, folderId: "root", secretPath: "/" });
|
||||
}
|
||||
}
|
||||
});
|
||||
if (importedSecByFid.length === 0) return;
|
||||
|
||||
const secret = await Secret.findOne({
|
||||
workspace: workspaceId,
|
||||
secretBlindIndex
|
||||
}).or(importedSecByFid.map(({ environment, folderId }) => ({ environment, folder: folderId }))).lean()
|
||||
|
||||
return secret;
|
||||
};
|
||||
|
||||
export const getAllImportedSecrets = async (
|
||||
workspaceId: string,
|
||||
environment: string,
|
||||
@@ -56,14 +108,6 @@ export const getAllImportedSecrets = async (
|
||||
type: "shared"
|
||||
}
|
||||
},
|
||||
{
|
||||
$lookup: {
|
||||
from: "tags", // note this is the name of the collection in the database, not the Mongoose model name
|
||||
localField: "tags",
|
||||
foreignField: "_id",
|
||||
as: "tags"
|
||||
}
|
||||
},
|
||||
{
|
||||
$group: {
|
||||
_id: {
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import { Types } from "mongoose";
|
||||
import {
|
||||
CreateSecretParams,
|
||||
DeleteSecretParams,
|
||||
GetSecretParams,
|
||||
GetSecretsParams,
|
||||
UpdateSecretParams,
|
||||
CreateSecretBatchParams,
|
||||
CreateSecretParams,
|
||||
DeleteSecretBatchParams,
|
||||
DeleteSecretParams,
|
||||
GetSecretParams,
|
||||
GetSecretsParams,
|
||||
UpdateSecretBatchParams,
|
||||
UpdateSecretParams
|
||||
} from "../interfaces/services/SecretService";
|
||||
import {
|
||||
createSecretBlindIndexDataHelper,
|
||||
createSecretHelper,
|
||||
deleteSecretHelper,
|
||||
generateSecretBlindIndexHelper,
|
||||
generateSecretBlindIndexWithSaltHelper,
|
||||
getSecretBlindIndexSaltHelper,
|
||||
getSecretHelper,
|
||||
getSecretsHelper,
|
||||
updateSecretHelper,
|
||||
import {
|
||||
createSecretBatchHelper,
|
||||
createSecretBlindIndexDataHelper,
|
||||
createSecretHelper,
|
||||
deleteSecretBatchHelper,
|
||||
deleteSecretHelper,
|
||||
generateSecretBlindIndexHelper,
|
||||
generateSecretBlindIndexWithSaltHelper,
|
||||
getSecretBlindIndexSaltHelper,
|
||||
getSecretHelper,
|
||||
getSecretsHelper,
|
||||
updateSecretBatchHelper,
|
||||
updateSecretHelper
|
||||
} from "../helpers/secrets";
|
||||
|
||||
class SecretService {
|
||||
@@ -26,13 +32,9 @@ class SecretService {
|
||||
* @param {Buffer} obj.salt - 16-byte random salt
|
||||
* @param {Types.ObjectId} obj.workspaceId
|
||||
*/
|
||||
static async createSecretBlindIndexData({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
static async createSecretBlindIndexData({ workspaceId }: { workspaceId: Types.ObjectId }) {
|
||||
return await createSecretBlindIndexDataHelper({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -42,13 +44,9 @@ class SecretService {
|
||||
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
|
||||
* @returns
|
||||
*/
|
||||
static async getSecretBlindIndexSalt({
|
||||
workspaceId,
|
||||
}: {
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
static async getSecretBlindIndexSalt({ workspaceId }: { workspaceId: Types.ObjectId }) {
|
||||
return await getSecretBlindIndexSaltHelper({
|
||||
workspaceId,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -61,14 +59,14 @@ class SecretService {
|
||||
*/
|
||||
static async generateSecretBlindIndexWithSalt({
|
||||
secretName,
|
||||
salt,
|
||||
salt
|
||||
}: {
|
||||
secretName: string;
|
||||
salt: string;
|
||||
}) {
|
||||
return await generateSecretBlindIndexWithSaltHelper({
|
||||
secretName,
|
||||
salt,
|
||||
salt
|
||||
});
|
||||
}
|
||||
|
||||
@@ -81,14 +79,14 @@ class SecretService {
|
||||
*/
|
||||
static async generateSecretBlindIndex({
|
||||
secretName,
|
||||
workspaceId,
|
||||
workspaceId
|
||||
}: {
|
||||
secretName: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
}) {
|
||||
return await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
workspaceId,
|
||||
workspaceId
|
||||
});
|
||||
}
|
||||
|
||||
@@ -163,6 +161,18 @@ class SecretService {
|
||||
static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
|
||||
return await deleteSecretHelper(deleteSecretParams);
|
||||
}
|
||||
|
||||
static async createSecretBatch(createSecretParams: CreateSecretBatchParams) {
|
||||
return await createSecretBatchHelper(createSecretParams);
|
||||
}
|
||||
|
||||
static async updateSecretBatch(updateSecretParams: UpdateSecretBatchParams) {
|
||||
return await updateSecretBatchHelper(updateSecretParams);
|
||||
}
|
||||
|
||||
static async deleteSecretBatch(deleteSecretParams: DeleteSecretBatchParams) {
|
||||
return await deleteSecretBatchHelper(deleteSecretParams);
|
||||
}
|
||||
}
|
||||
|
||||
export default SecretService;
|
||||
|
||||
@@ -819,3 +819,18 @@ export const backfillPermission = async () => {
|
||||
console.info("Could not acquire lock for script [backfillPermission], skipping");
|
||||
}
|
||||
};
|
||||
|
||||
export const migrateRoleFromOwnerToAdmin = async () => {
|
||||
await MembershipOrg.updateMany(
|
||||
{
|
||||
role: OWNER
|
||||
},
|
||||
{
|
||||
$set: {
|
||||
role: ADMIN
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.info("Backfill: Finished converting owner role to member");
|
||||
}
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
backfillServiceToken,
|
||||
backfillServiceTokenMultiScope,
|
||||
backfillTrustedIps,
|
||||
backfillUserAuthMethods
|
||||
backfillUserAuthMethods,
|
||||
migrateRoleFromOwnerToAdmin
|
||||
} from "./backfillData";
|
||||
import {
|
||||
reencryptBotOrgKeys,
|
||||
@@ -85,6 +86,7 @@ export const setup = async () => {
|
||||
await backfillTrustedIps();
|
||||
await backfillUserAuthMethods();
|
||||
// await backfillPermission();
|
||||
await migrateRoleFromOwnerToAdmin()
|
||||
|
||||
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
|
||||
// to base64 256-bit ROOT_ENCRYPTION_KEY
|
||||
|
||||
@@ -5,28 +5,30 @@ export const CreateFolderV1 = z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
folderName: z.string().trim(),
|
||||
parentFolderId: z.string().trim().optional()
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
export const UpdateFolderV1 = z.object({
|
||||
params: z.object({
|
||||
folderId: z.string().trim()
|
||||
folderName: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
name: z.string().trim()
|
||||
name: z.string().trim(),
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteFolderV1 = z.object({
|
||||
params: z.object({
|
||||
folderId: z.string().trim()
|
||||
folderName: z.string().trim()
|
||||
}),
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim()
|
||||
environment: z.string().trim(),
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -34,7 +36,6 @@ export const GetFoldersV1 = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
parentFolderId: z.string().trim().optional(),
|
||||
parentFolderPath: z.string().trim().optional()
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from "./secretApproval";
|
||||
export * from "./user";
|
||||
export * from "./workspace";
|
||||
export * from "./bot";
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const GetSecretApprovalRuleList = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const CreateSecretApprovalRule = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string(),
|
||||
environment: z.string(),
|
||||
secretPath: z.string().optional().nullable(),
|
||||
approvers: z.string().array().optional(),
|
||||
approvals: z.number().min(1).default(1)
|
||||
})
|
||||
});
|
||||
|
||||
export const UpdateSecretApprovalRule = z.object({
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
}),
|
||||
body: z.object({
|
||||
approvers: z.string().array().optional(),
|
||||
approvals: z.number().min(1).optional(),
|
||||
secretPath: z.string().optional().nullable()
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteSecretApprovalRule = z.object({
|
||||
params: z.object({
|
||||
id: z.string()
|
||||
})
|
||||
});
|
||||
@@ -4,7 +4,7 @@ export const CreateSecretImportV1 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root"),
|
||||
directory: z.string().trim().default("/"),
|
||||
secretImport: z.object({
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim()
|
||||
@@ -40,7 +40,7 @@ export const GetSecretImportsV1 = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root")
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -48,6 +48,6 @@ export const GetAllSecretsFromImportV1 = z.object({
|
||||
query: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root")
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@ export const CreateInstalLSessionv1 = z.object({
|
||||
|
||||
export const LinkInstallationToOrgv1 = z.object({
|
||||
body: z.object({
|
||||
installationId: z.number(),
|
||||
installationId: z.string(),
|
||||
sessionId: z.string().trim()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -244,7 +244,11 @@ export const GetSecretByNameRawV3 = z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional()
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional(),
|
||||
include_imports: z
|
||||
.enum(["true", "false"])
|
||||
.default("true")
|
||||
.transform((value) => value === "true")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -253,8 +257,11 @@ export const CreateSecretRawV3 = z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
secretValue: z.string().trim(),
|
||||
secretValue: z
|
||||
.string()
|
||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
|
||||
secretComment: z.string().trim().optional().default(""),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
|
||||
}),
|
||||
params: z.object({
|
||||
@@ -269,8 +276,11 @@ export const UpdateSecretByNameRawV3 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretValue: z.string().trim(),
|
||||
secretValue: z
|
||||
.string()
|
||||
.transform((val) => (val.at(-1) === "\n" ? `${val.trim()}\n` : val.trim())),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED)
|
||||
})
|
||||
});
|
||||
@@ -305,7 +315,11 @@ export const GetSecretByNameV3 = z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional()
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).optional(),
|
||||
include_imports: z
|
||||
.enum(["true", "false"])
|
||||
.default("true")
|
||||
.transform((value) => value === "true")
|
||||
}),
|
||||
params: z.object({
|
||||
secretName: z.string().trim()
|
||||
@@ -327,7 +341,8 @@ export const CreateSecretV3 = z.object({
|
||||
secretCommentCiphertext: z.string().trim().optional(),
|
||||
secretCommentIV: z.string().trim().optional(),
|
||||
secretCommentTag: z.string().trim().optional(),
|
||||
metadata: z.record(z.string()).optional()
|
||||
metadata: z.record(z.string()).optional(),
|
||||
skipMultilineEncoding: z.boolean().optional()
|
||||
}),
|
||||
params: z.object({
|
||||
secretName: z.string().trim()
|
||||
@@ -342,7 +357,17 @@ export const UpdateSecretByNameV3 = z.object({
|
||||
secretPath: z.string().trim().default("/"),
|
||||
secretValueCiphertext: z.string().trim(),
|
||||
secretValueIV: z.string().trim(),
|
||||
secretValueTag: z.string().trim()
|
||||
secretValueTag: z.string().trim(),
|
||||
secretCommentCiphertext: z.string().trim().optional(),
|
||||
secretCommentIV: z.string().trim().optional(),
|
||||
secretCommentTag: z.string().trim().optional(),
|
||||
tags: z.string().array().optional(),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
// to update secret name
|
||||
secretName: z.string().trim().optional(),
|
||||
secretKeyIV: z.string().trim().optional(),
|
||||
secretKeyTag: z.string().trim().optional(),
|
||||
secretKeyCiphertext: z.string().trim().optional()
|
||||
}),
|
||||
params: z.object({
|
||||
secretName: z.string()
|
||||
@@ -360,3 +385,67 @@ export const DeleteSecretByNameV3 = z.object({
|
||||
secretName: z.string()
|
||||
})
|
||||
});
|
||||
|
||||
export const CreateSecretByNameBatchV3 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
secrets: z
|
||||
.object({
|
||||
secretName: z.string().trim(),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
secretKeyCiphertext: z.string().trim(),
|
||||
secretKeyIV: z.string().trim(),
|
||||
secretKeyTag: z.string().trim(),
|
||||
secretValueCiphertext: z.string().trim(),
|
||||
secretValueIV: z.string().trim(),
|
||||
secretValueTag: z.string().trim(),
|
||||
secretCommentCiphertext: z.string().trim().optional(),
|
||||
secretCommentIV: z.string().trim().optional(),
|
||||
secretCommentTag: z.string().trim().optional(),
|
||||
metadata: z.record(z.string()).optional(),
|
||||
skipMultilineEncoding: z.boolean().optional()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
})
|
||||
});
|
||||
|
||||
export const UpdateSecretByNameBatchV3 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
secrets: z
|
||||
.object({
|
||||
secretName: z.string().trim(),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
|
||||
secretValueCiphertext: z.string().trim(),
|
||||
secretValueIV: z.string().trim(),
|
||||
secretValueTag: z.string().trim(),
|
||||
secretCommentCiphertext: z.string().trim().optional(),
|
||||
secretCommentIV: z.string().trim().optional(),
|
||||
secretCommentTag: z.string().trim().optional(),
|
||||
skipMultilineEncoding: z.boolean().optional(),
|
||||
tags: z.string().array().optional()
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
})
|
||||
});
|
||||
|
||||
export const DeleteSecretByNameBatchV3 = z.object({
|
||||
body: z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
secretPath: z.string().trim().default("/"),
|
||||
secrets: z
|
||||
.object({
|
||||
secretName: z.string().trim(),
|
||||
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
|
||||
})
|
||||
.array()
|
||||
.min(1)
|
||||
})
|
||||
});
|
||||
|
||||
@@ -34,9 +34,10 @@ export const validateClientForWorkspace = async ({
|
||||
}) => {
|
||||
const workspace = await Workspace.findById(workspaceId);
|
||||
|
||||
if (!workspace) throw WorkspaceNotFoundError({
|
||||
message: "Failed to find workspace"
|
||||
});
|
||||
if (!workspace)
|
||||
throw WorkspaceNotFoundError({
|
||||
message: "Failed to find workspace"
|
||||
});
|
||||
|
||||
let membership;
|
||||
switch (authData.actor.type) {
|
||||
@@ -71,7 +72,7 @@ export const GetWorkspaceSecretSnapshotsV1 = z.object({
|
||||
}),
|
||||
query: z.object({
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root"),
|
||||
directory: z.string().trim().default("/"),
|
||||
offset: z.coerce.number(),
|
||||
limit: z.coerce.number()
|
||||
})
|
||||
@@ -83,7 +84,7 @@ export const GetWorkspaceSecretSnapshotsCountV1 = z.object({
|
||||
}),
|
||||
query: z.object({
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root")
|
||||
directory: z.string().trim().default("/")
|
||||
})
|
||||
});
|
||||
|
||||
@@ -93,7 +94,7 @@ export const RollbackWorkspaceSecretSnapshotV1 = z.object({
|
||||
}),
|
||||
body: z.object({
|
||||
environment: z.string().trim(),
|
||||
folderId: z.string().trim().default("root"),
|
||||
directory: z.string().trim().default("/"),
|
||||
version: z.number()
|
||||
})
|
||||
});
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
// secrets
|
||||
export const SECRET_SHARED = "shared";
|
||||
export const SECRET_PERSONAL = "personal";
|
||||
export const SECRET_PERSONAL = "personal";
|
||||
|
||||
Reference in New Issue
Block a user