import { ForbiddenError, subject } from "@casl/ability"; import { TableName } from "@app/db/schemas"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { BadRequestError } from "@app/lib/errors"; import { TProjectDALFactory } from "../project/project-dal"; import { TProjectEnvDALFactory } from "../project-env/project-env-dal"; import { TSecretDALFactory } from "../secret/secret-dal"; import { TSecretQueueFactory } from "../secret/secret-queue"; import { SecretOperations } from "../secret/secret-types"; import { TSecretFolderDALFactory } from "../secret-folder/secret-folder-dal"; import { TSecretImportDALFactory } from "./secret-import-dal"; import { fnSecretsFromImports } from "./secret-import-fns"; import { TCreateSecretImportDTO, TDeleteSecretImportDTO, TGetSecretImportsDTO, TGetSecretsFromImportDTO, TResyncSecretImportReplicationDTO, TUpdateSecretImportDTO } from "./secret-import-types"; type TSecretImportServiceFactoryDep = { secretImportDAL: TSecretImportDALFactory; folderDAL: TSecretFolderDALFactory; secretDAL: Pick; projectDAL: Pick; projectEnvDAL: TProjectEnvDALFactory; permissionService: Pick; secretQueueService: Pick; licenseService: Pick; }; const ERR_SEC_IMP_NOT_FOUND = new BadRequestError({ message: "Secret import not found" }); export type TSecretImportServiceFactory = ReturnType; export const secretImportServiceFactory = ({ secretImportDAL, projectEnvDAL, permissionService, folderDAL, projectDAL, secretDAL, secretQueueService, licenseService }: TSecretImportServiceFactoryDep) => { const createImport = async ({ environment, data, actor, actorId, actorOrgId, actorAuthMethod, projectId, isReplication, path }: TCreateSecretImportDTO) => { const { permission } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); // 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 }) ); if (isReplication) { const plan = await licenseService.getPlan(actorOrgId); if (!plan.secretReplication) { throw new BadRequestError({ message: "Failed to create secret replication due to plan restriction. Upgrade plan to create replication." }); } } await projectDAL.checkProjectUpgradeStatus(projectId); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" }); const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]); if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); const sourceFolder = await folderDAL.findBySecretPath(projectId, data.environment, data.path); if (sourceFolder) { const existingImport = await secretImportDAL.findOne({ folderId: sourceFolder.id, importEnv: folder.environment.id, importPath: path }); if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); } const secImport = await secretImportDAL.transaction(async (tx) => { const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx); return secretImportDAL.create( { folderId: folder.id, position: lastPos + 1, importEnv: importEnv.id, importPath: data.path, isReplication }, tx ); }); if (secImport.isReplication && sourceFolder) { const importedSecrets = await secretDAL.find({ folderId: sourceFolder?.id }); await secretQueueService.replicateSecrets({ secretPath: secImport.importPath, projectId, environmentSlug: importEnv.slug, pickOnlyImportIds: [secImport.id], folderId: sourceFolder.id, secrets: importedSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id })), actorId, actor, environmentId: importEnv.id }); } else { await secretQueueService.syncSecrets({ secretPath: secImport.importPath, projectId, environmentSlug: importEnv.slug, excludeReplication: true }); } return { ...secImport, importEnv }; }; const updateImport = async ({ path, environment, projectId, actor, actorId, actorOrgId, actorAuthMethod, data, id }: TUpdateSecretImportDTO) => { const { permission } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Edit, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" }); const secImpDoc = await secretImportDAL.findOne({ folderId: folder.id, id }); if (!secImpDoc) throw ERR_SEC_IMP_NOT_FOUND; const importedEnv = data.environment // this is get env information of new one or old one ? (await projectEnvDAL.findBySlugs(projectId, [data.environment]))?.[0] : await projectEnvDAL.findById(secImpDoc.importEnv); if (!importedEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); const sourceFolder = await folderDAL.findBySecretPath( projectId, importedEnv.slug, data.path || secImpDoc.importPath ); if (sourceFolder) { const existingImport = await secretImportDAL.findOne({ folderId: sourceFolder.id, importEnv: folder.environment.id, importPath: path }); if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); } const updatedSecImport = await secretImportDAL.transaction(async (tx) => { const secImp = await secretImportDAL.findOne({ folderId: folder.id, id }); if (!secImp) throw ERR_SEC_IMP_NOT_FOUND; if (data.position) { await secretImportDAL.updateAllPosition(folder.id, secImp.position, data.position, tx); } const [doc] = await secretImportDAL.update( { id, folderId: folder.id }, { position: data?.position, importEnv: data?.environment ? importedEnv.id : undefined, importPath: data?.path }, tx ); return doc; }); return { ...updatedSecImport, importEnv: importedEnv }; }; const deleteImport = async ({ path, environment, projectId, actor, actorId, actorOrgId, actorAuthMethod, id }: TDeleteSecretImportDTO) => { const { permission } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Delete, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Delete import" }); const secImport = await secretImportDAL.transaction(async (tx) => { const [doc] = await secretImportDAL.delete({ folderId: folder.id, id }, tx); if (!doc) throw new BadRequestError({ name: "Sec imp del", message: "Secret import doc not found" }); await secretImportDAL.updateAllPosition(folder.id, doc.position, -1, tx); const importEnv = await projectEnvDAL.findById(doc.importEnv); if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); return { ...doc, importEnv }; }); await secretQueueService.syncSecrets({ secretPath: path, projectId, environmentSlug: environment, excludeReplication: true }); return secImport; }; const resyncSecretImportReplication = async ({ environment, actor, actorId, actorOrgId, actorAuthMethod, projectId, path, id: secretImportDocId }: TResyncSecretImportReplicationDTO) => { const { permission, membership } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); // check if user has permission to import into destination path ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const plan = await licenseService.getPlan(actorOrgId); if (!plan.secretReplication) { throw new BadRequestError({ message: "Failed to create secret replication due to plan restriction. Upgrade plan to create replication." }); } const folder = await folderDAL.findBySecretPath(projectId, environment, path); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" }); const [secretImportDoc] = await secretImportDAL.find({ folderId: folder.id, [`${TableName.SecretImport}.id` as "id"]: secretImportDocId }); if (!secretImportDoc) throw new BadRequestError({ message: "Failed to find secret import" }); if (!secretImportDoc.isReplication) throw new BadRequestError({ message: "Import is not in replication mode" }); // check if user has permission to import from target path ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Create, subject(ProjectPermissionSub.Secrets, { environment: secretImportDoc.importEnv.slug, secretPath: secretImportDoc.importPath }) ); await projectDAL.checkProjectUpgradeStatus(projectId); const sourceFolder = await folderDAL.findBySecretPath( projectId, secretImportDoc.importEnv.slug, secretImportDoc.importPath ); const importedSecrets = await secretDAL.find({ folderId: sourceFolder?.id }); if (membership && sourceFolder) { await secretQueueService.replicateSecrets({ secretPath: secretImportDoc.importPath, projectId, environmentSlug: secretImportDoc.importEnv.slug, pickOnlyImportIds: [secretImportDoc.id], folderId: sourceFolder.id, secrets: importedSecrets.map(({ id, version }) => ({ operation: SecretOperations.Create, version, id })), actorId, actor, environmentId: secretImportDoc.importEnv.id }); } return { message: "replication started" }; }; const getImports = async ({ path, environment, projectId, actor, actorId, actorAuthMethod, actorOrgId }: TGetSecretImportsDTO) => { const { permission } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, 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" }); const secImports = await secretImportDAL.find({ folderId: folder.id }); return secImports; }; const getSecretsFromImports = async ({ path, environment, projectId, actor, actorAuthMethod, actorId, actorOrgId }: TGetSecretsFromImportDTO) => { const { permission } = await permissionService.getProjectPermission( actor, actorId, projectId, actorAuthMethod, actorOrgId ); ForbiddenError.from(permission).throwUnlessCan( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) ); const folder = await folderDAL.findBySecretPath(projectId, environment, path); 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, isReplication: false }); const allowedImports = secretImports.filter(({ importEnv, importPath }) => permission.can( ProjectPermissionActions.Read, subject(ProjectPermissionSub.Secrets, { environment: importEnv.slug, secretPath: importPath }) ) ); return fnSecretsFromImports({ allowedImports, folderDAL, secretDAL, secretImportDAL }); }; return { createImport, updateImport, deleteImport, getImports, getSecretsFromImports, resyncSecretImportReplication, fnSecretsFromImports }; };