mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat: added support for recursive file creation
This commit is contained in:
@@ -9,7 +9,6 @@ import { Secret, ServiceTokenData } from "../../models";
|
||||
import { Folder } from "../../models/folder";
|
||||
import {
|
||||
appendFolder,
|
||||
generateFolderId,
|
||||
getAllFolderIds,
|
||||
getFolderByPath,
|
||||
getFolderWithPathFromId,
|
||||
@@ -132,9 +131,6 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
|
||||
// space has no folders initialized
|
||||
if (!folders) {
|
||||
if (directory !== "/") throw ERR_FOLDER_NOT_FOUND;
|
||||
|
||||
const id = generateFolderId();
|
||||
const folder = new Folder({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
@@ -142,14 +138,15 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
id: "root",
|
||||
name: "root",
|
||||
version: 1,
|
||||
children: [{ id, name: folderName, children: [], version: 1 }]
|
||||
children: []
|
||||
}
|
||||
});
|
||||
const { parent, child } = appendFolder(folder.nodes, { folderName, directory });
|
||||
await folder.save();
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: folder.nodes
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
@@ -163,9 +160,9 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: id,
|
||||
folderId: child.id,
|
||||
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);
|
||||
if (!parentFolder) throw ERR_FOLDER_NOT_FOUND;
|
||||
const { parent, child, hasCreated } = appendFolder(folders.nodes, { folderName, directory });
|
||||
|
||||
if (!hasCreated) return res.json({ folder: child });
|
||||
|
||||
const folder = appendFolder(folders.nodes, { folderName, parentFolderId: parentFolder.id });
|
||||
await Folder.findByIdAndUpdate(folders._id, folders);
|
||||
|
||||
const folderVersion = new FolderVersion({
|
||||
workspace: workspaceId,
|
||||
environment,
|
||||
nodes: parentFolder
|
||||
nodes: parent
|
||||
});
|
||||
await folderVersion.save();
|
||||
|
||||
await EESecretService.takeSecretSnapshot({
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
folderId: parentFolder.id
|
||||
folderId: child.id
|
||||
});
|
||||
|
||||
await EEAuditLogService.createAuditLog(
|
||||
@@ -201,7 +198,7 @@ export const createFolder = async (req: Request, res: Response) => {
|
||||
type: EventType.CREATE_FOLDER,
|
||||
metadata: {
|
||||
environment,
|
||||
folderId: folder.id,
|
||||
folderId: child.id,
|
||||
folderName,
|
||||
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) => {
|
||||
const {
|
||||
body: { type, environment, secretPath, workspaceId },
|
||||
body: { type, environment, secretPath, workspaceId, secretId },
|
||||
params: { secretName }
|
||||
} = await validateRequest(reqValidator.DeleteSecretByNameV3, req);
|
||||
|
||||
@@ -813,6 +813,7 @@ export const deleteSecretByName = async (req: Request, res: Response) => {
|
||||
|
||||
const { secret } = await SecretService.deleteSecret({
|
||||
secretName,
|
||||
secretId,
|
||||
workspaceId: new Types.ObjectId(workspaceId),
|
||||
environment,
|
||||
type,
|
||||
|
||||
@@ -57,10 +57,10 @@ import { getAnImportedSecret } from "../services/SecretImportService";
|
||||
|
||||
/**
|
||||
* Validate scope for service token v3
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
*/
|
||||
export const isValidScopeV3 = ({
|
||||
authPayload,
|
||||
@@ -68,37 +68,40 @@ export const isValidScopeV3 = ({
|
||||
secretPath,
|
||||
requiredPermissions
|
||||
}: {
|
||||
authPayload: IServiceTokenDataV3,
|
||||
environment: string,
|
||||
secretPath: string,
|
||||
requiredPermissions: Permission[]
|
||||
authPayload: IServiceTokenDataV3;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
requiredPermissions: Permission[];
|
||||
}) => {
|
||||
const { scopes } = authPayload;
|
||||
|
||||
|
||||
const validScope = scopes.find(
|
||||
(scope) =>
|
||||
picomatch.isMatch(secretPath, scope.secretPath, { strictSlashes: false }) &&
|
||||
scope.environment === environment
|
||||
);
|
||||
|
||||
if (validScope && !requiredPermissions.every(permission => validScope.permissions.includes(permission))) {
|
||||
if (
|
||||
validScope &&
|
||||
!requiredPermissions.every((permission) => validScope.permissions.includes(permission))
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
return Boolean(validScope);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate scope for service token v2
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
* @param authPayload
|
||||
* @param environment
|
||||
* @param secretPath
|
||||
* @returns
|
||||
*/
|
||||
export const isValidScope = (
|
||||
authPayload: IServiceTokenData,
|
||||
environment: string,
|
||||
secretPath: string,
|
||||
secretPath: string
|
||||
) => {
|
||||
const { scopes: tkScopes } = authPayload;
|
||||
const validScope = tkScopes.find(
|
||||
@@ -1000,12 +1003,22 @@ export const deleteSecretHelper = async ({
|
||||
environment,
|
||||
type,
|
||||
authData,
|
||||
secretPath = "/"
|
||||
secretPath = "/",
|
||||
// used for update corner case and blindIndex goes wrong way
|
||||
secretId
|
||||
}: DeleteSecretParams) => {
|
||||
const secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
let secretBlindIndex = await generateSecretBlindIndexHelper({
|
||||
secretName,
|
||||
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);
|
||||
|
||||
|
||||
@@ -64,6 +64,7 @@ export interface UpdateSecretParams {
|
||||
|
||||
export interface DeleteSecretParams {
|
||||
secretName: string;
|
||||
secretId?: string;
|
||||
workspaceId: Types.ObjectId;
|
||||
environment: string;
|
||||
type: "shared" | "personal";
|
||||
|
||||
@@ -5,7 +5,7 @@ import path from "path";
|
||||
|
||||
type TAppendFolderDTO = {
|
||||
folderName: string;
|
||||
parentFolderId?: string;
|
||||
directory: string;
|
||||
};
|
||||
|
||||
type TRenameFolderDTO = {
|
||||
@@ -50,9 +50,8 @@ export const folderBfsTraversal = async (
|
||||
// bfs and then append to the folder
|
||||
const appendChild = (folders: TFolderSchema, folderName: string) => {
|
||||
const folder = folders.children.find(({ name }) => name === folderName);
|
||||
if (folder) {
|
||||
throw new Error("Folder already exists");
|
||||
}
|
||||
if (folder) return { folder, hasCreated: false };
|
||||
|
||||
const id = generateFolderId();
|
||||
folders.version += 1;
|
||||
folders.children.push({
|
||||
@@ -61,24 +60,32 @@ const appendChild = (folders: TFolderSchema, folderName: string) => {
|
||||
children: [],
|
||||
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
|
||||
export const appendFolder = (
|
||||
folders: TFolderSchema,
|
||||
{ folderName, parentFolderId }: TAppendFolderDTO
|
||||
) => {
|
||||
const isRoot = !parentFolderId;
|
||||
{ folderName, directory }: TAppendFolderDTO
|
||||
): { parent: TFolderSchema; child: TFolderSchema; hasCreated?: boolean } => {
|
||||
if (directory === "/") {
|
||||
const newFolder = appendChild(folders, folderName);
|
||||
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
|
||||
}
|
||||
|
||||
if (isRoot) {
|
||||
return appendChild(folders, folderName);
|
||||
const segments = directory.split("/").filter(Boolean);
|
||||
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) {
|
||||
throw new Error("Parent Folder not found");
|
||||
}
|
||||
return appendChild(folder, folderName);
|
||||
|
||||
const newFolder = appendChild(folders, folderName);
|
||||
return { parent: folders, child: newFolder.folder, hasCreated: newFolder.hasCreated };
|
||||
};
|
||||
|
||||
export const renameFolder = (
|
||||
|
||||
@@ -379,7 +379,8 @@ export const DeleteSecretByNameV3 = z.object({
|
||||
workspaceId: z.string().trim(),
|
||||
environment: z.string().trim(),
|
||||
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({
|
||||
secretName: z.string()
|
||||
|
||||
@@ -37,7 +37,7 @@ export const DrawerContent = forwardRef<HTMLDivElement, DrawerContentProps>(
|
||||
) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<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)" }}
|
||||
/>
|
||||
<DialogPrimitive.Content
|
||||
|
||||
@@ -189,12 +189,20 @@ export const useDeleteSecretV3 = ({
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<{}, {}, TDeleteSecretsV3DTO>({
|
||||
mutationFn: async ({ secretPath = "/", type, environment, workspaceId, secretName }) => {
|
||||
mutationFn: async ({
|
||||
secretPath = "/",
|
||||
type,
|
||||
environment,
|
||||
workspaceId,
|
||||
secretName,
|
||||
secretId
|
||||
}) => {
|
||||
const reqBody = {
|
||||
workspaceId,
|
||||
environment,
|
||||
type,
|
||||
secretPath
|
||||
secretPath,
|
||||
secretId
|
||||
};
|
||||
|
||||
const { data } = await apiRequest.delete(`/api/v3/secrets/${secretName}`, {
|
||||
|
||||
@@ -120,6 +120,7 @@ export type TDeleteSecretsV3DTO = {
|
||||
type: "shared" | "personal";
|
||||
secretPath: string;
|
||||
secretName: string;
|
||||
secretId?: string;
|
||||
};
|
||||
|
||||
export type TCreateSecretBatchDTO = {
|
||||
|
||||
@@ -124,13 +124,15 @@ export const SecretListView = ({
|
||||
comment,
|
||||
tags,
|
||||
skipMultilineEncoding,
|
||||
newKey
|
||||
newKey,
|
||||
secretId
|
||||
}: Partial<{
|
||||
value: string;
|
||||
comment: string;
|
||||
tags: string[];
|
||||
skipMultilineEncoding: boolean;
|
||||
newKey: string;
|
||||
secretId: string;
|
||||
}> = {}
|
||||
) => {
|
||||
if (operation === "delete") {
|
||||
@@ -139,7 +141,8 @@ export const SecretListView = ({
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretName: key,
|
||||
type
|
||||
type,
|
||||
secretId
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -249,9 +252,9 @@ export const SecretListView = ({
|
||||
);
|
||||
|
||||
const handleSecretDelete = useCallback(async () => {
|
||||
const { key } = popUp.deleteSecret?.data as DecryptedSecret;
|
||||
const { key, _id: secretId } = popUp.deleteSecret?.data as DecryptedSecret;
|
||||
try {
|
||||
await handleSecretOperation("delete", "shared", key);
|
||||
await handleSecretOperation("delete", "shared", key, { secretId });
|
||||
queryClient.invalidateQueries(
|
||||
secretKeys.getProjectSecret({ workspaceId, environment, secretPath })
|
||||
);
|
||||
|
||||
@@ -31,6 +31,7 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useOrganization, useWorkspace } from "@app/context";
|
||||
import {
|
||||
useCreateFolder,
|
||||
useCreateSecretV3,
|
||||
useDeleteSecretV3,
|
||||
useGetFoldersByEnv,
|
||||
@@ -104,9 +105,24 @@ export const SecretOverviewPage = () => {
|
||||
const { mutateAsync: createSecretV3 } = useCreateSecretV3();
|
||||
const { mutateAsync: updateSecretV3 } = useUpdateSecretV3();
|
||||
const { mutateAsync: deleteSecretV3 } = useDeleteSecretV3();
|
||||
const { mutateAsync: createFolder } = useCreateFolder();
|
||||
|
||||
const handleSecretCreate = async (env: string, key: string, value: string) => {
|
||||
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({
|
||||
environment: env,
|
||||
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 {
|
||||
await deleteSecretV3({
|
||||
environment: env,
|
||||
workspaceId,
|
||||
secretPath,
|
||||
secretName: key,
|
||||
secretId,
|
||||
type: "shared"
|
||||
});
|
||||
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 envIndex = userAvailableEnvs.findIndex((el) => slug === el.slug);
|
||||
if (envIndex !== -1) {
|
||||
@@ -335,7 +365,14 @@ export const SecretOverviewPage = () => {
|
||||
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>
|
||||
</EmptyState>
|
||||
</Td>
|
||||
|
||||
@@ -12,13 +12,14 @@ import { useToggle } from "@app/hooks";
|
||||
type Props = {
|
||||
defaultValue?: string | null;
|
||||
secretName: string;
|
||||
secretId?: string;
|
||||
isCreatable?: boolean;
|
||||
isVisible?: boolean;
|
||||
environment: string;
|
||||
secretPath: string;
|
||||
onSecretCreate: (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 = ({
|
||||
@@ -30,7 +31,8 @@ export const SecretEditRow = ({
|
||||
onSecretDelete,
|
||||
environment,
|
||||
secretPath,
|
||||
isVisible
|
||||
isVisible,
|
||||
secretId
|
||||
}: Props) => {
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -77,7 +79,7 @@ export const SecretEditRow = ({
|
||||
const handleDeleteSecret = async () => {
|
||||
setIsDeleting.on();
|
||||
try {
|
||||
await onSecretDelete(environment, secretName);
|
||||
await onSecretDelete(environment, secretName, secretId);
|
||||
reset({ value: undefined });
|
||||
} finally {
|
||||
setIsDeleting.off();
|
||||
|
||||
@@ -24,7 +24,7 @@ type Props = {
|
||||
getSecretByKey: (slug: string, key: string) => DecryptedSecret | undefined;
|
||||
onSecretCreate: (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 = ({
|
||||
@@ -149,6 +149,7 @@ export const SecretOverviewTableRow = ({
|
||||
isVisible={isSecretVisible}
|
||||
secretName={secretKey}
|
||||
defaultValue={secret?.value}
|
||||
secretId={secret?._id}
|
||||
isCreatable={isCreatable}
|
||||
onSecretDelete={onSecretDelete}
|
||||
onSecretCreate={onSecretCreate}
|
||||
|
||||
Reference in New Issue
Block a user