mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 22:27:48 +00:00
525 lines
17 KiB
TypeScript
525 lines
17 KiB
TypeScript
import { ForbiddenError, subject } from "@casl/ability";
|
|
import path from "path";
|
|
import { v4 as uuidv4, validate as uuidValidate } from "uuid";
|
|
|
|
import { TSecretFoldersInsert } from "@app/db/schemas";
|
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
|
|
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
|
|
import { TSecretSnapshotServiceFactory } from "@app/ee/services/secret-snapshot/secret-snapshot-service";
|
|
import { BadRequestError, NotFoundError } from "@app/lib/errors";
|
|
import { OrderByDirection } from "@app/lib/types";
|
|
|
|
import { TProjectDALFactory } from "../project/project-dal";
|
|
import { TProjectEnvDALFactory } from "../project-env/project-env-dal";
|
|
import { TSecretFolderDALFactory } from "./secret-folder-dal";
|
|
import {
|
|
TCreateFolderDTO,
|
|
TDeleteFolderDTO,
|
|
TGetFolderByIdDTO,
|
|
TGetFolderDTO,
|
|
TUpdateFolderDTO,
|
|
TUpdateManyFoldersDTO
|
|
} from "./secret-folder-types";
|
|
import { TSecretFolderVersionDALFactory } from "./secret-folder-version-dal";
|
|
|
|
type TSecretFolderServiceFactoryDep = {
|
|
permissionService: Pick<TPermissionServiceFactory, "getProjectPermission">;
|
|
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
|
|
folderDAL: TSecretFolderDALFactory;
|
|
projectEnvDAL: Pick<TProjectEnvDALFactory, "findOne" | "findBySlugs">;
|
|
folderVersionDAL: TSecretFolderVersionDALFactory;
|
|
projectDAL: Pick<TProjectDALFactory, "findProjectBySlug">;
|
|
};
|
|
|
|
export type TSecretFolderServiceFactory = ReturnType<typeof secretFolderServiceFactory>;
|
|
|
|
export const secretFolderServiceFactory = ({
|
|
folderDAL,
|
|
snapshotService,
|
|
permissionService,
|
|
projectEnvDAL,
|
|
folderVersionDAL,
|
|
projectDAL
|
|
}: TSecretFolderServiceFactoryDep) => {
|
|
const createFolder = async ({
|
|
projectId,
|
|
actor,
|
|
actorId,
|
|
actorAuthMethod,
|
|
actorOrgId,
|
|
name,
|
|
environment,
|
|
path: secretPath
|
|
}: TCreateFolderDTO) => {
|
|
const { permission } = await permissionService.getProjectPermission(
|
|
actor,
|
|
actorId,
|
|
projectId,
|
|
actorAuthMethod,
|
|
actorOrgId
|
|
);
|
|
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
ProjectPermissionActions.Create,
|
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
|
);
|
|
|
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
|
if (!env) {
|
|
throw new NotFoundError({
|
|
message: `Environment with slug '${environment}' in project with ID '${projectId}' not found`
|
|
});
|
|
}
|
|
|
|
const folder = await folderDAL.transaction(async (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 NotFoundError({
|
|
message: `Folder with path '${pathWithFolder}' in environment with slug '${environment}' 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) => {
|
|
const newFolder = {
|
|
name: segment,
|
|
parentId: parentFolderId,
|
|
id: uuidv4(),
|
|
envId: env.id,
|
|
version: 1
|
|
};
|
|
parentFolderId = newFolder.id;
|
|
return newFolder;
|
|
});
|
|
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: parentFolderId }, tx);
|
|
await folderVersionDAL.create(
|
|
{
|
|
name: doc.name,
|
|
envId: doc.envId,
|
|
version: doc.version,
|
|
folderId: doc.id
|
|
},
|
|
tx
|
|
);
|
|
return doc;
|
|
});
|
|
|
|
await snapshotService.performSnapshot(folder.parentId as string);
|
|
return folder;
|
|
};
|
|
|
|
const updateManyFolders = async ({
|
|
actor,
|
|
actorId,
|
|
projectSlug,
|
|
actorAuthMethod,
|
|
actorOrgId,
|
|
folders
|
|
}: TUpdateManyFoldersDTO) => {
|
|
const project = await projectDAL.findProjectBySlug(projectSlug, actorOrgId);
|
|
if (!project) {
|
|
throw new NotFoundError({ message: `Project with slug '${projectSlug}' not found` });
|
|
}
|
|
|
|
const { permission } = await permissionService.getProjectPermission(
|
|
actor,
|
|
actorId,
|
|
project.id,
|
|
actorAuthMethod,
|
|
actorOrgId
|
|
);
|
|
|
|
folders.forEach(({ environment, path: secretPath }) => {
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
ProjectPermissionActions.Edit,
|
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
|
);
|
|
});
|
|
|
|
const result = await folderDAL.transaction(async (tx) =>
|
|
Promise.all(
|
|
folders.map(async (newFolder) => {
|
|
const { environment, path: secretPath, id, name } = newFolder;
|
|
|
|
const parentFolder = await folderDAL.findBySecretPath(project.id, environment, secretPath);
|
|
if (!parentFolder) {
|
|
throw new NotFoundError({
|
|
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`,
|
|
name: "UpdateManyFolders"
|
|
});
|
|
}
|
|
|
|
const env = await projectEnvDAL.findOne({ projectId: project.id, slug: environment });
|
|
if (!env) {
|
|
throw new NotFoundError({
|
|
message: `Environment with slug '${environment}' in project with ID '${project.id}' not found`,
|
|
name: "UpdateManyFolders"
|
|
});
|
|
}
|
|
const folder = await folderDAL
|
|
.findOne({ envId: env.id, id, parentId: parentFolder.id })
|
|
// now folder api accepts id based change
|
|
// this is for cli backward compatiability and when cli removes this, we will remove this logic
|
|
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
|
|
|
|
if (!folder) {
|
|
throw new NotFoundError({
|
|
message: `Folder with id '${id}' in environment with slug '${env.slug}' not found`,
|
|
name: "UpdateManyFolders"
|
|
});
|
|
}
|
|
if (name !== folder.name) {
|
|
// ensure that new folder name is unique
|
|
const folderToCheck = await folderDAL.findOne({
|
|
name,
|
|
envId: env.id,
|
|
parentId: parentFolder.id
|
|
});
|
|
|
|
if (folderToCheck) {
|
|
throw new BadRequestError({
|
|
message: "Folder with specified name already exists",
|
|
name: "Batch update folder"
|
|
});
|
|
}
|
|
}
|
|
|
|
const [doc] = await folderDAL.update(
|
|
{ envId: env.id, id: folder.id, parentId: parentFolder.id },
|
|
{ name },
|
|
tx
|
|
);
|
|
await folderVersionDAL.create(
|
|
{
|
|
name: doc.name,
|
|
envId: doc.envId,
|
|
version: doc.version,
|
|
folderId: doc.id
|
|
},
|
|
tx
|
|
);
|
|
if (!doc) {
|
|
throw new NotFoundError({
|
|
message: `Failed to update folder with id '${id}', not found`,
|
|
name: "UpdateManyFolders"
|
|
});
|
|
}
|
|
|
|
return { oldFolder: folder, newFolder: doc };
|
|
})
|
|
)
|
|
);
|
|
|
|
await Promise.all(result.map(async (res) => snapshotService.performSnapshot(res.newFolder.parentId as string)));
|
|
|
|
return {
|
|
projectId: project.id,
|
|
newFolders: result.map((res) => res.newFolder),
|
|
oldFolders: result.map((res) => res.oldFolder)
|
|
};
|
|
};
|
|
|
|
const updateFolder = async ({
|
|
projectId,
|
|
actor,
|
|
actorId,
|
|
actorOrgId,
|
|
actorAuthMethod,
|
|
name,
|
|
environment,
|
|
path: secretPath,
|
|
id
|
|
}: TUpdateFolderDTO) => {
|
|
const { permission } = await permissionService.getProjectPermission(
|
|
actor,
|
|
actorId,
|
|
projectId,
|
|
actorAuthMethod,
|
|
actorOrgId
|
|
);
|
|
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
ProjectPermissionActions.Edit,
|
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
|
);
|
|
|
|
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
|
if (!parentFolder)
|
|
throw new NotFoundError({
|
|
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`,
|
|
name: "UpdateFolder"
|
|
});
|
|
|
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
|
if (!env) {
|
|
throw new NotFoundError({ message: `Environment with slug '${environment}' not found`, name: "UpdateFolder" });
|
|
}
|
|
const folder = await folderDAL
|
|
.findOne({ envId: env.id, id, parentId: parentFolder.id, isReserved: false })
|
|
// now folder api accepts id based change
|
|
// this is for cli backward compatiability and when cli removes this, we will remove this logic
|
|
.catch(() => folderDAL.findOne({ envId: env.id, name: id, parentId: parentFolder.id }));
|
|
|
|
if (!folder) throw new NotFoundError({ message: `Folder with ID '${id}' not found`, name: "UpdateFolder" });
|
|
if (name !== folder.name) {
|
|
// ensure that new folder name is unique
|
|
const folderToCheck = await folderDAL.findOne({
|
|
name,
|
|
envId: env.id,
|
|
parentId: parentFolder.id
|
|
});
|
|
|
|
if (folderToCheck) {
|
|
throw new BadRequestError({
|
|
message: "Folder with specified name already exists",
|
|
name: "UpdateFolder"
|
|
});
|
|
}
|
|
}
|
|
|
|
const newFolder = await folderDAL.transaction(async (tx) => {
|
|
const [doc] = await folderDAL.update(
|
|
{ envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false },
|
|
{ name },
|
|
tx
|
|
);
|
|
await folderVersionDAL.create(
|
|
{
|
|
name: doc.name,
|
|
envId: doc.envId,
|
|
version: doc.version,
|
|
folderId: doc.id
|
|
},
|
|
tx
|
|
);
|
|
if (!doc) throw new NotFoundError({ message: `Failed to update folder with ID '${id}'`, name: "UpdateFolder" });
|
|
return doc;
|
|
});
|
|
|
|
await snapshotService.performSnapshot(newFolder.parentId as string);
|
|
return { folder: newFolder, old: folder };
|
|
};
|
|
|
|
const deleteFolder = async ({
|
|
projectId,
|
|
actor,
|
|
actorId,
|
|
actorOrgId,
|
|
actorAuthMethod,
|
|
environment,
|
|
path: secretPath,
|
|
idOrName
|
|
}: TDeleteFolderDTO) => {
|
|
const { permission } = await permissionService.getProjectPermission(
|
|
actor,
|
|
actorId,
|
|
projectId,
|
|
actorAuthMethod,
|
|
actorOrgId
|
|
);
|
|
|
|
ForbiddenError.from(permission).throwUnlessCan(
|
|
ProjectPermissionActions.Delete,
|
|
subject(ProjectPermissionSub.SecretFolders, { environment, secretPath })
|
|
);
|
|
|
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
|
if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` });
|
|
|
|
const folder = await folderDAL.transaction(async (tx) => {
|
|
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath, tx);
|
|
if (!parentFolder)
|
|
throw new NotFoundError({
|
|
message: `Folder with path '${secretPath}' in environment with slug '${environment}' not found`
|
|
});
|
|
|
|
const [doc] = await folderDAL.delete(
|
|
{
|
|
envId: env.id,
|
|
[uuidValidate(idOrName) ? "id" : "name"]: idOrName,
|
|
parentId: parentFolder.id,
|
|
isReserved: false
|
|
},
|
|
tx
|
|
);
|
|
if (!doc) throw new NotFoundError({ message: `Failed to delete folder with ID '${idOrName}', not found` });
|
|
return doc;
|
|
});
|
|
|
|
await snapshotService.performSnapshot(folder.parentId as string);
|
|
return folder;
|
|
};
|
|
|
|
const getFolders = async ({
|
|
projectId,
|
|
actor,
|
|
actorId,
|
|
actorOrgId,
|
|
actorAuthMethod,
|
|
environment,
|
|
path: secretPath,
|
|
search,
|
|
orderBy,
|
|
orderDirection,
|
|
limit,
|
|
offset
|
|
}: TGetFolderDTO) => {
|
|
// folder list is allowed to be read by anyone
|
|
// permission to check does user has access
|
|
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
|
|
|
const env = await projectEnvDAL.findOne({ projectId, slug: environment });
|
|
if (!env) throw new NotFoundError({ message: `Environment with slug '${environment}' not found` });
|
|
|
|
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
|
|
if (!parentFolder) return [];
|
|
|
|
const folders = await folderDAL.find(
|
|
{
|
|
envId: env.id,
|
|
parentId: parentFolder.id,
|
|
isReserved: false,
|
|
$search: search ? { name: `%${search}%` } : undefined
|
|
},
|
|
{
|
|
sort: orderBy ? [[orderBy, orderDirection ?? OrderByDirection.ASC]] : undefined,
|
|
limit,
|
|
offset
|
|
}
|
|
);
|
|
return folders;
|
|
};
|
|
|
|
// get folders for multiple envs
|
|
const getFoldersMultiEnv = async ({
|
|
projectId,
|
|
actor,
|
|
actorId,
|
|
actorOrgId,
|
|
actorAuthMethod,
|
|
environments,
|
|
path: secretPath,
|
|
...params
|
|
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
|
|
// folder list is allowed to be read by anyone
|
|
// permission to check does user has access
|
|
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
|
|
|
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
|
|
|
|
if (!envs.length)
|
|
throw new NotFoundError({
|
|
message: `Environments '${environments.join(", ")}' not found`,
|
|
name: "GetFoldersMultiEnv"
|
|
});
|
|
|
|
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
|
if (!parentFolders.length) return [];
|
|
|
|
const folders = await folderDAL.findByMultiEnv({
|
|
environmentIds: envs.map((env) => env.id),
|
|
parentIds: parentFolders.map((folder) => folder.id),
|
|
...params
|
|
});
|
|
|
|
return folders;
|
|
};
|
|
|
|
// get the unique count of folders within a project path
|
|
const getProjectFolderCount = async ({
|
|
projectId,
|
|
actor,
|
|
actorId,
|
|
actorOrgId,
|
|
actorAuthMethod,
|
|
environments,
|
|
path: secretPath,
|
|
search
|
|
}: Omit<TGetFolderDTO, "environment"> & { environments: string[] }) => {
|
|
// folder list is allowed to be read by anyone
|
|
// permission to check does user has access
|
|
await permissionService.getProjectPermission(actor, actorId, projectId, actorAuthMethod, actorOrgId);
|
|
|
|
const envs = await projectEnvDAL.findBySlugs(projectId, environments);
|
|
|
|
if (!envs.length) throw new NotFoundError({ message: `Environments '${environments.join(", ")}' not found` });
|
|
|
|
const parentFolders = await folderDAL.findBySecretPathMultiEnv(projectId, environments, secretPath);
|
|
if (!parentFolders.length) return 0;
|
|
|
|
const folders = await folderDAL.find(
|
|
{
|
|
$in: {
|
|
envId: envs.map((env) => env.id),
|
|
parentId: parentFolders.map((folder) => folder.id)
|
|
},
|
|
isReserved: false,
|
|
$search: search ? { name: `%${search}%` } : undefined
|
|
},
|
|
{ countDistinct: "name" }
|
|
);
|
|
|
|
return Number(folders[0]?.count ?? 0);
|
|
};
|
|
|
|
const getFolderById = async ({ actor, actorId, actorOrgId, actorAuthMethod, id }: TGetFolderByIdDTO) => {
|
|
const folder = await folderDAL.findById(id);
|
|
if (!folder) throw new NotFoundError({ message: `Folder with ID '${id}' not found` });
|
|
// folder list is allowed to be read by anyone
|
|
// permission to check does user has access
|
|
await permissionService.getProjectPermission(actor, actorId, folder.projectId, actorAuthMethod, actorOrgId);
|
|
|
|
const [folderWithPath] = await folderDAL.findSecretPathByFolderIds(folder.projectId, [folder.id]);
|
|
|
|
if (!folderWithPath) {
|
|
throw new NotFoundError({
|
|
message: `Folder with ID '${folder.id}' in project with ID '${folder.projectId}' not found`
|
|
});
|
|
}
|
|
|
|
return {
|
|
...folder,
|
|
path: folderWithPath.path
|
|
};
|
|
};
|
|
|
|
return {
|
|
createFolder,
|
|
updateFolder,
|
|
updateManyFolders,
|
|
deleteFolder,
|
|
getFolders,
|
|
getFolderById,
|
|
getProjectFolderCount,
|
|
getFoldersMultiEnv
|
|
};
|
|
};
|