From 010963a80c6cbd0f2a2b7cf9c99e51aa6676599f Mon Sep 17 00:00:00 2001 From: Akhil Mohan Date: Fri, 22 Dec 2023 14:15:28 +0530 Subject: [PATCH] feat(infisical-pg): completed nested folder support for secret,folder and import operations --- backend-pg/src/lib/knex/index.ts | 1 + backend-pg/src/lib/knex/select.ts | 5 + .../secret-folder/secret-folder-dal.ts | 92 ++++++++++++++++--- .../secret-folder/secret-folder-service.ts | 33 +++++-- 4 files changed, 109 insertions(+), 22 deletions(-) create mode 100644 backend-pg/src/lib/knex/select.ts diff --git a/backend-pg/src/lib/knex/index.ts b/backend-pg/src/lib/knex/index.ts index 5dfbea89e..68485709e 100644 --- a/backend-pg/src/lib/knex/index.ts +++ b/backend-pg/src/lib/knex/index.ts @@ -4,6 +4,7 @@ import { Tables } from "knex/types/tables"; import { DatabaseError } from "../errors"; export * from "./join"; +export * from "./select"; export const withTransaction = (db: Knex, dal: K) => ({ transaction: async (cb: (tx: Knex) => Promise) => diff --git a/backend-pg/src/lib/knex/select.ts b/backend-pg/src/lib/knex/select.ts new file mode 100644 index 000000000..3dfab971b --- /dev/null +++ b/backend-pg/src/lib/knex/select.ts @@ -0,0 +1,5 @@ +import { Knex } from "knex"; +import { Tables } from "knex/types/tables"; + +export const selectAllTableCols = (db: Knex, tableName: Tname) => + db.ref("*").withSchema(tableName) as unknown as keyof Tables[Tname]; diff --git a/backend-pg/src/services/secret-folder/secret-folder-dal.ts b/backend-pg/src/services/secret-folder/secret-folder-dal.ts index ba2490208..be3bc43b4 100644 --- a/backend-pg/src/services/secret-folder/secret-folder-dal.ts +++ b/backend-pg/src/services/secret-folder/secret-folder-dal.ts @@ -1,9 +1,79 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName,TSecretFolders } from "@app/db/schemas"; -import { DatabaseError } from "@app/lib/errors"; -import { ormify } from "@app/lib/knex"; +import { TableName, TSecretFolders } from "@app/db/schemas"; +import { BadRequestError, DatabaseError } from "@app/lib/errors"; +import { ormify, selectAllTableCols } from "@app/lib/knex"; + +export const validateFolderName = (folderName: string) => { + const validNameRegex = /^[a-zA-Z0-9-_]+$/; + return validNameRegex.test(folderName); +}; + +const sqlFindFolderByPathQuery = ( + db: Knex, + projectId: string, + environment: string, + secretPath: string +) => { + // this is removing an trailing slash like /folder1/folder2/ -> /folder1/folder2 + const formatedPath = + secretPath.at(-1) === "/" && secretPath.length > 1 ? secretPath.slice(0, -1) : secretPath; + // next goal to sanitize saw the raw sql query is safe + // for this we ensure folder name contains only string and - nothing else + const pathSegments = formatedPath.split("/").filter(Boolean); + if (pathSegments.some((segment) => !validateFolderName(segment))) { + throw new BadRequestError({ message: "Invalid folder name" }); + } + + return db + .withRecursive("parent", (baseQb) => { + // first remember our folders are connected as a link list or known as adjacency list + // Thus each node has connection to parent node + // for a given path from root we recursively reach to the leaf path or till we get null + // the below query is the base case where we select root folder which has parent folder id as null + baseQb + .select({ + depth: 1, + path: db.raw("'/'") + }) + .select(selectAllTableCols(db, TableName.SecretFolder)) + .from(TableName.SecretFolder) + .join( + TableName.Environment, + `${TableName.SecretFolder}.envId`, + `${TableName.Environment}.id` + ) + .where({ + projectId, + parentId: null + }) + .where(`${TableName.Environment}.slug`, environment) + .union((qb) => + // for here on we keep going to next child node. + // we also keep a measure of depth then we check the depth matches the array path segment and folder name + // that is at depth 1 for a path /folder1/folder2 -> the name should be folder1 + qb + .select({ + depth: db.raw("parent.depth + 1"), + path: db.raw( + "CONCAT((CASE WHEN parent.path = '/' THEN '' ELSE parent.path END),'/', secret_folders.name)" + ) + }) + .select(selectAllTableCols(db, TableName.SecretFolder)) + .whereRaw( + `depth = array_position(ARRAY[${pathSegments + .map(() => "?") + .join(",")}]::varchar[], secret_folders.name,depth)`, + [...pathSegments] + ) + .from(TableName.SecretFolder) + .join("parent", "parent.id", `${TableName.SecretFolder}.parentId`) + ); + }) + .select("*") + .from("parent"); +}; export type TSecretFolderDalFactory = ReturnType; // never change this. If u do write a migration for it @@ -18,18 +88,12 @@ export const secretFolderDalFactory = (db: TDbClient) => { tx?: Knex ) => { try { - const folder: TSecretFolders | undefined = await (tx || db)(TableName.SecretFolder) - .join( - TableName.Environment, - `${TableName.SecretFolder}.envId`, - `${TableName.Environment}.id` - ) - .join(TableName.Project, `${TableName.Environment}.projectId`, `${TableName.Project}.id`) - .where(`${TableName.Project}.id`, projectId) - .where(`${TableName.Environment}.slug`, environment) - .where(`${TableName.SecretFolder}.name`, "root") - .select(`${TableName.SecretFolder}.*`) + const folder = await sqlFindFolderByPathQuery(tx || db, projectId, environment, path) + .orderBy("depth", "desc") .first(); + if (folder && folder.path !== path) { + return; + } return folder; } catch (error) { throw new DatabaseError({ error, name: "Find by secret path" }); diff --git a/backend-pg/src/services/secret-folder/secret-folder-service.ts b/backend-pg/src/services/secret-folder/secret-folder-service.ts index 1d0bc47c9..14abc490a 100644 --- a/backend-pg/src/services/secret-folder/secret-folder-service.ts +++ b/backend-pg/src/services/secret-folder/secret-folder-service.ts @@ -48,7 +48,12 @@ export const secretFolderServiceFactory = ({ throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); const folder = await folderDal.transaction(async (tx) => { - const doc = await folderDal.create({ name, envId: env.id, version: 1 }, tx); + const parentFolder = await folderDal.findBySecretPath(projectId, environment, path, tx); + if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); + const doc = await folderDal.create( + { name, envId: env.id, version: 1, parentId: parentFolder.id }, + tx + ); return doc; }); @@ -75,7 +80,14 @@ export const secretFolderServiceFactory = ({ throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); const folder = await folderDal.transaction(async (tx) => { - const [doc] = await folderDal.update({ envId: env.id, id }, { name, version: 1 }, tx); + const parentFolder = await folderDal.findBySecretPath(projectId, environment, path, tx); + if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); + + const [doc] = await folderDal.update( + { envId: env.id, id, parentId: parentFolder.id }, + { name, version: 1 }, + tx + ); if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Update folder" }); return doc; }); @@ -102,7 +114,10 @@ export const secretFolderServiceFactory = ({ throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); const folder = await folderDal.transaction(async (tx) => { - const [doc] = await folderDal.delete({ envId: env.id, id }, tx); + const parentFolder = await folderDal.findBySecretPath(projectId, environment, path, tx); + if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); + + const [doc] = await folderDal.delete({ envId: env.id, id, parentId: parentFolder.id }, tx); if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Delete folder" }); return doc; }); @@ -110,17 +125,19 @@ export const secretFolderServiceFactory = ({ return folder; }; - const getFolders = async ({ projectId, actor, actorId, environment }: TGetFolderDTO) => { + const getFolders = async ({ projectId, actor, actorId, environment, path }: TGetFolderDTO) => { // folder list is allowed to be read by anyone // permission to check does user has access await permissionService.getProjectPermission(actor, actorId, projectId); const env = await projectEnvDal.findOne({ projectId, slug: environment }); - if (!env) - throw new BadRequestError({ message: "Environment not found", name: "Create folder" }); + if (!env) throw new BadRequestError({ message: "Environment not found", name: "get folders" }); - const folders = await folderDal.find({ envId: env.id, parentId: null }); - return folders.filter(({ name }) => name !== ROOT_FOLDER_NAME); + const parentFolder = await folderDal.findBySecretPath(projectId, environment, path); + if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" }); + + const folders = await folderDal.find({ envId: env.id, parentId: parentFolder.id }); + return folders; }; return {