mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-20 21:18:18 +00:00
feat(infisical-pg): idempotent folder creation
This commit is contained in:
@@ -252,6 +252,27 @@ export const secretFolderDalFactory = (db: TDbClient) => {
|
||||
}
|
||||
};
|
||||
|
||||
// used in folder creation
|
||||
// even if its the original given /path1/path2
|
||||
// it will stop automatically at /path2
|
||||
const findClosestFolder = async (
|
||||
projectId: string,
|
||||
environment: string,
|
||||
path: string,
|
||||
tx?: Knex
|
||||
) => {
|
||||
try {
|
||||
const folder = await sqlFindFolderByPathQuery(tx || db, projectId, environment, path)
|
||||
.orderBy("depth", "desc")
|
||||
.first();
|
||||
if (!folder) return;
|
||||
const { envId: id, envName: name, envSlug: slug, ...el } = folder;
|
||||
return { ...el, envId: id, environment: { id, name, slug } };
|
||||
} catch (error) {
|
||||
throw new DatabaseError({ error, name: "Find by secret path" });
|
||||
}
|
||||
};
|
||||
|
||||
const findByManySecretPath = async (
|
||||
query: Array<{ envId: string; secretPath: string }>,
|
||||
tx?: Knex
|
||||
@@ -329,6 +350,7 @@ export const secretFolderDalFactory = (db: TDbClient) => {
|
||||
findBySecretPath,
|
||||
findById,
|
||||
findByManySecretPath,
|
||||
findSecretPathByFolderIds
|
||||
findSecretPathByFolderIds,
|
||||
findClosestFolder
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { ForbiddenError, subject } from "@casl/ability";
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { TSecretFoldersInsert } from "@app/db/schemas";
|
||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
||||
import {
|
||||
ProjectPermissionActions,
|
||||
@@ -41,12 +44,12 @@ export const secretFolderServiceFactory = ({
|
||||
actorId,
|
||||
name,
|
||||
environment,
|
||||
path
|
||||
path: secretPath
|
||||
}: TCreateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||
@@ -54,10 +57,66 @@ export const secretFolderServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||
|
||||
const folder = await folderDal.transaction(async (tx) => {
|
||||
const parentFolder = await folderDal.findBySecretPath(projectId, environment, path, tx);
|
||||
// the logic is simple we need to avoid creating same folder in same path multiple times
|
||||
// that is this request must be idempotent
|
||||
// so we do a tricky move. we try to find the to be created folder path if that is exactly match return that
|
||||
// else we get some path before that then we will start creating remaining folder
|
||||
const pathWithFolder = path.join(secretPath, name);
|
||||
const parentFolder = await folderDal.findClosestFolder(
|
||||
projectId,
|
||||
environment,
|
||||
pathWithFolder,
|
||||
tx
|
||||
);
|
||||
// no folder found is not possible root should be their
|
||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||
// exact folder
|
||||
if (parentFolder.path === pathWithFolder) return parentFolder;
|
||||
|
||||
let parentFolderId = parentFolder.id;
|
||||
if (parentFolder.path !== secretPath) {
|
||||
// this is upsert folder in a path
|
||||
// we are not taking snapshots of this because
|
||||
// snapshot will be removed from automatic for all commits to user click or cron based
|
||||
const missingSegment = secretPath
|
||||
.substring(parentFolder.path.length)
|
||||
.split("/")
|
||||
.filter(Boolean);
|
||||
if (missingSegment.length) {
|
||||
const newFolders: Array<TSecretFoldersInsert & { id: string }> = missingSegment.map(
|
||||
(segment, i) =>
|
||||
i === 0
|
||||
? {
|
||||
name: segment,
|
||||
parentId: parentFolder.id,
|
||||
id: uuidv4(),
|
||||
envId: env.id,
|
||||
version: 1
|
||||
}
|
||||
: {
|
||||
name: segment,
|
||||
parentId: newFolders[i - 1].id,
|
||||
id: uuidv4(),
|
||||
envId: env.id,
|
||||
version: 1
|
||||
}
|
||||
);
|
||||
parentFolderId = newFolders.at(-1)?.id as string;
|
||||
const docs = await folderDal.insertMany(newFolders, tx);
|
||||
await folderVersionDal.insertMany(
|
||||
docs.map((doc) => ({
|
||||
name: doc.name,
|
||||
envId: doc.envId,
|
||||
version: doc.version,
|
||||
folderId: doc.id
|
||||
})),
|
||||
tx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const doc = await folderDal.create(
|
||||
{ name, envId: env.id, version: 1, parentId: parentFolder.id },
|
||||
{ name, envId: env.id, version: 1, parentId: parentFolderId },
|
||||
tx
|
||||
);
|
||||
await folderVersionDal.create(
|
||||
@@ -82,16 +141,16 @@ export const secretFolderServiceFactory = ({
|
||||
actorId,
|
||||
name,
|
||||
environment,
|
||||
path,
|
||||
path: secretPath,
|
||||
id
|
||||
}: TUpdateFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Edit,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const parentFolder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||
const parentFolder = await folderDal.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||
|
||||
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||
@@ -133,13 +192,13 @@ export const secretFolderServiceFactory = ({
|
||||
actor,
|
||||
actorId,
|
||||
environment,
|
||||
path,
|
||||
path: secretPath,
|
||||
id
|
||||
}: TDeleteFolderDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Delete,
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath })
|
||||
);
|
||||
|
||||
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||
@@ -147,7 +206,7 @@ export const secretFolderServiceFactory = ({
|
||||
throw new BadRequestError({ message: "Environment not found", name: "Create folder" });
|
||||
|
||||
const folder = await folderDal.transaction(async (tx) => {
|
||||
const parentFolder = await folderDal.findBySecretPath(projectId, environment, path, tx);
|
||||
const parentFolder = await folderDal.findBySecretPath(projectId, environment, secretPath, tx);
|
||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||
|
||||
const [doc] = await folderDal.delete({ envId: env.id, id, parentId: parentFolder.id }, tx);
|
||||
@@ -159,7 +218,13 @@ export const secretFolderServiceFactory = ({
|
||||
return folder;
|
||||
};
|
||||
|
||||
const getFolders = async ({ projectId, actor, actorId, environment, path }: TGetFolderDTO) => {
|
||||
const getFolders = async ({
|
||||
projectId,
|
||||
actor,
|
||||
actorId,
|
||||
environment,
|
||||
path: secretPath
|
||||
}: TGetFolderDTO) => {
|
||||
// folder list is allowed to be read by anyone
|
||||
// permission to check does user has access
|
||||
await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
@@ -167,8 +232,8 @@ export const secretFolderServiceFactory = ({
|
||||
const env = await projectEnvDal.findOne({ projectId, slug: environment });
|
||||
if (!env) throw new BadRequestError({ message: "Environment not found", name: "get folders" });
|
||||
|
||||
const parentFolder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||
if (!parentFolder) throw new BadRequestError({ message: "Secret path not found" });
|
||||
const parentFolder = await folderDal.findBySecretPath(projectId, environment, secretPath);
|
||||
if (!parentFolder) return [];
|
||||
|
||||
const folders = await folderDal.find({ envId: env.id, parentId: parentFolder.id });
|
||||
return folders;
|
||||
|
||||
@@ -48,7 +48,7 @@ export const secretImportServiceFactory = ({
|
||||
path
|
||||
}: TCreateSecretImportDTO) => {
|
||||
const { permission } = await permissionService.getProjectPermission(actor, actorId, projectId);
|
||||
|
||||
|
||||
// check if user has permission to import into destination path
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
@@ -58,7 +58,10 @@ export const secretImportServiceFactory = ({
|
||||
// check if user has permission to import from target path
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionActions.Create,
|
||||
subject(ProjectPermissionSub.Secrets, { environment: data.environment, secretPath: data.path })
|
||||
subject(ProjectPermissionSub.Secrets, {
|
||||
environment: data.environment,
|
||||
secretPath: data.path
|
||||
})
|
||||
);
|
||||
|
||||
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||
@@ -196,7 +199,7 @@ export const secretImportServiceFactory = ({
|
||||
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path })
|
||||
);
|
||||
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" });
|
||||
if (!folder) return [];
|
||||
// this will already order by position
|
||||
// so anything based on this order will also be in right position
|
||||
const secretImports = await secretImportDal.find({ folderId: folder.id });
|
||||
|
||||
@@ -499,7 +499,7 @@ export const secretServiceFactory = ({
|
||||
);
|
||||
|
||||
const folder = await folderDal.findBySecretPath(projectId, environment, path);
|
||||
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create secret" });
|
||||
if (!folder) return { secrets: [], imports: [] };
|
||||
const folderId = folder.id;
|
||||
|
||||
const secrets = await secretDal.findByFolderId(folderId, actorId);
|
||||
|
||||
Reference in New Issue
Block a user