Merge pull request #1078 from akhilmhdh/feat/secret-bug

feat: added support for recursive file creation
This commit is contained in:
Maidul Islam
2023-10-13 15:57:50 +01:00
committed by GitHub
13 changed files with 138 additions and 66 deletions
@@ -9,7 +9,6 @@ import { Secret, ServiceTokenData } from "../../models";
import { Folder } from "../../models/folder"; import { Folder } from "../../models/folder";
import { import {
appendFolder, appendFolder,
generateFolderId,
getAllFolderIds, getAllFolderIds,
getFolderByPath, getFolderByPath,
getFolderWithPathFromId, getFolderWithPathFromId,
@@ -132,9 +131,6 @@ export const createFolder = async (req: Request, res: Response) => {
// space has no folders initialized // space has no folders initialized
if (!folders) { if (!folders) {
if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
const id = generateFolderId();
const folder = new Folder({ const folder = new Folder({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
@@ -142,14 +138,15 @@ export const createFolder = async (req: Request, res: Response) => {
id: "root", id: "root",
name: "root", name: "root",
version: 1, version: 1,
children: [{ id, name: folderName, children: [], version: 1 }] children: []
} }
}); });
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
await folder.save(); await folder.save();
const folderVersion = new FolderVersion({ const folderVersion = new FolderVersion({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
nodes: folder.nodes nodes: parent
}); });
await folderVersion.save(); await folderVersion.save();
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
@@ -163,9 +160,9 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER, type: EventType.CREATE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId: id, folderId: child.id,
folderName, folderName,
folderPath: `root/${folderName}` folderPath: directory
} }
}, },
{ {
@@ -173,26 +170,26 @@ export const createFolder = async (req: Request, res: Response) => {
} }
); );
return res.json({ folder: { id, name: folderName } }); return res.json({ folder: { id: child.id, name: folderName } });
} }
const parentFolder = getFolderByPath(folders.nodes, directory); const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
if (!hasCreated) return res.json({ folder: child });
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({
workspace: workspaceId, workspace: workspaceId,
environment, environment,
nodes: parentFolder nodes: parent
}); });
await folderVersion.save(); await folderVersion.save();
await EESecretService.takeSecretSnapshot({ await EESecretService.takeSecretSnapshot({
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
folderId: parentFolder.id folderId: child.id
}); });
await EEAuditLogService.createAuditLog( await EEAuditLogService.createAuditLog(
@@ -201,7 +198,7 @@ export const createFolder = async (req: Request, res: Response) => {
type: EventType.CREATE_FOLDER, type: EventType.CREATE_FOLDER,
metadata: { metadata: {
environment, environment,
folderId: folder.id, folderId: child.id,
folderName, folderName,
folderPath: directory folderPath: directory
} }
@@ -211,7 +208,7 @@ export const createFolder = async (req: Request, res: Response) => {
} }
); );
return res.json({ folder }); return res.json({ folder: child });
}; };
/** /**
@@ -777,7 +777,7 @@ export const updateSecretByName = async (req: Request, res: Response) => {
*/ */
export const deleteSecretByName = async (req: Request, res: Response) => { export const deleteSecretByName = async (req: Request, res: Response) => {
const { const {
body: { type, environment, secretPath, workspaceId }, body: { type, environment, secretPath, workspaceId, secretId },
params: { secretName } params: { secretName }
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req); } = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
@@ -813,6 +813,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
const { secret } = await SecretService.deleteSecret({ const { secret } = await SecretService.deleteSecret({
secretName, secretName,
secretId,
workspaceId: new Types.ObjectId(workspaceId), workspaceId: new Types.ObjectId(workspaceId),
environment, environment,
type, type,
+22 -9
View File
@@ -68,10 +68,10 @@ export const isValidScopeV3 = ({
secretPath, secretPath,
requiredPermissions requiredPermissions
}: { }: {
authPayload: IServiceTokenDataV3, authPayload: IServiceTokenDataV3;
environment: string, environment: string;
secretPath: string, secretPath: string;
requiredPermissions: Permission[] requiredPermissions: Permission[];
}) => { }) => {
const { scopes } = authPayload; const { scopes } = authPayload;
@@ -81,12 +81,15 @@ export const isValidScopeV3 = ({
scope.environment === environment scope.environment === environment
); );
if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(permission))) { if (
validScope &&
!requiredPermissions.every((permission) => validScope.permissions.includes(permission))
) {
return false; return false;
} }
return Boolean(validScope); return Boolean(validScope);
} };
/** /**
* Validate scope for service token v2 * Validate scope for service token v2
@@ -98,7 +101,7 @@ export const isValidScopeV3 = ({
export const isValidScope = ( export const isValidScope = (
authPayload: IServiceTokenData, authPayload: IServiceTokenData,
environment: string, environment: string,
secretPath: string, secretPath: string
) => { ) => {
const { scopes: tkScopes } = authPayload; const { scopes: tkScopes } = authPayload;
const validScope = tkScopes.find( const validScope = tkScopes.find(
@@ -1000,12 +1003,22 @@ export const deleteSecretHelper = async ({
environment, environment,
type, type,
authData, authData,
secretPath = "/" secretPath = "/",
// used for update corner case and blindIndex goes wrong way
secretId
}: DeleteSecretParams) => { }: DeleteSecretParams) => {
const secretBlindIndex = await generateSecretBlindIndexHelper({ let secretBlindIndex = await generateSecretBlindIndexHelper({
secretName, secretName,
workspaceId: new Types.ObjectId(workspaceId) workspaceId: new Types.ObjectId(workspaceId)
}); });
if (secretId) {
const secret = await Secret.findOne({
workspace: workspaceId,
environment,
_id: secretId
}).select("secretBlindIndex");
if (secret && secret.secretBlindIndex) secretBlindIndex = secret.secretBlindIndex;
}
const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath); const folderId = await getFolderIdFromServiceToken(workspaceId, environment, secretPath);
@@ -64,6 +64,7 @@ export interface UpdateSecretParams {
export interface DeleteSecretParams { export interface DeleteSecretParams {
secretName: string; secretName: string;
secretId?: string;
workspaceId: Types.ObjectId; workspaceId: Types.ObjectId;
environment: string; environment: string;
type: "shared" | "personal"; type: "shared" | "personal";
+22 -15
View File
@@ -5,7 +5,7 @@ import path from "path";
type TAppendFolderDTO = { type TAppendFolderDTO = {
folderName: string; folderName: string;
parentFolderId?: string; directory: string;
}; };
type TRenameFolderDTO = { type TRenameFolderDTO = {
@@ -50,9 +50,8 @@ export const folderBfsTraversal = async (
// bfs and then append to the folder // bfs and then append to the folder
const appendChild = (folders: TFolderSchema, folderName: string) => { const appendChild = (folders: TFolderSchema, folderName: string) => {
const folder = folders.children.find(({ name }) => name === folderName); const folder = folders.children.find(({ name }) => name === folderName);
if (folder) { if (folder) return { folder, hasCreated: false };
throw new Error("Folder already exists");
}
const id = generateFolderId(); const id = generateFolderId();
folders.version += 1; folders.version += 1;
folders.children.push({ folders.children.push({
@@ -61,24 +60,32 @@ const appendChild = (folders: TFolderSchema, folderName: string) => {
children: [], children: [],
version: 1 version: 1
}); });
return { id, name: folderName }; // last element that is the new one
return { folder: folders.children[folders.children.length - 1], hasCreated: true };
}; };
// root of append child wrapper // root of append child wrapper
export const appendFolder = ( export const appendFolder = (
folders: TFolderSchema, folders: TFolderSchema,
{ folderName, parentFolderId }: TAppendFolderDTO { folderName, directory }: TAppendFolderDTO
) => { ): { parent: TFolderSchema; child: TFolderSchema; hasCreated?: boolean } => {
const isRoot = !parentFolderId; if (directory === "/") {
const newFolder = appendChild(folders, folderName);
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
}
if (isRoot) { const segments = directory.split("/").filter(Boolean);
return appendChild(folders, folderName); const segment = segments.shift();
if (segment) {
const nestedFolders = appendChild(folders, segment);
return appendFolder(nestedFolders.folder, {
folderName,
directory: path.join("/", ...segments)
});
} }
const folder = searchByFolderId(folders, parentFolderId);
if (!folder) { const newFolder = appendChild(folders, folderName);
throw new Error("Parent Folder not found"); return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
}
return appendChild(folder, folderName);
}; };
export const renameFolder = ( export const renameFolder = (
+2 -1
View File
@@ -379,7 +379,8 @@ export const DeleteSecretByNameV3 = z.object({
workspaceId: z.string().trim(), workspaceId: z.string().trim(),
environment: z.string().trim(), environment: z.string().trim(),
type: z.enum([SECRET_SHARED, SECRET_PERSONAL]), type: z.enum([SECRET_SHARED, SECRET_PERSONAL]),
secretPath: z.string().trim().default("/") secretPath: z.string().trim().default("/"),
secretId: z.string().trim().optional()
}), }),
params: z.object({ params: z.object({
secretName: z.string() secretName: z.string()
+1 -1
View File
@@ -37,7 +37,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
) => ( ) => (
<DialogPrimitive.Portal> <DialogPrimitive.Portal>
<DialogPrimitive.Overlay <DialogPrimitive.Overlay
className="fixed inset-0 z-[70] h-full w-full" className="fixed inset-0 z-20 h-full w-full"
style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }} style={{ backgroundColor: "rgba(0, 0, 0, 0.7)" }}
/> />
<DialogPrimitive.Content <DialogPrimitive.Content
+10 -2
View File
@@ -189,12 +189,20 @@ export const useDeleteSecretV3 = ({
const queryClient = useQueryClient(); const queryClient = useQueryClient();
return useMutation<{}, {}, TDeleteSecretsV3DTO>({ return useMutation<{}, {}, TDeleteSecretsV3DTO>({
mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => { mutationFn: async ({
secretPath = "/",
type,
environment,
workspaceId,
secretName,
secretId
}) => {
const reqBody = { const reqBody = {
workspaceId, workspaceId,
environment, environment,
type, type,
secretPath secretPath,
secretId
}; };
const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, { const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {
+1
View File
@@ -120,6 +120,7 @@ export type TDeleteSecretsV3DTO = {
type: "shared" | "personal"; type: "shared" | "personal";
secretPath: string; secretPath: string;
secretName: string; secretName: string;
secretId?: string;
}; };
export type TCreateSecretBatchDTO = { export type TCreateSecretBatchDTO = {
@@ -124,13 +124,15 @@ export const SecretListView = ({
comment, comment,
tags, tags,
skipMultilineEncoding, skipMultilineEncoding,
newKey newKey,
secretId
}: Partial<{ }: Partial<{
value: string; value: string;
comment: string; comment: string;
tags: string[]; tags: string[];
skipMultilineEncoding: boolean; skipMultilineEncoding: boolean;
newKey: string; newKey: string;
secretId: string;
}> = {} }> = {}
) => { ) => {
if (operation === "delete") { if (operation === "delete") {
@@ -139,7 +141,8 @@ export const SecretListView = ({
workspaceId, workspaceId,
secretPath, secretPath,
secretName: key, secretName: key,
type type,
secretId
}); });
return; return;
} }
@@ -249,9 +252,9 @@ export const SecretListView = ({
); );
const handleSecretDelete = useCallback(async () => { const handleSecretDelete = useCallback(async () => {
const { key } = popUp.deleteSecret?.data as DecryptedSecret; const { key, _id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
try { try {
await handleSecretOperation("delete", "shared", key); await handleSecretOperation("delete", "shared", key, { secretId });
queryClient.invalidateQueries( queryClient.invalidateQueries(
secretKeys.getProjectSecret({ workspaceId, environment, secretPath }) secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
); );
@@ -31,6 +31,7 @@ import {
} from "@app/components/v2"; } from "@app/components/v2";
import { useOrganization, useWorkspace } from "@app/context"; import { useOrganization, useWorkspace } from "@app/context";
import { import {
useCreateFolder,
useCreateSecretV3, useCreateSecretV3,
useDeleteSecretV3, useDeleteSecretV3,
useGetFoldersByEnv, useGetFoldersByEnv,
@@ -104,9 +105,24 @@ export const SecretOverviewPage = () => {
const { mutateAsync: createSecretV3 } = useCreateSecretV3(); const { mutateAsync: createSecretV3 } = useCreateSecretV3();
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3(); const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3(); const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
const { mutateAsync: createFolder } = useCreateFolder();
const handleSecretCreate = async (env: string, key: string, value: string) => { const handleSecretCreate = async (env: string, key: string, value: string) => {
try { try {
// create folder if not existing
if (secretPath !== "/") {
const path = secretPath.split("/");
const directory = path.slice(0, -1).join("/");
const folderName = path.at(-1);
if (folderName && directory) {
await createFolder({
workspaceId,
environment: env,
directory,
folderName
});
}
}
await createSecretV3({ await createSecretV3({
environment: env, environment: env,
workspaceId, workspaceId,
@@ -154,13 +170,14 @@ export const SecretOverviewPage = () => {
} }
}; };
const handleSecretDelete = async (env: string, key: string) => { const handleSecretDelete = async (env: string, key: string, secretId?: string) => {
try { try {
await deleteSecretV3({ await deleteSecretV3({
environment: env, environment: env,
workspaceId, workspaceId,
secretPath, secretPath,
secretName: key, secretName: key,
secretId,
type: "shared" type: "shared"
}); });
createNotification({ createNotification({
@@ -188,7 +205,20 @@ export const SecretOverviewPage = () => {
}); });
}; };
const handleExploreEnvClick = (slug: string) => { const handleExploreEnvClick = async (slug: string) => {
if (secretPath !== "/") {
const path = secretPath.split("/");
const directory = path.slice(0, -1).join("/");
const folderName = path.at(-1);
if (folderName && directory) {
await createFolder({
workspaceId,
environment: slug,
directory,
folderName
});
}
}
const query: Record<string, string> = { ...router.query, env: slug }; const query: Record<string, string> = { ...router.query, env: slug };
const envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug); const envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug);
if (envIndex !== -1) { if (envIndex !== -1) {
@@ -335,7 +365,14 @@ export const SecretOverviewPage = () => {
query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug } query: { id: workspaceId, env: userAvailableEnvs?.[0]?.slug }
}} }}
> >
<Button className="mt-4" variant="outline_bg" colorSchema="primary" size="md">Go to {userAvailableEnvs?.[0]?.name}</Button> <Button
className="mt-4"
variant="outline_bg"
colorSchema="primary"
size="md"
>
Go to {userAvailableEnvs?.[0]?.name}
</Button>
</Link> </Link>
</EmptyState> </EmptyState>
</Td> </Td>
@@ -12,13 +12,14 @@ import { useToggle } from "@app/hooks";
type Props = { type Props = {
defaultValue?: string | null; defaultValue?: string | null;
secretName: string; secretName: string;
secretId?: string;
isCreatable?: boolean; isCreatable?: boolean;
isVisible?: boolean; isVisible?: boolean;
environment: string; environment: string;
secretPath: string; secretPath: string;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>; onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>; onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>; onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
}; };
export const SecretEditRow = ({ export const SecretEditRow = ({
@@ -30,7 +31,8 @@ export const SecretEditRow = ({
onSecretDelete, onSecretDelete,
environment, environment,
secretPath, secretPath,
isVisible isVisible,
secretId
}: Props) => { }: Props) => {
const { const {
handleSubmit, handleSubmit,
@@ -77,7 +79,7 @@ export const SecretEditRow = ({
const handleDeleteSecret = async () => { const handleDeleteSecret = async () => {
setIsDeleting.on(); setIsDeleting.on();
try { try {
await onSecretDelete(environment, secretName); await onSecretDelete(environment, secretName, secretId);
reset({ value: undefined }); reset({ value: undefined });
} finally { } finally {
setIsDeleting.off(); setIsDeleting.off();
@@ -24,7 +24,7 @@ type Props = {
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined; getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
onSecretCreate: (env: string, key: string, value: string) => Promise<void>; onSecretCreate: (env: string, key: string, value: string) => Promise<void>;
onSecretUpdate: (env: string, key: string, value: string) => Promise<void>; onSecretUpdate: (env: string, key: string, value: string) => Promise<void>;
onSecretDelete: (env: string, key: string) => Promise<void>; onSecretDelete: (env: string, key: string, secretId?: string) => Promise<void>;
}; };
export const SecretOverviewTableRow = ({ export const SecretOverviewTableRow = ({
@@ -149,6 +149,7 @@ export const SecretOverviewTableRow = ({
isVisible={isSecretVisible} isVisible={isSecretVisible}
secretName={secretKey} secretName={secretKey}
defaultValue={secret?.value} defaultValue={secret?.value}
secretId={secret?._id}
isCreatable={isCreatable} isCreatable={isCreatable}
onSecretDelete={onSecretDelete} onSecretDelete={onSecretDelete}
onSecretCreate={onSecretCreate} onSecretCreate={onSecretCreate}