mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
feat(infisical-pg): completed nested folder support for secret,folder and import operations
This commit is contained in:
@@ -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>) =>
|
||||
|
||||
5
backend-pg/src/lib/knex/select.ts
Normal file
5
backend-pg/src/lib/knex/select.ts
Normal 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];
|
||||
@@ -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" });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user