Fix merge conflicts

This commit is contained in:
Tuan Dang
2023-09-29 11:42:47 +01:00
145 changed files with 8601 additions and 5093 deletions
+3 -1
View File
@@ -16,6 +16,7 @@ import * as workspaceController from "./workspaceController";
import * as secretScanningController from "./secretScanningController"; import * as secretScanningController from "./secretScanningController";
import * as webhookController from "./webhookController"; import * as webhookController from "./webhookController";
import * as secretImpsController from "./secretImpsController"; import * as secretImpsController from "./secretImpsController";
import * as secretApprovalPolicyController from "./secretApprovalPolicyController";
export { export {
authController, authController,
@@ -35,5 +36,6 @@ export {
workspaceController, workspaceController,
secretScanningController, secretScanningController,
webhookController, 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 { isValidScope } from "../../helpers";
import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models"; import { Folder, IServiceTokenData, SecretImport, ServiceTokenData } from "../../models";
import { getAllImportedSecrets } from "../../services/SecretImportService"; import { getAllImportedSecrets } from "../../services/SecretImportService";
import { getFolderWithPathFromId } from "../../services/FolderService"; import { getFolderByPath, getFolderWithPathFromId } from "../../services/FolderService";
import { import {
BadRequestError, BadRequestError,
ResourceNotFoundError, ResourceNotFoundError,
@@ -95,37 +95,12 @@ export const createSecretImp = async (req: Request, res: Response) => {
*/ */
const { const {
body: { workspaceId, environment, folderId, secretImport } body: { workspaceId, environment, directory, secretImport }
} = await validateRequest(reqValidator.CreateSecretImportV1, req); } = await validateRequest(reqValidator.CreateSecretImportV1, req);
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && folderId !== "root") {
throw ResourceNotFoundError({
message: "Failed to find folder"
});
}
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
// root check // root check
let isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
isValidScopeAccess = isValidScope(
req.authData.authPayload,
secretImport.environment,
secretImport.secretPath
);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -133,27 +108,31 @@ export const createSecretImp = async (req: Request, res: Response) => {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: secretImport.environment,
secretPath: secretImport.secretPath
})
); );
} }
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/")
throw ResourceNotFoundError({ message: "Failed to find folder" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId folderId
}); });
const importToSecretPath = folders
? getFolderWithPathFromId(folders.nodes, folderId).folderPath
: "/";
if (!importSecDoc) { if (!importSecDoc) {
const doc = new SecretImport({ const doc = new SecretImport({
workspace: workspaceId, workspace: workspaceId,
@@ -173,7 +152,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
importFromEnvironment: secretImport.environment, importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath, importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment, importToEnvironment: environment,
importToSecretPath importToSecretPath: directory
} }
}, },
{ {
@@ -206,7 +185,7 @@ export const createSecretImp = async (req: Request, res: Response) => {
importFromEnvironment: secretImport.environment, importFromEnvironment: secretImport.environment,
importFromSecretPath: secretImport.secretPath, importFromSecretPath: secretImport.secretPath,
importToEnvironment: environment, importToEnvironment: environment,
importToSecretPath importToSecretPath: directory
} }
}, },
{ {
@@ -563,8 +542,38 @@ export const getSecretImports = async (req: Request, res: Response) => {
} }
*/ */
const { const {
query: { workspaceId, environment, folderId } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetSecretImportsV1, req); } = await validateRequest(reqValidator.GetSecretImportsV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -575,41 +584,6 @@ export const getSecretImports = async (req: Request, res: Response) => {
return res.status(200).json({ secretImport: {} }); return res.status(200).json({ secretImport: {} });
} }
// check for service token validity
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/";
if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
secretPath = folderPath;
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
importSecDoc.environment,
secretPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(
req.user._id,
importSecDoc.workspace.toString()
);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment: importSecDoc.environment,
secretPath
})
);
}
return res.status(200).json({ secretImport: importSecDoc }); return res.status(200).json({ secretImport: importSecDoc });
}; };
@@ -621,9 +595,39 @@ export const getSecretImports = async (req: Request, res: Response) => {
*/ */
export const getAllSecretsFromImport = async (req: Request, res: Response) => { export const getAllSecretsFromImport = async (req: Request, res: Response) => {
const { const {
query: { workspaceId, environment, folderId } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req); } = await validateRequest(reqValidator.GetAllSecretsFromImportV1, req);
if (req.authData.authPayload instanceof ServiceTokenData) {
// check for service token validity
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, {
environment,
secretPath: directory
})
);
}
const folders = await Folder.findOne({
workspace: workspaceId,
environment
}).lean();
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
let folderId = "root";
if (folders) {
const folder = getFolderByPath(folders.nodes, directory);
if (!folder) throw BadRequestError({ message: "Folder not found" });
folderId = folder.id;
}
const importSecDoc = await SecretImport.findOne({ const importSecDoc = await SecretImport.findOne({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -634,11 +638,6 @@ export const getAllSecretsFromImport = async (req: Request, res: Response) => {
return res.status(200).json({ secrets: [] }); return res.status(200).json({ secrets: [] });
} }
const folders = await Folder.findOne({
workspace: importSecDoc.workspace,
environment: importSecDoc.environment
}).lean();
let secretPath = "/"; let secretPath = "/";
if (folders) { if (folders) {
const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId); const { folderPath } = getFolderWithPathFromId(folders.nodes, importSecDoc.folderId);
@@ -9,12 +9,10 @@ import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder"; import { Folder } from "../../models/folder";
import { import {
appendFolder, appendFolder,
deleteFolderById,
generateFolderId, generateFolderId,
getAllFolderIds, getAllFolderIds,
getFolderByPath, getFolderByPath,
getFolderWithPathFromId, getFolderWithPathFromId,
getParentFromFolderId,
validateFolderName validateFolderName
} from "../../services/FolderService"; } from "../../services/FolderService";
import { import {
@@ -25,13 +23,9 @@ import {
import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors"; import { BadRequestError, UnauthorizedRequestError } from "../../utils/errors";
import * as reqValidator from "../../validation/folders"; import * as reqValidator from "../../validation/folders";
/** const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "The folder doesn't exist" });
* Create folder with name [folderName] for workspace with id [workspaceId]
* and environment [environment] // verify workspace id/environment
* @param req
* @param res
* @returns
*/
export const createFolder = async (req: Request, res: Response) => { export const createFolder = async (req: Request, res: Response) => {
/* /*
#swagger.summary = 'Create a folder' #swagger.summary = 'Create a folder'
@@ -107,7 +101,7 @@ export const createFolder = async (req: Request, res: Response) => {
} }
*/ */
const { const {
body: { workspaceId, environment, folderName, parentFolderId } body: { workspaceId, environment, folderName, directory }
} = await validateRequest(reqValidator.CreateFolderV1, req); } = await validateRequest(reqValidator.CreateFolderV1, req);
if (!validateFolderName(folderName)) { if (!validateFolderName(folderName)) {
@@ -116,33 +110,29 @@ export const createFolder = async (req: Request, res: Response) => {
}); });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
// token check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
// user check
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ const folders = await Folder.findOne({
workspace: workspaceId, workspace: workspaceId,
environment environment
}).lean(); }).lean();
if (req.user) {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
const secretPath =
folders && parentFolderId
? getFolderWithPathFromId(folders.nodes, parentFolderId).folderPath
: "/";
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
// space has no folders initialized // space has no folders initialized
if (!folders) { if (!folders) {
if (req.authData.authPayload instanceof ServiceTokenData) { if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const id = generateFolderId(); const id = generateFolderId();
const folder = new Folder({ const folder = new Folder({
@@ -186,27 +176,10 @@ export const createFolder = async (req: Request, res: Response) => {
return res.json({ folder: { id, name: folderName } }); return res.json({ folder: { id, name: folderName } });
} }
const folder = appendFolder(folders.nodes, { folderName, parentFolderId }); const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
await Folder.findByIdAndUpdate(folders._id, folders);
const { folder: parentFolder, folderPath: parentFolderPath } = getFolderWithPathFromId(
folders.nodes,
parentFolderId || "root"
);
if (req.authData.authPayload instanceof ServiceTokenData) {
// root check
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = appendFolder(folders.nodes, { folderName, parentFolderId: parentFolder.id });
await Folder.findByIdAndUpdate(folders._id, folders); await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
@@ -219,11 +192,9 @@ export const createFolder = async (req: Request, res: Response) => {
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId: parentFolderId folderId: parentFolder.id
}); });
const { folderPath } = getFolderWithPathFromId(folders.nodes, folder.id);
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
req.authData, req.authData,
{ {
@@ -232,7 +203,7 @@ export const createFolder = async (req: Request, res: Response) => {
environment, environment,
folderId: folder.id, folderId: folder.id,
folderName, folderName,
folderPath folderPath: directory
} }
}, },
{ {
@@ -332,8 +303,8 @@ export const updateFolderById = async (req: Request, res: Response) => {
} }
*/ */
const { const {
body: { workspaceId, environment, name }, body: { workspaceId, environment, name, directory },
params: { folderId } params: { folderName }
} = await validateRequest(reqValidator.UpdateFolderV1, req); } = await validateRequest(reqValidator.UpdateFolderV1, req);
if (!validateFolderName(name)) { if (!validateFolderName(name)) {
@@ -342,38 +313,31 @@ export const updateFolderById = async (req: Request, res: Response) => {
}); });
} }
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
} else {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
);
}
const folders = await Folder.findOne({ workspace: workspaceId, environment }); const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) { if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
const parentFolder = getParentFromFolderId(folders.nodes, folderId); const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) { if (!parentFolder) {
throw BadRequestError({ message: "The folder doesn't exist" }); throw BadRequestError({ message: "The folder doesn't exist" });
} }
if (req.user) { const folder = parentFolder.children.find(({ name }) => name === folderName);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); if (!folder) throw ERR_FOLDER_NOT_FOUND;
const secretPath = getFolderWithPathFromId(folders.nodes, parentFolder.id).folderPath;
ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
);
}
const folder = parentFolder.children.find(({ id }) => id === folderId);
if (!folder) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
if (req.authData.authPayload instanceof ServiceTokenData) {
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
// root check
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const oldFolderName = folder.name; const oldFolderName = folder.name;
parentFolder.version += 1; parentFolder.version += 1;
@@ -505,24 +469,12 @@ export const deleteFolder = async (req: Request, res: Response) => {
} }
*/ */
const { const {
params: { folderId }, params: { folderName },
body: { environment, workspaceId } body: { environment, workspaceId, directory }
} = await validateRequest(reqValidator.DeleteFolderV1, req); } = await validateRequest(reqValidator.DeleteFolderV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const delOp = deleteFolderById(folders.nodes, folderId);
if (!delOp) {
throw BadRequestError({ message: "The folder doesn't exist" });
}
const { deletedNode: delFolder, parent: parentFolder } = delOp;
const { folderPath: secretPath } = getFolderWithPathFromId(folders.nodes, parentFolder.id);
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, secretPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
@@ -531,12 +483,23 @@ export const deleteFolder = async (req: Request, res: Response) => {
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath }) subject(ProjectPermissionSub.Secrets, { environment, secretPath: directory })
); );
} }
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders) throw ERR_FOLDER_NOT_FOUND;
const parentFolder = getFolderByPath(folders.nodes, directory);
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
const index = parentFolder.children.findIndex(({ name }) => name === folderName);
if (index === -1) throw ERR_FOLDER_NOT_FOUND;
const deletedFolder = parentFolder.children.splice(index, 1)[0];
parentFolder.version += 1; parentFolder.version += 1;
const delFolderIds = getAllFolderIds(delFolder); const delFolderIds = getAllFolderIds(deletedFolder);
await Folder.findByIdAndUpdate(folders._id, folders); await Folder.findByIdAndUpdate(folders._id, folders);
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
@@ -565,9 +528,9 @@ export const deleteFolder = async (req: Request, res: Response) => {
type: EventType.DELETE_FOLDER, type: EventType.DELETE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId, folderId: deletedFolder.id,
folderName: delFolder.name, folderName: deletedFolder.name,
folderPath: secretPath folderPath: directory
} }
}, },
{ {
@@ -575,7 +538,7 @@ export const deleteFolder = async (req: Request, res: Response) => {
} }
); );
res.send({ message: "successfully deleted folders", folders: delFolderIds }); return res.send({ message: "successfully deleted folders", folders: delFolderIds });
}; };
/** /**
@@ -677,69 +640,27 @@ export const getFolders = async (req: Request, res: Response) => {
} }
*/ */
const { const {
query: { workspaceId, environment, parentFolderId, parentFolderPath } query: { workspaceId, environment, directory }
} = await validateRequest(reqValidator.GetFoldersV1, req); } = await validateRequest(reqValidator.GetFoldersV1, req);
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (req.user) await getUserProjectPermissions(req.user._id, workspaceId);
if (!folders) {
res.send({ folders: [], dir: [] });
return;
}
// if instead of parentFolderId given a path like /folder1/folder2
if (parentFolderPath) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(
req.authData.authPayload,
environment,
parentFolderPath
);
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const folder = getFolderByPath(folders.nodes, parentFolderPath);
if (!folder) {
res.send({ folders: [], dir: [] });
return;
}
// dir is not needed at present as this is only used in overview section of secrets
res.send({
folders: folder.children.map(({ id, name }) => ({ id, name })),
dir: [{ name: folder.name, id: folder.id }]
});
}
if (!parentFolderId) {
if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, "/");
if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
}
}
const rootFolders = folders.nodes.children.map(({ id, name }) => ({
id,
name
}));
res.send({ folders: rootFolders });
return;
}
const { folder, folderPath, dir } = getFolderWithPathFromId(folders.nodes, parentFolderId);
if (req.authData.authPayload instanceof ServiceTokenData) { if (req.authData.authPayload instanceof ServiceTokenData) {
const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, folderPath); const isValidScopeAccess = isValidScope(req.authData.authPayload, environment, directory);
if (!isValidScopeAccess) { if (!isValidScopeAccess) {
throw UnauthorizedRequestError({ message: "Folder Permission Denied" }); throw UnauthorizedRequestError({ message: "Folder Permission Denied" });
} }
} else {
// check that user is a member of the workspace
await getUserProjectPermissions(req.user._id, workspaceId);
} }
res.send({ const folders = await Folder.findOne({ workspace: workspaceId, environment });
folders: folder.children.map(({ id, name }) => ({ id, name })), if (!folders) {
dir return res.send({ folders: [], dir: [] });
}
const folder = getFolderByPath(folders.nodes, directory);
return res.send({
folders: folder?.children?.map(({ id, name }) => ({ id, name })) || []
}); });
}; };
@@ -1,6 +1,7 @@
import { Request, Response } from "express"; import { Request, Response } from "express";
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
Folder,
Integration, Integration,
Membership, Membership,
Secret, Secret,
@@ -21,9 +22,12 @@ import {
getUserProjectPermissions getUserProjectPermissions
} from "../../ee/services/ProjectRoleService"; } from "../../ee/services/ProjectRoleService";
import { ForbiddenError } from "@casl/ability"; 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 * with slug [environmentSlug] under workspace with id
* @param req * @param req
* @param res * @param res
@@ -369,13 +373,38 @@ export const renameWorkspaceEnvironment = async (req: Request, res: Response) =>
{ environment: environmentSlug } { environment: environmentSlug }
); );
await ServiceTokenData.updateMany( 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( await Integration.updateMany(
{ workspace: workspaceId, environment: oldEnvironmentSlug }, { workspace: workspaceId, environment: oldEnvironmentSlug },
{ environment: environmentSlug } { 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( await Membership.updateMany(
{ {
workspace: workspaceId, workspace: workspaceId,
+195 -12
View File
@@ -184,7 +184,7 @@ export const getSecretsRaw = async (req: Request, res: Response) => {
*/ */
export const getSecretByNameRaw = async (req: Request, res: Response) => { export const getSecretByNameRaw = async (req: Request, res: Response) => {
const { const {
query: { secretPath, environment, workspaceId, type }, query: { secretPath, environment, workspaceId, type, include_imports },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameRawV3, req); } = await validateRequest(reqValidator.GetSecretByNameRawV3, req);
@@ -225,7 +225,8 @@ export const getSecretByNameRaw = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData authData: req.authData,
include_imports
}); });
const key = await BotService.getWorkspaceKeyWithBot({ 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) => { export const createSecretRaw = async (req: Request, res: Response) => {
const { const {
params: { secretName }, params: { secretName },
body: { secretPath, environment, workspaceId, type, secretValue, secretComment } body: {
secretPath,
environment,
workspaceId,
type,
secretValue,
secretComment,
skipMultilineEncoding
}
} = await validateRequest(reqValidator.CreateSecretRawV3, req); } = await validateRequest(reqValidator.CreateSecretRawV3, req);
switch (req.authData.actor.type) { switch (req.authData.actor.type) {
@@ -316,7 +325,8 @@ export const createSecretRaw = async (req: Request, res: Response) => {
secretPath, secretPath,
secretCommentCiphertext: secretCommentEncrypted.ciphertext, secretCommentCiphertext: secretCommentEncrypted.ciphertext,
secretCommentIV: secretCommentEncrypted.iv, secretCommentIV: secretCommentEncrypted.iv,
secretCommentTag: secretCommentEncrypted.tag secretCommentTag: secretCommentEncrypted.tag,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -346,7 +356,7 @@ export const createSecretRaw = async (req: Request, res: Response) => {
export const updateSecretByNameRaw = async (req: Request, res: Response) => { export const updateSecretByNameRaw = async (req: Request, res: Response) => {
const { const {
params: { secretName }, params: { secretName },
body: { secretValue, environment, secretPath, type, workspaceId } body: { secretValue, environment, secretPath, type, workspaceId, skipMultilineEncoding }
} = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameRawV3, req);
switch (req.authData.actor.type) { switch (req.authData.actor.type) {
@@ -398,7 +408,8 @@ export const updateSecretByNameRaw = async (req: Request, res: Response) => {
secretValueCiphertext: secretValueEncrypted.ciphertext, secretValueCiphertext: secretValueEncrypted.ciphertext,
secretValueIV: secretValueEncrypted.iv, secretValueIV: secretValueEncrypted.iv,
secretValueTag: secretValueEncrypted.tag, secretValueTag: secretValueEncrypted.tag,
secretPath secretPath,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -604,7 +615,7 @@ export const getSecrets = async (req: Request, res: Response) => {
*/ */
export const getSecretByName = async (req: Request, res: Response) => { export const getSecretByName = async (req: Request, res: Response) => {
const { const {
query: { secretPath, environment, workspaceId, type }, query: { secretPath, environment, workspaceId, type, include_imports },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.GetSecretByNameV3, req); } = await validateRequest(reqValidator.GetSecretByNameV3, req);
@@ -645,7 +656,8 @@ export const getSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
secretPath, secretPath,
authData: req.authData authData: req.authData,
include_imports
}); });
return res.status(200).send({ return res.status(200).send({
@@ -674,7 +686,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentTag, secretCommentTag,
secretKeyCiphertext, secretKeyCiphertext,
secretValueCiphertext, secretValueCiphertext,
secretCommentCiphertext secretCommentCiphertext,
skipMultilineEncoding
}, },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.CreateSecretV3, req); } = await validateRequest(reqValidator.CreateSecretV3, req);
@@ -726,7 +739,8 @@ export const createSecret = async (req: Request, res: Response) => {
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
metadata metadata,
skipMultilineEncoding
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -759,10 +773,23 @@ export const updateSecretByName = async (req: Request, res: Response) => {
type, type,
environment, environment,
secretPath, secretPath,
workspaceId workspaceId,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
secretName: newSecretName,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
skipMultilineEncoding
}, },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.UpdateSecretByNameV3, req); } = await validateRequest(reqValidator.UpdateSecretByNameV3, req);
if (newSecretName && (!secretKeyIV || !secretKeyTag || !secretKeyCiphertext)) {
throw BadRequestError({ message: "Missing encrypted key" });
}
switch (req.authData.actor.type) { switch (req.authData.actor.type) {
case ActorType.USER: { case ActorType.USER: {
@@ -801,10 +828,19 @@ export const updateSecretByName = async (req: Request, res: Response) => {
environment, environment,
type, type,
authData: req.authData, authData: req.authData,
newSecretName,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath secretPath,
tags,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV
}); });
await EventService.handleEvent({ await EventService.handleEvent({
@@ -883,3 +919,150 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
secret secret
}); });
}; };
export const createSecretByNameBatch = async (req: Request, res: Response) => {
const {
body: { secrets, secretPath, environment, workspaceId }
} = await validateRequest(reqValidator.CreateSecretByNameBatchV3, req);
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 { EESecretService } from "../../services";
import { getLatestSecretVersionIds } from "../../helpers/secretVersion"; import { getLatestSecretVersionIds } from "../../helpers/secretVersion";
// import Folder, { TFolderSchema } from "../../../models/folder"; // import Folder, { TFolderSchema } from "../../../models/folder";
import { searchByFolderId } from "../../../services/FolderService"; import { getFolderByPath, searchByFolderId } from "../../../services/FolderService";
import { EEAuditLogService, EELicenseService } from "../../services"; import { EEAuditLogService, EELicenseService } from "../../services";
import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip"; import { extractIPDetails, isValidIpOrCidr } from "../../../utils/ip";
import { validateRequest } from "../../../helpers/validation"; import { validateRequest } from "../../../helpers/validation";
@@ -106,7 +106,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
*/ */
const { const {
params: { workspaceId }, params: { workspaceId },
query: { environment, folderId, offset, limit } query: { environment, directory, offset, limit }
} = await validateRequest(GetWorkspaceSecretSnapshotsV1, req); } = await validateRequest(GetWorkspaceSecretSnapshotsV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -115,10 +115,20 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
const secretSnapshots = await SecretSnapshot.find({ const secretSnapshots = await SecretSnapshot.find({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root" folderId
}) })
.sort({ createdAt: -1 }) .sort({ createdAt: -1 })
.skip(offset) .skip(offset)
@@ -137,7 +147,7 @@ export const getWorkspaceSecretSnapshots = async (req: Request, res: Response) =
export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => { export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Response) => {
const { const {
params: { workspaceId }, params: { workspaceId },
query: { environment, folderId } query: { environment, directory }
} = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req); } = await validateRequest(GetWorkspaceSecretSnapshotsCountV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -146,10 +156,20 @@ export const getWorkspaceSecretSnapshotsCount = async (req: Request, res: Respon
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
const count = await SecretSnapshot.countDocuments({ const count = await SecretSnapshot.countDocuments({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
folderId: folderId || "root" folderId
}); });
return res.status(200).send({ return res.status(200).send({
@@ -217,7 +237,7 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
const { const {
params: { workspaceId }, params: { workspaceId },
body: { folderId, environment, version } body: { directory, environment, version }
} = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req); } = await validateRequest(RollbackWorkspaceSecretSnapshotV1, req);
const { permission } = await getUserProjectPermissions(req.user._id, workspaceId); const { permission } = await getUserProjectPermissions(req.user._id, workspaceId);
@@ -226,6 +246,16 @@ export const rollbackWorkspaceSecretSnapshot = async (req: Request, res: Respons
ProjectPermissionSub.SecretRollback ProjectPermissionSub.SecretRollback
); );
let folderId = "root";
const folders = await Folder.findOne({ workspace: workspaceId, environment });
if (!folders && directory !== "/") throw BadRequestError({ message: "Folder not found" });
if (folders) {
const folder = getFolderByPath(folders?.nodes, directory);
if (!folder) throw BadRequestError({ message: "Invalid folder id" });
folderId = folder.id;
}
// validate secret snapshot // validate secret snapshot
const secretSnapshot = await SecretSnapshot.findOne({ const secretSnapshot = await SecretSnapshot.findOne({
workspace: workspaceId, workspace: workspaceId,
+7 -4
View File
@@ -5,10 +5,10 @@ export enum ActorType {
} }
export enum UserAgentType { export enum UserAgentType {
WEB = "web", WEB = "web",
CLI = "cli", CLI = "cli",
K8_OPERATOR = "k8-operator", K8_OPERATOR = "k8-operator",
OTHER = "other" OTHER = "other"
} }
export enum EventType { export enum EventType {
@@ -16,8 +16,11 @@ export enum EventType {
GET_SECRET = "get-secret", GET_SECRET = "get-secret",
REVEAL_SECRET = "reveal-secret", REVEAL_SECRET = "reveal-secret",
CREATE_SECRET = "create-secret", CREATE_SECRET = "create-secret",
CREATE_SECRETS = "create-secrets",
UPDATE_SECRET = "update-secret", UPDATE_SECRET = "update-secret",
UPDATE_SECRETS = "update-secrets",
DELETE_SECRET = "delete-secret", DELETE_SECRET = "delete-secret",
DELETE_SECRETS = "delete-secrets",
GET_WORKSPACE_KEY = "get-workspace-key", GET_WORKSPACE_KEY = "get-workspace-key",
AUTHORIZE_INTEGRATION = "authorize-integration", AUTHORIZE_INTEGRATION = "authorize-integration",
UNAUTHORIZE_INTEGRATION = "unauthorize-integration", UNAUTHORIZE_INTEGRATION = "unauthorize-integration",
+274 -244
View File
@@ -7,23 +7,23 @@ import {
} from "../../../models/serviceTokenDataV3"; } from "../../../models/serviceTokenDataV3";
interface UserActorMetadata { interface UserActorMetadata {
userId: string; userId: string;
email: string; email: string;
} }
interface ServiceActorMetadata { interface ServiceActorMetadata {
serviceId: string; serviceId: string;
name: string; name: string;
} }
export interface UserActor { export interface UserActor {
type: ActorType.USER; type: ActorType.USER;
metadata: UserActorMetadata; metadata: UserActorMetadata;
} }
export interface ServiceActor { export interface ServiceActor {
type: ActorType.SERVICE; type: ActorType.SERVICE;
metadata: ServiceActorMetadata; metadata: ServiceActorMetadata;
} }
export interface ServiceActorV3 { export interface ServiceActorV3 {
@@ -37,164 +37,191 @@ export type Actor =
| ServiceActorV3; | ServiceActorV3;
interface GetSecretsEvent { interface GetSecretsEvent {
type: EventType.GET_SECRETS; type: EventType.GET_SECRETS;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
numberOfSecrets: number; numberOfSecrets: number;
}; };
} }
interface GetSecretEvent { interface GetSecretEvent {
type: EventType.GET_SECRET; type: EventType.GET_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
}; };
} }
interface CreateSecretEvent { interface CreateSecretEvent {
type: EventType.CREATE_SECRET; type: EventType.CREATE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface CreateSecretBatchEvent {
type: EventType.CREATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface UpdateSecretEvent { interface UpdateSecretEvent {
type: EventType.UPDATE_SECRET; type: EventType.UPDATE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface UpdateSecretBatchEvent {
type: EventType.UPDATE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface DeleteSecretEvent { interface DeleteSecretEvent {
type: EventType.DELETE_SECRET; type: EventType.DELETE_SECRET;
metadata: { metadata: {
environment: string; environment: string;
secretPath: string; secretPath: string;
secretId: string; secretId: string;
secretKey: string; secretKey: string;
secretVersion: number; secretVersion: number;
} };
}
interface DeleteSecretBatchEvent {
type: EventType.DELETE_SECRETS;
metadata: {
environment: string;
secretPath: string;
secrets: Array<{ secretId: string; secretKey: string; secretVersion: number }>;
};
} }
interface GetWorkspaceKeyEvent { interface GetWorkspaceKeyEvent {
type: EventType.GET_WORKSPACE_KEY, type: EventType.GET_WORKSPACE_KEY;
metadata: { metadata: {
keyId: string; keyId: string;
} };
} }
interface AuthorizeIntegrationEvent { interface AuthorizeIntegrationEvent {
type: EventType.AUTHORIZE_INTEGRATION; type: EventType.AUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface UnauthorizeIntegrationEvent { interface UnauthorizeIntegrationEvent {
type: EventType.UNAUTHORIZE_INTEGRATION; type: EventType.UNAUTHORIZE_INTEGRATION;
metadata: { metadata: {
integration: string; integration: string;
} };
} }
interface CreateIntegrationEvent { interface CreateIntegrationEvent {
type: EventType.CREATE_INTEGRATION; type: EventType.CREATE_INTEGRATION;
metadata: { metadata: {
integrationId: string; integrationId: string;
integration: string; // TODO: fix type integration: string; // TODO: fix type
environment: string; environment: string;
secretPath: string; secretPath: string;
url?: string; url?: string;
app?: string; app?: string;
appId?: string; appId?: string;
targetEnvironment?: string; targetEnvironment?: string;
targetEnvironmentId?: string; targetEnvironmentId?: string;
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface DeleteIntegrationEvent { interface DeleteIntegrationEvent {
type: EventType.DELETE_INTEGRATION; type: EventType.DELETE_INTEGRATION;
metadata: { metadata: {
integrationId: string; integrationId: string;
integration: string; // TODO: fix type integration: string; // TODO: fix type
environment: string; environment: string;
secretPath: string; secretPath: string;
url?: string; url?: string;
app?: string; app?: string;
appId?: string; appId?: string;
targetEnvironment?: string; targetEnvironment?: string;
targetEnvironmentId?: string; targetEnvironmentId?: string;
targetService?: string; targetService?: string;
targetServiceId?: string; targetServiceId?: string;
path?: string; path?: string;
region?: string; region?: string;
} };
} }
interface AddTrustedIPEvent { interface AddTrustedIPEvent {
type: EventType.ADD_TRUSTED_IP; type: EventType.ADD_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface UpdateTrustedIPEvent { interface UpdateTrustedIPEvent {
type: EventType.UPDATE_TRUSTED_IP; type: EventType.UPDATE_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface DeleteTrustedIPEvent { interface DeleteTrustedIPEvent {
type: EventType.DELETE_TRUSTED_IP; type: EventType.DELETE_TRUSTED_IP;
metadata: { metadata: {
trustedIpId: string; trustedIpId: string;
ipAddress: string; ipAddress: string;
prefix?: number; prefix?: number;
} };
} }
interface CreateServiceTokenEvent { interface CreateServiceTokenEvent {
type: EventType.CREATE_SERVICE_TOKEN; type: EventType.CREATE_SERVICE_TOKEN;
metadata: { metadata: {
name: string; name: string;
scopes: Array<{ scopes: Array<{
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
} };
} }
interface DeleteServiceTokenEvent { interface DeleteServiceTokenEvent {
type: EventType.DELETE_SERVICE_TOKEN; type: EventType.DELETE_SERVICE_TOKEN;
metadata: { metadata: {
name: string; name: string;
scopes: Array<{ scopes: Array<{
environment: string; environment: string;
secretPath: string; secretPath: string;
}>; }>;
} };
} }
interface CreateServiceTokenV3Event { interface CreateServiceTokenV3Event {
@@ -228,171 +255,171 @@ interface DeleteServiceTokenV3Event {
} }
interface CreateEnvironmentEvent { interface CreateEnvironmentEvent {
type: EventType.CREATE_ENVIRONMENT; type: EventType.CREATE_ENVIRONMENT;
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface UpdateEnvironmentEvent { interface UpdateEnvironmentEvent {
type: EventType.UPDATE_ENVIRONMENT; type: EventType.UPDATE_ENVIRONMENT;
metadata: { metadata: {
oldName: string; oldName: string;
newName: string; newName: string;
oldSlug: string; oldSlug: string;
newSlug: string; newSlug: string;
} };
} }
interface DeleteEnvironmentEvent { interface DeleteEnvironmentEvent {
type: EventType.DELETE_ENVIRONMENT; type: EventType.DELETE_ENVIRONMENT;
metadata: { metadata: {
name: string; name: string;
slug: string; slug: string;
} };
} }
interface AddWorkspaceMemberEvent { interface AddWorkspaceMemberEvent {
type: EventType.ADD_WORKSPACE_MEMBER; type: EventType.ADD_WORKSPACE_MEMBER;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface RemoveWorkspaceMemberEvent { interface RemoveWorkspaceMemberEvent {
type: EventType.REMOVE_WORKSPACE_MEMBER; type: EventType.REMOVE_WORKSPACE_MEMBER;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
} };
} }
interface CreateFolderEvent { interface CreateFolderEvent {
type: EventType.CREATE_FOLDER; type: EventType.CREATE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface UpdateFolderEvent { interface UpdateFolderEvent {
type: EventType.UPDATE_FOLDER; type: EventType.UPDATE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
oldFolderName: string; oldFolderName: string;
newFolderName: string; newFolderName: string;
folderPath: string; folderPath: string;
} };
} }
interface DeleteFolderEvent { interface DeleteFolderEvent {
type: EventType.DELETE_FOLDER; type: EventType.DELETE_FOLDER;
metadata: { metadata: {
environment: string; environment: string;
folderId: string; folderId: string;
folderName: string; folderName: string;
folderPath: string; folderPath: string;
} };
} }
interface CreateWebhookEvent { interface CreateWebhookEvent {
type: EventType.CREATE_WEBHOOK, type: EventType.CREATE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface UpdateWebhookStatusEvent { interface UpdateWebhookStatusEvent {
type: EventType.UPDATE_WEBHOOK_STATUS, type: EventType.UPDATE_WEBHOOK_STATUS;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface DeleteWebhookEvent { interface DeleteWebhookEvent {
type: EventType.DELETE_WEBHOOK, type: EventType.DELETE_WEBHOOK;
metadata: { metadata: {
webhookId: string; webhookId: string;
environment: string; environment: string;
secretPath: string; secretPath: string;
webhookUrl: string; webhookUrl: string;
isDisabled: boolean; isDisabled: boolean;
} };
} }
interface GetSecretImportsEvent { interface GetSecretImportsEvent {
type: EventType.GET_SECRET_IMPORTS, type: EventType.GET_SECRET_IMPORTS;
metadata: { metadata: {
environment: string; environment: string;
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
numberOfImports: number; numberOfImports: number;
} };
} }
interface CreateSecretImportEvent { interface CreateSecretImportEvent {
type: EventType.CREATE_SECRET_IMPORT, type: EventType.CREATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importFromEnvironment: string; importFromEnvironment: string;
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateSecretImportEvent { interface UpdateSecretImportEvent {
type: EventType.UPDATE_SECRET_IMPORT, type: EventType.UPDATE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
orderBefore: { orderBefore: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[], }[];
orderAfter: { orderAfter: {
environment: string; environment: string;
secretPath: string; secretPath: string;
}[] }[];
} };
} }
interface DeleteSecretImportEvent { interface DeleteSecretImportEvent {
type: EventType.DELETE_SECRET_IMPORT, type: EventType.DELETE_SECRET_IMPORT;
metadata: { metadata: {
secretImportId: string; secretImportId: string;
folderId: string; folderId: string;
importFromEnvironment: string; importFromEnvironment: string;
importFromSecretPath: string; importFromSecretPath: string;
importToEnvironment: string; importToEnvironment: string;
importToSecretPath: string; importToSecretPath: string;
} };
} }
interface UpdateUserRole { interface UpdateUserRole {
type: EventType.UPDATE_USER_WORKSPACE_ROLE, type: EventType.UPDATE_USER_WORKSPACE_ROLE;
metadata: { metadata: {
userId: string; userId: string;
email: string; email: string;
oldRole: string; oldRole: string;
newRole: string; newRole: string;
} };
} }
interface UpdateUserDeniedPermissions { interface UpdateUserDeniedPermissions {
@@ -411,8 +438,11 @@ export type Event =
| GetSecretsEvent | GetSecretsEvent
| GetSecretEvent | GetSecretEvent
| CreateSecretEvent | CreateSecretEvent
| CreateSecretBatchEvent
| UpdateSecretEvent | UpdateSecretEvent
| UpdateSecretBatchEvent
| DeleteSecretEvent | DeleteSecretEvent
| DeleteSecretBatchEvent
| GetWorkspaceKeyEvent | GetWorkspaceKeyEvent
| AuthorizeIntegrationEvent | AuthorizeIntegrationEvent
| UnauthorizeIntegrationEvent | UnauthorizeIntegrationEvent
+26 -24
View File
@@ -4,7 +4,7 @@ import {
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../../variables"; } from "../../variables";
export interface ISecretVersion { export interface ISecretVersion {
@@ -23,6 +23,7 @@ export interface ISecretVersion {
secretValueCiphertext: string; secretValueCiphertext: string;
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm"; algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64"; keyEncoding: "utf8" | "base64";
createdAt: string; createdAt: string;
@@ -36,95 +37,96 @@ const secretVersionSchema = new Schema<ISecretVersion>(
// could be deleted // could be deleted
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Secret", ref: "Secret",
required: true, required: true
}, },
version: { version: {
type: Number, type: Number,
default: 1, default: 1,
required: true, required: true
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
type: { type: {
type: String, type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL], enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true, required: true
}, },
user: { user: {
// user associated with the personal secret // user associated with the personal secret
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
environment: { environment: {
type: String, type: String,
required: true, required: true
}, },
isDeleted: { isDeleted: {
// consider removing field // consider removing field
type: Boolean, type: Boolean,
default: false, default: false,
required: true, required: true
}, },
secretBlindIndex: { secretBlindIndex: {
type: String, type: String,
select: false, select: false
}, },
secretKeyCiphertext: { secretKeyCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretKeyIV: { secretKeyIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyTag: { secretKeyTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueCiphertext: { secretValueCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretValueIV: { secretValueIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueTag: { secretValueTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
},
skipMultilineEncoding: {
type: Boolean,
required: false
}, },
algorithm: { algorithm: {
// the encryption algorithm used // the encryption algorithm used
type: String, type: String,
enum: [ALGORITHM_AES_256_GCM], enum: [ALGORITHM_AES_256_GCM],
required: true, required: true,
default: ALGORITHM_AES_256_GCM, default: ALGORITHM_AES_256_GCM
}, },
keyEncoding: { keyEncoding: {
type: String, type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true, required: true,
default: ENCODING_SCHEME_UTF8, default: ENCODING_SCHEME_UTF8
}, },
folder: { folder: {
type: String, type: String,
required: true, required: true
}, },
tags: { tags: {
ref: "Tag", ref: "Tag",
type: [Schema.Types.ObjectId], type: [Schema.Types.ObjectId],
default: [], default: []
} }
}, },
{ {
timestamps: true, timestamps: true
} }
); );
export const SecretVersion = model<ISecretVersion>( export const SecretVersion = model<ISecretVersion>("SecretVersion", secretVersionSchema);
"SecretVersion",
secretVersionSchema
);
+11 -1
View File
@@ -49,7 +49,8 @@ export enum ProjectPermissionSub {
IpAllowList = "ip-allowlist", IpAllowList = "ip-allowlist",
Workspace = "workspace", Workspace = "workspace",
Secrets = "secrets", Secrets = "secrets",
SecretRollback = "secret-rollback" SecretRollback = "secret-rollback",
SecretApproval = "secret-approval"
} }
type SubjectFields = { type SubjectFields = {
@@ -72,6 +73,7 @@ export type ProjectPermissionSet =
| [ProjectPermissionActions, ProjectPermissionSub.IpAllowList] | [ProjectPermissionActions, ProjectPermissionSub.IpAllowList]
| [ProjectPermissionActions, ProjectPermissionSub.Settings] | [ProjectPermissionActions, ProjectPermissionSub.Settings]
| [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens] | [ProjectPermissionActions, ProjectPermissionSub.ServiceTokens]
| [ProjectPermissionActions, ProjectPermissionSub.SecretApproval]
| [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Delete, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace] | [ProjectPermissionActions.Edit, ProjectPermissionSub.Workspace]
| [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback] | [ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback]
@@ -85,6 +87,11 @@ const buildAdminPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, 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.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
@@ -154,6 +161,8 @@ const buildMemberPermission = () => {
can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Edit, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Delete, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Create, ProjectPermissionSub.SecretRollback);
@@ -203,6 +212,7 @@ const buildViewerPermission = () => {
const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility); const { can, build } = new AbilityBuilder<MongoAbility<ProjectPermissionSet>>(createMongoAbility);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets); can(ProjectPermissionActions.Read, ProjectPermissionSub.Secrets);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretApproval);
can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback); can(ProjectPermissionActions.Read, ProjectPermissionSub.SecretRollback);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Member); can(ProjectPermissionActions.Read, ProjectPermissionSub.Member);
can(ProjectPermissionActions.Read, ProjectPermissionSub.Role); can(ProjectPermissionActions.Read, ProjectPermissionSub.Role);
+8 -1
View File
@@ -103,7 +103,10 @@ export const getSecretsBotHelper = async ({
environment: string; environment: string;
secretPath: 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 }); const key = await getKey({ workspaceId: workspaceId });
let folderId = "root"; let folderId = "root";
@@ -165,6 +168,8 @@ export const getSecretsBotHelper = async ({
}); });
content[secretKey].comment = commentValue; content[secretKey].comment = commentValue;
} }
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
}); });
}); });
@@ -194,6 +199,8 @@ export const getSecretsBotHelper = async ({
}); });
content[secretKey].comment = commentValue; content[secretKey].comment = commentValue;
} }
content[secretKey].skipMultilineEncoding = secret.skipMultilineEncoding;
}); });
await expandSecrets(workspaceId.toString(), key, content); await expandSecrets(workspaceId.toString(), key, content);
+579 -15
View File
@@ -1,9 +1,12 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
CreateSecretBatchParams,
CreateSecretParams, CreateSecretParams,
DeleteSecretBatchParams,
DeleteSecretParams, DeleteSecretParams,
GetSecretParams, GetSecretParams,
GetSecretsParams, GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams UpdateSecretParams
} from "../interfaces/services/SecretService"; } from "../interfaces/services/SecretService";
import { import {
@@ -53,6 +56,7 @@ import { getAuthDataPayloadIdObj, getAuthDataPayloadUserObj } from "../utils/aut
import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService"; import { getFolderByPath, getFolderIdFromServiceToken } from "../services/FolderService";
import picomatch from "picomatch"; import picomatch from "picomatch";
import path from "path"; import path from "path";
import { getAnImportedSecret } from "../services/SecretImportService";
/** /**
* Validate scope for service token v3 * Validate scope for service token v3
@@ -115,6 +119,8 @@ export function containsGlobPatterns(secretPath: string) {
return globChars.some((char) => normalizedPath.includes(char)); return globChars.some((char) => normalizedPath.includes(char));
} }
const ERR_FOLDER_NOT_FOUND = BadRequestError({ message: "Folder not found" });
/** /**
* Returns an object containing secret [secret] but with its value, key, comment decrypted. * Returns an object containing secret [secret] but with its value, key, comment decrypted.
* *
@@ -374,7 +380,8 @@ export const createSecretHelper = async ({
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
secretPath = "/", secretPath = "/",
metadata metadata,
skipMultilineEncoding
}: CreateSecretParams) => { }: CreateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
@@ -438,6 +445,7 @@ export const createSecretHelper = async ({
secretCommentCiphertext, secretCommentCiphertext,
secretCommentIV, secretCommentIV,
secretCommentTag, secretCommentTag,
skipMultilineEncoding,
folder: folderId, folder: folderId,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8, keyEncoding: ENCODING_SCHEME_UTF8,
@@ -460,6 +468,7 @@ export const createSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}); });
@@ -667,13 +676,14 @@ export const getSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/" secretPath = "/",
include_imports = true
}: GetSecretParams) => { }: GetSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ const secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
let secret: ISecret | null = null; let secret: ISecret | null | undefined = null;
// if using service token filter towards the folderId by secretpath // if using service token filter towards the folderId by secretpath
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
@@ -700,6 +710,11 @@ export const getSecretHelper = async ({
}).lean(); }).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(); if (!secret) throw SecretNotFoundError();
// (EE) create (audit) log // (EE) create (audit) log
@@ -778,24 +793,57 @@ export const updateSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
newSecretName,
secretKeyTag,
secretKeyCiphertext,
secretKeyIV,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretPath secretPath,
tags,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag,
skipMultilineEncoding
}: UpdateSecretParams) => { }: UpdateSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ // get secret blind index salt
secretName, const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
const oldSecretBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName,
salt
});
let secret: ISecret | null = null; let secret: ISecret | null = null;
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
let newSecretNameBlindIndex = undefined;
if (newSecretName) {
newSecretNameBlindIndex = await generateSecretBlindIndexWithSaltHelper({
secretName: 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) { if (type === SECRET_SHARED) {
// case: update shared secret // case: update shared secret
secret = await Secret.findOneAndUpdate( secret = await Secret.findOneAndUpdate(
{ {
secretBlindIndex, secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
folder: folderId, folder: folderId,
@@ -805,6 +853,15 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretCommentIV,
secretCommentTag,
secretCommentCiphertext,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
$inc: { version: 1 } $inc: { version: 1 }
}, },
{ {
@@ -816,7 +873,7 @@ export const updateSecretHelper = async ({
secret = await Secret.findOneAndUpdate( secret = await Secret.findOneAndUpdate(
{ {
secretBlindIndex, secretBlindIndex: oldSecretBlindIndex,
workspace: new Types.ObjectId(workspaceId), workspace: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
@@ -827,10 +884,13 @@ export const updateSecretHelper = async ({
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
secretKeyIV,
secretKeyTag,
secretKeyCiphertext,
tags,
skipMultilineEncoding,
secretBlindIndex: newSecretNameBlindIndex,
$inc: { version: 1 } $inc: { version: 1 }
},
{
new: true
} }
); );
} }
@@ -843,16 +903,18 @@ export const updateSecretHelper = async ({
workspace: secret.workspace, workspace: secret.workspace,
folder: folderId, folder: folderId,
type, type,
tags,
...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}), ...(type === SECRET_PERSONAL ? getAuthDataPayloadUserObj(authData) : {}),
environment: secret.environment, environment: secret.environment,
isDeleted: false, isDeleted: false,
secretBlindIndex, secretBlindIndex: newSecretName ? newSecretNameBlindIndex : oldSecretBlindIndex,
secretKeyCiphertext: secret.secretKeyCiphertext, secretKeyCiphertext: secret.secretKeyCiphertext,
secretKeyIV: secret.secretKeyIV, secretKeyIV: secret.secretKeyIV,
secretKeyTag: secret.secretKeyTag, secretKeyTag: secret.secretKeyTag,
secretValueCiphertext, secretValueCiphertext,
secretValueIV, secretValueIV,
secretValueTag, secretValueTag,
skipMultilineEncoding,
algorithm: ALGORITHM_AES_256_GCM, algorithm: ALGORITHM_AES_256_GCM,
keyEncoding: ENCODING_SCHEME_UTF8 keyEncoding: ENCODING_SCHEME_UTF8
}); });
@@ -1136,6 +1198,7 @@ const recursivelyExpandSecret = async (
let interpolatedValue = interpolatedSec[key]; let interpolatedValue = interpolatedSec[key];
if (!interpolatedValue) { if (!interpolatedValue) {
// eslint-disable-next-line no-console
console.error(`Couldn't find referenced value - ${key}`); console.error(`Couldn't find referenced value - ${key}`);
return ""; return "";
} }
@@ -1185,7 +1248,7 @@ const formatMultiValueEnv = (val?: string) => {
export const expandSecrets = async ( export const expandSecrets = async (
workspaceId: string, workspaceId: string,
rootEncKey: 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 expandedSec: Record<string, string> = {};
const interpolatedSec: Record<string, string> = {}; const interpolatedSec: Record<string, string> = {};
@@ -1203,7 +1266,10 @@ export const expandSecrets = async (
for (const key of Object.keys(secrets)) { for (const key of Object.keys(secrets)) {
if (expandedSec?.[key]) { 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; continue;
} }
@@ -1218,8 +1284,506 @@ export const expandSecrets = async (
key key
); );
secrets[key].value = formatMultiValueEnv(expandedVal); secrets[key].value = secrets[key].skipMultilineEncoding
? expandedVal
: formatMultiValueEnv(expandedVal);
} }
return secrets; return secrets;
}; };
export const createSecretBatchHelper = async ({
secrets,
workspaceId,
authData,
secretPath,
environment
}: CreateSecretBatchParams) => {
let folderId = "root";
const folders = await Folder.findOne({
workspace: workspaceId,
environment
});
if (!folders && secretPath !== "/") throw ERR_FOLDER_NOT_FOUND;
if (folders) {
const folder = getFolderByPath(folders.nodes, secretPath);
if (!folder) throw ERR_FOLDER_NOT_FOUND;
folderId = folder.id;
}
// get secret blind index salt
const salt = await getSecretBlindIndexSaltHelper({
workspaceId: new Types.ObjectId(workspaceId)
});
const secretBlindIndexToKey: Record<string, string> = {}; // used at audit log point
const secretBlindIndexes = await Promise.all(
secrets.map(({ secretName }) =>
generateSecretBlindIndexWithSaltHelper({
secretName,
salt
})
)
).then((blindIndexes) =>
blindIndexes.reduce<Record<string, string>>((prev, curr, i) => {
prev[secrets[i].secretName] = curr;
secretBlindIndexToKey[curr] = secrets[i].secretName;
return prev;
}, {})
);
const exists = await Secret.exists({
workspace: new Types.ObjectId(workspaceId),
folder: folderId,
environment
})
.or(
secrets.map(({ secretName, type }) => ({
secretBlindIndex: secretBlindIndexes[secretName],
type: type,
...(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
};
};
+2
View File
@@ -38,6 +38,7 @@ import {
membership as v1MembershipRouter, membership as v1MembershipRouter,
organization as v1OrganizationRouter, organization as v1OrganizationRouter,
password as v1PasswordRouter, password as v1PasswordRouter,
secretApprovalPolicy as v1SecretApprovalPolicy,
secretImps as v1SecretImpsRouter, secretImps as v1SecretImpsRouter,
secret as v1SecretRouter, secret as v1SecretRouter,
secretsFolder as v1SecretsFolder, secretsFolder as v1SecretsFolder,
@@ -177,6 +178,7 @@ const main = async () => {
app.use("/api/v1/webhooks", v1WebhooksRouter); app.use("/api/v1/webhooks", v1WebhooksRouter);
app.use("/api/v1/secret-imports", v1SecretImpsRouter); app.use("/api/v1/secret-imports", v1SecretImpsRouter);
app.use("/api/v1/roles", v1RoleRouter); app.use("/api/v1/roles", v1RoleRouter);
app.use("/api/v1/secret-approvals", v1SecretApprovalPolicy);
// v2 routes (improvements) // v2 routes (improvements)
app.use("/api/v2/signup", v2SignupRouter); app.use("/api/v2/signup", v2SignupRouter);
+112 -96
View File
@@ -65,10 +65,10 @@ import sodium from "libsodium-wrappers";
import { standardRequest } from "../config/request"; import { standardRequest } from "../config/request";
const getSecretKeyValuePair = ( 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) => { Object.keys(secrets).reduce<Record<string, string | null | undefined>>((prev, key) => {
if (secrets[key]) prev[key] = secrets[key]?.value || ""; prev[key] = secrets?.[key] === null ? null : secrets?.[key]?.value;
return prev; return prev;
}, {}); }, {});
@@ -325,40 +325,42 @@ const syncSecretsGCPSecretManager = async ({
name: string; name: string;
createTime: string; createTime: string;
} }
interface GCPSMListSecretsRes { interface GCPSMListSecretsRes {
secrets?: GCPSecret[]; secrets?: GCPSecret[];
totalSize?: number; totalSize?: number;
nextPageToken?: string; nextPageToken?: string;
} }
let gcpSecrets: GCPSecret[] = []; let gcpSecrets: GCPSecret[] = [];
const pageSize = 100; const pageSize = 100;
let pageToken: string | undefined; let pageToken: string | undefined;
let hasMorePages = true; let hasMorePages = true;
const filterParam = integration.metadata.secretGCPLabel const filterParam = integration.metadata.secretGCPLabel
? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}` ? `?filter=labels.${integration.metadata.secretGCPLabel.labelName}=${integration.metadata.secretGCPLabel.labelValue}`
: ""; : "";
while (hasMorePages) { while (hasMorePages) {
const params = new URLSearchParams({ const params = new URLSearchParams({
pageSize: String(pageSize), pageSize: String(pageSize),
...(pageToken ? { pageToken } : {}) ...(pageToken ? { pageToken } : {})
}); });
const res: GCPSMListSecretsRes = (await standardRequest.get( const res: GCPSMListSecretsRes = (
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`, await standardRequest.get(
{ `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets${filterParam}`,
params, {
headers: { params,
"Authorization": `Bearer ${accessToken}`, headers: {
"Accept-Encoding": "application/json" Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
} }
} )
)).data; ).data;
if (res.secrets) { if (res.secrets) {
const filteredSecrets = res.secrets?.filter((gcpSecret) => { const filteredSecrets = res.secrets?.filter((gcpSecret) => {
const arr = gcpSecret.name.split("/"); const arr = gcpSecret.name.split("/");
@@ -366,54 +368,58 @@ const syncSecretsGCPSecretManager = async ({
let isValid = true; let isValid = true;
if (integration.metadata.secretPrefix && !key.startsWith(integration.metadata.secretPrefix)) { if (
integration.metadata.secretPrefix &&
!key.startsWith(integration.metadata.secretPrefix)
) {
isValid = false; isValid = false;
} }
if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) { if (integration.metadata.secretSuffix && !key.endsWith(integration.metadata.secretSuffix)) {
isValid = false; isValid = false;
} }
return isValid; return isValid;
}); });
gcpSecrets = gcpSecrets.concat(filteredSecrets); gcpSecrets = gcpSecrets.concat(filteredSecrets);
} }
if (!res.nextPageToken) { if (!res.nextPageToken) {
hasMorePages = false; hasMorePages = false;
} }
pageToken = res.nextPageToken; pageToken = res.nextPageToken;
} }
const res: { [key: string]: string; } = {}; const res: { [key: string]: string } = {};
interface GCPLatestSecretVersionAccess { interface GCPLatestSecretVersionAccess {
name: string; name: string;
payload: { payload: {
data: string; data: string;
} };
} }
for await (const gcpSecret of gcpSecrets) { for await (const gcpSecret of gcpSecrets) {
const arr = gcpSecret.name.split("/"); const arr = gcpSecret.name.split("/");
const key = arr[arr.length - 1]; const key = arr[arr.length - 1];
const secretLatest: GCPLatestSecretVersionAccess = (await standardRequest.get( const secretLatest: GCPLatestSecretVersionAccess = (
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`, await standardRequest.get(
{ `${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}/versions/latest:access`,
headers: { {
Authorization: `Bearer ${accessToken}`, headers: {
"Accept-Encoding": "application/json" Authorization: `Bearer ${accessToken}`,
"Accept-Encoding": "application/json"
}
} }
} )
)).data; ).data;
res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8"); res[key] = Buffer.from(secretLatest.payload.data, "base64").toString("utf-8");
} }
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in res)) { if (!(key in res)) {
// case: create secret // case: create secret
@@ -423,11 +429,14 @@ const syncSecretsGCPSecretManager = async ({
replication: { replication: {
automatic: {} automatic: {}
}, },
...(integration.metadata.secretGCPLabel ? { ...(integration.metadata.secretGCPLabel
labels: { ? {
[integration.metadata.secretGCPLabel.labelName]: integration.metadata.secretGCPLabel.labelValue labels: {
} [integration.metadata.secretGCPLabel.labelName]:
} : {}) integration.metadata.secretGCPLabel.labelValue
}
}
: {})
}, },
{ {
params: { params: {
@@ -439,7 +448,7 @@ const syncSecretsGCPSecretManager = async ({
} }
} }
); );
await standardRequest.post( await standardRequest.post(
`${INTEGRATION_GCP_SECRET_MANAGER_URL}/v1/projects/${integration.appId}/secrets/${key}:addVersion`, `${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)) { for await (const key of Object.keys(res)) {
if (!(key in secrets)) { if (!(key in secrets)) {
// case: delete secret // case: delete secret
@@ -489,7 +498,7 @@ const syncSecretsGCPSecretManager = async ({
} }
} }
} }
} };
/** /**
* Sync/push [secrets] to Azure Key Vault with vault URI [integration.app] * Sync/push [secrets] to Azure Key Vault with vault URI [integration.app]
@@ -729,15 +738,12 @@ const syncSecretsAWSParameterStore = async ({
} = {}; } = {};
if (parameterList) { if (parameterList) {
awsParameterStoreSecretsObj = parameterList.reduce( awsParameterStoreSecretsObj = parameterList.reduce((obj: any, secret: any) => {
(obj: any, secret: any) => { return {
return ({ ...obj,
...obj, [secret.Name.substring(integration.path.length)]: secret
[secret.Name.substring(integration.path.length)]: secret };
}); }, {});
},
{}
);
} }
// Identify secrets to create // Identify secrets to create
@@ -1869,8 +1875,10 @@ const syncSecretsGitLab = async ({
value: string; value: string;
environment_scope: 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 getAllEnvVariables = async (integrationAppId: string, accessToken: string) => {
const headers = { const headers = {
@@ -1880,7 +1888,9 @@ const syncSecretsGitLab = async ({
}; };
let allEnvVariables: GitLabSecret[] = []; 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) { while (url) {
const response: any = await standardRequest.get(url, { headers }); const response: any = await standardRequest.get(url, { headers });
@@ -1901,23 +1911,27 @@ const syncSecretsGitLab = async ({
const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken); const allEnvVariables = await getAllEnvVariables(integration?.appId, accessToken);
const getSecretsRes: GitLabSecret[] = allEnvVariables const getSecretsRes: GitLabSecret[] = allEnvVariables
.filter( .filter((secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment)
(secret: GitLabSecret) => secret.environment_scope === integration.targetEnvironment
)
.filter((gitLabSecret) => { .filter((gitLabSecret) => {
let isValid = true; 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; isValid = false;
} }
if (integration.metadata.secretSuffix && !gitLabSecret.key.endsWith(integration.metadata.secretSuffix)) { if (
integration.metadata.secretSuffix &&
!gitLabSecret.key.endsWith(integration.metadata.secretSuffix)
) {
isValid = false; isValid = false;
} }
return isValid; return isValid;
}); });
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
const existingSecret = getSecretsRes.find((s: any) => s.key == key); const existingSecret = getSecretsRes.find((s: any) => s.key == key);
if (!existingSecret) { if (!existingSecret) {
@@ -2371,41 +2385,43 @@ const syncSecretsTeamCity = async ({
if (integration.targetEnvironment && integration.targetEnvironmentId) { if (integration.targetEnvironment && integration.targetEnvironmentId) {
// case: sync to specific build-config in TeamCity project // case: sync to specific build-config in TeamCity project
const res = (await standardRequest.get<GetTeamCityBuildConfigParametersRes>( const res = (
`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, await standardRequest.get<GetTeamCityBuildConfigParametersRes>(
{ `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
headers: { {
Authorization: `Bearer ${accessToken}`, headers: {
Accept: "application/json", Authorization: `Bearer ${accessToken}`,
}, Accept: "application/json"
} }
)) }
.data )
.property ).data.property
.filter((parameter) => !parameter.inherited) .filter((parameter) => !parameter.inherited)
.reduce((obj: any, secret: TeamCitySecret) => { .reduce((obj: any, secret: TeamCitySecret) => {
const secretName = secret.name.replace(/^env\./, ""); const secretName = secret.name.replace(/^env\./, "");
return { return {
...obj, ...obj,
[secretName]: secret.value [secretName]: secret.value
}; };
}, {}); }, {});
for await (const key of Object.keys(secrets)) { for await (const key of Object.keys(secrets)) {
if (!(key in res) || (key in res && secrets[key].value !== res[key])) { if (!(key in res) || (key in res && secrets[key].value !== res[key])) {
// case: secret does not exist in TeamCity or secret value has changed // case: secret does not exist in TeamCity or secret value has changed
// -> create/update secret // -> create/update secret
await standardRequest.post(`${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`, await standardRequest.post(
{ `${integrationAuth.url}/app/rest/buildTypes/${integration.targetEnvironmentId}/parameters`,
name:`env.${key}`, {
value: secrets[key].value name: `env.${key}`,
}, value: secrets[key].value
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
}, },
}); {
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; secretCommentCiphertext?: string;
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
skipMultilineEncoding?: boolean;
secretPath: string; secretPath: string;
metadata?: { metadata?: {
source?: string; source?: string;
} };
} }
export interface GetSecretsParams { export interface GetSecretsParams {
@@ -37,10 +38,15 @@ export interface GetSecretParams {
environment: string; environment: string;
type?: "shared" | "personal"; type?: "shared" | "personal";
authData: AuthData; authData: AuthData;
include_imports?: boolean;
} }
export interface UpdateSecretParams { export interface UpdateSecretParams {
secretName: string; secretName: string;
newSecretName?: string;
secretKeyCiphertext?: string;
secretKeyIV?: string;
secretKeyTag?: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
type: "shared" | "personal"; type: "shared" | "personal";
@@ -49,6 +55,11 @@ export interface UpdateSecretParams {
secretValueIV: string; secretValueIV: string;
secretValueTag: string; secretValueTag: string;
secretPath: string; secretPath: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
} }
export interface DeleteSecretParams { export interface DeleteSecretParams {
@@ -59,3 +70,57 @@ export interface DeleteSecretParams {
authData: AuthData; authData: AuthData;
secretPath: string; secretPath: string;
} }
export interface CreateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
metadata?: {
source?: string;
};
}>;
}
export interface UpdateSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext?: string;
secretCommentIV?: string;
secretCommentTag?: string;
skipMultilineEncoding?: boolean;
tags?: string[];
}>;
}
export interface DeleteSecretBatchParams {
workspaceId: Types.ObjectId;
environment: string;
authData: AuthData;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
}>;
}
+32 -27
View File
@@ -4,7 +4,7 @@ import {
ENCODING_SCHEME_BASE64, ENCODING_SCHEME_BASE64,
ENCODING_SCHEME_UTF8, ENCODING_SCHEME_UTF8,
SECRET_PERSONAL, SECRET_PERSONAL,
SECRET_SHARED, SECRET_SHARED
} from "../variables"; } from "../variables";
export interface ISecret { export interface ISecret {
@@ -12,7 +12,7 @@ export interface ISecret {
version: number; version: number;
workspace: Types.ObjectId; workspace: Types.ObjectId;
type: string; type: string;
user: Types.ObjectId; user?: Types.ObjectId;
environment: string; environment: string;
secretBlindIndex?: string; secretBlindIndex?: string;
secretKeyCiphertext: string; secretKeyCiphertext: string;
@@ -27,13 +27,14 @@ export interface ISecret {
secretCommentIV?: string; secretCommentIV?: string;
secretCommentTag?: string; secretCommentTag?: string;
secretCommentHash?: string; secretCommentHash?: string;
skipMultilineEncoding?: boolean;
algorithm: "aes-256-gcm"; algorithm: "aes-256-gcm";
keyEncoding: "utf8" | "base64"; keyEncoding: "utf8" | "base64";
tags?: string[]; tags?: string[];
folder?: string; folder?: string;
metadata?: { metadata?: {
[key: string]: string; [key: string]: string;
} };
} }
const secretSchema = new Schema<ISecret>( const secretSchema = new Schema<ISecret>(
@@ -41,108 +42,112 @@ const secretSchema = new Schema<ISecret>(
version: { version: {
type: Number, type: Number,
required: true, required: true,
default: 1, default: 1
}, },
workspace: { workspace: {
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "Workspace", ref: "Workspace",
required: true, required: true
}, },
type: { type: {
type: String, type: String,
enum: [SECRET_SHARED, SECRET_PERSONAL], enum: [SECRET_SHARED, SECRET_PERSONAL],
required: true, required: true
}, },
user: { user: {
// user associated with the personal secret // user associated with the personal secret
type: Schema.Types.ObjectId, type: Schema.Types.ObjectId,
ref: "User", ref: "User"
}, },
tags: { tags: {
ref: "Tag", ref: "Tag",
type: [Schema.Types.ObjectId], type: [Schema.Types.ObjectId],
default: [], default: []
}, },
environment: { environment: {
type: String, type: String,
required: true, required: true
}, },
secretBlindIndex: { secretBlindIndex: {
type: String, type: String,
select: false, select: false
}, },
secretKeyCiphertext: { secretKeyCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretKeyIV: { secretKeyIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyTag: { secretKeyTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretKeyHash: { secretKeyHash: {
type: String, type: String
}, },
secretValueCiphertext: { secretValueCiphertext: {
type: String, type: String,
required: true, required: true
}, },
secretValueIV: { secretValueIV: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueTag: { secretValueTag: {
type: String, // symmetric type: String, // symmetric
required: true, required: true
}, },
secretValueHash: { secretValueHash: {
type: String, type: String
}, },
secretCommentCiphertext: { secretCommentCiphertext: {
type: String, type: String,
required: false, required: false
}, },
secretCommentIV: { secretCommentIV: {
type: String, // symmetric type: String, // symmetric
required: false, required: false
}, },
secretCommentTag: { secretCommentTag: {
type: String, // symmetric type: String, // symmetric
required: false, required: false
}, },
secretCommentHash: { secretCommentHash: {
type: String, type: String,
required: false, required: false
},
skipMultilineEncoding: {
type: Boolean,
required: false
}, },
algorithm: { algorithm: {
// the encryption algorithm used // the encryption algorithm used
type: String, type: String,
enum: [ALGORITHM_AES_256_GCM], enum: [ALGORITHM_AES_256_GCM],
required: true, required: true,
default: ALGORITHM_AES_256_GCM, default: ALGORITHM_AES_256_GCM
}, },
keyEncoding: { keyEncoding: {
type: String, type: String,
enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64], enum: [ENCODING_SCHEME_UTF8, ENCODING_SCHEME_BASE64],
required: true, required: true,
default: ENCODING_SCHEME_UTF8, default: ENCODING_SCHEME_UTF8
}, },
folder: { folder: {
type: String, type: String,
default: "root", default: "root"
}, },
metadata: { metadata: {
type: Schema.Types.Mixed type: Schema.Types.Mixed
} }
}, },
{ {
timestamps: true, timestamps: true
} }
); );
secretSchema.index({ tags: 1 }, { background: true }); secretSchema.index({ tags: 1 }, { background: true });
export const Secret = model<ISecret>("Secret", secretSchema); export const Secret = model<ISecret>("Secret", secretSchema);
@@ -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
);
+58 -71
View File
@@ -1,81 +1,68 @@
import mongoose, { Schema, model } from "mongoose"; import { Schema, Types, model } from "mongoose";
import { ISecret, Secret } from "./secret"; import { ISecretVersion, SecretVersion } from "../ee/models/secretVersion";
interface ISecretApprovalRequest { enum ApprovalStatus {
secret: mongoose.Types.ObjectId; PENDING = "pending",
requestedChanges: ISecret; APPROVED = "approved",
requestedBy: mongoose.Types.ObjectId; REJECTED = "rejected"
approvers: IApprover[];
status: ApprovalStatus;
timestamp: Date;
requestType: RequestType;
requestId: string;
} }
interface IApprover { enum CommitType {
userId: mongoose.Types.ObjectId; DELETE = "delete",
status: ApprovalStatus; UPDATE = "update",
CREATE = "create"
} }
export enum ApprovalStatus { export interface ISecretApprovalRequest {
PENDING = "pending", _id: Types.ObjectId;
APPROVED = "approved", committer: Types.ObjectId;
REJECTED = "rejected" 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>( const secretApprovalRequestSchema = new Schema<ISecretApprovalRequest>(
{ {
secret: { approvers: [
type: mongoose.Schema.Types.ObjectId, {
ref: "Secret", member: {
}, // user associated with the personal secret
requestedChanges: Secret, type: Schema.Types.ObjectId,
requestedBy: { ref: "Membership"
type: mongoose.Schema.Types.ObjectId, },
ref: "User", status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING }
}, }
approvers: [approverSchema], ],
status: { approvals: {
type: String, type: Number,
enum: ApprovalStatus, required: true
default: ApprovalStatus.PENDING, },
}, hasMerged: { type: Boolean, default: false },
timestamp: { status: { type: String, enum: ApprovalStatus, default: ApprovalStatus.PENDING },
type: Date, committer: { type: Schema.Types.ObjectId, ref: "Membership" },
default: Date.now, commits: [
}, {
requestType: { secretVersion: { type: Types.ObjectId, ref: "SecretVersion" },
type: String, newVersion: SecretVersion,
enum: RequestType, op: { type: String, enum: [CommitType], required: true }
required: true, }
}, ]
requestId: { },
type: String, {
required: false, timestamps: 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 = { type TScanPushEventQueueDetails = {
organizationId: string, organizationId: string,
installationId: number, installationId: string,
repository: { repository: {
id: number, id: number,
fullName: string, fullName: string,
@@ -30,7 +30,8 @@ githubFullRepositorySecretScan.process(async (job: Job, done: Queue.DoneCallback
installationId: installationId 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) { for (const finding of findings) {
await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint }, await GitRisks.findOneAndUpdate({ fingerprint: finding.Fingerprint },
{ {
+3 -1
View File
@@ -17,6 +17,7 @@ import integrationAuth from "./integrationAuth";
import secretsFolder from "./secretsFolder"; import secretsFolder from "./secretsFolder";
import webhooks from "./webhook"; import webhooks from "./webhook";
import secretImps from "./secretImps"; import secretImps from "./secretImps";
import secretApprovalPolicy from "./secretApprovalPolicy";
export { export {
signup, signup,
@@ -37,5 +38,6 @@ export {
integrationAuth, integrationAuth,
secretsFolder, secretsFolder,
webhooks, 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;
+2 -2
View File
@@ -18,7 +18,7 @@ router.post(
); );
router.patch( router.patch(
"/:folderId", "/:folderName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
}), }),
@@ -26,7 +26,7 @@ router.patch(
); );
router.delete( router.delete(
"/:folderId", "/:folderName",
requireAuth({ requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN] acceptedAuthModes: [AuthMode.JWT, AuthMode.SERVICE_TOKEN]
}), }),
+36 -8
View File
@@ -1,14 +1,8 @@
import express from "express"; import express from "express";
const router = express.Router(); const router = express.Router();
import { import { requireAuth, requireBlindIndicesEnabled, requireE2EEOff } from "../../middleware";
requireAuth,
requireBlindIndicesEnabled,
requireE2EEOff
} from "../../middleware";
import { secretsController } from "../../controllers/v3"; import { secretsController } from "../../controllers/v3";
import { import { AuthMode } from "../../variables";
AuthMode
} from "../../variables";
router.get( router.get(
"/raw", "/raw",
@@ -85,6 +79,40 @@ router.get(
secretsController.getSecrets secretsController.getSecrets
); );
// akhilmhdh: dont put batch router below the individual operation as those have arbitory name as params
router.post(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.createSecretByNameBatch
);
router.patch(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.updateSecretByNameBatch
);
router.delete(
"/batch",
requireAuth({
acceptedAuthModes: [AuthMode.JWT, AuthMode.API_KEY, AuthMode.SERVICE_TOKEN]
}),
requireBlindIndicesEnabled({
locationWorkspaceId: "body"
}),
secretsController.deleteSecretByNameBatch
);
router.post( router.post(
"/:secretName", "/:secretName",
requireAuth({ requireAuth({
+52 -8
View File
@@ -1,9 +1,61 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { generateSecretBlindIndexHelper } from "../helpers";
import { Folder, ISecret, Secret, SecretImport } from "../models"; import { Folder, ISecret, Secret, SecretImport } from "../models";
import { getFolderByPath } from "./FolderService"; import { getFolderByPath } from "./FolderService";
type TSecretImportFid = { environment: string; folderId: string; secretPath: string }; 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 ( export const getAllImportedSecrets = async (
workspaceId: string, workspaceId: string,
environment: string, environment: string,
@@ -56,14 +108,6 @@ export const getAllImportedSecrets = async (
type: "shared" type: "shared"
} }
}, },
{
$lookup: {
from: "tags", // note this is the name of the collection in the database, not the Mongoose model name
localField: "tags",
foreignField: "_id",
as: "tags"
}
},
{ {
$group: { $group: {
_id: { _id: {
+41 -31
View File
@@ -1,21 +1,27 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { import {
CreateSecretParams, CreateSecretBatchParams,
DeleteSecretParams, CreateSecretParams,
GetSecretParams, DeleteSecretBatchParams,
GetSecretsParams, DeleteSecretParams,
UpdateSecretParams, GetSecretParams,
GetSecretsParams,
UpdateSecretBatchParams,
UpdateSecretParams
} from "../interfaces/services/SecretService"; } from "../interfaces/services/SecretService";
import { import {
createSecretBlindIndexDataHelper, createSecretBatchHelper,
createSecretHelper, createSecretBlindIndexDataHelper,
deleteSecretHelper, createSecretHelper,
generateSecretBlindIndexHelper, deleteSecretBatchHelper,
generateSecretBlindIndexWithSaltHelper, deleteSecretHelper,
getSecretBlindIndexSaltHelper, generateSecretBlindIndexHelper,
getSecretHelper, generateSecretBlindIndexWithSaltHelper,
getSecretsHelper, getSecretBlindIndexSaltHelper,
updateSecretHelper, getSecretHelper,
getSecretsHelper,
updateSecretBatchHelper,
updateSecretHelper
} from "../helpers/secrets"; } from "../helpers/secrets";
class SecretService { class SecretService {
@@ -26,13 +32,9 @@ class SecretService {
* @param {Buffer} obj.salt - 16-byte random salt * @param {Buffer} obj.salt - 16-byte random salt
* @param {Types.ObjectId} obj.workspaceId * @param {Types.ObjectId} obj.workspaceId
*/ */
static async createSecretBlindIndexData({ static async createSecretBlindIndexData({ workspaceId }: { workspaceId: Types.ObjectId }) {
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await createSecretBlindIndexDataHelper({ return await createSecretBlindIndexDataHelper({
workspaceId, workspaceId
}); });
} }
@@ -42,13 +44,9 @@ class SecretService {
* @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for * @param {Types.ObjectId} obj.workspaceId - id of workspace to get salt for
* @returns * @returns
*/ */
static async getSecretBlindIndexSalt({ static async getSecretBlindIndexSalt({ workspaceId }: { workspaceId: Types.ObjectId }) {
workspaceId,
}: {
workspaceId: Types.ObjectId;
}) {
return await getSecretBlindIndexSaltHelper({ return await getSecretBlindIndexSaltHelper({
workspaceId, workspaceId
}); });
} }
@@ -61,14 +59,14 @@ class SecretService {
*/ */
static async generateSecretBlindIndexWithSalt({ static async generateSecretBlindIndexWithSalt({
secretName, secretName,
salt, salt
}: { }: {
secretName: string; secretName: string;
salt: string; salt: string;
}) { }) {
return await generateSecretBlindIndexWithSaltHelper({ return await generateSecretBlindIndexWithSaltHelper({
secretName, secretName,
salt, salt
}); });
} }
@@ -81,14 +79,14 @@ class SecretService {
*/ */
static async generateSecretBlindIndex({ static async generateSecretBlindIndex({
secretName, secretName,
workspaceId, workspaceId
}: { }: {
secretName: string; secretName: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
}) { }) {
return await generateSecretBlindIndexHelper({ return await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId, workspaceId
}); });
} }
@@ -163,6 +161,18 @@ class SecretService {
static async deleteSecret(deleteSecretParams: DeleteSecretParams) { static async deleteSecret(deleteSecretParams: DeleteSecretParams) {
return await deleteSecretHelper(deleteSecretParams); return await deleteSecretHelper(deleteSecretParams);
} }
static async createSecretBatch(createSecretParams: CreateSecretBatchParams) {
return await createSecretBatchHelper(createSecretParams);
}
static async updateSecretBatch(updateSecretParams: UpdateSecretBatchParams) {
return await updateSecretBatchHelper(updateSecretParams);
}
static async deleteSecretBatch(deleteSecretParams: DeleteSecretBatchParams) {
return await deleteSecretBatchHelper(deleteSecretParams);
}
} }
export default SecretService; export default SecretService;
+15
View File
@@ -819,3 +819,18 @@ export const backfillPermission = async () => {
console.info("Could not acquire lock for script [backfillPermission], skipping"); 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");
}
+3 -1
View File
@@ -18,7 +18,8 @@ import {
backfillServiceToken, backfillServiceToken,
backfillServiceTokenMultiScope, backfillServiceTokenMultiScope,
backfillTrustedIps, backfillTrustedIps,
backfillUserAuthMethods backfillUserAuthMethods,
migrateRoleFromOwnerToAdmin
} from "./backfillData"; } from "./backfillData";
import { import {
reencryptBotOrgKeys, reencryptBotOrgKeys,
@@ -85,6 +86,7 @@ export const setup = async () => {
await backfillTrustedIps(); await backfillTrustedIps();
await backfillUserAuthMethods(); await backfillUserAuthMethods();
// await backfillPermission(); // await backfillPermission();
await migrateRoleFromOwnerToAdmin()
// re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY // re-encrypt any data previously encrypted under server hex 128-bit ENCRYPTION_KEY
// to base64 256-bit ROOT_ENCRYPTION_KEY // to base64 256-bit ROOT_ENCRYPTION_KEY
+8 -7
View File
@@ -5,28 +5,30 @@ export const CreateFolderV1 = z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderName: z.string().trim(), folderName: z.string().trim(),
parentFolderId: z.string().trim().optional() directory: z.string().trim().default("/")
}) })
}); });
export const UpdateFolderV1 = z.object({ export const UpdateFolderV1 = z.object({
params: z.object({ params: z.object({
folderId: z.string().trim() folderName: z.string().trim()
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
name: z.string().trim() name: z.string().trim(),
directory: z.string().trim().default("/")
}) })
}); });
export const DeleteFolderV1 = z.object({ export const DeleteFolderV1 = z.object({
params: z.object({ params: z.object({
folderId: z.string().trim() folderName: z.string().trim()
}), }),
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim() environment: z.string().trim(),
directory: z.string().trim().default("/")
}) })
}); });
@@ -34,7 +36,6 @@ export const GetFoldersV1 = z.object({
query: z.object({ query: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
parentFolderId: z.string().trim().optional(), directory: z.string().trim().default("/")
parentFolderPath: z.string().trim().optional()
}) })
}); });
+1
View File
@@ -1,3 +1,4 @@
export * from "./secretApproval";
export * from "./user"; export * from "./user";
export * from "./workspace"; export * from "./workspace";
export * from "./bot"; export * from "./bot";
+34
View File
@@ -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()
})
});
+3 -3
View File
@@ -4,7 +4,7 @@ export const CreateSecretImportV1 = z.object({
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root"), directory: z.string().trim().default("/"),
secretImport: z.object({ secretImport: z.object({
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim() secretPath: z.string().trim()
@@ -40,7 +40,7 @@ export const GetSecretImportsV1 = z.object({
query: z.object({ query: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root") directory: z.string().trim().default("/")
}) })
}); });
@@ -48,6 +48,6 @@ export const GetAllSecretsFromImportV1 = z.object({
query: z.object({ query: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root") directory: z.string().trim().default("/")
}) })
}); });
+1 -1
View File
@@ -6,7 +6,7 @@ export const CreateInstalLSessionv1 = z.object({
export const LinkInstallationToOrgv1 = z.object({ export const LinkInstallationToOrgv1 = z.object({
body: z.object({ body: z.object({
installationId: z.number(), installationId: z.string(),
sessionId: z.string().trim() sessionId: z.string().trim()
}) })
}); });
+95 -6
View File
@@ -244,7 +244,11 @@ export const GetSecretByNameRawV3 = z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/"), 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(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/"), 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(""), secretComment: z.string().trim().optional().default(""),
skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]) type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
}), }),
params: z.object({ params: z.object({
@@ -269,8 +276,11 @@ export const UpdateSecretByNameRawV3 = z.object({
body: z.object({ body: z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: 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("/"), secretPath: z.string().trim().default("/"),
skipMultilineEncoding: z.boolean().optional(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED) type: z.enum([SECRET_SHARED, SECRET_PERSONAL]).default(SECRET_SHARED)
}) })
}); });
@@ -305,7 +315,11 @@ export const GetSecretByNameV3 = z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
secretPath: z.string().trim().default("/"), 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({ params: z.object({
secretName: z.string().trim() secretName: z.string().trim()
@@ -327,7 +341,8 @@ export const CreateSecretV3 = z.object({
secretCommentCiphertext: z.string().trim().optional(), secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(), secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(), secretCommentTag: z.string().trim().optional(),
metadata: z.record(z.string()).optional() metadata: z.record(z.string()).optional(),
skipMultilineEncoding: z.boolean().optional()
}), }),
params: z.object({ params: z.object({
secretName: z.string().trim() secretName: z.string().trim()
@@ -342,7 +357,17 @@ export const UpdateSecretByNameV3 = z.object({
secretPath: z.string().trim().default("/"), secretPath: z.string().trim().default("/"),
secretValueCiphertext: z.string().trim(), secretValueCiphertext: z.string().trim(),
secretValueIV: z.string().trim(), secretValueIV: z.string().trim(),
secretValueTag: z.string().trim() secretValueTag: z.string().trim(),
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
tags: z.string().array().optional(),
skipMultilineEncoding: z.boolean().optional(),
// to update secret name
secretName: z.string().trim().optional(),
secretKeyIV: z.string().trim().optional(),
secretKeyTag: z.string().trim().optional(),
secretKeyCiphertext: z.string().trim().optional()
}), }),
params: z.object({ params: z.object({
secretName: z.string() secretName: z.string()
@@ -360,3 +385,67 @@ export const DeleteSecretByNameV3 = z.object({
secretName: z.string() secretName: z.string()
}) })
}); });
export const CreateSecretByNameBatchV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secrets: z
.object({
secretName: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretKeyCiphertext: z.string().trim(),
secretKeyIV: z.string().trim(),
secretKeyTag: z.string().trim(),
secretValueCiphertext: z.string().trim(),
secretValueIV: z.string().trim(),
secretValueTag: z.string().trim(),
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
metadata: z.record(z.string()).optional(),
skipMultilineEncoding: z.boolean().optional()
})
.array()
.min(1)
})
});
export const UpdateSecretByNameBatchV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secrets: z
.object({
secretName: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretValueCiphertext: z.string().trim(),
secretValueIV: z.string().trim(),
secretValueTag: z.string().trim(),
secretCommentCiphertext: z.string().trim().optional(),
secretCommentIV: z.string().trim().optional(),
secretCommentTag: z.string().trim().optional(),
skipMultilineEncoding: z.boolean().optional(),
tags: z.string().array().optional()
})
.array()
.min(1)
})
});
export const DeleteSecretByNameBatchV3 = z.object({
body: z.object({
workspaceId: z.string().trim(),
environment: z.string().trim(),
secretPath: z.string().trim().default("/"),
secrets: z
.object({
secretName: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL])
})
.array()
.min(1)
})
});
+7 -6
View File
@@ -34,9 +34,10 @@ export const validateClientForWorkspace = async ({
}) => { }) => {
const workspace = await Workspace.findById(workspaceId); const workspace = await Workspace.findById(workspaceId);
if (!workspace) throw WorkspaceNotFoundError({ if (!workspace)
message: "Failed to find workspace" throw WorkspaceNotFoundError({
}); message: "Failed to find workspace"
});
let membership; let membership;
switch (authData.actor.type) { switch (authData.actor.type) {
@@ -71,7 +72,7 @@ export const GetWorkspaceSecretSnapshotsV1 = z.object({
}), }),
query: z.object({ query: z.object({
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root"), directory: z.string().trim().default("/"),
offset: z.coerce.number(), offset: z.coerce.number(),
limit: z.coerce.number() limit: z.coerce.number()
}) })
@@ -83,7 +84,7 @@ export const GetWorkspaceSecretSnapshotsCountV1 = z.object({
}), }),
query: z.object({ query: z.object({
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root") directory: z.string().trim().default("/")
}) })
}); });
@@ -93,7 +94,7 @@ export const RollbackWorkspaceSecretSnapshotV1 = z.object({
}), }),
body: z.object({ body: z.object({
environment: z.string().trim(), environment: z.string().trim(),
folderId: z.string().trim().default("root"), directory: z.string().trim().default("/"),
version: z.number() version: z.number()
}) })
}); });
+1 -1
View File
@@ -1,3 +1,3 @@
// secrets // secrets
export const SECRET_SHARED = "shared"; export const SECRET_SHARED = "shared";
export const SECRET_PERSONAL = "personal"; export const SECRET_PERSONAL = "personal";
+1 -1
View File
@@ -75,4 +75,4 @@ require (
github.com/zalando/go-keyring v0.2.3 github.com/zalando/go-keyring v0.2.3
) )
replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.1 replace github.com/zalando/go-keyring => github.com/Infisical/go-keyring v1.0.2
+2 -2
View File
@@ -39,8 +39,8 @@ cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/Infisical/go-keyring v1.0.1 h1:E8XpqoT0H1G9C1kgxU+NeReXOeobmH7LbBHNpcOI380= github.com/Infisical/go-keyring v1.0.2 h1:dWOkI/pB/7RocfSJgGXbXxLDcVYsdslgjEPmVhb+nl8=
github.com/Infisical/go-keyring v1.0.1/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto= github.com/Infisical/go-keyring v1.0.2/go.mod h1:LWOnn/sw9FxDW/0VY+jHFAfOFEe03xmwBVSfJnBowto=
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0= github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30= github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+21 -1
View File
@@ -25,7 +25,7 @@ func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncrypted
} }
if response.IsError() { if response.IsError() {
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response) return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response [%v %v] [status-code=%v]", response.Request.Method, response.Request.URL, response.StatusCode())
} }
return result, nil return result, nil
@@ -339,3 +339,23 @@ func CallGetSingleSecretByNameV3(httpClient *resty.Client, request CreateSecretV
return nil return nil
} }
func CallCreateServiceToken(httpClient *resty.Client, request CreateServiceTokenRequest) (CreateServiceTokenResponse, error) {
var createServiceTokenResponse CreateServiceTokenResponse
response, err := httpClient.
R().
SetResult(&createServiceTokenResponse).
SetHeader("User-Agent", USER_AGENT).
SetBody(request).
Post(fmt.Sprintf("%v/v2/service-token/", config.INFISICAL_URL))
if err != nil {
return CreateServiceTokenResponse{}, fmt.Errorf("CallCreateServiceToken: Unable to complete api request [err=%s]", err)
}
if response.IsError() {
return CreateServiceTokenResponse{}, fmt.Errorf("CallCreateServiceToken: Unsuccessful response [%v %v] [status-code=%v]", response.Request.Method, response.Request.URL, response.StatusCode())
}
return createServiceTokenResponse, nil
}
+34
View File
@@ -387,3 +387,37 @@ type GetSingleSecretByNameSecretResponse struct {
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} `json:"secrets"` } `json:"secrets"`
} }
type ScopePermission struct {
Environment string `json:"environment"`
SecretPath string `json:"secretPath"`
}
type CreateServiceTokenRequest struct {
Name string `json:"name"`
WorkspaceId string `json:"workspaceId"`
Scopes []ScopePermission `json:"scopes"`
ExpiresIn int `json:"expiresIn"`
EncryptedKey string `json:"encryptedKey"`
Iv string `json:"iv"`
Tag string `json:"tag"`
RandomBytes string `json:"randomBytes"`
Permissions []string `json:"permissions"`
}
type ServiceTokenData struct {
ID string `json:"_id"`
Name string `json:"name"`
Workspace string `json:"workspace"`
Scopes []interface{} `json:"scopes"`
User string `json:"user"`
LastUsed time.Time `json:"lastUsed"`
Permissions []string `json:"permissions"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
type CreateServiceTokenResponse struct {
ServiceToken string `json:"serviceToken"`
ServiceTokenData ServiceTokenData `json:"serviceTokenData"`
}
+190
View File
@@ -0,0 +1,190 @@
/*
Copyright (c) 2023 Infisical Inc.
*/
package cmd
import (
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"strings"
"github.com/Infisical/infisical-merge/packages/api"
"github.com/Infisical/infisical-merge/packages/crypto"
"github.com/Infisical/infisical-merge/packages/util"
"github.com/go-resty/resty/v2"
"github.com/spf13/cobra"
)
var tokensCmd = &cobra.Command{
Use: "service-token",
Short: "Manage service tokens",
DisableFlagsInUseLine: true,
Example: "infisical service-token",
Args: cobra.ExactArgs(0),
PreRun: func(cmd *cobra.Command, args []string) {
util.RequireLogin()
},
Run: func(cmd *cobra.Command, args []string) {
},
}
var tokensCreateCmd = &cobra.Command{
Use: "create",
Short: "Used to create service tokens",
DisableFlagsInUseLine: true,
Example: "infisical service-token create",
Args: cobra.ExactArgs(0),
PreRun: func(cmd *cobra.Command, args []string) {
util.RequireLogin()
},
Run: func(cmd *cobra.Command, args []string) {
// get plain text workspace key
loggedInUserDetails, err := util.GetCurrentLoggedInUserDetails()
if err != nil {
util.HandleError(err, "Unable to retrieve your logged in your details. Please login in then try again")
}
if loggedInUserDetails.LoginExpired {
util.PrintErrorMessageAndExit("Your login session has expired, please run [infisical login] and try again")
}
tokenOnly, err := cmd.Flags().GetBool("token-only")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
workspaceId, err := cmd.Flags().GetString("projectId")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if workspaceId == "" {
configFile, err := util.GetWorkSpaceFromFile()
if err != nil {
util.PrintErrorMessageAndExit("Please either run infisical init to connect to a project or pass in project id with --projectId flag")
}
workspaceId = configFile.WorkspaceId
}
serviceTokenName, err := cmd.Flags().GetString("name")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
expireSeconds, err := cmd.Flags().GetInt("expiry-seconds")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
scopes, err := cmd.Flags().GetStringSlice("scope")
if err != nil {
util.HandleError(err, "Unable to parse flag")
}
if len(scopes) == 0 {
util.PrintErrorMessageAndExit("You must define the environments and paths your service token should have access to via the --scope flag")
}
permissions := []api.ScopePermission{}
for _, scope := range scopes {
parts := strings.Split(scope, ":")
if len(parts) != 2 {
fmt.Println("--scope flag is malformed. Each scope flag should be in the following format: <env-slug>:<folder-path>")
return
}
permissions = append(permissions, api.ScopePermission{Environment: parts[0], SecretPath: parts[1]})
}
accessLevels, err := cmd.Flags().GetStringSlice("access-level")
if err != nil {
util.HandleError(err, "Unable to parse flag accessLevels")
}
if len(accessLevels) == 0 {
util.PrintErrorMessageAndExit("You must define whether your service token can be used to read and or write via the --access-level flag")
}
for _, accessLevel := range accessLevels {
if accessLevel != "read" && accessLevel != "write" {
util.PrintErrorMessageAndExit("--access-level can only be of values read and write")
}
}
workspaceKey, err := util.GetPlainTextWorkspaceKey(loggedInUserDetails.UserCredentials.JTWToken, loggedInUserDetails.UserCredentials.PrivateKey, workspaceId)
if err != nil {
util.HandleError(err, "Unable to get workspace key needed to create service token")
}
newWorkspaceEncryptionKey := make([]byte, 16)
_, err = rand.Read(newWorkspaceEncryptionKey)
if err != nil {
util.HandleError(err)
}
newWorkspaceEncryptionKeyHexFormat := hex.EncodeToString(newWorkspaceEncryptionKey)
// encrypt the workspace key symmetrically
encryptedDetails, err := crypto.EncryptSymmetric(workspaceKey, []byte(newWorkspaceEncryptionKeyHexFormat))
if err != nil {
util.HandleError(err)
}
// make a call to the api to save the encrypted symmetric key details
httpClient := resty.New()
httpClient.SetAuthToken(loggedInUserDetails.UserCredentials.JTWToken).
SetHeader("Accept", "application/json")
createServiceTokenResponse, err := api.CallCreateServiceToken(httpClient, api.CreateServiceTokenRequest{
Name: serviceTokenName,
WorkspaceId: workspaceId,
Scopes: permissions,
ExpiresIn: expireSeconds,
EncryptedKey: base64.StdEncoding.EncodeToString(encryptedDetails.CipherText),
Iv: base64.StdEncoding.EncodeToString(encryptedDetails.Nonce),
Tag: base64.StdEncoding.EncodeToString(encryptedDetails.AuthTag),
RandomBytes: newWorkspaceEncryptionKeyHexFormat,
Permissions: accessLevels,
})
if err != nil {
util.HandleError(err, "Unable to create service token")
}
serviceToken := createServiceTokenResponse.ServiceToken + "." + newWorkspaceEncryptionKeyHexFormat
if tokenOnly {
fmt.Println(serviceToken)
} else {
printablePermission := []string{}
for _, permission := range permissions {
printablePermission = append(printablePermission, fmt.Sprintf("([environment: %v] [path: %v])", permission.Environment, permission.SecretPath))
}
fmt.Printf("New service token created\n")
fmt.Printf("Name: %v\n", serviceTokenName)
fmt.Printf("Project ID: %v\n", workspaceId)
fmt.Printf("Access type: [%v]\n", strings.Join(accessLevels, ", "))
fmt.Printf("Permission(s): %v\n", strings.Join(printablePermission, ", "))
fmt.Printf("Service Token: %v\n", serviceToken)
}
},
}
func init() {
tokensCreateCmd.Flags().String("projectId", "", "The project ID you'd like to create the service token for. Default: will use linked Infisical project in .infisical.json")
tokensCreateCmd.Flags().StringSliceP("scope", "s", []string{}, "Environment and secret path. Example format: <env-slug>:<folder-path>")
tokensCreateCmd.Flags().StringP("name", "n", "Service token generated via CLI", "Service token name")
tokensCreateCmd.Flags().StringSliceP("access-level", "a", []string{}, "The type of access the service token should have. Can be 'read' and or 'write'")
tokensCreateCmd.Flags().Bool("token-only", false, "When true, only the service token will be printed")
tokensCreateCmd.Flags().IntP("expiry-seconds", "e", 86400, "Set the service token's expiration time in seconds from now. To never expire set to zero. Default: 1 day ")
tokensCmd.AddCommand(tokensCreateCmd)
rootCmd.AddCommand(tokensCmd)
}
+1 -1
View File
@@ -14,7 +14,7 @@ import (
var userCmd = &cobra.Command{ var userCmd = &cobra.Command{
Use: "user", Use: "user",
Short: "Used to manage user credentials", Short: "Used to manage local user credentials",
DisableFlagsInUseLine: true, DisableFlagsInUseLine: true,
Example: "infisical user", Example: "infisical user",
Args: cobra.ExactArgs(0), Args: cobra.ExactArgs(0),
+41
View File
@@ -684,3 +684,44 @@ func GetEnvelopmentBasedOnGitBranch(workspaceFile models.WorkspaceConfigFile) st
return "" return ""
} }
} }
func GetPlainTextWorkspaceKey(authenticationToken string, receiverPrivateKey string, workspaceId string) ([]byte, error) {
httpClient := resty.New()
httpClient.SetAuthToken(authenticationToken).
SetHeader("Accept", "application/json")
request := api.GetEncryptedWorkspaceKeyRequest{
WorkspaceId: workspaceId,
}
workspaceKeyResponse, err := api.CallGetEncryptedWorkspaceKey(httpClient, request)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: unable to retrieve your encrypted workspace key. [err=%v]", err)
}
encryptedWorkspaceKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.EncryptedKey)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKey [err=%v]", err)
}
encryptedWorkspaceKeySenderPublicKey, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Sender.PublicKey)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeySenderPublicKey [err=%v]", err)
}
encryptedWorkspaceKeyNonce, err := base64.StdEncoding.DecodeString(workspaceKeyResponse.Nonce)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for encryptedWorkspaceKeyNonce [err=%v]", err)
}
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(receiverPrivateKey)
if err != nil {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Unable to get bytes represented by the base64 for currentUsersPrivateKey [err=%v]", err)
}
if len(currentUsersPrivateKey) == 0 || len(encryptedWorkspaceKeySenderPublicKey) == 0 {
return nil, fmt.Errorf("GetPlainTextWorkspaceKey: Missing credentials for generating plainTextEncryptionKey")
}
return crypto.DecryptAsymmetric(encryptedWorkspaceKey, encryptedWorkspaceKeyNonce, encryptedWorkspaceKeySenderPublicKey, currentUsersPrivateKey), nil
}
+81
View File
@@ -0,0 +1,81 @@
---
title: "infisical service-token"
description: "Manage Infisical service tokens"
---
```bash
infisical service-token create --scope=dev:/global --scope=dev:/backend --access-level=read --access-level=write
```
## Description
The Infisical `service-token` command allows you to manage service tokens for a given Infisical project.
With this command, you can create, view, and delete service tokens.
<Accordion title="service-token create" defaultOpen="true">
Use this command to create a service token
```bash
$ infisical service-token create --scope=dev:/backend/** --access-level=read --access-level=write
```
### Flags
<Accordion title="--scope">
```bash
infisical service-token create --scope=dev:/global --scope=dev:/backend/** --access-level=read
```
Use the scope flag to define which environments and paths your service token should be authorized to access.
The value of your scope flag should be in the following `<environment slug>:<path>`.
Here, `environment slug` refers to the slug name of the environment, and `path` indicates the folder path where your secrets are stored.
For specifying multiple scopes, you can use multiple --scope flags.
<Info>
The `path` can be a Glob pattern
</Info>
</Accordion>
<Accordion title="--projectId">
```bash
infisical service-token create --scope=dev:/global --access-level=read --projectId=63cefb15c8d3175601cfa989
```
The project ID you'd like to create the service token for.
By default, the CLI will attempt to use the linked Infisical project in `.infisical.json` generated by `infisical init` command.
</Accordion>
<Accordion title="--name">
```bash
infisical service-token create --scope=dev:/global --access-level=read --name service-token-name
```
Service token name
Default: `Service token generated via CLI`
</Accordion>
<Accordion title="--expiry-seconds">
```bash
infisical service-token create --scope=dev:/global --access-level=read --expiry-seconds 120
```
Set the service token's expiration time in seconds from now. To never expire set to zero.
Default: `1 day`
</Accordion>
<Accordion title="--access-level">
```bash
infisical service-token create --scope=dev:/global --access-level=read --access-level=write
```
The type of access the service token should have. Can be `read` and or `write`
</Accordion>
<Accordion title="--token-only">
```bash
infisical service-token create --scope=dev:/global --access-level=read --access-level=write --token-only
```
When true, only the service token will be printed
Default: `false`
</Accordion>
</Accordion>
+1
View File
@@ -169,6 +169,7 @@
"cli/commands/run", "cli/commands/run",
"cli/commands/secrets", "cli/commands/secrets",
"cli/commands/export", "cli/commands/export",
"cli/commands/service-token",
"cli/commands/vault", "cli/commands/vault",
"cli/commands/user", "cli/commands/user",
"cli/commands/reset", "cli/commands/reset",
+61 -1
View File
@@ -51,6 +51,7 @@
"cookies": "^0.8.0", "cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0", "cva": "npm:class-variance-authority@^0.4.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"file-saver": "^2.0.5",
"framer-motion": "^6.2.3", "framer-motion": "^6.2.3",
"fs": "^0.0.2", "fs": "^0.0.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
@@ -92,7 +93,8 @@
"uuidv4": "^6.2.13", "uuidv4": "^6.2.13",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.0" "zod": "^3.22.0",
"zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.23", "@storybook/addon-essentials": "^7.0.23",
@@ -105,6 +107,7 @@
"@storybook/react": "^7.0.23", "@storybook/react": "^7.0.23",
"@storybook/testing-library": "^0.2.0", "@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@types/file-saver": "^2.0.5",
"@types/jsrp": "^0.2.4", "@types/jsrp": "^0.2.4",
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@types/picomatch": "^2.3.0", "@types/picomatch": "^2.3.0",
@@ -8208,6 +8211,12 @@
"@types/send": "*" "@types/send": "*"
} }
}, },
"node_modules/@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
"dev": true
},
"node_modules/@types/find-cache-dir": { "node_modules/@types/find-cache-dir": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz",
@@ -13444,6 +13453,11 @@
"node": "^10.12.0 || >=12.0.0" "node": "^10.12.0 || >=12.0.0"
} }
}, },
"node_modules/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"node_modules/file-system-cache": { "node_modules/file-system-cache": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz",
@@ -23640,6 +23654,33 @@
"funding": { "funding": {
"url": "https://github.com/sponsors/colinhacks" "url": "https://github.com/sponsors/colinhacks"
} }
},
"node_modules/zustand": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
"integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
"dependencies": {
"use-sync-external-store": "1.2.0"
},
"engines": {
"node": ">=12.7.0"
},
"peerDependencies": {
"@types/react": ">=16.8",
"immer": ">=9.0",
"react": ">=16.8"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
}
}
} }
}, },
"dependencies": { "dependencies": {
@@ -29227,6 +29268,12 @@
"@types/send": "*" "@types/send": "*"
} }
}, },
"@types/file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-zv9kNf3keYegP5oThGLaPk8E081DFDuwfqjtiTzm6PoxChdJ1raSuADf2YGCVIyrSynLrgc8JWv296s7Q7pQSQ==",
"dev": true
},
"@types/find-cache-dir": { "@types/find-cache-dir": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz", "resolved": "https://registry.npmjs.org/@types/find-cache-dir/-/find-cache-dir-3.2.1.tgz",
@@ -33358,6 +33405,11 @@
"flat-cache": "^3.0.4" "flat-cache": "^3.0.4"
} }
}, },
"file-saver": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz",
"integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA=="
},
"file-system-cache": { "file-system-cache": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz", "resolved": "https://registry.npmjs.org/file-system-cache/-/file-system-cache-2.3.0.tgz",
@@ -40768,6 +40820,14 @@
"version": "3.22.0", "version": "3.22.0",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz", "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.0.tgz",
"integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q==" "integrity": "sha512-y5KZY/ssf5n7hCGDGGtcJO/EBJEm5Pa+QQvFBeyMOtnFYOSflalxIFFvdaYevPhePcmcKC4aTbFkCcXN7D0O8Q=="
},
"zustand": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-4.4.1.tgz",
"integrity": "sha512-QCPfstAS4EBiTQzlaGP1gmorkh/UL1Leaj2tdj+zZCZ/9bm0WS7sI2wnfD5lpOszFqWJ1DcPnGoY8RDL61uokw==",
"requires": {
"use-sync-external-store": "1.2.0"
}
} }
} }
} }
+4 -1
View File
@@ -59,6 +59,7 @@
"cookies": "^0.8.0", "cookies": "^0.8.0",
"cva": "npm:class-variance-authority@^0.4.0", "cva": "npm:class-variance-authority@^0.4.0",
"date-fns": "^2.30.0", "date-fns": "^2.30.0",
"file-saver": "^2.0.5",
"framer-motion": "^6.2.3", "framer-motion": "^6.2.3",
"fs": "^0.0.2", "fs": "^0.0.2",
"gray-matter": "^4.0.3", "gray-matter": "^4.0.3",
@@ -100,7 +101,8 @@
"uuidv4": "^6.2.13", "uuidv4": "^6.2.13",
"yaml": "^2.2.2", "yaml": "^2.2.2",
"yup": "^0.32.11", "yup": "^0.32.11",
"zod": "^3.22.0" "zod": "^3.22.0",
"zustand": "^4.4.1"
}, },
"devDependencies": { "devDependencies": {
"@storybook/addon-essentials": "^7.0.23", "@storybook/addon-essentials": "^7.0.23",
@@ -113,6 +115,7 @@
"@storybook/react": "^7.0.23", "@storybook/react": "^7.0.23",
"@storybook/testing-library": "^0.2.0", "@storybook/testing-library": "^0.2.0",
"@tailwindcss/typography": "^0.5.4", "@tailwindcss/typography": "^0.5.4",
"@types/file-saver": "^2.0.5",
"@types/jsrp": "^0.2.4", "@types/jsrp": "^0.2.4",
"@types/node": "^18.11.9", "@types/node": "^18.11.9",
"@types/picomatch": "^2.3.0", "@types/picomatch": "^2.3.0",
@@ -107,6 +107,11 @@
} }
} }
}, },
"approval": {
"title": "Admin Panel",
"og-title": "Manage your secret change management",
"og-description": "Infisical a simple end-to-end encrypted platform that enables teams to sync and manage their .env files."
},
"integrations": { "integrations": {
"title": "Project Integrations", "title": "Project Integrations",
"description": "Manage your integrations of Infisical with third-party services.", "description": "Manage your integrations of Infisical with third-party services.",
@@ -1,4 +1,3 @@
import { useMemo } from "react";
import Link from "next/link"; import Link from "next/link";
import { useRouter } from "next/router"; import { useRouter } from "next/router";
import { faAngleRight } from "@fortawesome/free-solid-svg-icons"; import { faAngleRight } from "@fortawesome/free-solid-svg-icons";
@@ -15,7 +14,7 @@ type Props = {
currentEnv?: string; currentEnv?: string;
userAvailableEnvs?: any[]; userAvailableEnvs?: any[];
onEnvChange?: (slug: string) => void; onEnvChange?: (slug: string) => void;
folders?: Array<{ id: string; name: string }>; secretPath?: string;
isFolderMode?: boolean; isFolderMode?: boolean;
}; };
@@ -42,19 +41,14 @@ export default function NavHeader({
currentEnv, currentEnv,
userAvailableEnvs = [], userAvailableEnvs = [],
onEnvChange, onEnvChange,
folders = [], isFolderMode,
isFolderMode secretPath = "/"
}: Props): JSX.Element { }: Props): JSX.Element {
const { currentWorkspace } = useWorkspace(); const { currentWorkspace } = useWorkspace();
const { currentOrg } = useOrganization(); const { currentOrg } = useOrganization();
const router = useRouter(); const router = useRouter();
const isInRootFolder = isFolderMode && folders.length <= 1; const secretPathSegments = secretPath.split("/").filter(Boolean);
const selectedEnv = useMemo(
() => userAvailableEnvs?.find((uae) => uae.name === currentEnv),
[userAvailableEnvs, currentEnv]
);
return ( return (
<div className="flex flex-row items-center pt-6"> <div className="flex flex-row items-center pt-6">
@@ -90,13 +84,13 @@ export default function NavHeader({
) : ( ) : (
<div className="text-sm text-gray-400">{pageName}</div> <div className="text-sm text-gray-400">{pageName}</div>
)} )}
{currentEnv && isInRootFolder && ( {currentEnv && secretPath === "/" && (
<> <>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" /> <FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
<div className="rounded-md pl-3 hover:bg-bunker-100/10"> <div className="rounded-md pl-3 hover:bg-bunker-100/10">
<Tooltip content="Select environment"> <Tooltip content="Select environment">
<Select <Select
value={selectedEnv?.slug} value={currentEnv}
onValueChange={(value) => { onValueChange={(value) => {
if (value && onEnvChange) onEnvChange(value); if (value && onEnvChange) onEnvChange(value);
}} }}
@@ -113,16 +107,36 @@ export default function NavHeader({
</div> </div>
</> </>
)} )}
{isFolderMode && Boolean(secretPathSegments.length) && (
<div className="flex items-center space-x-3">
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
<Link
passHref
legacyBehavior
href={{
pathname: "/project/[id]/secrets/v2/[env]",
query: { id: router.query.id, env: router.query.env }
}}
>
<a className="text-sm font-semibold text-primary/80 hover:text-primary">
{userAvailableEnvs?.find(({ slug }) => slug === currentEnv)?.name}
</a>
</Link>
</div>
)}
{isFolderMode && {isFolderMode &&
folders?.map(({ id, name }, index) => { secretPathSegments?.map((folderName, index) => {
const query = { ...router.query }; const query = { ...router.query };
if (name !== "root") query.folderId = id; query.secretPath = secretPathSegments.slice(0, index + 1);
else delete query.folderId;
return ( return (
<div className="flex items-center space-x-3" key={`breadcrumb-folder-${id}`}> <div
className="flex items-center space-x-3"
key={`breadcrumb-secret-path-${folderName}`}
>
<FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" /> <FontAwesomeIcon icon={faAngleRight} className="ml-3 mr-1.5 text-xs text-gray-400" />
{index + 1 === folders?.length ? ( {index + 1 === secretPathSegments?.length ? (
<span className="text-sm font-semibold text-bunker-300">{name}</span> <span className="text-sm font-semibold text-bunker-300">{folderName}</span>
) : ( ) : (
<Link <Link
passHref passHref
@@ -130,7 +144,7 @@ export default function NavHeader({
href={{ pathname: "/project/[id]/secrets/[env]", query }} href={{ pathname: "/project/[id]/secrets/[env]", query }}
> >
<a className="text-sm font-semibold text-primary/80 hover:text-primary"> <a className="text-sm font-semibold text-primary/80 hover:text-primary">
{name === "root" ? selectedEnv?.name : name} folderName
</a> </a>
</Link> </Link>
)} )}
@@ -0,0 +1,266 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { faCheck } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import {
Button,
FormControl,
Input,
Modal,
ModalClose,
ModalContent,
Tooltip
} from "@app/components/v2";
import { useWorkspace } from "@app/context";
import { useCreateWsTag } from "@app/hooks/api";
export const secretTagsColors = [
{
id: 1,
hex: "#bec2c8",
rgba: "rgb(128,128,128, 0.8)",
name: "Grey"
},
{
id: 2,
hex: "#95a2b3",
rgba: "rgb(0,0,255, 0.8)",
name: "blue"
},
{
id: 3,
hex: "#5e6ad2",
rgba: "rgb(128,0,128, 0.8)",
name: "Purple"
},
{
id: 4,
hex: "#26b5ce",
rgba: "rgb(0,128,128, 0.8)",
name: "Teal"
},
{
id: 5,
hex: "#4cb782",
rgba: "rgb(0,128,0, 0.8)",
name: "Green"
},
{
id: 6,
hex: "#f2c94c",
rgba: "rgb(255,255,0, 0.8)",
name: "Yellow"
},
{
id: 7,
hex: "#f2994a",
rgba: "rgb(128,128,0, 0.8)",
name: "Orange"
},
{
id: 8,
hex: "#f7c8c1",
rgba: "rgb(128,0,0, 0.8)",
name: "Pink"
},
{
id: 9,
hex: "#eb5757",
rgba: "rgb(255,0,0, 0.8)",
name: "Red"
}
];
const isValidHexColor = (hexColor: string) => {
const hexColorPattern = /^#?([0-9A-Fa-f]{3}|[0-9A-Fa-f]{6})$/;
return hexColorPattern.test(hexColor);
};
type Props = {
isOpen?: boolean;
onToggle: (isOpen: boolean) => void;
};
const createTagSchema = z.object({
name: z.string().trim(),
color: z.string().trim()
});
type FormData = z.infer<typeof createTagSchema>;
type TagColor = {
id: number;
hex: string;
rgba: string;
name: string;
};
export const CreateTagModal = ({ isOpen, onToggle }: Props): JSX.Element => {
const {
control,
reset,
watch,
setValue,
formState: { isSubmitting },
handleSubmit
} = useForm<FormData>({
resolver: zodResolver(createTagSchema)
});
const { createNotification } = useNotificationContext();
const { currentWorkspace } = useWorkspace();
const workspaceId = currentWorkspace?._id || "";
const { mutateAsync: createWsTag } = useCreateWsTag();
const [showHexInput, setShowHexInput] = useState<boolean>(false);
const selectedTagColor = watch("color", secretTagsColors[0].hex);
useEffect(()=>{
if(!isOpen) reset();
},[isOpen])
const onFormSubmit = async ({ name, color }: FormData) => {
try {
await createWsTag({
workspaceID: workspaceId,
tagName: name,
tagColor: color,
tagSlug: name.replace(" ", "_")
});
onToggle(false);
reset();
createNotification({
text: "Successfully created a tag",
type: "success"
});
} catch (error) {
console.error(error);
createNotification({
text: "Failed to create a tag",
type: "error"
});
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onToggle}>
<ModalContent
title="Create tag"
subTitle="Specify your tag name, and the slug will be created automatically."
>
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Type your tag name" />
</FormControl>
)}
/>
<div className="mt-2">
<div className="mb-0.5 ml-1 block text-sm font-normal text-mineshaft-400">
Tag Color
</div>
<div className="flex space-x-2">
<div className="p-2 rounded flex items-center justify-center border border-mineshaft-500 bg-mineshaft-900 ">
<div
className="w-6 h-6 rounded-full"
style={{ background: `${selectedTagColor}` }}
/>
</div>
<div className="flex-grow flex items-center rounded border-mineshaft-500 bg-mineshaft-900 px-1 pr-2">
{!showHexInput ? (
<div className="inline-flex gap-3 items-center pl-3">
{secretTagsColors.map(($tagColor: TagColor) => {
return (
<div key={`tag-color-${$tagColor.id}`}>
<Tooltip content={`${$tagColor.name}`}>
<div
className=" flex items-center justify-center w-[26px] h-[26px] hover:ring-offset-2 hover:ring-2 bg-[#bec2c8] border-2 p-2 hover:shadow-lg border-transparent hover:border-black rounded-full"
key={`tag-${$tagColor.id}`}
style={{ backgroundColor: `${$tagColor.hex}` }}
onClick={() => setValue("color", $tagColor.hex)}
tabIndex={0}
role="button"
onKeyDown={() => {}}
>
{$tagColor.hex === selectedTagColor && (
<FontAwesomeIcon icon={faCheck} style={{ color: "#00000070" }} />
)}
</div>
</Tooltip>
</div>
);
})}
</div>
) : (
<div className="flex flex-grow items-center px-2 tags-hex-wrapper">
<div className="flex items-center relative rounded-md ">
{isValidHexColor(selectedTagColor) && (
<div
className="w-7 h-7 rounded-full flex items-center justify-center"
style={{ background: `${selectedTagColor}` }}
>
<FontAwesomeIcon icon={faCheck} style={{ color: "#00000070" }} />
</div>
)}
{!isValidHexColor(selectedTagColor) && (
<div className="border-dashed border bg-blue rounded-full w-7 h-7 border-mineshaft-500" />
)}
</div>
<div className="flex-grow">
<Input
variant="plain"
value={selectedTagColor}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
setValue("color", e.target.value)
}
/>
</div>
</div>
)}
<div className="border-mineshaft-500 border h-8 mx-4" />
<div className="w-7 h-7 flex items-center justify-center">
<div
className={`flex items-center justify-center w-7 h-7 bg-transparent cursor-pointer hover:ring-offset-1 hover:ring-2 border-mineshaft-500 border bg-mineshaft-900 rounded-sm p-2 ${
showHexInput ? "tags-conic-bg rounded-full" : ""
}`}
onClick={() => setShowHexInput((prev) => !prev)}
style={{ border: "1px solid rgba(220, 216, 254, 0.376)" }}
tabIndex={0}
role="button"
onKeyDown={() => {}}
>
{!showHexInput && <span>#</span>}
</div>
</div>
</div>
</div>
</div>
<div className="mt-8 flex items-center">
<Button
className="mr-4"
type="submit"
isDisabled={isSubmitting}
isLoading={isSubmitting}
>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
</ModalContent>
</Modal>
);
};
@@ -0,0 +1 @@
export { CreateTagModal } from "./CreateTagModal";
@@ -30,9 +30,10 @@ export const Checkbox = ({
<div className="flex items-center font-inter text-bunker-300"> <div className="flex items-center font-inter text-bunker-300">
<CheckboxPrimitive.Root <CheckboxPrimitive.Root
className={twMerge( className={twMerge(
"flex items-center justify-center w-4 h-4 mr-3 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600", "flex items-center justify-center w-4 h-4 transition-all rounded shadow border border-mineshaft-400 hover:bg-mineshaft-500 bg-mineshaft-600",
isDisabled && "bg-bunker-400 hover:bg-bunker-400", isDisabled && "bg-bunker-400 hover:bg-bunker-400",
isChecked && "bg-primary hover:bg-primary", isChecked && "bg-primary hover:bg-primary",
Boolean(children) && "mr-3",
className className
)} )}
required={isRequired} required={isRequired}
@@ -0,0 +1,46 @@
// this will show a loading animation with text below
// if you pass array it will say it one by one giving user clear instruction on what's happening
import { useEffect, useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
type Props = {
text?: string | string[];
frequency?: number;
};
export const ContentLoader = ({ text, frequency = 2000 }: Props) => {
const [pos, setPos] = useState(0);
const isTextArray = Array.isArray(text);
useEffect(() => {
let interval: NodeJS.Timer;
if (isTextArray) {
interval = setInterval(() => {
setPos((state) => (state + 1) % text.length);
}, frequency);
}
return () => clearInterval(interval);
}, []);
return (
<div className="container mx-auto flex relative flex-col h-1/2 w-full items-center justify-center px-8 text-mineshaft-50 dark:[color-scheme:dark] space-y-8">
<div>
<img src="/images/loading/loading.gif" height={210} width={240} alt="loading animation" />
</div>
{text && isTextArray && (
<AnimatePresence exitBeforeEnter>
<motion.div
className="text-primary"
key={`content-loader-${pos}`}
initial={{ opacity: 0, translateY: 20 }}
animate={{ opacity: 1, translateY: 0 }}
exit={{ opacity: 0, translateY: -20 }}
>
{text[pos]}
</motion.div>
</AnimatePresence>
)}
{text && !isTextArray && <div className="text-primary text-sm">{text}</div>}
</div>
);
};
@@ -0,0 +1 @@
export { ContentLoader } from "./ContentLoader";
@@ -6,6 +6,9 @@ import { twMerge } from "tailwind-merge";
export type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps; export type DropdownMenuProps = DropdownMenuPrimitive.DropdownMenuProps;
export const DropdownMenu = DropdownMenuPrimitive.Root; export const DropdownMenu = DropdownMenuPrimitive.Root;
export type DropdownSubMenuProps = DropdownMenuPrimitive.DropdownMenuSubProps;
export const DropdownSubMenu = DropdownMenuPrimitive.Sub;
// trigger // trigger
export type DropdownMenuTriggerProps = DropdownMenuPrimitive.DropdownMenuTriggerProps; export type DropdownMenuTriggerProps = DropdownMenuPrimitive.DropdownMenuTriggerProps;
export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger; export const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
@@ -34,6 +37,30 @@ export const DropdownMenuContent = forwardRef<HTMLDivElement, DropdownMenuConten
DropdownMenuContent.displayName = "DropdownMenuContent"; DropdownMenuContent.displayName = "DropdownMenuContent";
// item container
export type DropdownSubMenuContentProps = DropdownMenuPrimitive.MenuSubContentProps;
export const DropdownSubMenuContent = forwardRef<HTMLDivElement, DropdownSubMenuContentProps>(
({ children, className, ...props }, forwardedRef) => {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.SubContent
sideOffset={2}
{...props}
ref={forwardedRef}
className={twMerge(
"min-w-[220px] z-30 bg-mineshaft-900 border border-mineshaft-600 will-change-auto text-bunker-300 rounded-md shadow data-[side=top]:animate-slideDownAndFade data-[side=left]:animate-slideRightAndFade data-[side=right]:animate-slideLeftAndFade data-[side=bottom]:animate-slideUpAndFade",
className
)}
>
{children}
</DropdownMenuPrimitive.SubContent>
</DropdownMenuPrimitive.Portal>
);
}
);
DropdownSubMenuContent.displayName = "DropdownMenuContent";
// item label component // item label component
export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps; export type DropdownLabelProps = DropdownMenuPrimitive.MenuLabelProps;
export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => ( export const DropdownMenuLabel = ({ className, ...props }: DropdownLabelProps) => (
@@ -76,11 +103,50 @@ export const DropdownMenuItem = <T extends ElementType = "button">({
</DropdownMenuPrimitive.Item> </DropdownMenuPrimitive.Item>
); );
// trigger
export type DropdownSubMenuTriggerProps<T extends ElementType> =
DropdownMenuPrimitive.DropdownMenuSubTriggerProps & {
icon?: ReactNode;
as?: T;
inputRef?: Ref<T>;
iconPos?: "left" | "right";
};
export const DropdownSubMenuTrigger = <T extends ElementType = "button">({
children,
inputRef,
className,
icon,
as: Item = "button",
iconPos = "left",
...props
}: DropdownMenuItemProps<T> & ComponentPropsWithRef<T>) => (
<DropdownMenuPrimitive.SubTrigger
{...props}
className={twMerge(
"text-xs text-mineshaft-200 block font-inter px-4 py-2 data-[highlighted]:bg-mineshaft-700 rounded-sm outline-none cursor-pointer",
className
)}
>
<Item type="button" role="menuitem" className="flex w-full items-center" ref={inputRef}>
{icon && iconPos === "left" && <span className="flex items-center mr-2">{icon}</span>}
<span className="flex-grow text-left">{children}</span>
{icon && iconPos === "right" && <span className="flex items-center ml-2">{icon}</span>}
</Item>
</DropdownMenuPrimitive.SubTrigger>
);
// grouping items into 1 // grouping items into 1
export type DropdownMenuGroupProps = DropdownMenuPrimitive.DropdownMenuGroupProps; export type DropdownMenuGroupProps = DropdownMenuPrimitive.DropdownMenuGroupProps;
export const DropdownMenuGroup = forwardRef<HTMLDivElement, DropdownMenuGroupProps>( export const DropdownMenuGroup = forwardRef<HTMLDivElement, DropdownMenuGroupProps>(
({ ...props }, ref) => <DropdownMenuPrimitive.Group {...props} ref={ref} /> ({ ...props }, ref) => (
<DropdownMenuPrimitive.Group
{...props}
className={twMerge("text-xs py-2 pl-3", props.className)}
ref={ref}
/>
)
); );
DropdownMenuGroup.displayName = "DropdownMenuGroup"; DropdownMenuGroup.displayName = "DropdownMenuGroup";
@@ -98,3 +164,5 @@ export const DropdownMenuSeparator = forwardRef<
)); ));
DropdownMenuSeparator.displayName = "DropdownMenuSeperator"; DropdownMenuSeparator.displayName = "DropdownMenuSeperator";
DropdownMenuSeparator.displayName = "DropdownMenuSeperator";
@@ -4,7 +4,10 @@ export type {
DropdownMenuGroupProps, DropdownMenuGroupProps,
DropdownMenuItemProps, DropdownMenuItemProps,
DropdownMenuProps, DropdownMenuProps,
DropdownMenuTriggerProps DropdownMenuTriggerProps,
DropdownSubMenuContentProps,
DropdownSubMenuProps,
DropdownSubMenuTriggerProps
} from "./Dropdown"; } from "./Dropdown";
export { export {
DropdownMenu, DropdownMenu,
@@ -13,5 +16,8 @@ export {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuLabel, DropdownMenuLabel,
DropdownMenuSeparator, DropdownMenuSeparator,
DropdownMenuTrigger DropdownMenuTrigger,
DropdownSubMenu,
DropdownSubMenuContent,
DropdownSubMenuTrigger
} from "./Dropdown"; } from "./Dropdown";
+40 -29
View File
@@ -2,8 +2,8 @@
/* eslint-disable import/no-extraneous-dependencies */ /* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable global-require */ /* eslint-disable global-require */
import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react"; import { ComponentPropsWithRef, ElementType, ReactNode, Ref, useRef } from "react";
import { motion } from "framer-motion" import { motion } from "framer-motion";
import Lottie from "lottie-react" import Lottie from "lottie-react";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
export type MenuProps = { export type MenuProps = {
@@ -39,13 +39,10 @@ export const MenuItem = <T extends ElementType = "button">({
inputRef, inputRef,
...props ...props
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => { }: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
const iconRef = useRef() const iconRef = useRef();
return( return (
<a <a onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
>
<li <li
className={twMerge( className={twMerge(
"group px-1 py-2 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50", "group px-1 py-2 mt-0.5 font-inter flex flex-col text-sm text-bunker-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
@@ -55,25 +52,37 @@ export const MenuItem = <T extends ElementType = "button">({
)} )}
> >
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm"> <motion.span className="w-full flex flex-row items-center justify-start rounded-sm">
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}> <Item
<div className={`${isSelected ? "visisble" : "invisible"} -left-[0.28rem] absolute w-[0.07rem] rounded-md h-5 bg-primary`}/> type="button"
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */} role="menuitem"
<Lottie className="flex items-center relative"
lottieRef={iconRef} ref={inputRef}
style={{ width: 22, height: 22 }} {...props}
// eslint-disable-next-line import/no-dynamic-require >
animationData={require(`../../../../public/lotties/${icon}.json`)} <div
loop={false} className={`${
autoplay={false} isSelected ? "visisble" : "invisible"
className="my-auto ml-[0.1rem] mr-3" } -left-[0.28rem] absolute w-[0.07rem] rounded-md h-5 bg-primary`}
/> />
{/* {icon && <span className="mr-3 ml-4 w-5 block group-hover:hidden">{icon}</span>} */}
{icon && (
<Lottie
lottieRef={iconRef}
style={{ width: 22, height: 22 }}
// eslint-disable-next-line import/no-dynamic-require
animationData={require(`../../../../public/lotties/${icon}.json`)}
loop={false}
autoplay={false}
className="my-auto ml-[0.1rem] mr-3"
/>
)}
<span className="flex-grow text-left">{children}</span> <span className="flex-grow text-left">{children}</span>
</Item> </Item>
{description && <span className="mt-2 text-xs">{description}</span>} {description && <span className="mt-2 text-xs">{description}</span>}
</motion.span> </motion.span>
</li> </li>
</a> </a>
) );
}; };
export const SubMenuItem = <T extends ElementType = "button">({ export const SubMenuItem = <T extends ElementType = "button">({
@@ -88,13 +97,10 @@ export const SubMenuItem = <T extends ElementType = "button">({
inputRef, inputRef,
...props ...props
}: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => { }: MenuItemProps<T> & ComponentPropsWithRef<T>): JSX.Element => {
const iconRef = useRef() const iconRef = useRef();
return( return (
<a <a onMouseEnter={() => iconRef.current?.play()} onMouseLeave={() => iconRef.current?.stop()}>
onMouseEnter={() => iconRef.current?.play()}
onMouseLeave={() => iconRef.current?.stop()}
>
<li <li
className={twMerge( className={twMerge(
"group px-1 py-1 mt-0.5 font-inter flex flex-col text-sm text-mineshaft-300 hover:text-mineshaft-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50", "group px-1 py-1 mt-0.5 font-inter flex flex-col text-sm text-mineshaft-300 hover:text-mineshaft-100 transition-all rounded cursor-pointer hover:bg-mineshaft-700 duration-50",
@@ -103,7 +109,13 @@ export const SubMenuItem = <T extends ElementType = "button">({
)} )}
> >
<motion.span className="w-full flex flex-row items-center justify-start rounded-sm pl-6"> <motion.span className="w-full flex flex-row items-center justify-start rounded-sm pl-6">
<Item type="button" role="menuitem" className="flex items-center relative" ref={inputRef} {...props}> <Item
type="button"
role="menuitem"
className="flex items-center relative"
ref={inputRef}
{...props}
>
<Lottie <Lottie
lottieRef={iconRef} lottieRef={iconRef}
style={{ width: 16, height: 16 }} style={{ width: 16, height: 16 }}
@@ -119,10 +131,9 @@ export const SubMenuItem = <T extends ElementType = "button">({
</motion.span> </motion.span>
</li> </li>
</a> </a>
) );
}; };
MenuItem.displayName = "MenuItem"; MenuItem.displayName = "MenuItem";
export type MenuGroupProps = { export type MenuGroupProps = {
+2 -2
View File
@@ -22,14 +22,14 @@ export const ModalContent = forwardRef<HTMLDivElement, ModalContentProps>(
) => ( ) => (
<DialogPrimitive.Portal> <DialogPrimitive.Portal>
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
className={twMerge("fixed inset-0 z-[70] h-full w-full animate-fadeIn", overlayClassName)} className={twMerge("fixed inset-0 z-30 h-full w-full animate-fadeIn", overlayClassName)}
style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }} style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }}
/> />
<DialogPrimitive.Content {...props} ref={forwardedRef}> <DialogPrimitive.Content {...props} ref={forwardedRef}>
<Card <Card
isRounded isRounded
className={twMerge( className={twMerge(
"fixed top-1/2 left-1/2 z-[90] dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-xl -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl", "fixed top-1/2 left-1/2 z-30 dark:[color-scheme:dark] max-h-screen overflow-y-auto thin-scrollbar max-w-xl -translate-y-2/4 -translate-x-2/4 animate-popIn border border-mineshaft-600 drop-shadow-2xl",
className className
)} )}
> >
@@ -1,6 +1,7 @@
/* eslint-disable react/no-danger */ /* eslint-disable react/no-danger */
import { forwardRef, HTMLAttributes } from "react"; import { forwardRef, TextareaHTMLAttributes } from "react";
import sanitizeHtml, { DisallowedTagsModes } from "sanitize-html"; import sanitizeHtml, { DisallowedTagsModes } from "sanitize-html";
import { twMerge } from "tailwind-merge";
import { useToggle } from "@app/hooks"; import { useToggle } from "@app/hooks";
@@ -39,20 +40,28 @@ const syntaxHighlight = (content?: string | null, isVisible?: boolean) => {
return `${newContent}<br/>`; return `${newContent}<br/>`;
}; };
type Props = HTMLAttributes<HTMLTextAreaElement> & { type Props = TextareaHTMLAttributes<HTMLTextAreaElement> & {
value?: string | null; value?: string | null;
isVisible?: boolean; isVisible?: boolean;
isReadOnly?: boolean;
isDisabled?: boolean; isDisabled?: boolean;
containerClassName?: string;
}; };
const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all"; const commonClassName = "font-mono text-sm caret-white border-none outline-none w-full break-all";
export const SecretInput = forwardRef<HTMLTextAreaElement, Props>( export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
({ value, isVisible, onBlur, isDisabled, onFocus, ...props }, ref) => { (
{ value, isVisible, containerClassName, onBlur, isDisabled, isReadOnly, onFocus, ...props },
ref
) => {
const [isSecretFocused, setIsSecretFocused] = useToggle(); const [isSecretFocused, setIsSecretFocused] = useToggle();
return ( return (
<div className="overflow-auto w-full" style={{ maxHeight: `${21 * 7}px` }}> <div
className={twMerge("overflow-auto w-full no-scrollbar rounded-md", containerClassName)}
style={{ maxHeight: `${21 * 7}px` }}
>
<div className="relative overflow-hidden"> <div className="relative overflow-hidden">
<pre aria-hidden className="m-0 "> <pre aria-hidden className="m-0 ">
<code className={`inline-block w-full ${commonClassName}`}> <code className={`inline-block w-full ${commonClassName}`}>
@@ -78,6 +87,7 @@ export const SecretInput = forwardRef<HTMLTextAreaElement, Props>(
}} }}
value={value || ""} value={value || ""}
{...props} {...props}
readOnly={isReadOnly}
/> />
</div> </div>
</div> </div>
@@ -20,7 +20,7 @@ export const Spinner = ({ className, size = "md" }: Props): JSX.Element => {
<svg <svg
aria-hidden="true" aria-hidden="true"
className={twMerge( className={twMerge(
" text-gray-200 animate-spin dark:text-gray-600 fill-primary m-1", "text-gray-200 animate-spin dark:text-gray-600 fill-primary m-1",
sizeChart[size], sizeChart[size],
className className
)} )}
+11 -9
View File
@@ -1,14 +1,17 @@
import { ReactNode } from "react"; import { ReactNode } from "react";
import { faClose } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { cva, VariantProps } from "cva"; import { cva, VariantProps } from "cva";
import { twMerge } from "tailwind-merge"; import { twMerge } from "tailwind-merge";
type Props = { type Props = {
children: ReactNode; children: ReactNode;
className?: string; className?: string;
onClose?: () => void;
} & VariantProps<typeof tagVariants>; } & VariantProps<typeof tagVariants>;
const tagVariants = cva( const tagVariants = cva(
"inline-flex items-center whitespace-nowrap text-sm rounded-sm mr-1.5 text-bunker-200 rounded-[30px] text-gray-400 ", "inline-flex items-center whitespace-nowrap text-sm rounded mr-1.5 text-bunker-200 text-gray-400 ",
{ {
variants: { variants: {
colorSchema: { colorSchema: {
@@ -23,14 +26,13 @@ const tagVariants = cva(
} }
); );
export const Tag = ({ export const Tag = ({ children, className, colorSchema = "gray", size = "sm", onClose }: Props) => (
children, <div className={twMerge(tagVariants({ colorSchema, className, size }))}>
className,
colorSchema = "gray",
size = "sm" }: Props) => (
<div
className={twMerge(tagVariants({ colorSchema, className, size }))}
>
{children} {children}
{onClose && (
<button type="button" onClick={onClose} className="ml-2 flex items-center justify-center">
<FontAwesomeIcon icon={faClose} />
</button>
)}
</div> </div>
); );
+1
View File
@@ -2,6 +2,7 @@ export * from "./Accordion";
export * from "./Button"; export * from "./Button";
export * from "./Card"; export * from "./Card";
export * from "./Checkbox"; export * from "./Checkbox";
export * from "./ContentLoader";
export * from "./DatePicker"; export * from "./DatePicker";
export * from "./DeleteActionModal"; export * from "./DeleteActionModal";
export * from "./Drawer"; export * from "./Drawer";
+87 -89
View File
@@ -3,77 +3,75 @@ import crypto from "crypto";
import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto"; import { encryptAssymmetric } from "@app/components/utilities/cryptography/crypto";
import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets"; import encryptSecrets from "@app/components/utilities/secrets/encryptSecrets";
import { uploadWsKey } from "@app/hooks/api/keys/queries"; import { uploadWsKey } from "@app/hooks/api/keys/queries";
import { createSecret } from "@app/hooks/api/secrets/queries"; import { createSecret } from "@app/hooks/api/secrets/mutations";
import { fetchUserDetails } from "@app/hooks/api/users/queries"; import { fetchUserDetails } from "@app/hooks/api/users/queries";
import { createWorkspace } from "@app/hooks/api/workspace/queries"; import { createWorkspace } from "@app/hooks/api/workspace/queries";
const secretsToBeAdded = [ const secretsToBeAdded = [
{ {
pos: 0, pos: 0,
key: "DATABASE_URL", key: "DATABASE_URL",
// eslint-disable-next-line no-template-curly-in-string // eslint-disable-next-line no-template-curly-in-string
value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net", value: "mongodb+srv://${DB_USERNAME}:${DB_PASSWORD}@mongodb.net",
valueOverride: undefined, valueOverride: undefined,
comment: "Secret referencing example", comment: "Secret referencing example",
id: "", id: "",
tags: [] tags: []
}, },
{ {
pos: 1, pos: 1,
key: "DB_USERNAME", key: "DB_USERNAME",
value: "OVERRIDE_THIS", value: "OVERRIDE_THIS",
valueOverride: undefined, valueOverride: undefined,
comment: comment: "Override secrets with personal value",
"Override secrets with personal value", id: "",
id: "", tags: []
tags: [] },
}, {
{ pos: 2,
pos: 2, key: "DB_PASSWORD",
key: "DB_PASSWORD", value: "OVERRIDE_THIS",
value: "OVERRIDE_THIS", valueOverride: undefined,
valueOverride: undefined, comment: "Another secret override",
comment: id: "",
"Another secret override", tags: []
id: "", },
tags: [] {
}, pos: 3,
{ key: "DB_USERNAME",
pos: 3, value: "user1234",
key: "DB_USERNAME", valueOverride: "user1234",
value: "user1234", comment: "",
valueOverride: "user1234", id: "",
comment: "", tags: []
id: "", },
tags: [] {
}, pos: 4,
{ key: "DB_PASSWORD",
pos: 4, value: "example_password",
key: "DB_PASSWORD", valueOverride: "example_password",
value: "example_password", comment: "",
valueOverride: "example_password", id: "",
comment: "", tags: []
id: "", },
tags: [] {
}, pos: 5,
{ key: "TWILIO_AUTH_TOKEN",
pos: 5, value: "example_twillio_token",
key: "TWILIO_AUTH_TOKEN", valueOverride: undefined,
value: "example_twillio_token", comment: "",
valueOverride: undefined, id: "",
comment: "", tags: []
id: "", },
tags: [] {
}, pos: 6,
{ key: "WEBSITE_URL",
pos: 6, value: "http://localhost:3000",
key: "WEBSITE_URL", valueOverride: undefined,
value: "http://localhost:3000", comment: "",
valueOverride: undefined, id: "",
comment: "", tags: []
id: "", }
tags: []
}
]; ];
/** /**
@@ -85,30 +83,32 @@ const secretsToBeAdded = [
* @returns {Project} project - new project * @returns {Project} project - new project
*/ */
const initProjectHelper = async ({ const initProjectHelper = async ({
organizationId, organizationId,
projectName projectName
}: { }: {
organizationId: string; organizationId: string;
projectName: string; projectName: string;
}) => { }) => {
// create new project // create new project
const { data: { workspace } } = await createWorkspace({ const {
workspaceName: projectName, data: { workspace }
organizationId } = await createWorkspace({
workspaceName: projectName,
organizationId
}); });
// create and upload new (encrypted) project key // create and upload new (encrypted) project key
const randomBytes = crypto.randomBytes(16).toString("hex"); const randomBytes = crypto.randomBytes(16).toString("hex");
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY"); const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY");
if (!PRIVATE_KEY) throw new Error("Failed to find private key"); if (!PRIVATE_KEY) throw new Error("Failed to find private key");
const user = await fetchUserDetails(); const user = await fetchUserDetails();
const { ciphertext, nonce } = encryptAssymmetric({ const { ciphertext, nonce } = encryptAssymmetric({
plaintext: randomBytes, plaintext: randomBytes,
publicKey: user.publicKey, publicKey: user.publicKey,
privateKey: PRIVATE_KEY privateKey: PRIVATE_KEY
}); });
await uploadWsKey({ await uploadWsKey({
@@ -120,11 +120,11 @@ const initProjectHelper = async ({
// encrypt and upload secrets to new project // encrypt and upload secrets to new project
const secrets = await encryptSecrets({ const secrets = await encryptSecrets({
secretsToEncrypt: secretsToBeAdded, secretsToEncrypt: secretsToBeAdded,
workspaceId: workspace._id, workspaceId: workspace._id,
env: "dev" env: "dev"
}); });
secrets?.forEach((secret) => { secrets?.forEach((secret) => {
createSecret({ createSecret({
workspaceId: workspace._id, workspaceId: workspace._id,
@@ -146,10 +146,8 @@ const initProjectHelper = async ({
} }
}); });
}); });
return workspace;
}
export { return workspace;
initProjectHelper };
}
export { initProjectHelper };
+1
View File
@@ -7,6 +7,7 @@ export * from "./integrations";
export * from "./keys"; export * from "./keys";
export * from "./organization"; export * from "./organization";
export * from "./roles"; export * from "./roles";
export * from "./secretApproval";
export * from "./secretFolders"; export * from "./secretFolders";
export * from "./secretImports"; export * from "./secretImports";
export * from "./secrets"; export * from "./secrets";
@@ -0,0 +1,6 @@
export {
useCreateSecretApprovalPolicy,
useDeleteSecretApprovalPolicy,
useUpdateSecretApprovalPolicy
} from "./mutation";
export { useGetSecretApprovalPolicies } from "./queries";
@@ -0,0 +1,58 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { secretApprovalKeys } from "./queries";
import { TCreateSecretPolicyDTO, TDeleteSecretPolicyDTO, TUpdateSecretPolicyDTO } from "./types";
export const useCreateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretPolicyDTO>({
mutationFn: async ({ environment, workspaceId, approvals, approvers, secretPath }) => {
const { data } = await apiRequest.post("/api/v1/secret-approvals", {
environment,
workspaceId,
approvals,
approvers,
secretPath
});
return data;
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId));
}
});
};
export const useUpdateSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretPolicyDTO>({
mutationFn: async ({ id, approvers, approvals, secretPath }) => {
const { data } = await apiRequest.patch(`/api/v1/secret-approvals/${id}`, {
approvals,
approvers,
secretPath
});
return data;
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId));
}
});
};
export const useDeleteSecretApprovalPolicy = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretPolicyDTO>({
mutationFn: async ({ id }) => {
const { data } = await apiRequest.delete(`/api/v1/secret-approvals/${id}`);
return data;
},
onSuccess: (_, { workspaceId }) => {
queryClient.invalidateQueries(secretApprovalKeys.getApprovalPolicies(workspaceId));
}
});
};
@@ -0,0 +1,36 @@
import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { apiRequest } from "@app/config/request";
import { TSecretApprovalPolicy } from "./types";
export const secretApprovalKeys = {
getApprovalPolicies: (workspaceId: string) =>
[{ workspaceId }, "secret-approval-policies"] as const
};
const fetchApprovalPolicies = async (workspaceId: string) => {
const { data } = await apiRequest.get<{ approvals: TSecretApprovalPolicy[] }>(
"/api/v1/secret-approvals",
{ params: { workspaceId } }
);
return data.approvals;
};
export const useGetSecretApprovalPolicies = ({
workspaceId,
options = {}
}: { workspaceId: string } & {
options?: UseQueryOptions<
TSecretApprovalPolicy[],
unknown,
TSecretApprovalPolicy[],
ReturnType<typeof secretApprovalKeys.getApprovalPolicies>
>;
}) =>
useQuery({
queryKey: secretApprovalKeys.getApprovalPolicies(workspaceId),
queryFn: () => fetchApprovalPolicies(workspaceId),
...options,
enabled: Boolean(workspaceId) && (options?.enabled ?? true)
});
@@ -0,0 +1,31 @@
export type TSecretApprovalPolicy = {
_id: string;
workspace: string;
environment: string;
secretPath?: string;
approvers: string[];
approvals: number;
};
export type TCreateSecretPolicyDTO = {
workspaceId: string;
environment: string;
secretPath?: string | null;
approvers?: string[];
approvals?: number;
};
export type TUpdateSecretPolicyDTO = {
id: string;
approvers?: string[];
secretPath?: string | null;
approvals?: number;
// for invalidating list
workspaceId: string;
};
export type TDeleteSecretPolicyDTO = {
id: string;
// for invalidating list
workspaceId: string;
};
@@ -3,6 +3,5 @@ export {
useDeleteFolder, useDeleteFolder,
useGetFoldersByEnv, useGetFoldersByEnv,
useGetProjectFolders, useGetProjectFolders,
useGetProjectFoldersBatch,
useUpdateFolder useUpdateFolder
} from "./queries"; } from "./queries";
@@ -1,86 +1,80 @@
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import {
useMutation,
useQueries,
useQuery,
useQueryClient,
UseQueryOptions
} from "@tanstack/react-query";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { secretSnapshotKeys } from "../secretSnapshots/queries"; import { secretSnapshotKeys } from "../secretSnapshots/queries";
import { import {
CreateFolderDTO, TCreateFolderDTO,
DeleteFolderDTO, TDeleteFolderDTO,
GetProjectFoldersBatchDTO,
GetProjectFoldersDTO,
TGetFoldersByEnvDTO, TGetFoldersByEnvDTO,
TGetProjectFoldersDTO,
TSecretFolder, TSecretFolder,
UpdateFolderDTO TUpdateFolderDTO
} from "./types"; } from "./types";
const queryKeys = { const queryKeys = {
getSecretFolders: (workspaceId: string, environment: string, parentFolderId?: string) => getSecretFolders: ({ workspaceId, environment, directory }: TGetProjectFoldersDTO) =>
["secret-folders", { workspaceId, environment, parentFolderId }] as const ["secret-folders", { workspaceId, environment, directory }] as const
}; };
const fetchProjectFolders = async ( const fetchProjectFolders = async (workspaceId: string, environment: string, directory = "/") => {
workspaceId: string, const { data } = await apiRequest.get<{ folders: TSecretFolder[] }>("/api/v1/folders", {
environment: string, params: {
parentFolderId?: string, workspaceId,
parentFolderPath?: string environment,
) => { directory
const { data } = await apiRequest.get<{ folders: TSecretFolder[]; dir: TSecretFolder[] }>(
"/api/v1/folders",
{
params: {
workspaceId,
environment,
parentFolderId,
parentFolderPath
}
} }
); });
return data; return data.folders;
}; };
export const useGetProjectFolders = ({ export const useGetProjectFolders = ({
workspaceId, workspaceId,
parentFolderId,
environment, environment,
isPaused, directory = "/",
sortDir options = {}
}: GetProjectFoldersDTO) => }: TGetProjectFoldersDTO & {
options?: Omit<
UseQueryOptions<
TSecretFolder[],
unknown,
TSecretFolder[],
ReturnType<typeof queryKeys.getSecretFolders>
>,
"queryKey" | "queryFn"
>;
}) =>
useQuery({ useQuery({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderId), ...options,
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused, queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
queryFn: async () => fetchProjectFolders(workspaceId, environment, parentFolderId), enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
select: useCallback( queryFn: async () => fetchProjectFolders(workspaceId, environment, directory)
({ folders, dir }: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
dir,
folders: folders.sort((a, b) =>
sortDir === "asc"
? a?.name?.localeCompare(b?.name || "")
: b?.name?.localeCompare(a?.name || "")
)
}),
[sortDir]
)
}); });
export const useGetFoldersByEnv = ({ export const useGetFoldersByEnv = ({
parentFolderPath, directory = "/",
workspaceId, workspaceId,
environments, environments
parentFolderId
}: TGetFoldersByEnvDTO) => { }: TGetFoldersByEnvDTO) => {
const folders = useQueries({ const folders = useQueries({
queries: environments.map((env) => ({ queries: environments.map((environment) => ({
queryKey: queryKeys.getSecretFolders(workspaceId, env, parentFolderPath || parentFolderId), queryKey: queryKeys.getSecretFolders({ workspaceId, environment, directory }),
queryFn: async () => fetchProjectFolders(workspaceId, env, parentFolderId, parentFolderPath), queryFn: async () => fetchProjectFolders(workspaceId, environment, directory),
enabled: Boolean(workspaceId) && Boolean(env) enabled: Boolean(workspaceId) && Boolean(environment)
})) }))
}); });
const folderNames = useMemo(() => { const folderNames = useMemo(() => {
const names = new Set<string>(); const names = new Set<string>();
folders?.forEach(({ data }) => { folders?.forEach(({ data }) => {
data?.folders.forEach(({ name }) => { data?.forEach(({ name }) => {
names.add(name); names.add(name);
}); });
}); });
@@ -92,9 +86,7 @@ export const useGetFoldersByEnv = ({
const selectedEnvIndex = environments.indexOf(env); const selectedEnvIndex = environments.indexOf(env);
if (selectedEnvIndex !== -1) { if (selectedEnvIndex !== -1) {
return Boolean( return Boolean(
folders?.[selectedEnvIndex]?.data?.folders?.find( folders?.[selectedEnvIndex]?.data?.find(({ name: folderName }) => folderName === name)
({ name: folderName }) => folderName === name
)
); );
} }
return false; return false;
@@ -105,95 +97,78 @@ export const useGetFoldersByEnv = ({
return { folders, folderNames, isFolderPresentInEnv }; return { folders, folderNames, isFolderPresentInEnv };
}; };
export const useGetProjectFoldersBatch = ({
folders = [],
isPaused,
parentFolderPath
}: GetProjectFoldersBatchDTO) =>
useQueries({
queries: folders.map(({ workspaceId, environment, parentFolderId }) => ({
queryKey: queryKeys.getSecretFolders(workspaceId, environment, parentFolderPath),
queryFn: async () =>
fetchProjectFolders(workspaceId, environment, parentFolderId, parentFolderPath),
enabled: Boolean(workspaceId) && Boolean(environment) && !isPaused,
select: (data: { folders: TSecretFolder[]; dir: TSecretFolder[] }) => ({
environment,
folders: data.folders,
dir: data.dir
})
}))
});
export const useCreateFolder = () => { export const useCreateFolder = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, CreateFolderDTO>({ return useMutation<{}, {}, TCreateFolderDTO>({
mutationFn: async (dto) => { mutationFn: async (dto) => {
const { data } = await apiRequest.post("/api/v1/folders", dto); const { data } = await apiRequest.post("/api/v1/folders", dto);
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, parentFolderId }) => { onSuccess: (_, { workspaceId, environment, directory }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId) queryKeys.getSecretFolders({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count(workspaceId, environment, parentFolderId) secretSnapshotKeys.list({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.list(workspaceId, environment, parentFolderId) secretSnapshotKeys.count({ workspaceId, environment, directory })
); );
} }
}); });
}; };
export const useUpdateFolder = (parentFolderId: string) => { export const useUpdateFolder = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, UpdateFolderDTO>({ return useMutation<{}, {}, TUpdateFolderDTO>({
mutationFn: async ({ folderId, name, environment, workspaceId }) => { mutationFn: async ({ directory = "/", folderName, name, environment, workspaceId }) => {
const { data } = await apiRequest.patch(`/api/v1/folders/${folderId}`, { const { data } = await apiRequest.patch(`/api/v1/folders/${folderName}`, {
name, name,
environment, environment,
workspaceId workspaceId,
directory
}); });
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment }) => { onSuccess: (_, { workspaceId, environment, directory }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId) queryKeys.getSecretFolders({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count(workspaceId, environment, parentFolderId) secretSnapshotKeys.list({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.list(workspaceId, environment, parentFolderId) secretSnapshotKeys.count({ workspaceId, environment, directory })
); );
} }
}); });
}; };
export const useDeleteFolder = (parentFolderId: string) => { export const useDeleteFolder = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, DeleteFolderDTO>({ return useMutation<{}, {}, TDeleteFolderDTO>({
mutationFn: async ({ folderId, environment, workspaceId }) => { mutationFn: async ({ directory = "/", folderName, environment, workspaceId }) => {
const { data } = await apiRequest.delete(`/api/v1/folders/${folderId}`, { const { data } = await apiRequest.delete(`/api/v1/folders/${folderName}`, {
data: { data: {
environment, environment,
workspaceId workspaceId,
directory
} }
}); });
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment }) => { onSuccess: (_, { directory = "/", workspaceId, environment }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
queryKeys.getSecretFolders(workspaceId, environment, parentFolderId) queryKeys.getSecretFolders({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.count(workspaceId, environment, parentFolderId) secretSnapshotKeys.list({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretSnapshotKeys.list(workspaceId, environment, parentFolderId) secretSnapshotKeys.count({ workspaceId, environment, directory })
); );
} }
}); });
+11 -18
View File
@@ -3,43 +3,36 @@ export type TSecretFolder = {
name: string; name: string;
}; };
export type GetProjectFoldersDTO = { export type TGetProjectFoldersDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
parentFolderId?: string; directory?: string;
isPaused?: boolean;
sortDir?: "asc" | "desc";
};
export type GetProjectFoldersBatchDTO = {
folders: Omit<GetProjectFoldersDTO, "isPaused" | "sortDir">[];
isPaused?: boolean;
parentFolderPath?: string;
}; };
export type TGetFoldersByEnvDTO = { export type TGetFoldersByEnvDTO = {
environments: string[]; environments: string[];
workspaceId: string; workspaceId: string;
parentFolderPath?: string; directory?: string;
parentFolderId?: string;
}; };
export type CreateFolderDTO = { export type TCreateFolderDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderName: string; folderName: string;
parentFolderId?: string; directory?: string;
}; };
export type UpdateFolderDTO = { export type TUpdateFolderDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
name: string; name: string;
folderId: string; folderName: string;
directory?: string;
}; };
export type DeleteFolderDTO = { export type TDeleteFolderDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId: string; folderName: string;
directory?: string;
}; };
@@ -9,21 +9,21 @@ export const useCreateSecretImport = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretImportDTO>({ return useMutation<{}, {}, TCreateSecretImportDTO>({
mutationFn: async ({ secretImport, environment, workspaceId, folderId }) => { mutationFn: async ({ secretImport, environment, workspaceId, directory }) => {
const { data } = await apiRequest.post("/api/v1/secret-imports", { const { data } = await apiRequest.post("/api/v1/secret-imports", {
secretImport, secretImport,
environment, environment,
workspaceId, workspaceId,
folderId directory
}); });
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, folderId }) => { onSuccess: (_, { workspaceId, environment, directory }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId) secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId) secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
); );
} }
}); });
@@ -33,21 +33,21 @@ export const useUpdateSecretImport = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretImportDTO>({ return useMutation<{}, {}, TUpdateSecretImportDTO>({
mutationFn: async ({ environment, workspaceId, folderId, secretImports, id }) => { mutationFn: async ({ environment, workspaceId, directory, secretImports, id }) => {
const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, { const { data } = await apiRequest.put(`/api/v1/secret-imports/${id}`, {
secretImports, secretImports,
environment, environment,
workspaceId, workspaceId,
folderId directory
}); });
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, folderId }) => { onSuccess: (_, { workspaceId, environment, directory }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId) secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId) secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
); );
} }
}); });
@@ -66,12 +66,12 @@ export const useDeleteSecretImport = () => {
}); });
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, folderId }) => { onSuccess: (_, { workspaceId, environment, directory }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretImportKeys.getProjectSecretImports(workspaceId, environment, folderId) secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory })
); );
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId) secretImportKeys.getSecretImportSecrets({ workspaceId, environment, directory })
); );
} }
}); });
@@ -1,5 +1,5 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useQuery } from "@tanstack/react-query"; import { useQuery, UseQueryOptions } from "@tanstack/react-query";
import { import {
decryptAssymmetric, decryptAssymmetric,
@@ -7,52 +7,68 @@ import {
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { TGetImportedSecrets, TImportedSecrets, TSecretImports } from "./types"; import { TGetImportedSecrets, TGetSecretImports, TImportedSecrets, TSecretImports } from "./types";
export const secretImportKeys = { export const secretImportKeys = {
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [ getProjectSecretImports: ({ environment, workspaceId, directory }: TGetSecretImports) =>
{ workspaceId, env, folderId }, [{ workspaceId, directory, environment }, "secrets-imports"] as const,
"secrets-imports" getSecretImportSecrets: ({
], workspaceId,
getSecretImportSecrets: (workspaceId: string, env: string | string[], folderId?: string) => [ environment,
{ workspaceId, env, folderId }, directory
"secrets-import-sec" }: Omit<TGetImportedSecrets, "decryptFileKey">) =>
] [{ workspaceId, environment, directory }, "secrets-import-sec"] as const
}; };
const fetchSecretImport = async (workspaceId: string, environment: string, folderId?: string) => { const fetchSecretImport = async ({ workspaceId, environment, directory }: TGetSecretImports) => {
const { data } = await apiRequest.get<{ secretImport: TSecretImports }>( const { data } = await apiRequest.get<{ secretImport: TSecretImports }>(
"/api/v1/secret-imports", "/api/v1/secret-imports",
{ {
params: { params: {
workspaceId, workspaceId,
environment, environment,
folderId directory
} }
} }
); );
return data.secretImport; return data.secretImport;
}; };
export const useGetSecretImports = (workspaceId: string, env: string, folderId?: string) => export const useGetSecretImports = ({
workspaceId,
environment,
directory = "/",
options = {}
}: TGetSecretImports & {
options?: Omit<
UseQueryOptions<
TSecretImports,
unknown,
TSecretImports,
ReturnType<typeof secretImportKeys.getProjectSecretImports>
>,
"queryKey" | "queryFn"
>;
}) =>
useQuery({ useQuery({
enabled: Boolean(workspaceId) && Boolean(env), ...options,
queryKey: secretImportKeys.getProjectSecretImports(workspaceId, env, folderId), queryKey: secretImportKeys.getProjectSecretImports({ workspaceId, environment, directory }),
queryFn: () => fetchSecretImport(workspaceId, env, folderId) enabled: Boolean(workspaceId) && Boolean(environment) && (options?.enabled ?? true),
queryFn: () => fetchSecretImport({ workspaceId, environment, directory })
}); });
const fetchImportedSecrets = async ( const fetchImportedSecrets = async (
workspaceId: string, workspaceId: string,
environment: string, environment: string,
folderId?: string directory?: string
) => { ) => {
const { data } = await apiRequest.get<{ secrets: TImportedSecrets }>( const { data } = await apiRequest.get<{ secrets: TImportedSecrets[] }>(
"/api/v1/secret-imports/secrets", "/api/v1/secret-imports/secrets",
{ {
params: { params: {
workspaceId, workspaceId,
environment, environment,
folderId directory
} }
} }
); );
@@ -62,15 +78,34 @@ const fetchImportedSecrets = async (
export const useGetImportedSecrets = ({ export const useGetImportedSecrets = ({
workspaceId, workspaceId,
environment, environment,
folderId, decryptFileKey,
decryptFileKey directory,
}: TGetImportedSecrets) => options = {}
}: TGetImportedSecrets & {
options?: Omit<
UseQueryOptions<
TImportedSecrets[],
unknown,
TImportedSecrets[],
ReturnType<typeof secretImportKeys.getSecretImportSecrets>
>,
"queryKey" | "queryFn"
>;
}) =>
useQuery({ useQuery({
enabled: Boolean(workspaceId) && Boolean(environment) && Boolean(decryptFileKey), enabled:
queryKey: secretImportKeys.getSecretImportSecrets(workspaceId, environment, folderId), Boolean(workspaceId) &&
queryFn: () => fetchImportedSecrets(workspaceId, environment, folderId), Boolean(environment) &&
Boolean(decryptFileKey) &&
(options?.enabled ?? true),
queryKey: secretImportKeys.getSecretImportSecrets({
workspaceId,
environment,
directory
}),
queryFn: () => fetchImportedSecrets(workspaceId, environment, directory),
select: useCallback( select: useCallback(
(data: TImportedSecrets) => { (data: TImportedSecrets[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey; const latestKey = decryptFileKey;
const key = decryptAssymmetric({ const key = decryptAssymmetric({
@@ -114,7 +149,8 @@ export const useGetImportedSecrets = ({
tags: encSecret.tags, tags: encSecret.tags,
comment: secretComment, comment: secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt updatedAt: encSecret.updatedAt,
version: encSecret.version
}; };
}) })
})); }));
+12 -6
View File
@@ -1,5 +1,5 @@
import { UserWsKeyPair } from "../keys/types";
import { EncryptedSecret } from "../secrets/types"; import { EncryptedSecret } from "../secrets/types";
import { UserWsKeyPair } from "../types";
export type TSecretImports = { export type TSecretImports = {
_id: string; _id: string;
@@ -16,19 +16,25 @@ export type TImportedSecrets = {
secretPath: string; secretPath: string;
folderId: string; folderId: string;
secrets: EncryptedSecret[]; secrets: EncryptedSecret[];
}[]; };
export type TGetSecretImports = {
workspaceId: string;
environment: string;
directory?: string;
};
export type TGetImportedSecrets = { export type TGetImportedSecrets = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId?: string; directory?: string;
decryptFileKey: UserWsKeyPair; decryptFileKey: UserWsKeyPair;
}; };
export type TCreateSecretImportDTO = { export type TCreateSecretImportDTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId?: string; directory?: string;
secretImport: { secretImport: {
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -39,7 +45,7 @@ export type TUpdateSecretImportDTO = {
id: string; id: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId?: string; directory?: string;
secretImports: Array<{ secretImports: Array<{
environment: string; environment: string;
secretPath: string; secretPath: string;
@@ -50,7 +56,7 @@ export type TDeleteSecretImportDTO = {
id: string; id: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
folderId?: string; directory?: string;
secretImportPath: string; secretImportPath: string;
secretImportEnv: string; secretImportEnv: string;
}; };
@@ -1,6 +1,6 @@
export { export {
useGetSnapshotSecrets, useGetSnapshotSecrets,
useGetWorkspaceSecretSnapshots, useGetWorkspaceSnapshotList,
useGetWsSnapshotCount, useGetWsSnapshotCount,
usePerformSecretRollback usePerformSecretRollback
} from "./queries"; } from "./queries";
@@ -9,39 +9,39 @@ import { apiRequest } from "@app/config/request";
import { DecryptedSecret } from "../secrets/types"; import { DecryptedSecret } from "../secrets/types";
import { import {
GetWorkspaceSecretSnapshotsDTO, TGetSecretSnapshotsDTO,
TSecretRollbackDTO, TSecretRollbackDTO,
TSnapshotSecret, TSecretSnapshot,
TSnapshotSecretProps, TSnapshotData,
TWorkspaceSecretSnapshot TSnapshotDataProps
} from "./types"; } from "./types";
export const secretSnapshotKeys = { export const secretSnapshotKeys = {
list: (workspaceId: string, env: string, folderId?: string) => list: ({ workspaceId, environment, directory }: Omit<TGetSecretSnapshotsDTO, "limit">) =>
[{ workspaceId, env, folderId }, "secret-snapshot"] as const, [{ workspaceId, environment, directory }, "secret-snapshot"] as const,
snapshotSecrets: (snapshotId: string) => [{ snapshotId }, "secret-snapshot"] as const, snapshotData: (snapshotId: string) => [{ snapshotId }, "secret-snapshot"] as const,
count: (workspaceId: string, env: string, folderId?: string) => [ count: ({ environment, workspaceId, directory }: Omit<TGetSecretSnapshotsDTO, "limit">) => [
{ workspaceId, env, folderId }, { workspaceId, environment, directory },
"count", "count",
"secret-snapshot" "secret-snapshot"
] ]
}; };
const fetchWorkspaceSecretSnaphots = async ( const fetchWorkspaceSnaphots = async ({
workspaceId: string, workspaceId,
environment: string, environment,
folderId?: string, directory = "/",
limit = 10, limit = 10,
offset = 0 offset = 0
) => { }: TGetSecretSnapshotsDTO & { offset: number }) => {
const res = await apiRequest.get<{ secretSnapshots: TWorkspaceSecretSnapshot[] }>( const res = await apiRequest.get<{ secretSnapshots: TSecretSnapshot[] }>(
`/api/v1/workspace/${workspaceId}/secret-snapshots`, `/api/v1/workspace/${workspaceId}/secret-snapshots`,
{ {
params: { params: {
limit, limit,
offset, offset,
environment, environment,
folderId directory
} }
} }
); );
@@ -49,32 +49,25 @@ const fetchWorkspaceSecretSnaphots = async (
return res.data.secretSnapshots; return res.data.secretSnapshots;
}; };
export const useGetWorkspaceSecretSnapshots = (dto: GetWorkspaceSecretSnapshotsDTO) => export const useGetWorkspaceSnapshotList = (dto: TGetSecretSnapshotsDTO & { isPaused?: boolean }) =>
useInfiniteQuery({ useInfiniteQuery({
enabled: Boolean(dto.workspaceId && dto.environment), enabled: Boolean(dto.workspaceId && dto.environment) && !dto.isPaused,
queryKey: secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folder), queryKey: secretSnapshotKeys.list({ ...dto }),
queryFn: ({ pageParam }) => queryFn: ({ pageParam }) => fetchWorkspaceSnaphots({ ...dto, offset: pageParam }),
fetchWorkspaceSecretSnaphots(
dto.workspaceId,
dto.environment,
dto?.folder,
dto.limit,
pageParam
),
getNextPageParam: (lastPage, pages) => getNextPageParam: (lastPage, pages) =>
lastPage.length !== 0 ? pages.length * dto.limit : undefined lastPage.length !== 0 ? pages.length * dto.limit : undefined
}); });
const fetchSnapshotEncSecrets = async (snapshotId: string) => { const fetchSnapshotEncSecrets = async (snapshotId: string) => {
const res = await apiRequest.get<{ secretSnapshot: TSnapshotSecret }>( const res = await apiRequest.get<{ secretSnapshot: TSnapshotData }>(
`/api/v1/secret-snapshot/${snapshotId}` `/api/v1/secret-snapshot/${snapshotId}`
); );
return res.data.secretSnapshot; return res.data.secretSnapshot;
}; };
export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnapshotSecretProps) => export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnapshotDataProps) =>
useQuery({ useQuery({
queryKey: secretSnapshotKeys.snapshotSecrets(snapshotId), queryKey: secretSnapshotKeys.snapshotData(snapshotId),
enabled: Boolean(snapshotId && decryptFileKey), enabled: Boolean(snapshotId && decryptFileKey),
queryFn: () => fetchSnapshotEncSecrets(snapshotId), queryFn: () => fetchSnapshotEncSecrets(snapshotId),
select: (data) => { select: (data) => {
@@ -117,7 +110,8 @@ export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnap
comment: secretComment, comment: secretComment,
createdAt: encSecret.createdAt, createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt, updatedAt: encSecret.updatedAt,
type: "modified" type: "modified",
version: encSecret.version
}; };
if (encSecret.type === "personal") { if (encSecret.type === "personal") {
@@ -147,25 +141,30 @@ export const useGetSnapshotSecrets = ({ decryptFileKey, env, snapshotId }: TSnap
const fetchWorkspaceSecretSnaphotCount = async ( const fetchWorkspaceSecretSnaphotCount = async (
workspaceId: string, workspaceId: string,
environment: string, environment: string,
folderId?: string directory = "/"
) => { ) => {
const res = await apiRequest.get<{ count: number }>( const res = await apiRequest.get<{ count: number }>(
`/api/v1/workspace/${workspaceId}/secret-snapshots/count`, `/api/v1/workspace/${workspaceId}/secret-snapshots/count`,
{ {
params: { params: {
environment, environment,
folderId directory
} }
} }
); );
return res.data.count; return res.data.count;
}; };
export const useGetWsSnapshotCount = (workspaceId: string, env: string, folderId?: string) => export const useGetWsSnapshotCount = ({
workspaceId,
environment,
directory,
isPaused
}: Omit<TGetSecretSnapshotsDTO, "limit"> & { isPaused?: boolean }) =>
useQuery({ useQuery({
enabled: Boolean(workspaceId && env), enabled: Boolean(workspaceId && environment) && !isPaused,
queryKey: secretSnapshotKeys.count(workspaceId, env, folderId), queryKey: secretSnapshotKeys.count({ workspaceId, environment, directory }),
queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId, env, folderId) queryFn: () => fetchWorkspaceSecretSnaphotCount(workspaceId, environment, directory)
}); });
export const usePerformSecretRollback = () => { export const usePerformSecretRollback = () => {
@@ -179,10 +178,17 @@ export const usePerformSecretRollback = () => {
); );
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, folderId }) => { onSuccess: (_, { workspaceId, environment, directory }) => {
queryClient.invalidateQueries([{ workspaceId, environment }, "secrets"]); queryClient.invalidateQueries([
queryClient.invalidateQueries(secretSnapshotKeys.list(workspaceId, environment, folderId)); { workspaceId, environment, secretPath: directory },
queryClient.invalidateQueries(secretSnapshotKeys.count(workspaceId, environment, folderId)); "secrets"
]);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ workspaceId, environment, directory })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ workspaceId, environment, directory })
);
} }
}); });
}; };
@@ -1,7 +1,7 @@
import { UserWsKeyPair } from "../keys/types"; import { UserWsKeyPair } from "../keys/types";
import { EncryptedSecretVersion } from "../secrets/types"; import { EncryptedSecretVersion } from "../secrets/types";
export type TWorkspaceSecretSnapshot = { export type TSecretSnapshot = {
_id: string; _id: string;
workspace: string; workspace: string;
version: number; version: number;
@@ -11,27 +11,27 @@ export type TWorkspaceSecretSnapshot = {
__v: number; __v: number;
}; };
export type TSnapshotSecret = Omit<TWorkspaceSecretSnapshot, "secretVersions"> & { export type TSnapshotData = Omit<TSecretSnapshot, "secretVersions"> & {
secretVersions: EncryptedSecretVersion[]; secretVersions: EncryptedSecretVersion[];
folderVersion: Array<{ name: string; id: string }>; folderVersion: Array<{ name: string; id: string }>;
}; };
export type TSnapshotSecretProps = { export type TSnapshotDataProps = {
snapshotId: string; snapshotId: string;
env: string; env: string;
decryptFileKey: UserWsKeyPair; decryptFileKey: UserWsKeyPair;
}; };
export type GetWorkspaceSecretSnapshotsDTO = { export type TGetSecretSnapshotsDTO = {
workspaceId: string; workspaceId: string;
limit: number; limit: number;
environment: string; environment: string;
folder?: string; directory?: string;
}; };
export type TSecretRollbackDTO = { export type TSecretRollbackDTO = {
workspaceId: string; workspaceId: string;
version: number; version: number;
environment: string; environment: string;
folderId?: string; directory?: string;
}; };
+8 -6
View File
@@ -1,7 +1,9 @@
export { useCreateSecretV3, useDeleteSecretV3, useUpdateSecretV3 } from "./mutations";
export { export {
useBatchSecretsOp, useCreateSecretBatch,
useGetProjectSecrets, useCreateSecretV3,
useGetProjectSecretsAllEnv, useDeleteSecretBatch,
useGetSecretVersion useDeleteSecretV3,
} from "./queries"; useUpdateSecretBatch,
useUpdateSecretV3
} from "./mutations";
export { useGetProjectSecrets, useGetProjectSecretsAllEnv, useGetSecretVersion } from "./queries";
+211 -23
View File
@@ -1,6 +1,6 @@
import crypto from "crypto"; import crypto from "crypto";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { MutationOptions, useMutation, useQueryClient } from "@tanstack/react-query";
import { import {
decryptAssymmetric, decryptAssymmetric,
@@ -8,8 +8,17 @@ import {
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { secretSnapshotKeys } from "../secretSnapshots/queries";
import { secretKeys } from "./queries"; import { secretKeys } from "./queries";
import { TCreateSecretsV3DTO, TDeleteSecretsV3DTO, TUpdateSecretsV3DTO } from "./types"; import {
CreateSecretDTO,
TCreateSecretBatchDTO,
TCreateSecretsV3DTO,
TDeleteSecretBatchDTO,
TDeleteSecretsV3DTO,
TUpdateSecretBatchDTO,
TUpdateSecretsV3DTO
} from "./types";
const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => { const encryptSecret = (randomBytes: string, key: string, value?: string, comment?: string) => {
// encrypt key // encrypt key
@@ -55,7 +64,11 @@ const encryptSecret = (randomBytes: string, key: string, value?: string, comment
}; };
}; };
export const useCreateSecretV3 = () => { export const useCreateSecretV3 = ({
options
}: {
options?: Omit<MutationOptions<{}, {}, TCreateSecretsV3DTO>, "mutationFn">;
} = {}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretsV3DTO>({ return useMutation<{}, {}, TCreateSecretsV3DTO>({
mutationFn: async ({ mutationFn: async ({
@@ -66,7 +79,8 @@ export const useCreateSecretV3 = () => {
secretName, secretName,
secretValue, secretValue,
latestFileKey, latestFileKey,
secretComment secretComment,
skipMultilineEncoding
}) => { }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
@@ -84,20 +98,32 @@ export const useCreateSecretV3 = () => {
environment, environment,
type, type,
secretPath, secretPath,
...encryptSecret(randomBytes, secretName, secretValue, secretComment) ...encryptSecret(randomBytes, secretName, secretValue, secretComment),
skipMultilineEncoding
}; };
const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody); const { data } = await apiRequest.post(`/api/v3/secrets/${secretName}`, reqBody);
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, secretPath }) => { onSuccess: (_, { workspaceId, environment, secretPath }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretKeys.getProjectSecret(workspaceId, environment, secretPath) secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
); );
} queryClient.invalidateQueries(
secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
);
},
...options
}); });
}; };
export const useUpdateSecretV3 = () => { export const useUpdateSecretV3 = ({
options
}: {
options?: Omit<MutationOptions<{}, {}, TUpdateSecretsV3DTO>, "mutationFn">;
} = {}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretsV3DTO>({ return useMutation<{}, {}, TUpdateSecretsV3DTO>({
mutationFn: async ({ mutationFn: async ({
@@ -107,7 +133,11 @@ export const useUpdateSecretV3 = () => {
workspaceId, workspaceId,
secretName, secretName,
secretValue, secretValue,
latestFileKey latestFileKey,
tags,
secretComment,
newSecretName,
skipMultilineEncoding
}) => { }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
@@ -119,34 +149,40 @@ export const useUpdateSecretV3 = () => {
privateKey: PRIVATE_KEY privateKey: PRIVATE_KEY
}) })
: crypto.randomBytes(16).toString("hex"); : crypto.randomBytes(16).toString("hex");
const { secretValueIV, secretValueTag, secretValueCiphertext } = encryptSecret(
randomBytes,
secretName,
secretValue,
""
);
const reqBody = { const reqBody = {
workspaceId, workspaceId,
environment, environment,
type, type,
secretPath, secretPath,
secretValueIV, ...encryptSecret(randomBytes, newSecretName ?? secretName, secretValue, secretComment),
secretValueTag, tags,
secretValueCiphertext skipMultilineEncoding,
secretName: newSecretName
}; };
const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody); const { data } = await apiRequest.patch(`/api/v3/secrets/${secretName}`, reqBody);
return data; return data;
}, },
onSuccess: (_, { workspaceId, environment, secretPath }) => { onSuccess: (_, { workspaceId, environment, secretPath }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretKeys.getProjectSecret(workspaceId, environment, secretPath) secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
); );
} queryClient.invalidateQueries(
secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
);
},
...options
}); });
}; };
export const useDeleteSecretV3 = () => { export const useDeleteSecretV3 = ({
options
}: {
options?: Omit<MutationOptions<{}, {}, TDeleteSecretsV3DTO>, "mutationFn">;
} = {}) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretsV3DTO>({ return useMutation<{}, {}, TDeleteSecretsV3DTO>({
@@ -165,8 +201,160 @@ export const useDeleteSecretV3 = () => {
}, },
onSuccess: (_, { workspaceId, environment, secretPath }) => { onSuccess: (_, { workspaceId, environment, secretPath }) => {
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretKeys.getProjectSecret(workspaceId, environment, secretPath) secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
); );
} queryClient.invalidateQueries(
secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
);
},
...options
}); });
}; };
export const useCreateSecretBatch = ({
options
}: {
options?: Omit<MutationOptions<{}, {}, TCreateSecretBatchDTO>, "mutationFn">;
} = {}) => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TCreateSecretBatchDTO>({
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets, latestFileKey }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
workspaceId,
environment,
secretPath,
secrets: secrets.map(
({ secretName, secretValue, secretComment, metadata, type, skipMultilineEncoding }) => ({
secretName,
...encryptSecret(randomBytes, secretName, secretValue, secretComment),
type,
metadata,
skipMultilineEncoding
})
)
};
const { data } = await apiRequest.post("/api/v3/secrets/batch", reqBody);
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
);
},
...options
});
};
export const useUpdateSecretBatch = ({
options
}: {
options?: Omit<MutationOptions<{}, {}, TUpdateSecretBatchDTO>, "mutationFn">;
} = {}) => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TUpdateSecretBatchDTO>({
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets, latestFileKey }) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
const reqBody = {
workspaceId,
environment,
secretPath,
secrets: secrets.map(
({ secretName, secretValue, secretComment, type, tags, skipMultilineEncoding }) => ({
secretName,
...encryptSecret(randomBytes, secretName, secretValue, secretComment),
type,
tags,
skipMultilineEncoding
})
)
};
const { data } = await apiRequest.patch("/api/v3/secrets/batch", reqBody);
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
);
},
...options
});
};
export const useDeleteSecretBatch = ({
options
}: {
options?: Omit<MutationOptions<{}, {}, TDeleteSecretBatchDTO>, "mutationFn">;
} = {}) => {
const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretBatchDTO>({
mutationFn: async ({ secretPath = "/", workspaceId, environment, secrets }) => {
const reqBody = {
workspaceId,
environment,
secretPath,
secrets
};
const { data } = await apiRequest.delete("/api/v3/secrets/batch", {
data: reqBody
});
return data;
},
onSuccess: (_, { workspaceId, environment, secretPath }) => {
queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.list({ environment, workspaceId, directory: secretPath })
);
queryClient.invalidateQueries(
secretSnapshotKeys.count({ environment, workspaceId, directory: secretPath })
);
},
...options
});
};
export const createSecret = async (dto: CreateSecretDTO) => {
const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto);
return data;
};
+111 -224
View File
@@ -1,6 +1,6 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useMutation, useQueries, useQuery, useQueryClient } from "@tanstack/react-query"; import { useQueries, useQuery, UseQueryOptions } from "@tanstack/react-query";
import { import {
decryptAssymmetric, decryptAssymmetric,
@@ -8,218 +8,148 @@ import {
} from "@app/components/utilities/cryptography/crypto"; } from "@app/components/utilities/cryptography/crypto";
import { apiRequest } from "@app/config/request"; import { apiRequest } from "@app/config/request";
import { secretSnapshotKeys } from "../secretSnapshots/queries"; import { UserWsKeyPair } from "../keys/types";
import { import {
BatchSecretDTO,
CreateSecretDTO,
DecryptedSecret, DecryptedSecret,
EncryptedSecret, EncryptedSecret,
EncryptedSecretVersion, EncryptedSecretVersion,
GetProjectSecretsDTO,
GetSecretVersionsDTO, GetSecretVersionsDTO,
TGetProjectSecretsAllEnvDTO} from "./types"; TGetProjectSecretsAllEnvDTO,
TGetProjectSecretsDTO,
TGetProjectSecretsKey
} from "./types";
export const secretKeys = { export const secretKeys = {
// this is also used in secretSnapshot part // this is also used in secretSnapshot part
getProjectSecret: (workspaceId: string, env: string | string[], folderId?: string) => [ getProjectSecret: ({ workspaceId, environment, secretPath }: TGetProjectSecretsKey) =>
{ workspaceId, env, folderId }, [{ workspaceId, environment, secretPath }, "secrets"] as const,
"secrets" getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"] as const
],
getProjectSecretImports: (workspaceId: string, env: string | string[], folderId?: string) => [
{ workspaceId, env, folderId },
"secrets-imports"
],
getSecretVersion: (secretId: string) => [{ secretId }, "secret-versions"]
}; };
const fetchProjectEncryptedSecrets = async ( const decryptSecrets = (encryptedSecrets: EncryptedSecret[], decryptFileKey: UserWsKeyPair) => {
workspaceId: string, const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
env: string | string[], const key = decryptAssymmetric({
folderId?: string, ciphertext: decryptFileKey.encryptedKey,
secretPath?: string nonce: decryptFileKey.nonce,
) => { publicKey: decryptFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const personalSecrets: Record<string, { id: string; value: string }> = {};
const secrets: DecryptedSecret[] = [];
encryptedSecrets.forEach((encSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret: DecryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt,
version: encSecret.version,
skipMultilineEncoding: encSecret.skipMultilineEncoding
};
if (encSecret.type === "personal") {
personalSecrets[decryptedSecret.key] = {
id: encSecret._id,
value: secretValue
};
} else {
secrets.push(decryptedSecret);
}
});
secrets.forEach((sec) => {
if (personalSecrets?.[sec.key]) {
sec.idOverride = personalSecrets[sec.key].id;
sec.valueOverride = personalSecrets[sec.key].value;
sec.overrideAction = "modified";
}
});
return secrets;
};
const fetchProjectEncryptedSecrets = async ({
workspaceId,
environment,
secretPath
}: TGetProjectSecretsKey) => {
const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v3/secrets", { const { data } = await apiRequest.get<{ secrets: EncryptedSecret[] }>("/api/v3/secrets", {
params: { params: {
environment: env, environment,
workspaceId, workspaceId,
folderId: folderId || undefined,
secretPath secretPath
} }
}); });
return data.secrets; return data.secrets;
}; };
export const useGetProjectSecrets = ({ export const useGetProjectSecrets = ({
workspaceId, workspaceId,
env, environment,
decryptFileKey, decryptFileKey,
isPaused, secretPath,
folderId, options
secretPath }: TGetProjectSecretsDTO & {
}: GetProjectSecretsDTO) => options?: Omit<
UseQueryOptions<
EncryptedSecret[],
unknown,
DecryptedSecret[],
ReturnType<typeof secretKeys.getProjectSecret>
>,
"queryKey" | "queryFn"
>;
}) =>
useQuery({ useQuery({
...options,
// wait for all values to be available // wait for all values to be available
enabled: Boolean(decryptFileKey && workspaceId && env) && !isPaused, enabled: Boolean(decryptFileKey && workspaceId && environment) && (options?.enabled ?? true),
queryKey: secretKeys.getProjectSecret(workspaceId, env, folderId || secretPath), queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), queryFn: async () => fetchProjectEncryptedSecrets({ workspaceId, environment, secretPath }),
select: useCallback( select: (secrets: EncryptedSecret[]) => decryptSecrets(secrets, decryptFileKey)
(data: EncryptedSecret[]) => {
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const latestKey = decryptFileKey;
const key = decryptAssymmetric({
ciphertext: latestKey.encryptedKey,
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const sharedSecrets: DecryptedSecret[] = [];
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
if (encSecret.type === "personal") {
personalSecrets[`${decryptedSecret.key}-${decryptedSecret.env}`] = {
id: encSecret._id,
value: secretValue
};
} else {
if (!duplicateSecretKey?.[`${decryptedSecret.key}-${decryptedSecret.env}`]) {
sharedSecrets.push(decryptedSecret);
}
duplicateSecretKey[`${decryptedSecret.key}-${decryptedSecret.env}`] = true;
}
});
sharedSecrets.forEach((val) => {
const dupKey = `${val.key}-${val.env}`;
if (personalSecrets?.[dupKey]) {
val.idOverride = personalSecrets[dupKey].id;
val.valueOverride = personalSecrets[dupKey].value;
val.overrideAction = "modified";
}
});
return { secrets: sharedSecrets };
},
[decryptFileKey]
)
}); });
export const useGetProjectSecretsAllEnv = ({ export const useGetProjectSecretsAllEnv = ({
workspaceId, workspaceId,
envs, envs,
decryptFileKey, decryptFileKey,
folderId,
secretPath secretPath
}: TGetProjectSecretsAllEnvDTO) => { }: TGetProjectSecretsAllEnvDTO) => {
const secrets = useQueries({ const secrets = useQueries({
queries: envs.map((env) => ({ queries: envs.map((environment) => ({
queryKey: secretKeys.getProjectSecret(workspaceId, env, secretPath || folderId), queryKey: secretKeys.getProjectSecret({ workspaceId, environment, secretPath }),
enabled: Boolean(decryptFileKey && workspaceId && env), enabled: Boolean(decryptFileKey && workspaceId && environment),
queryFn: () => fetchProjectEncryptedSecrets(workspaceId, env, folderId, secretPath), queryFn: async () => fetchProjectEncryptedSecrets({ workspaceId, environment, secretPath }),
select: (data: EncryptedSecret[]) => { select: (secs: EncryptedSecret[]) =>
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string; decryptSecrets(secs, decryptFileKey).reduce<Record<string, DecryptedSecret>>(
const latestKey = decryptFileKey; (prev, curr) => ({ ...prev, [curr.key]: curr }),
const key = decryptAssymmetric({ {}
ciphertext: latestKey.encryptedKey, )
nonce: latestKey.nonce,
publicKey: latestKey.sender.publicKey,
privateKey: PRIVATE_KEY
});
const sharedSecrets: Record<string, DecryptedSecret> = {};
const personalSecrets: Record<string, { id: string; value: string }> = {};
// this used for add-only mode in dashboard
// type won't be there thus only one key is shown
const duplicateSecretKey: Record<string, boolean> = {};
data.forEach((encSecret: EncryptedSecret) => {
const secretKey = decryptSymmetric({
ciphertext: encSecret.secretKeyCiphertext,
iv: encSecret.secretKeyIV,
tag: encSecret.secretKeyTag,
key
});
const secretValue = decryptSymmetric({
ciphertext: encSecret.secretValueCiphertext,
iv: encSecret.secretValueIV,
tag: encSecret.secretValueTag,
key
});
const secretComment = decryptSymmetric({
ciphertext: encSecret.secretCommentCiphertext,
iv: encSecret.secretCommentIV,
tag: encSecret.secretCommentTag,
key
});
const decryptedSecret = {
_id: encSecret._id,
env: encSecret.environment,
key: secretKey,
value: secretValue,
tags: encSecret.tags,
comment: secretComment,
createdAt: encSecret.createdAt,
updatedAt: encSecret.updatedAt
};
if (encSecret.type === "personal") {
personalSecrets[decryptedSecret.key] = {
id: encSecret._id,
value: secretValue
};
} else {
if (!duplicateSecretKey?.[decryptedSecret.key]) {
sharedSecrets[decryptedSecret.key] = decryptedSecret;
}
duplicateSecretKey[decryptedSecret.key] = true;
}
});
Object.keys(sharedSecrets).forEach((val) => {
if (personalSecrets?.[val]) {
sharedSecrets[val].idOverride = personalSecrets[val].id;
sharedSecrets[val].valueOverride = personalSecrets[val].value;
sharedSecrets[val].overrideAction = "modified";
}
});
return sharedSecrets;
}
})) }))
}); });
@@ -303,46 +233,3 @@ export const useGetSecretVersion = (dto: GetSecretVersionsDTO) =>
[dto.decryptFileKey] [dto.decryptFileKey]
) )
}); });
export const useBatchSecretsOp = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, BatchSecretDTO>({
mutationFn: async (dto) => {
const { data } = await apiRequest.post("/api/v2/secrets/batch", dto);
return data;
},
onSuccess: (_, dto) => {
queryClient.invalidateQueries(
secretKeys.getProjectSecret(dto.workspaceId, dto.environment, dto.folderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.list(dto.workspaceId, dto.environment, dto?.folderId)
);
queryClient.invalidateQueries(
secretSnapshotKeys.count(dto.workspaceId, dto.environment, dto?.folderId)
);
}
});
};
export const createSecret = async (dto: CreateSecretDTO) => {
const { data } = await apiRequest.post(`/api/v3/secrets/${dto.secretKey}`, dto);
return data;
}
export const useCreateSecret = () => {
const queryClient = useQueryClient();
return useMutation<{}, {}, CreateSecretDTO>({
mutationFn: async (dto) => {
const data = createSecret(dto);
return data;
},
onSuccess: (_, dto) => {
queryClient.invalidateQueries(
secretKeys.getProjectSecret(dto.workspaceId, dto.environment)
);
}
});
};
+57 -43
View File
@@ -16,6 +16,7 @@ export type EncryptedSecret = {
__v: number; __v: number;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
skipMultilineEncoding?: boolean;
secretCommentCiphertext: string; secretCommentCiphertext: string;
secretCommentIV: string; secretCommentIV: string;
secretCommentTag: string; secretCommentTag: string;
@@ -24,6 +25,7 @@ export type EncryptedSecret = {
export type DecryptedSecret = { export type DecryptedSecret = {
_id: string; _id: string;
version: number;
key: string; key: string;
value: string; value: string;
comment: string; comment: string;
@@ -35,6 +37,7 @@ export type DecryptedSecret = {
idOverride?: string; idOverride?: string;
overrideAction?: string; overrideAction?: string;
folderId?: string; folderId?: string;
skipMultilineEncoding?: boolean;
}; };
export type EncryptedSecretVersion = { export type EncryptedSecretVersion = {
@@ -53,55 +56,21 @@ export type EncryptedSecretVersion = {
secretValueTag: string; secretValueTag: string;
tags: WsTag[]; tags: WsTag[];
__v: number; __v: number;
skipMultilineEncoding?: boolean;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
// dto // dto
type SecretTagArg = { _id: string; name: string; slug: string }; export type TGetProjectSecretsKey = {
export type UpdateSecretArg = {
_id: string;
folderId?: string;
type: "shared" | "personal";
secretName: string;
secretKeyCiphertext: string;
secretKeyIV: string;
secretKeyTag: string;
secretValueCiphertext: string;
secretValueIV: string;
secretValueTag: string;
secretCommentCiphertext: string;
secretCommentIV: string;
secretCommentTag: string;
tags: SecretTagArg[];
};
export type CreateSecretArg = Omit<UpdateSecretArg, "_id">;
export type DeleteSecretArg = { _id: string, secretName: string; };
export type BatchSecretDTO = {
workspaceId: string; workspaceId: string;
folderId: string;
environment: string; environment: string;
requests: Array< secretPath?: string;
| { method: "POST"; secret: CreateSecretArg }
| { method: "PATCH"; secret: UpdateSecretArg }
| { method: "DELETE"; secret: DeleteSecretArg }
>;
}; };
export type GetProjectSecretsDTO = { export type TGetProjectSecretsDTO = {
workspaceId: string;
env: string | string[];
decryptFileKey: UserWsKeyPair; decryptFileKey: UserWsKeyPair;
folderId?: string; } & TGetProjectSecretsKey;
secretPath?: string;
isPaused?: boolean;
include_imports?: boolean;
onSuccess?: (data: DecryptedSecret[]) => void;
};
export type TGetProjectSecretsAllEnvDTO = { export type TGetProjectSecretsAllEnvDTO = {
workspaceId: string; workspaceId: string;
@@ -124,6 +93,7 @@ export type TCreateSecretsV3DTO = {
secretName: string; secretName: string;
secretValue: string; secretValue: string;
secretComment: string; secretComment: string;
skipMultilineEncoding?: boolean;
secretPath: string; secretPath: string;
workspaceId: string; workspaceId: string;
environment: string; environment: string;
@@ -136,19 +106,63 @@ export type TUpdateSecretsV3DTO = {
environment: string; environment: string;
type: string; type: string;
secretPath: string; secretPath: string;
skipMultilineEncoding?: boolean;
newSecretName?: string;
secretName: string; secretName: string;
secretValue: string; secretValue: string;
secretComment?: string;
tags?: string[];
}; };
export type TDeleteSecretsV3DTO = { export type TDeleteSecretsV3DTO = {
workspaceId: string; workspaceId: string;
environment: string; environment: string;
type: string; type: "shared" | "personal";
secretPath: string; secretPath: string;
secretName: string; secretName: string;
}; };
// --- v3 export type TCreateSecretBatchDTO = {
workspaceId: string;
environment: string;
secretPath: string;
latestFileKey: UserWsKeyPair;
secrets: Array<{
secretName: string;
secretValue: string;
secretComment: string;
skipMultilineEncoding?: boolean;
type: "shared" | "personal";
metadata?: {
source?: string;
};
}>;
};
export type TUpdateSecretBatchDTO = {
workspaceId: string;
environment: string;
secretPath: string;
latestFileKey: UserWsKeyPair;
secrets: Array<{
type: "shared" | "personal";
secretName: string;
skipMultilineEncoding?: boolean;
secretValue: string;
secretComment: string;
tags?: string[];
}>;
};
export type TDeleteSecretBatchDTO = {
workspaceId: string;
environment: string;
secretPath: string;
secrets: Array<{
secretName: string;
type: "shared" | "personal";
}>;
};
export type CreateSecretDTO = { export type CreateSecretDTO = {
workspaceId: string; workspaceId: string;
@@ -167,5 +181,5 @@ export type CreateSecretDTO = {
secretPath: string; secretPath: string;
metadata?: { metadata?: {
source?: string; source?: string;
} };
} };
+4
View File
@@ -4,6 +4,10 @@ export type { IntegrationAuth } from "./integrationAuth/types";
export type { TCloudIntegration, TIntegration } from "./integrations/types"; export type { TCloudIntegration, TIntegration } from "./integrations/types";
export type { UserWsKeyPair } from "./keys/types"; export type { UserWsKeyPair } from "./keys/types";
export type { Organization } from "./organization/types"; export type { Organization } from "./organization/types";
export type { TSecretApprovalPolicy } from "./secretApproval/types";
export type { TSecretFolder } from "./secretFolders/types";
export type { TImportedSecrets, TSecretImports } from "./secretImports/types";
export * from "./secrets/types";
export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types"; export type { CreateServiceTokenDTO, ServiceToken } from "./serviceTokens/types";
export type { SubscriptionPlan } from "./subscriptions/types"; export type { SubscriptionPlan } from "./subscriptions/types";
export type { WsTag } from "./tags/types"; export type { WsTag } from "./tags/types";
@@ -475,6 +475,20 @@ export const AppLayout = ({ children }: LayoutProps) => {
</MenuItem> </MenuItem>
</a> </a>
</Link> </Link>
{process.env.NEXT_PUBLIC_SECRET_APPROVAL === "true" && (
<Link href={`/project/${currentWorkspace?._id}/approval`} passHref>
<a>
<MenuItem
isSelected={
router.asPath === `/project/${currentWorkspace?._id}/allowlist`
}
icon="system-outline-126-verified"
>
Admin Panel
</MenuItem>
</a>
</Link>
)}
<Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref> <Link href={`/project/${currentWorkspace?._id}/allowlist`} passHref>
<a> <a>
<MenuItem <MenuItem
@@ -0,0 +1,27 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { SecretApprovalPage } from "@app/views/SecretApprovalPage";
const SecretApproval = () => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("approval.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={String(t("approval.og-title"))} />
<meta name="og:description" content={String(t("approval.og-description"))} />
</Head>
<div className="h-full">
<SecretApprovalPage />
</div>
</>
);
};
export default SecretApproval;
SecretApproval.requireAuth = true;
@@ -1,7 +1,7 @@
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import Head from "next/head"; import Head from "next/head";
import { DashboardPage } from "@app/views/DashboardPage"; import { SecretMainPage } from "@app/views/SecretMainPage";
const Dashboard = () => { const Dashboard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
@@ -16,7 +16,7 @@ const Dashboard = () => {
<meta name="og:description" content={String(t("dashboard.og-description"))} /> <meta name="og:description" content={String(t("dashboard.og-description"))} />
</Head> </Head>
<div className="h-full"> <div className="h-full">
<DashboardPage /> <SecretMainPage />
</div> </div>
</> </>
); );
@@ -0,0 +1,27 @@
import { useTranslation } from "react-i18next";
import Head from "next/head";
import { SecretMainPage } from "@app/views/SecretMainPage";
const Dashboard = () => {
const { t } = useTranslation();
return (
<>
<Head>
<title>{t("common.head-title", { title: t("dashboard.title") })}</title>
<link rel="icon" href="/infisical.ico" />
<meta property="og:image" content="/images/message.png" />
<meta property="og:title" content={String(t("dashboard.og-title"))} />
<meta name="og:description" content={String(t("dashboard.og-description"))} />
</Head>
<div className="h-full">
<SecretMainPage />
</div>
</>
);
};
export default Dashboard;
Dashboard.requireAuth = true;
File diff suppressed because it is too large Load Diff
@@ -1,289 +0,0 @@
/* eslint-disable @typescript-eslint/naming-convention */
import crypto from "crypto";
import * as yup from "yup";
import {
decryptAssymmetric,
encryptSymmetric
} from "@app/components/utilities/cryptography/crypto";
import { BatchSecretDTO, DecryptedSecret } from "@app/hooks/api/secrets/types";
export enum SecretActionType {
Created = "created",
Modified = "modified",
Deleted = "deleted"
}
export const DEFAULT_SECRET_VALUE = {
_id: undefined,
overrideAction: undefined,
idOverride: undefined,
valueOverride: undefined,
comment: "",
key: "",
value: "",
tags: []
};
const secretSchema = yup.object({
_id: yup.string(),
key: yup
.string()
.trim()
.required()
.label("Secret key")
.test("starts-with-number", "Should start with an alphabet", (val) =>
Boolean(val?.charAt(0)?.match(/[a-zA-Z]/i))
)
.test({
name: "duplicate-keys",
// TODO:(akhilmhdh) ts keeps throwing from not found need to see how to resolve this
test: (val, ctx: any) => {
const secrets: Array<{ key: string }> = ctx?.from?.[1]?.value?.secrets || [];
const duplicateKeys: Record<number, boolean> = {};
secrets?.forEach(({ key }, index) => {
if (key === val) duplicateKeys[index + 1] = true;
});
const pos = Object.keys(duplicateKeys);
if (pos.length <= 1) {
return true;
}
return ctx.createError({ message: `Same key in row ${pos.join(", ")}` });
}
}),
value: yup.string().trim(),
comment: yup.string().trim(),
tags: yup.array(
yup.object({
_id: yup.string().required(),
name: yup.string().required(),
slug: yup.string().required(),
tagColor: yup.string().nullable(),
})
),
overrideAction: yup.string().notRequired().oneOf(Object.values(SecretActionType)),
idOverride: yup.string().notRequired(),
valueOverride: yup.string().trim().notRequired()
});
export const schema = yup.object({
isSnapshotMode: yup.bool().notRequired(),
secrets: yup.array(secretSchema)
});
export type FormData = yup.InferType<typeof schema>;
export type TSecretDetailsOpen = { index: number; id: string };
export type TSecOverwriteOpt = { secrets: Record<string, { comments: string[]; value: string }> };
// to convert multi line into single line ones by quoting them and changing to string \n
const formatMultiValueEnv = (val?: string) => {
if (!val) return "";
if (!val.match("\n")) return val;
return `"${val.replace(/\n/g, "\\n")}"`;
};
export const downloadSecret = (
secrets: FormData["secrets"] = [],
importedSecrets: { key: string; value?: string; comment?: string }[] = [],
env: string = "unknown"
) => {
const importSecPos: Record<string, number> = {};
importedSecrets.forEach((el, index) => {
importSecPos[el.key] = index;
});
const finalSecret = [...importedSecrets];
secrets.forEach(({ key, value, valueOverride, overrideAction, comment }) => {
const finalVal =
overrideAction && overrideAction !== SecretActionType.Deleted ? valueOverride : value;
const newValue = {
key,
value: formatMultiValueEnv(finalVal),
comment
};
// can also be zero thus failing
if (typeof importSecPos?.[key] === "undefined") {
finalSecret.push(newValue);
} else {
finalSecret[importSecPos[key]] = newValue;
}
});
let file = "";
finalSecret.forEach(({ key, value, comment }) => {
if (comment) {
file += `# ${comment}\n${key}=${value}\n`;
return;
}
file += `${key}=${value}\n`;
});
const blob = new Blob([file]);
const fileDownloadUrl = URL.createObjectURL(blob);
const alink = document.createElement("a");
alink.href = fileDownloadUrl;
alink.download = `${env}.env`;
alink.click();
};
/*
* Below functions are used convert the dashboard secrets to the bulk secret creation request format
* They are encrypted back
* Formatted to [ { request: "", secret:{} } ]
*/
const encryptASecret = (randomBytes: string, key: string, value?: string, comment?: string) => {
// encrypt key
const {
ciphertext: secretKeyCiphertext,
iv: secretKeyIV,
tag: secretKeyTag
} = encryptSymmetric({
plaintext: key,
key: randomBytes
});
// encrypt value
const {
ciphertext: secretValueCiphertext,
iv: secretValueIV,
tag: secretValueTag
} = encryptSymmetric({
plaintext: value ?? "",
key: randomBytes
});
// encrypt comment
const {
ciphertext: secretCommentCiphertext,
iv: secretCommentIV,
tag: secretCommentTag
} = encryptSymmetric({
plaintext: comment ?? "",
key: randomBytes
});
return {
secretKeyCiphertext,
secretKeyIV,
secretKeyTag,
secretValueCiphertext,
secretValueIV,
secretValueTag,
secretCommentCiphertext,
secretCommentIV,
secretCommentTag
};
};
const deepCompareSecrets = (lhs: DecryptedSecret, rhs: any) =>
lhs.key === rhs.key &&
lhs.value === rhs.value &&
lhs.comment === rhs.comment &&
lhs?.valueOverride === rhs?.valueOverride &&
JSON.stringify(lhs.tags) === JSON.stringify(rhs.tags);
export const transformSecretsToBatchSecretReq = (
deletedSecretIds: { id: string; secretName: string; }[],
latestFileKey: any,
secrets: FormData["secrets"],
intialValues: DecryptedSecret[] = []
) => {
// deleted secrets
const secretsToBeDeleted: BatchSecretDTO["requests"] = deletedSecretIds.map(({ id, secretName }) => ({
method: "DELETE",
secret: {
_id: id,
secretName
}
}));
const secretsToBeUpdated: BatchSecretDTO["requests"] = [];
const secretsToBeCreated: BatchSecretDTO["requests"] = [];
const PRIVATE_KEY = localStorage.getItem("PRIVATE_KEY") as string;
const randomBytes = latestFileKey
? decryptAssymmetric({
ciphertext: latestFileKey.encryptedKey,
nonce: latestFileKey.nonce,
publicKey: latestFileKey.sender.publicKey,
privateKey: PRIVATE_KEY
})
: crypto.randomBytes(16).toString("hex");
secrets?.forEach((secret) => {
const {
_id,
idOverride,
value,
valueOverride,
overrideAction,
tags = [],
comment,
key
} = secret;
if (!idOverride && overrideAction === SecretActionType.Created) {
secretsToBeCreated.push({
method: "POST",
secret: {
type: "personal",
tags,
secretName: key,
...encryptASecret(randomBytes, key, valueOverride, comment)
}
});
}
// to be created ones as they don't have server generated id
if (!_id) {
secretsToBeCreated.push({
method: "POST",
secret: {
type: "shared",
tags,
secretName: key,
...encryptASecret(randomBytes, key, value, comment)
}
});
return; // exit as updated and delete case won't happen when created
}
// has an id means this is updated one
if (_id) {
// check value has changed or not
const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!;
if (!deepCompareSecrets(initialSecretValue, secret)) {
secretsToBeUpdated.push({
method: "PATCH",
secret: {
_id,
type: "shared",
tags,
secretName: key,
...encryptASecret(randomBytes, key, value, comment)
}
});
}
}
if (idOverride) {
// if action is deleted meaning override has been removed but id is kept to collect at this point
if (overrideAction === SecretActionType.Deleted) {
secretsToBeDeleted.push({ method: "DELETE", secret: { _id: idOverride, secretName: key } });
} else {
// if not deleted action then as id is there its an updated
const initialSecretValue = intialValues?.find(({ _id: secId }) => secId === _id)!;
if (!deepCompareSecrets(initialSecretValue, secret)) {
secretsToBeUpdated.push({
method: "PATCH",
secret: {
_id: idOverride,
type: "personal",
tags,
secretName: key,
...encryptASecret(randomBytes, key, valueOverride, comment)
}
});
}
}
}
});
return secretsToBeCreated.concat(secretsToBeUpdated, secretsToBeDeleted);
};
@@ -1,64 +0,0 @@
import { useCallback } from "react";
import { FormControl, Input, Spinner } from "@app/components/v2";
import { useGetProjectSecrets, useGetUserWsKey } from "@app/hooks/api";
type SecretValueProps = {
workspaceId: string;
envName: string;
env: string;
secretKey: string;
};
const SecretValue = ({ workspaceId, env, envName, secretKey }: SecretValueProps) => {
const { data: latestFileKey } = useGetUserWsKey(workspaceId);
const { data: secret, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env,
decryptFileKey: latestFileKey!
});
const getValue = useCallback(
(data: typeof secret) => {
const sec = data?.secrets?.find(({ key: secKey }) => secKey === secretKey);
return sec?.value || "Not found";
},
[secretKey]
);
return (
<FormControl label={envName}>
<Input
className={`w-full text-ellipsis font-mono focus:ring-transparent ${getValue(secret) === "Not found" && "text-mineshaft-500"}`}
value={getValue(secret)}
isReadOnly
rightIcon={isSecretsLoading ? <Spinner /> : undefined}
/>
</FormControl>
);
};
type Props = {
workspaceId: string;
secretKey: string;
envs: Array<{ name: string; slug: string }>;
};
export const CompareSecret = ({ workspaceId, secretKey, envs }: Props): JSX.Element => {
// should not do anything until secretKey is available
if (!secretKey) return <div />;
return (
<div className="flex flex-col">
{envs.map(({ name, slug }) => (
<SecretValue
workspaceId={workspaceId}
key={`secret-comparison-${slug}`}
envName={name}
env={slug}
secretKey={secretKey}
/>
))}
</div>
);
};
@@ -1 +0,0 @@
export { CompareSecret } from "./CompareSecret";
@@ -1,191 +0,0 @@
import { useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import {
faCheck
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Button, FormControl, Input, ModalClose, Tooltip } from "@app/components/v2";
import { isValidHexColor } from "../../../../components/utilities/isValidHexColor";
import { secretTagsColors } from "../../../../const"
import { TagColor } from "../../../../hooks/api/tags/types";
type Props = {
onCreateTag: (tagName: string, tagColor: string) => Promise<void>;
};
const createTagSchema = yup.object({
name: yup.string().required().trim().label("Tag Name")
});
type FormData = yup.InferType<typeof createTagSchema>;
export const CreateTagModal = ({ onCreateTag }: Props): JSX.Element => {
const {
control,
reset,
formState: { isSubmitting },
handleSubmit
} = useForm<FormData>({
resolver: yupResolver(createTagSchema)
});
const [tagsColors] = useState<TagColor[]>(secretTagsColors)
const [selectedTagColor, setSelectedTagColor] = useState<TagColor>(tagsColors[0])
const [showHexInput, setShowHexInput] = useState<boolean>(false)
const [tagColor, setTagColor] = useState<string>("")
const onFormSubmit = async ({ name }: FormData) => {
await onCreateTag(name, tagColor);
reset();
};
useEffect(() => {
const clonedTagColors = [...tagsColors]
const selectedTagBgColor = clonedTagColors.find($tagColor => $tagColor.selected);
if (selectedTagBgColor) {
setSelectedTagColor(selectedTagBgColor);
setTagColor(selectedTagBgColor.hex);
}
}, [])
useEffect(() => {
const tagsList = document.querySelector(".secret-tags-wrapper")
const tagsHexWrapper = document.querySelector(".tags-hex-wrapper")
if (showHexInput) {
tagsList?.classList.add("hide-tags")
tagsList?.classList.remove("show-tags")
tagsHexWrapper?.classList.add("show-hex-input")
tagsHexWrapper?.classList.remove("hide-hex-input")
} else {
tagsList?.classList.remove("hide-tags")
tagsList?.classList.add("show-tags")
tagsHexWrapper?.classList.remove("show-hex-input")
tagsHexWrapper?.classList.add("hide-hex-input")
}
}, [showHexInput])
const handleColorChange = (clickedTagColor: TagColor) => {
const updatedTagColors = [...tagsColors];
const clickedTagColorIndex = updatedTagColors.findIndex(($tagColor) => $tagColor.id === clickedTagColor.id);
const updatedClickedTagColor = updatedTagColors[clickedTagColorIndex];
updatedTagColors.forEach((tgColor) => {
// eslint-disable-next-line no-param-reassign
tgColor.selected = false;
});
if (selectedTagColor.id !== clickedTagColor.id) {
updatedClickedTagColor.selected = !updatedClickedTagColor.selected;
setSelectedTagColor(updatedClickedTagColor);
setTagColor(updatedClickedTagColor.hex);
}
};
return (
<form onSubmit={handleSubmit(onFormSubmit)}>
<Controller
control={control}
name="name"
defaultValue=""
render={({ field, fieldState: { error } }) => (
<FormControl label="Tag Name" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} placeholder="Type your tag name" />
</FormControl>
)}
/>
<div className="mt-2">
<div className="mb-0.5 ml-1 block text-sm font-normal text-mineshaft-400">Tag Color</div>
<div className="flex gap-2 h-[50px]">
<div className="w-[12%] h-[2.813rem] inline-flex font-inter items-center justify-center border relative rounded-md border-mineshaft-500 bg-mineshaft-900 hover:bg-mineshaft-800">
<div className="w-[26px] h-[26px] rounded-full" style={{ background: `${tagColor}` }} />
</div>
<div className="w-[88%] h-[2.813rem] flex-wrap inline-flex gap-3 items-center border rounded-md border-mineshaft-500 bg-mineshaft-900 hover:bg-mineshaft-800 relative">
<div className="flex-wrap inline-flex gap-3 items-center secret-tags-wrapper pl-3">
{
tagsColors.map(($tagColor: TagColor) => {
return (
<div key={`tag-color-${$tagColor.id}`}>
<Tooltip content={`${$tagColor.name}`}>
<div className=" flex items-center justify-center w-[26px] h-[26px] hover:ring-offset-2 hover:ring-2 bg-[#bec2c8] border-2 p-2 hover:shadow-lg border-transparent hover:border-black rounded-full"
key={`tag-${$tagColor.id}`}
style={{ backgroundColor: `${$tagColor.hex}` }}
onClick={() => handleColorChange($tagColor)}
tabIndex={0} role="button"
onKeyDown={() => { }}
>
{
$tagColor.selected && <FontAwesomeIcon icon={faCheck} style={{ color: "#00000070" }} />
}
</div>
</Tooltip>
</div>
)
})
}
</div>
<div className="flex items-center gap-2 px-2 tags-hex-wrapper" >
<div className="w-1/6 flex items-center relative rounded-md hover:bg-mineshaft-800">
{
isValidHexColor(tagColor) && (
<div className="w-[26px] h-[26px] rounded-full flex items-center justify-center" style={{ background: `${tagColor}` }}>
<FontAwesomeIcon icon={faCheck} style={{ color: "#00000070" }} />
</div>
)
}
{
!isValidHexColor(tagColor) && (
<div className="border-dashed border bg-blue rounded-full w-[26px] h-[26px] border-mineshaft-500" />
)
}
</div>
<div className="w-10/12">
<Input
variant="plain"
className="w-full focus:text-bunker-100 focus:ring-transparent bg-transparent"
autoCapitalization={false}
value={tagColor}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setTagColor(e.target.value)}
/>
</div>
</div>
<div className="w-[26px] h-[26px] flex items-center justify-center absolute top-[10px] right-[-4px] translate-x-[-50%]">
<div className="border-mineshaft-500 border h-[2.1rem] mr-4 absolute right-5" />
<div className={`flex items-center justify-center w-[26px] h-[26px] bg-transparent cursor-pointer hover:ring-offset-1 hover:ring-2 border-mineshaft-500 border bg-mineshaft-900 rounded-[3px] p-2 ${showHexInput ? "tags-conic-bg rounded-full" : ""}`} onClick={() => setShowHexInput((prev) => !prev)} style={{ border: "1px solid rgba(220, 216, 254, 0.376)" }}
tabIndex={0} role="button"
onKeyDown={() => { }}>
{
!showHexInput && <span>#</span>
}
</div>
</div>
</div>
</div>
</div>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
@@ -1 +0,0 @@
export {CreateTagModal} from "./CreateTagModal"
@@ -1,108 +0,0 @@
import { memo } from "react";
import { subject } from "@casl/ability";
import { faEdit, faFolder, faXmark } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import { IconButton, Tooltip } from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
type Props = {
folders?: Array<{ id: string; name: string }>;
search?: string;
environment: string;
secretPath: string;
onFolderUpdate: (folderId: string, name: string) => void;
onFolderDelete: (folderId: string, name: string) => void;
onFolderOpen: (folderId: string) => void;
};
export const FolderSection = memo(
({
onFolderUpdate: handleFolderUpdate,
onFolderDelete: handleFolderDelete,
onFolderOpen: handleFolderOpen,
search = "",
folders = [],
environment,
secretPath
}: Props) => {
return (
<>
{folders
.filter(({ name }) => name.toLowerCase().includes(search.toLowerCase()))
.map(({ id, name }) => (
<tr
key={id}
className="group flex cursor-default flex-row items-center hover:bg-mineshaft-700"
>
<td className="ml-0.5 flex h-10 w-10 items-center justify-center border-none px-4">
<FontAwesomeIcon icon={faFolder} className="text-yellow-700" />
</td>
<td
colSpan={2}
className="relative flex w-full min-w-[220px] items-center justify-between overflow-hidden text-ellipsis lg:min-w-[240px] xl:min-w-[280px]"
style={{ paddingTop: "0", paddingBottom: "0" }}
>
<div
className="flex-grow cursor-default p-2"
onKeyDown={() => null}
tabIndex={0}
role="button"
onClick={() => handleFolderOpen(id)}
>
{name}
</div>
<div className="duration-0 flex h-10 w-16 items-center justify-end space-x-2.5 overflow-hidden border-l border-mineshaft-600 transition-all">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Settings" className="capitalize">
<IconButton
size="md"
colorSchema="primary"
variant="plain"
isDisabled={!isAllowed}
onClick={() => handleFolderUpdate(id, name)}
ariaLabel="expand"
>
<FontAwesomeIcon icon={faEdit} />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<div className="opacity-0 group-hover:opacity-100">
<Tooltip content="Delete" className="capitalize">
<IconButton
size="md"
variant="plain"
colorSchema="danger"
ariaLabel="delete"
isDisabled={!isAllowed}
onClick={() => handleFolderDelete(id, name)}
>
<FontAwesomeIcon icon={faXmark} size="lg" />
</IconButton>
</Tooltip>
</div>
)}
</ProjectPermissionCan>
</div>
</td>
</tr>
))}
</>
);
}
);
FolderSection.displayName = "FolderSection";
@@ -1,3 +0,0 @@
export { FolderForm } from "./FolderForm";
export { FolderSection } from "./FolderSection";
export * from "./types";
@@ -1,2 +0,0 @@
export type TEditFolderForm = { id: string; name: string };
export type TDeleteFolderForm = { id: string; name: string };
@@ -1,237 +0,0 @@
import { useFormContext, useWatch } from "react-hook-form";
import { subject } from "@casl/ability";
import { faCircle, faCircleDot, faShuffle } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { ProjectPermissionCan } from "@app/components/permissions";
import {
Button,
Drawer,
DrawerContent,
FormControl,
Input,
Popover,
PopoverContent,
PopoverTrigger,
Switch,
TextArea
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useToggle } from "@app/hooks";
import { FormData, SecretActionType } from "../../DashboardPage.utils";
import { GenRandomNumber } from "./GenRandomNumber";
type Props = {
isDrawerOpen: boolean;
environment: string;
secretPath: string;
onOpenChange: (isOpen: boolean) => void;
index: number;
isReadOnly?: boolean;
onEnvCompare: (secretKey: string) => void;
secretVersion?: Array<{ id: string; createdAt: string; value: string }>;
// to record the ids of deleted ones
onSecretDelete: (index: number, secretName: string, id?: string, overrideId?: string) => void;
onSave: () => void;
};
export const SecretDetailDrawer = ({
isDrawerOpen,
onOpenChange,
index,
secretVersion = [],
isReadOnly,
onSecretDelete,
onSave,
onEnvCompare,
environment,
secretPath
}: Props): JSX.Element => {
const [canRevealSecVal, setCanRevealSecVal] = useToggle();
const [canRevealSecOverride, setCanRevealSecOverride] = useToggle();
const { register, setValue, control, getValues } = useFormContext<FormData>();
const overrideAction = useWatch({ control, name: `secrets.${index}.overrideAction` });
const isOverridden =
overrideAction === SecretActionType.Created || overrideAction === SecretActionType.Modified;
const onSecretOverride = () => {
const secret = getValues(`secrets.${index}`);
if (isOverridden) {
// when user created a new override but then removes
if (SecretActionType.Created) {
setValue(`secrets.${index}.valueOverride`, "", { shouldDirty: true });
}
setValue(`secrets.${index}.overrideAction`, SecretActionType.Deleted, { shouldDirty: true });
} else {
setValue(
`secrets.${index}.overrideAction`,
secret?.idOverride ? SecretActionType.Modified : SecretActionType.Created,
{ shouldDirty: true }
);
}
};
return (
<Drawer onOpenChange={onOpenChange} isOpen={isDrawerOpen}>
<DrawerContent
className="dark border-l border-mineshaft-500 bg-bunker"
title="Secret"
footerContent={
<div className="flex flex-col space-y-2 pt-4 shadow-md">
<div>
<Button
variant="star"
onClick={() => onEnvCompare(getValues(`secrets.${index}.key`))}
isFullWidth
isDisabled={isReadOnly}
>
Compare secret across environments
</Button>
</div>
<div className="flex w-full space-x-2">
<ProjectPermissionCan
I={ProjectPermissionActions.Edit}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button isFullWidth onClick={onSave} isDisabled={isReadOnly || !isAllowed}>
Save Changes
</Button>
)}
</ProjectPermissionCan>
<ProjectPermissionCan
I={ProjectPermissionActions.Delete}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button
colorSchema="danger"
isDisabled={isReadOnly || !isAllowed}
onClick={() => {
const secret = getValues(`secrets.${index}`);
onSecretDelete(index, secret.key, secret._id, secret.idOverride);
}}
>
Delete
</Button>
)}
</ProjectPermissionCan>
</div>
</div>
}
>
<div className="dark:[color-scheme:dark]">
<FormControl label="Key">
<Input isDisabled {...register(`secrets.${index}.key`)} />
</FormControl>
<FormControl label="Value">
<Popover>
<Input
isReadOnly={isOverridden || isReadOnly}
{...register(`secrets.${index}.value`)}
placeholder="EMPTY"
onBlur={setCanRevealSecVal.off}
onFocus={setCanRevealSecVal.on}
type={canRevealSecVal ? "text" : "password"}
rightIcon={
<PopoverTrigger disabled={isOverridden || isReadOnly}>
<FontAwesomeIcon icon={faShuffle} />
</PopoverTrigger>
}
/>
<PopoverContent
hideCloseBtn
className="w-auto border-mineshaft-500 bg-bunker p-0"
align="end"
>
<GenRandomNumber
onGenerate={(val) =>
setValue(`secrets.${index}.value`, val, { shouldDirty: true })
}
/>
</PopoverContent>
</Popover>
</FormControl>
<div className="mb-2 border-t border-mineshaft-600 pt-4">
<Switch
id="personal-override"
onCheckedChange={onSecretOverride}
isChecked={isOverridden}
isDisabled={isReadOnly}
>
Override with a personal value
</Switch>
</div>
<FormControl>
<Popover>
<Input
isReadOnly={!isOverridden || isReadOnly}
{...register(`secrets.${index}.valueOverride`)}
placeholder="EMPTY"
type={canRevealSecOverride ? "text" : "password"}
onBlur={setCanRevealSecOverride.off}
onFocus={setCanRevealSecOverride.on}
rightIcon={
<PopoverTrigger disabled={!isOverridden || isReadOnly}>
<FontAwesomeIcon icon={faShuffle} />
</PopoverTrigger>
}
/>
<PopoverContent
hideCloseBtn
className="w-auto border-mineshaft-500 bg-bunker p-0"
align="end"
>
<GenRandomNumber
onGenerate={(val) =>
setValue(`secrets.${index}.valueOverride`, val, { shouldDirty: true })
}
/>
</PopoverContent>
</Popover>
</FormControl>
<div className="dark mb-4 text-sm text-bunker-300">
<div className="mb-2">Version History</div>
<div className="flex h-48 flex-col space-y-2 overflow-y-auto overflow-x-hidden rounded-md border border-mineshaft-600 bg-bunker-800 p-2 dark:[color-scheme:dark]">
{secretVersion?.map(({ createdAt, value, id }, i) => (
<div key={id} className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<div>
<FontAwesomeIcon icon={i === 0 ? faCircleDot : faCircle} size="sm" />
</div>
<div>
{new Date(createdAt).toLocaleDateString("en-US", {
year: "numeric",
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit"
})}
</div>
</div>
<div className="ml-1.5 flex items-center space-x-2 border-l border-bunker-300 pl-4">
<div className="self-start rounded-sm bg-primary-500/30 px-1">Value:</div>
<div className="break-all font-mono">{value}</div>
</div>
</div>
))}
</div>
</div>
<FormControl label="Comments & Notes">
<TextArea
className="border border-mineshaft-600 text-sm"
isDisabled={isReadOnly}
{...register(`secrets.${index}.comment`)}
rows={5}
/>
</FormControl>
</div>
</DrawerContent>
</Drawer>
);
};
@@ -1 +0,0 @@
export {SecretDetailDrawer} from "./SecretDetailDrawer"
@@ -1,451 +0,0 @@
import { ChangeEvent, DragEvent, useEffect, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { subject } from "@casl/ability";
import { faSquareCheck } from "@fortawesome/free-regular-svg-icons";
import {
faClone,
faKey,
faSearch,
faSquareXmark,
faUpload
} from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { yupResolver } from "@hookform/resolvers/yup";
import { twMerge } from "tailwind-merge";
import * as yup from "yup";
import GlobPatternExamples from "@app/components/basic/popups/GlobPatternExamples";
import { useNotificationContext } from "@app/components/context/Notifications/NotificationProvider";
import { ProjectPermissionCan } from "@app/components/permissions";
// TODO:(akhilmhdh) convert all the util functions like this into a lib folder grouped by functionality
import { parseDotEnv } from "@app/components/utilities/parseDotEnv";
import {
Button,
Checkbox,
EmptyState,
FormControl,
IconButton,
Input,
Modal,
ModalContent,
ModalTrigger,
Select,
SelectItem,
Skeleton,
Tooltip
} from "@app/components/v2";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context";
import { useDebounce, usePopUp, useToggle } from "@app/hooks";
import { useGetProjectSecrets } from "@app/hooks/api";
import { UserWsKeyPair } from "@app/hooks/api/types";
const formSchema = yup.object({
environment: yup.string().required().label("Environment").trim(),
secretPath: yup
.string()
.required()
.label("Secret Path")
.trim()
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
),
secrets: yup.lazy((val) => {
const valSchema: Record<string, yup.StringSchema> = {};
Object.keys(val).forEach((key) => {
valSchema[key] = yup.string().trim();
});
return yup.object(valSchema);
})
});
type TFormSchema = yup.InferType<typeof formSchema>;
const parseJson = (src: ArrayBuffer) => {
const file = src.toString();
const formatedData: Record<string, string> = JSON.parse(file);
const env: Record<string, { value: string; comments: string[] }> = {};
Object.keys(formatedData).forEach((key) => {
if (typeof formatedData[key] === "string") {
env[key] = { value: formatedData[key], comments: [] };
}
});
return env;
};
type Props = {
isSmaller: boolean;
onParsedEnv: (env: Record<string, { value: string; comments: string[] }>) => void;
onAddNewSecret?: () => void;
environments?: { name: string; slug: string }[];
workspaceId: string;
decryptFileKey: UserWsKeyPair;
environment: string;
secretPath: string;
};
export const SecretDropzone = ({
isSmaller,
onParsedEnv,
onAddNewSecret,
environments = [],
workspaceId,
decryptFileKey,
environment,
secretPath
}: Props): JSX.Element => {
const { t } = useTranslation();
const [isDragActive, setDragActive] = useToggle();
const [isLoading, setIsLoading] = useToggle();
const { createNotification } = useNotificationContext();
const { popUp, handlePopUpClose, handlePopUpToggle } = usePopUp(["importSecEnv"] as const);
const [searchFilter, setSearchFilter] = useState("");
const [shouldIncludeValues, setShouldIncludeValues] = useState(true);
const {
handleSubmit,
control,
watch,
register,
reset,
setValue,
formState: { isDirty }
} = useForm<TFormSchema>({
resolver: yupResolver(formSchema),
defaultValues: { secretPath: "/", environment: environments?.[0]?.slug }
});
const envCopySecPath = watch("secretPath");
const selectedEnvSlug = watch("environment");
const debouncedEnvCopySecretPath = useDebounce(envCopySecPath);
const { data: secrets, isLoading: isSecretsLoading } = useGetProjectSecrets({
workspaceId,
env: selectedEnvSlug,
secretPath: debouncedEnvCopySecretPath,
isPaused:
!(Boolean(workspaceId) && Boolean(selectedEnvSlug) && Boolean(debouncedEnvCopySecretPath)) &&
!popUp.importSecEnv.isOpen,
decryptFileKey
});
useEffect(() => {
setValue("secrets", {});
setSearchFilter("");
}, [debouncedEnvCopySecretPath]);
const handleDrag = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (e.type === "dragenter" || e.type === "dragover") {
setDragActive.on();
} else if (e.type === "dragleave") {
setDragActive.off();
}
};
const parseFile = (file?: File, isJson?: boolean) => {
const reader = new FileReader();
if (!file) {
createNotification({
text: "You can't inject files from VS Code. Click 'Reveal in finder', and drag your file directly from the directory where it's located.",
type: "error",
timeoutMs: 10000
});
return;
}
// const fileType = file.name.split('.')[1];
setIsLoading.on();
reader.onload = (event) => {
if (!event?.target?.result) return;
// parse function's argument looks like to be ArrayBuffer
const env = isJson
? parseJson(event.target.result as ArrayBuffer)
: parseDotEnv(event.target.result as ArrayBuffer);
setIsLoading.off();
onParsedEnv(env);
};
// If something is wrong show an error
try {
reader.readAsText(file);
} catch (error) {
console.log(error);
}
};
const handleDrop = (e: DragEvent) => {
e.preventDefault();
e.stopPropagation();
if (!e.dataTransfer) {
return;
}
e.dataTransfer.dropEffect = "copy";
setDragActive.off();
parseFile(e.dataTransfer.files[0]);
};
const handleFileUpload = (e: ChangeEvent<HTMLInputElement>) => {
e.preventDefault();
parseFile(e.target?.files?.[0], e.target?.files?.[0]?.type === "application/json");
};
const handleFormSubmit = (data: TFormSchema) => {
const secretsToBePulled: Record<string, { value: string; comments: string[] }> = {};
Object.keys(data.secrets || {}).forEach((key) => {
if (data.secrets[key]) {
secretsToBePulled[key] = {
value: (shouldIncludeValues && data.secrets[key]) || "",
comments: [""]
};
}
});
onParsedEnv(secretsToBePulled);
handlePopUpClose("importSecEnv");
reset();
};
const handleSecSelectAll = () => {
if (secrets?.secrets) {
setValue(
"secrets",
secrets?.secrets?.reduce((prev, curr) => ({ ...prev, [curr.key]: curr.value }), {}),
{ shouldDirty: true }
);
}
};
return (
<div
onDragEnter={handleDrag}
onDragLeave={handleDrag}
onDragOver={handleDrag}
onDrop={handleDrop}
className={twMerge(
"relative mx-0.5 mb-4 mt-4 flex cursor-pointer items-center justify-center rounded-md bg-mineshaft-900 py-4 px-2 text-sm text-mineshaft-200 opacity-60 outline-dashed outline-2 outline-chicago-600 duration-200 hover:opacity-100",
isDragActive && "opacity-100",
!isSmaller && "w-full max-w-3xl flex-col space-y-4 py-20",
isLoading && "bg-bunker-800"
)}
>
{isLoading ? (
<div className="mb-16 flex items-center justify-center pt-16">
<img src="/images/loading/loading.gif" height={70} width={120} alt="loading animation" />
</div>
) : (
<form onSubmit={handleSubmit(handleFormSubmit)}>
<div className="justify-cente flex flex-col items-center space-y-2">
<div>
<FontAwesomeIcon icon={faUpload} size={isSmaller ? "2x" : "5x"} />
</div>
<div>
<p className="">{t(isSmaller ? "common.drop-zone-keys" : "common.drop-zone")}</p>
</div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<input
id="fileSelect"
disabled={!isAllowed}
type="file"
className="absolute h-full w-full cursor-pointer opacity-0"
accept=".txt,.env,.yml,.yaml,.json"
onChange={handleFileUpload}
/>
)}
</ProjectPermissionCan>
<div
className={twMerge(
"flex w-full flex-row items-center justify-center py-4",
isSmaller && "py-1"
)}
>
<div className="w-1/5 border-t border-mineshaft-700" />
<p className="mx-4 text-xs text-mineshaft-400">OR</p>
<div className="w-1/5 border-t border-mineshaft-700" />
</div>
<div className="flex items-center justify-center space-x-8">
<Modal
isOpen={popUp.importSecEnv.isOpen}
onOpenChange={(isOpen) => {
handlePopUpToggle("importSecEnv", isOpen);
reset();
setSearchFilter("");
}}
>
<ModalTrigger asChild>
<div>
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button
isDisabled={!isAllowed}
variant="star"
size={isSmaller ? "xs" : "sm"}
>
Copy Secrets From An Environment
</Button>
)}
</ProjectPermissionCan>
</div>
</ModalTrigger>
<ModalContent
className="max-w-2xl"
title="Copy Secret From An Environment"
subTitle="Copy/paste secrets from other environments into this context"
>
<form>
<div className="flex items-center space-x-2">
<Controller
control={control}
name="environment"
render={({ field: { value, onChange } }) => (
<FormControl label="Environment" isRequired className="w-1/3">
<Select
value={value}
onValueChange={(val) => onChange(val)}
className="w-full border border-mineshaft-500"
defaultValue={environments?.[0]?.slug}
position="popper"
>
{environments.map((sourceEnvironment) => (
<SelectItem
value={sourceEnvironment.slug}
key={`source-environment-${sourceEnvironment.slug}`}
>
{sourceEnvironment.name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<FormControl
label="Secret Path"
className="flex-grow"
isRequired
icon={<GlobPatternExamples />}
>
<Input
{...register("secretPath")}
placeholder="Provide a path, default is /"
/>
</FormControl>
</div>
<div className="border-t border-mineshaft-600 pt-4">
<div className="mb-4 flex items-center justify-between">
<div>Secrets</div>
<div className="flex w-1/2 items-center space-x-2">
<Input
placeholder="Search for secret"
value={searchFilter}
size="xs"
leftIcon={<FontAwesomeIcon icon={faSearch} />}
onChange={(evt) => setSearchFilter(evt.target.value)}
/>
<Tooltip content="Select All">
<IconButton
ariaLabel="Select all"
variant="outline_bg"
size="xs"
onClick={handleSecSelectAll}
>
<FontAwesomeIcon icon={faSquareCheck} size="lg" />
</IconButton>
</Tooltip>
<Tooltip content="Unselect All">
<IconButton
ariaLabel="UnSelect all"
variant="outline_bg"
size="xs"
onClick={() => reset()}
>
<FontAwesomeIcon icon={faSquareXmark} size="lg" />
</IconButton>
</Tooltip>
</div>
</div>
{!isSecretsLoading && !secrets?.secrets?.length && (
<EmptyState title="No secrets found" icon={faKey} />
)}
<div className="thin-scrollbar grid max-h-64 grid-cols-2 gap-4 overflow-auto ">
{isSecretsLoading &&
Array.apply(0, Array(2)).map((_x, i) => (
<Skeleton
key={`secret-pull-loading-${i + 1}`}
className="bg-mineshaft-700"
/>
))}
{secrets?.secrets
?.filter(({ key }) =>
key.toLowerCase().includes(searchFilter.toLowerCase())
)
?.map(({ _id, key, value: secVal }) => (
<Controller
key={`pull-secret--${_id}`}
control={control}
name={`secrets.${key}`}
render={({ field: { value, onChange } }) => (
<Checkbox
id={`pull-secret-${_id}`}
isChecked={Boolean(value)}
onCheckedChange={(isChecked) => onChange(isChecked ? secVal : "")}
>
{key}
</Checkbox>
)}
/>
))}
</div>
<div className="mt-6 mb-4">
<Checkbox
id="populate-include-value"
isChecked={shouldIncludeValues}
onCheckedChange={(isChecked) =>
setShouldIncludeValues(isChecked as boolean)
}
>
Include secret values
</Checkbox>
</div>
<div className="flex items-center space-x-2">
<Button
leftIcon={<FontAwesomeIcon icon={faClone} />}
type="submit"
isDisabled={!isDirty}
>
Paste Secrets
</Button>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</div>
</div>
</form>
</ModalContent>
</Modal>
{!isSmaller && (
<ProjectPermissionCan
I={ProjectPermissionActions.Create}
a={subject(ProjectPermissionSub.Secrets, { environment, secretPath })}
>
{(isAllowed) => (
<Button variant="star" onClick={onAddNewSecret} isDisabled={!isAllowed}>
Add a new secret
</Button>
)}
</ProjectPermissionCan>
)}
</div>
</div>
</form>
)}
</div>
);
};
@@ -1,86 +0,0 @@
import { Controller, useForm } from "react-hook-form";
import { yupResolver } from "@hookform/resolvers/yup";
import * as yup from "yup";
import { Button, FormControl, Input, ModalClose, Select, SelectItem } from "@app/components/v2";
type Props = {
onCreate: (environment: string, secretPath: string) => Promise<void>;
environments?: Array<{ slug: string; name: string }>;
};
const formSchema = yup.object({
environment: yup.string().required().label("Environment").trim(),
secretPath: yup
.string()
.required()
.label("Secret Path")
.trim()
.transform((val) =>
typeof val === "string" && val.at(-1) === "/" && val.length > 1 ? val.slice(0, -1) : val
)
});
type TFormData = yup.InferType<typeof formSchema>;
export const SecretImportForm = ({ onCreate, environments = [] }: Props): JSX.Element => {
const {
control,
reset,
formState: { isSubmitting },
handleSubmit
} = useForm<TFormData>({
resolver: yupResolver(formSchema)
});
const onSubmit = async ({ environment, secretPath }: TFormData) => {
await onCreate(environment, secretPath);
reset();
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<Controller
control={control}
name="environment"
defaultValue={environments?.[0]?.slug}
render={({ field: { onChange, ...field }, fieldState: { error } }) => (
<FormControl label="Environment" errorText={error?.message} isError={Boolean(error)}>
<Select
defaultValue={field.value}
{...field}
onValueChange={(e) => onChange(e)}
className="w-full"
>
{environments.map(({ name, slug }) => (
<SelectItem value={slug} key={slug}>
{name}
</SelectItem>
))}
</Select>
</FormControl>
)}
/>
<Controller
control={control}
name="secretPath"
defaultValue="/"
render={({ field, fieldState: { error } }) => (
<FormControl label="Secret Path" isError={Boolean(error)} errorText={error?.message}>
<Input {...field} />
</FormControl>
)}
/>
<div className="mt-8 flex items-center">
<Button className="mr-4" type="submit" isDisabled={isSubmitting} isLoading={isSubmitting}>
Create
</Button>
<ModalClose asChild>
<Button variant="plain" colorSchema="secondary">
Cancel
</Button>
</ModalClose>
</div>
</form>
);
};
@@ -1 +0,0 @@
export { SecretImportForm } from "./SecretImportForm";

Some files were not shown because too many files have changed in this diff Show More