mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 16:27:46 +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";
|
import { DatabaseError } from "../errors";
|
||||||
|
|
||||||
export * from "./join";
|
export * from "./join";
|
||||||
|
export * from "./select";
|
||||||
|
|
||||||
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
export const withTransaction = <K extends object>(db: Knex, dal: K) => ({
|
||||||
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
|
transaction: async <T>(cb: (tx: Knex) => Promise<T>) =>
|
||||||
|
|||||||
@@ -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 { Knex } from "knex";
|
||||||
|
|
||||||
import { TDbClient } from "@app/db";
|
import { TDbClient } from "@app/db";
|
||||||
import { TableName,TSecretFolders } from "@app/db/schemas";
|
import { TableName, TSecretFolders } from "@app/db/schemas";
|
||||||
import { DatabaseError } from "@app/lib/errors";
|
import { BadRequestError, DatabaseError } from "@app/lib/errors";
|
||||||
import { ormify } from "@app/lib/knex";
|
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>;
|
export type TSecretFolderDalFactory = ReturnType<typeof secretFolderDalFactory>;
|
||||||
// never change this. If u do write a migration for it
|
// never change this. If u do write a migration for it
|
||||||
@@ -18,18 +88,12 @@ export const secretFolderDalFactory = (db: TDbClient) => {
|
|||||||
tx?: Knex
|
tx?: Knex
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const folder: TSecretFolders | undefined = await (tx || db)(TableName.SecretFolder)
|
const folder = await sqlFindFolderByPathQuery(tx || db, projectId, environment, path)
|
||||||
.join(
|
.orderBy("depth", "desc")
|
||||||
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}.*`)
|
|
||||||
.first();
|
.first();
|
||||||
|
if (folder && folder.path !== path) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
return folder;
|
return folder;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new DatabaseError({ error, name: "Find by secret path" });
|
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" });
|
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|
||||||
const folder = await folderDal.transaction(async (tx) => {
|
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;
|
return doc;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -75,7 +80,14 @@ export const secretFolderServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|
||||||
const folder = await folderDal.transaction(async (tx) => {
|
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" });
|
if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Update folder" });
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
@@ -102,7 +114,10 @@ export const secretFolderServiceFactory = ({
|
|||||||
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||||
|
|
||||||
const folder = await folderDal.transaction(async (tx) => {
|
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" });
|
if (!doc) throw new BadRequestError({ message: "Folder not found", name: "Delete folder" });
|
||||||
return doc;
|
return doc;
|
||||||
});
|
});
|
||||||
@@ -110,17 +125,19 @@ export const secretFolderServiceFactory = ({
|
|||||||
return folder;
|
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
|
// folder list is allowed to be read by anyone
|
||||||
// permission to check does user has access
|
// permission to check does user has access
|
||||||
await permissionService.getProjectPermission(actor, actorId, projectId);
|
await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||||
|
|
||||||
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||||
if (!env)
|
if (!env) throw new BadRequestError({ message: "Environment not found", name: "get folders" });
|
||||||
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
|
||||||
|
|
||||||
const folders = await folderDal.find({ envId: env.id, parentId: null });
|
const parentFolder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||||
return folders.filter(({ name }) => name !== ROOT_FOLDER_NAME);
|
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||||
|
|
||||||
|
const folders = await folderDal.find({ envId: env.id, parentId: parentFolder.id });
|
||||||
|
return folders;
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user