feat(infisical-pg): completed nested folder support for secret,folder and import operations

This commit is contained in:
Akhil Mohan
2023-12-22 14:15:28 +05:30
parent 0e1191f2ea
commit 010963a80c
4 changed files with 109 additions and 22 deletions

View File

@@ -4,6 +4,7 @@ import { Tables } from "knex/types/tables";
import { DatabaseError } from "../errors";
export * from "./join";
export * from "./select";
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>

View File

@@ -0,0 +1,5 @@
import { Knex } from "knex";
import { Tables } from "knex/types/tables";
export const selectAllTableCols = <Tname extends keyof Tables>(db: Knex, tableName: Tname) =>
db.ref("*").withSchema(tableName) as unknown as keyof Tables[Tname];

View File

@@ -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<TSecretFolders & { depth: number; path: string }>("parent");
};
export type TSecretFolderDalFactory = ReturnType<typeof secretFolderDalFactory>;
// 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" });

View File

@@ -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 {