feat: implemented replication to a folder strategy

This commit is contained in:
=
2024-05-31 23:10:03 +05:30
parent f426025fd5
commit 5db6ac711c
12 changed files with 147 additions and 56 deletions
@@ -13,6 +13,7 @@ export async function up(knex: Knex): Promise<void> {
"replicationStatus" "replicationStatus"
); );
const doesSecretImportLastReplicatedExist = await knex.schema.hasColumn(TableName.SecretImport, "lastReplicated"); const doesSecretImportLastReplicatedExist = await knex.schema.hasColumn(TableName.SecretImport, "lastReplicated");
const doesSecretImportIsReservedExist = await knex.schema.hasColumn(TableName.SecretImport, "isReserved");
if (await knex.schema.hasTable(TableName.SecretImport)) { if (await knex.schema.hasTable(TableName.SecretImport)) {
await knex.schema.alterTable(TableName.SecretImport, (t) => { await knex.schema.alterTable(TableName.SecretImport, (t) => {
@@ -20,6 +21,14 @@ export async function up(knex: Knex): Promise<void> {
if (!doesSecretImportIsReplicationSuccessExist) t.boolean("isReplicationSuccess").nullable(); if (!doesSecretImportIsReplicationSuccessExist) t.boolean("isReplicationSuccess").nullable();
if (!doesSecretImportReplicationStatusExist) t.text("replicationStatus").nullable(); if (!doesSecretImportReplicationStatusExist) t.text("replicationStatus").nullable();
if (!doesSecretImportLastReplicatedExist) t.datetime("lastReplicated").nullable(); if (!doesSecretImportLastReplicatedExist) t.datetime("lastReplicated").nullable();
if (!doesSecretImportIsReservedExist) t.boolean("isReserved").defaultTo(false);
});
}
const doesSecretFolderReservedExist = await knex.schema.hasColumn(TableName.SecretFolder, "isReserved");
if (await knex.schema.hasTable(TableName.SecretFolder)) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
if (!doesSecretFolderReservedExist) t.boolean("isReserved").defaultTo(false);
}); });
} }
@@ -69,12 +78,22 @@ export async function down(knex: Knex): Promise<void> {
"replicationStatus" "replicationStatus"
); );
const doesSecretImportLastReplicatedExist = await knex.schema.hasColumn(TableName.SecretImport, "lastReplicated"); const doesSecretImportLastReplicatedExist = await knex.schema.hasColumn(TableName.SecretImport, "lastReplicated");
const doesSecretImportIsReservedExist = await knex.schema.hasColumn(TableName.SecretImport, "isReserved");
if (await knex.schema.hasTable(TableName.SecretImport)) { if (await knex.schema.hasTable(TableName.SecretImport)) {
await knex.schema.alterTable(TableName.SecretImport, (t) => { await knex.schema.alterTable(TableName.SecretImport, (t) => {
if (doesSecretImportIsReplicationExist) t.dropColumn("isReplication"); if (doesSecretImportIsReplicationExist) t.dropColumn("isReplication");
if (doesSecretImportIsReplicationSuccessExist) t.dropColumn("isReplicationSuccess"); if (doesSecretImportIsReplicationSuccessExist) t.dropColumn("isReplicationSuccess");
if (doesSecretImportReplicationStatusExist) t.dropColumn("replicationStatus"); if (doesSecretImportReplicationStatusExist) t.dropColumn("replicationStatus");
if (doesSecretImportLastReplicatedExist) t.dropColumn("lastReplicated"); if (doesSecretImportLastReplicatedExist) t.dropColumn("lastReplicated");
if (doesSecretImportIsReservedExist) t.dropColumn("isReserved");
});
}
const doesSecretFolderReservedExist = await knex.schema.hasColumn(TableName.SecretFolder, "isReserved");
if (await knex.schema.hasTable(TableName.SecretFolder)) {
await knex.schema.alterTable(TableName.SecretFolder, (t) => {
if (doesSecretFolderReservedExist) t.dropColumn("isReserved");
}); });
} }
+2 -1
View File
@@ -14,7 +14,8 @@ export const SecretFoldersSchema = z.object({
createdAt: z.date(), createdAt: z.date(),
updatedAt: z.date(), updatedAt: z.date(),
envId: z.string().uuid(), envId: z.string().uuid(),
parentId: z.string().uuid().nullable().optional() parentId: z.string().uuid().nullable().optional(),
isReserved: z.boolean().default(false).nullable().optional()
}); });
export type TSecretFolders = z.infer<typeof SecretFoldersSchema>; export type TSecretFolders = z.infer<typeof SecretFoldersSchema>;
+2 -1
View File
@@ -19,7 +19,8 @@ export const SecretImportsSchema = z.object({
isReplication: z.boolean().default(false).nullable().optional(), isReplication: z.boolean().default(false).nullable().optional(),
isReplicationSuccess: z.boolean().nullable().optional(), isReplicationSuccess: z.boolean().nullable().optional(),
replicationStatus: z.string().nullable().optional(), replicationStatus: z.string().nullable().optional(),
lastReplicated: z.date().nullable().optional() lastReplicated: z.date().nullable().optional(),
isReserved: z.boolean().default(false).nullable().optional()
}); });
export type TSecretImports = z.infer<typeof SecretImportsSchema>; export type TSecretImports = z.infer<typeof SecretImportsSchema>;
@@ -601,7 +601,7 @@ export const secretApprovalRequestServiceFactory = ({
// same process as above // same process as above
const nameUpdatedSecrets = updatedSecrets.filter(({ newSecretName }) => Boolean(newSecretName)); const nameUpdatedSecrets = updatedSecrets.filter(({ newSecretName }) => Boolean(newSecretName));
const { keyName2BlindIndex: newKeyName2BlindIndex } = await fnSecretBlindIndexCheck({ const { keyName2BlindIndex: newKeyName2BlindIndex } = await fnSecretBlindIndexCheck({
inputSecrets: nameUpdatedSecrets, inputSecrets: nameUpdatedSecrets.map(({ newSecretName }) => ({ secretName: newSecretName as string })),
folderId, folderId,
isNew: true, isNew: true,
blindIndexCfg, blindIndexCfg,
@@ -618,7 +618,7 @@ export const secretApprovalRequestServiceFactory = ({
const secretId = secsGroupedByBlindIndex[keyName2BlindIndex[secretName]][0].id; const secretId = secsGroupedByBlindIndex[keyName2BlindIndex[secretName]][0].id;
const secretBlindIndex = const secretBlindIndex =
newSecretName && newKeyName2BlindIndex[newSecretName] newSecretName && newKeyName2BlindIndex[newSecretName]
? newKeyName2BlindIndex?.[secretName] ? newKeyName2BlindIndex?.[newSecretName]
: keyName2BlindIndex[secretName]; : keyName2BlindIndex[secretName];
// add tags // add tags
if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds; if (tagIds?.length) commitTagIds[keyName2BlindIndex[secretName]] = tagIds;
@@ -18,6 +18,7 @@ import { TSecretVersionDALFactory } from "@app/services/secret/secret-version-da
import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal"; import { TSecretVersionTagDALFactory } from "@app/services/secret/secret-version-tag-dal";
import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal"; import { TSecretBlindIndexDALFactory } from "@app/services/secret-blind-index/secret-blind-index-dal";
import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal"; import { TSecretFolderDALFactory } from "@app/services/secret-folder/secret-folder-dal";
import { ReservedFolders } from "@app/services/secret-folder/secret-folder-types";
import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal"; import { TSecretImportDALFactory } from "@app/services/secret-import/secret-import-dal";
import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal"; import { TSecretTagDALFactory } from "@app/services/secret-tag/secret-tag-dal";
@@ -31,7 +32,7 @@ type TSecretReplicationServiceFactoryDep = {
>; >;
secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">; secretVersionDAL: Pick<TSecretVersionDALFactory, "find" | "insertMany" | "update" | "findLatestVersionMany">;
secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById">; secretImportDAL: Pick<TSecretImportDALFactory, "find" | "updateById">;
folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds" | "findBySecretPath">; folderDAL: Pick<TSecretFolderDALFactory, "findSecretPathByFolderIds" | "findBySecretPath" | "create" | "findOne">;
secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">; secretVersionTagDAL: Pick<TSecretVersionTagDALFactory, "find" | "insertMany">;
secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">; secretQueueService: Pick<TSecretQueueFactory, "syncSecrets">;
snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">; snapshotService: Pick<TSecretSnapshotServiceFactory, "performSnapshot">;
@@ -52,6 +53,7 @@ export type TSecretReplicationServiceFactory = ReturnType<typeof secretReplicati
const SECRET_IMPORT_SUCCESS_LOCK = 10; const SECRET_IMPORT_SUCCESS_LOCK = 10;
const keystoreReplicationSuccessKey = (jobId: string, secretImportId: string) => `${jobId}-${secretImportId}`; const keystoreReplicationSuccessKey = (jobId: string, secretImportId: string) => `${jobId}-${secretImportId}`;
const getReplicationKeyLockPrefix = (keyName: string) => `REPLICATION_SECRET_${keyName}`; const getReplicationKeyLockPrefix = (keyName: string) => `REPLICATION_SECRET_${keyName}`;
export const getReplicationFolderName = (importId: string) => `${ReservedFolders.SecretReplication}${importId}`;
export const secretReplicationServiceFactory = ({ export const secretReplicationServiceFactory = ({
secretReplicationDAL, secretReplicationDAL,
@@ -67,7 +69,6 @@ export const secretReplicationServiceFactory = ({
secretApprovalRequestSecretDAL, secretApprovalRequestSecretDAL,
secretApprovalRequestDAL, secretApprovalRequestDAL,
secretQueueService, secretQueueService,
snapshotService,
projectMembershipDAL projectMembershipDAL
}: TSecretReplicationServiceFactoryDep) => { }: TSecretReplicationServiceFactoryDep) => {
queueService.start(QueueName.SecretReplication, async (job) => { queueService.start(QueueName.SecretReplication, async (job) => {
@@ -131,11 +132,25 @@ export const secretReplicationServiceFactory = ({
const [importedFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [secretImport.folderId]); const [importedFolder] = await folderDAL.findSecretPathByFolderIds(projectId, [secretImport.folderId]);
if (!importedFolder) throw new BadRequestError({ message: "Imported folder not found" }); if (!importedFolder) throw new BadRequestError({ message: "Imported folder not found" });
const importFolderId = importedFolder.id;
let replicationFolder = await folderDAL.findOne({
parentId: importedFolder.id,
name: getReplicationFolderName(secretImport.id),
isReserved: true
});
if (!replicationFolder) {
replicationFolder = await folderDAL.create({
parentId: importedFolder.id,
name: getReplicationFolderName(secretImport.id),
envId: importedFolder.envId,
isReserved: true
});
}
const replicationFolderId = replicationFolder.id;
const localSecrets = await secretDAL.find({ const localSecrets = await secretDAL.find({
$in: { secretBlindIndex: replicatedSecrets.map(({ secretBlindIndex }) => secretBlindIndex) }, $in: { secretBlindIndex: replicatedSecrets.map(({ secretBlindIndex }) => secretBlindIndex) },
folderId: importFolderId folderId: replicationFolderId
}); });
const localSecretsGroupedByBlindIndex = groupBy(localSecrets, (i) => i.secretBlindIndex as string); const localSecretsGroupedByBlindIndex = groupBy(localSecrets, (i) => i.secretBlindIndex as string);
@@ -181,13 +196,13 @@ export const secretReplicationServiceFactory = ({
const localSecretsLatestVersions = localSecrets.map(({ id }) => id); const localSecretsLatestVersions = localSecrets.map(({ id }) => id);
const latestSecretVersions = await secretVersionDAL.findLatestVersionMany( const latestSecretVersions = await secretVersionDAL.findLatestVersionMany(
importFolderId, replicationFolderId,
localSecretsLatestVersions localSecretsLatestVersions
); );
await secretApprovalRequestDAL.transaction(async (tx) => { await secretApprovalRequestDAL.transaction(async (tx) => {
const approvalRequestDoc = await secretApprovalRequestDAL.create( const approvalRequestDoc = await secretApprovalRequestDAL.create(
{ {
folderId: importFolderId, folderId: replicationFolderId,
slug: alphaNumericNanoId(), slug: alphaNumericNanoId(),
policyId: policy.id, policyId: policy.id,
status: "open", status: "open",
@@ -237,7 +252,7 @@ export const secretReplicationServiceFactory = ({
await secretReplicationDAL.transaction(async (tx) => { await secretReplicationDAL.transaction(async (tx) => {
if (locallyCreatedSecrets.length) { if (locallyCreatedSecrets.length) {
const newSecrets = await fnSecretBulkInsert({ const newSecrets = await fnSecretBulkInsert({
folderId: importFolderId, folderId: replicationFolderId,
secretVersionDAL, secretVersionDAL,
secretDAL, secretDAL,
tx, tx,
@@ -272,7 +287,7 @@ export const secretReplicationServiceFactory = ({
if (locallyUpdatedSecrets.length) { if (locallyUpdatedSecrets.length) {
const newSecrets = await fnSecretBulkUpdate({ const newSecrets = await fnSecretBulkUpdate({
projectId, projectId,
folderId: importFolderId, folderId: replicationFolderId,
secretVersionDAL, secretVersionDAL,
secretDAL, secretDAL,
tx, tx,
@@ -282,7 +297,7 @@ export const secretReplicationServiceFactory = ({
const doc = replicatedSecretsGroupBySecretId[id][0]; const doc = replicatedSecretsGroupBySecretId[id][0];
return { return {
filter: { filter: {
folderId: importFolderId, folderId: replicationFolderId,
id: localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id id: localSecretsGroupedByBlindIndex[doc.secretBlindIndex as string][0].id
}, },
data: { data: {
@@ -317,7 +332,7 @@ export const secretReplicationServiceFactory = ({
id: locallyDeletedSecrets.map(({ id }) => id) id: locallyDeletedSecrets.map(({ id }) => id)
}, },
isReplicated: true, isReplicated: true,
folderId: importFolderId folderId: replicationFolderId
}, },
tx tx
); );
@@ -327,14 +342,6 @@ export const secretReplicationServiceFactory = ({
} }
}); });
const folderLock = await keyStore
.acquireLock([`secret-replication-${importFolderId}`], 5000)
.catch(() => null);
if (folderLock) {
await snapshotService.performSnapshot(importFolderId);
await folderLock.release();
}
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
projectId, projectId,
secretPath: importedFolder.path, secretPath: importedFolder.path,
@@ -220,7 +220,7 @@ export const secretSnapshotServiceFactory = ({
const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id); const deletedTopLevelSecsGroupById = groupBy(deletedTopLevelSecs, (item) => item.id);
// this will remove all secrets and folders on child // this will remove all secrets and folders on child
// due to sql foreign key and link list connection removing the folders removes everything below too // due to sql foreign key and link list connection removing the folders removes everything below too
const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId }, tx); const deletedFolders = await folderDAL.delete({ parentId: snapshot.folderId, isReserved: false }, tx);
const deletedTopLevelFolders = groupBy( const deletedTopLevelFolders = groupBy(
deletedFolders.filter(({ parentId }) => parentId === snapshot.folderId), deletedFolders.filter(({ parentId }) => parentId === snapshot.folderId),
(item) => item.id (item) => item.id
@@ -276,7 +276,11 @@ export const secretFolderServiceFactory = ({
} }
const newFolder = await folderDAL.transaction(async (tx) => { const newFolder = await folderDAL.transaction(async (tx) => {
const [doc] = await folderDAL.update({ envId: env.id, id: folder.id, parentId: parentFolder.id }, { name }, tx); const [doc] = await folderDAL.update(
{ envId: env.id, id: folder.id, parentId: parentFolder.id, isReserved: false },
{ name },
tx
);
await folderVersionDAL.create( await folderVersionDAL.create(
{ {
name: doc.name, name: doc.name,
@@ -354,7 +358,7 @@ export const secretFolderServiceFactory = ({
const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath); const parentFolder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!parentFolder) return []; if (!parentFolder) return [];
const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id }); const folders = await folderDAL.find({ envId: env.id, parentId: parentFolder.id, isReserved: false });
return folders; return folders;
}; };
@@ -1,5 +1,9 @@
import { TProjectPermission } from "@app/lib/types"; import { TProjectPermission } from "@app/lib/types";
export enum ReservedFolders {
SecretReplication = "__reserve_replication_"
}
export type TCreateFolderDTO = { export type TCreateFolderDTO = {
environment: string; environment: string;
path: string; path: string;
@@ -15,7 +15,7 @@ export const secretFolderVersionDALFactory = (db: TDbClient) => {
try { try {
const docs = await (tx || db)(TableName.SecretFolderVersion) const docs = await (tx || db)(TableName.SecretFolderVersion)
.join(TableName.SecretFolder, `${TableName.SecretFolderVersion}.folderId`, `${TableName.SecretFolder}.id`) .join(TableName.SecretFolder, `${TableName.SecretFolderVersion}.folderId`, `${TableName.SecretFolder}.id`)
.where({ parentId: folderId }) .where({ parentId: folderId, isReserved: false })
.join<TSecretFolderVersions>( .join<TSecretFolderVersions>(
(tx || db)(TableName.SecretFolderVersion) (tx || db)(TableName.SecretFolderVersion)
.groupBy("envId", "folderId") .groupBy("envId", "folderId")
@@ -20,14 +20,14 @@ export const secretImportDALFactory = (db: TDbClient) => {
return lastPos?.position || 0; return lastPos?.position || 0;
}; };
const updateAllPosition = async (folderId: string, pos: number, targetPos: number, tx?: Knex) => { const updateAllPosition = async (folderId: string, pos: number, targetPos: number, positionInc = 1, tx?: Knex) => {
try { try {
if (targetPos === -1) { if (targetPos === -1) {
// this means delete // this means delete
await (tx || db)(TableName.SecretImport) await (tx || db)(TableName.SecretImport)
.where({ folderId }) .where({ folderId })
.andWhere("position", ">", pos) .andWhere("position", ">", pos)
.decrement("position", 1); .decrement("position", positionInc);
return; return;
} }
@@ -36,13 +36,13 @@ export const secretImportDALFactory = (db: TDbClient) => {
.where({ folderId }) .where({ folderId })
.where("position", "<=", targetPos) .where("position", "<=", targetPos)
.andWhere("position", ">", pos) .andWhere("position", ">", pos)
.decrement("position", 1); .decrement("position", positionInc);
} else { } else {
await (tx || db)(TableName.SecretImport) await (tx || db)(TableName.SecretImport)
.where({ folderId }) .where({ folderId })
.where("position", ">=", targetPos) .where("position", ">=", targetPos)
.andWhere("position", "<", pos) .andWhere("position", "<", pos)
.increment("position", 1); .increment("position", positionInc);
} }
} catch (error) { } catch (error) {
throw new DatabaseError({ error, name: "Update position" }); throw new DatabaseError({ error, name: "Update position" });
@@ -74,6 +74,7 @@ export const secretImportDALFactory = (db: TDbClient) => {
try { try {
const docs = await (tx || db)(TableName.SecretImport) const docs = await (tx || db)(TableName.SecretImport)
.whereIn("folderId", folderIds) .whereIn("folderId", folderIds)
.where("isReplication", false)
.join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`) .join(TableName.Environment, `${TableName.SecretImport}.importEnv`, `${TableName.Environment}.id`)
.select( .select(
db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports, db.ref("*").withSchema(TableName.SecretImport) as unknown as keyof TSecretImports,
@@ -1,9 +1,12 @@
import path from "node:path";
import { ForbiddenError, subject } from "@casl/ability"; import { ForbiddenError, subject } from "@casl/ability";
import { TableName } from "@app/db/schemas"; import { TableName } from "@app/db/schemas";
import { TLicenseServiceFactory } from "@app/ee/services/license/license-service"; import { TLicenseServiceFactory } from "@app/ee/services/license/license-service";
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service";
import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission"; import { ProjectPermissionActions, ProjectPermissionSub } from "@app/ee/services/permission/project-permission";
import { getReplicationFolderName } from "@app/ee/services/secret-replication/secret-replication-service";
import { BadRequestError } from "@app/lib/errors"; import { BadRequestError } from "@app/lib/errors";
import { TProjectDALFactory } from "../project/project-dal"; import { TProjectDALFactory } from "../project/project-dal";
@@ -57,7 +60,7 @@ export const secretImportServiceFactory = ({
actorAuthMethod, actorAuthMethod,
projectId, projectId,
isReplication, isReplication,
path path: secretPath
}: TCreateSecretImportDTO) => { }: TCreateSecretImportDTO) => {
const { permission } = await permissionService.getProjectPermission( const { permission } = await permissionService.getProjectPermission(
actor, actor,
@@ -70,7 +73,7 @@ export const secretImportServiceFactory = ({
// check if user has permission to import into destination path // check if user has permission to import into destination path
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
// check if user has permission to import from target path // check if user has permission to import from target path
@@ -92,7 +95,7 @@ export const secretImportServiceFactory = ({
await projectDAL.checkProjectUpgradeStatus(projectId); await projectDAL.checkProjectUpgradeStatus(projectId);
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Create import" });
const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]); const [importEnv] = await projectEnvDAL.findBySlugs(projectId, [data.environment]);
@@ -103,14 +106,14 @@ export const secretImportServiceFactory = ({
const existingImport = await secretImportDAL.findOne({ const existingImport = await secretImportDAL.findOne({
folderId: sourceFolder.id, folderId: sourceFolder.id,
importEnv: folder.environment.id, importEnv: folder.environment.id,
importPath: path importPath: secretPath
}); });
if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
} }
const secImport = await secretImportDAL.transaction(async (tx) => { const secImport = await secretImportDAL.transaction(async (tx) => {
const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx); const lastPos = await secretImportDAL.findLastImportPosition(folder.id, tx);
return secretImportDAL.create( const doc = await secretImportDAL.create(
{ {
folderId: folder.id, folderId: folder.id,
position: lastPos + 1, position: lastPos + 1,
@@ -120,6 +123,19 @@ export const secretImportServiceFactory = ({
}, },
tx tx
); );
if (doc.isReplication) {
await secretImportDAL.create(
{
folderId: folder.id,
position: lastPos + 2,
isReserved: true,
importEnv: folder.environment.id,
importPath: path.join(secretPath, getReplicationFolderName(doc.id))
},
tx
);
}
return doc;
}); });
if (secImport.isReplication && sourceFolder) { if (secImport.isReplication && sourceFolder) {
@@ -148,7 +164,7 @@ export const secretImportServiceFactory = ({
}; };
const updateImport = async ({ const updateImport = async ({
path, path: secretPath,
environment, environment,
projectId, projectId,
actor, actor,
@@ -167,10 +183,10 @@ export const secretImportServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Edit, ProjectPermissionActions.Edit,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" });
const secImpDoc = await secretImportDAL.findOne({ folderId: folder.id, id }); const secImpDoc = await secretImportDAL.findOne({ folderId: folder.id, id });
@@ -190,7 +206,7 @@ export const secretImportServiceFactory = ({
const existingImport = await secretImportDAL.findOne({ const existingImport = await secretImportDAL.findOne({
folderId: sourceFolder.id, folderId: sourceFolder.id,
importEnv: folder.environment.id, importEnv: folder.environment.id,
importPath: path importPath: secretPath
}); });
if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" }); if (existingImport) throw new BadRequestError({ message: "Cyclic import not allowed" });
} }
@@ -199,12 +215,31 @@ export const secretImportServiceFactory = ({
const secImp = await secretImportDAL.findOne({ folderId: folder.id, id }); const secImp = await secretImportDAL.findOne({ folderId: folder.id, id });
if (!secImp) throw ERR_SEC_IMP_NOT_FOUND; if (!secImp) throw ERR_SEC_IMP_NOT_FOUND;
if (data.position) { if (data.position) {
await secretImportDAL.updateAllPosition(folder.id, secImp.position, data.position, tx); if (secImp.isReplication) {
await secretImportDAL.updateAllPosition(folder.id, secImp.position, data.position, 2, tx);
} else {
await secretImportDAL.updateAllPosition(folder.id, secImp.position, data.position, 1, tx);
}
}
if (secImp.isReplication) {
const replicationFolderPath = path.join(secretPath, getReplicationFolderName(secImp.id));
await secretImportDAL.update(
{
folderId: folder.id,
importEnv: folder.environment.id,
importPath: replicationFolderPath,
isReserved: true
},
{ position: data?.position ? data.position + 1 : undefined },
tx
);
} }
const [doc] = await secretImportDAL.update( const [doc] = await secretImportDAL.update(
{ id, folderId: folder.id }, { id, folderId: folder.id },
{ {
position: data?.position, // when moving replicated import, the position is meant for reserved import
// replicated one should always be behind the reserved import
position: data.position,
importEnv: data?.environment ? importedEnv.id : undefined, importEnv: data?.environment ? importedEnv.id : undefined,
importPath: data?.path importPath: data?.path
}, },
@@ -216,7 +251,7 @@ export const secretImportServiceFactory = ({
}; };
const deleteImport = async ({ const deleteImport = async ({
path, path: secretPath,
environment, environment,
projectId, projectId,
actor, actor,
@@ -234,16 +269,34 @@ export const secretImportServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Delete, ProjectPermissionActions.Delete,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Delete import" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Delete import" });
const secImport = await secretImportDAL.transaction(async (tx) => { const secImport = await secretImportDAL.transaction(async (tx) => {
const [doc] = await secretImportDAL.delete({ folderId: folder.id, id }, 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" }); if (!doc) throw new BadRequestError({ name: "Sec imp del", message: "Secret import doc not found" });
await secretImportDAL.updateAllPosition(folder.id, doc.position, -1, tx); if (doc.isReplication) {
const replicationFolderPath = path.join(secretPath, getReplicationFolderName(doc.id));
const replicatedFolder = await folderDAL.findBySecretPath(projectId, environment, replicationFolderPath, tx);
if (replicatedFolder) {
await secretImportDAL.delete(
{
folderId: folder.id,
importEnv: folder.environment.id,
importPath: replicationFolderPath,
isReserved: true
},
tx
);
await folderDAL.deleteById(replicatedFolder.id, tx);
}
await secretImportDAL.updateAllPosition(folder.id, doc.position, -1, 2, tx);
} else {
await secretImportDAL.updateAllPosition(folder.id, doc.position, -1, 1, tx);
}
const importEnv = await projectEnvDAL.findById(doc.importEnv); const importEnv = await projectEnvDAL.findById(doc.importEnv);
if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" }); if (!importEnv) throw new BadRequestError({ error: "Imported env not found", name: "Create import" });
@@ -251,7 +304,7 @@ export const secretImportServiceFactory = ({
}); });
await secretQueueService.syncSecrets({ await secretQueueService.syncSecrets({
secretPath: path, secretPath,
projectId, projectId,
environmentSlug: environment, environmentSlug: environment,
excludeReplication: true excludeReplication: true
@@ -267,7 +320,7 @@ export const secretImportServiceFactory = ({
actorOrgId, actorOrgId,
actorAuthMethod, actorAuthMethod,
projectId, projectId,
path, path: secretPath,
id: secretImportDocId id: secretImportDocId
}: TResyncSecretImportReplicationDTO) => { }: TResyncSecretImportReplicationDTO) => {
const { permission, membership } = await permissionService.getProjectPermission( const { permission, membership } = await permissionService.getProjectPermission(
@@ -281,7 +334,7 @@ export const secretImportServiceFactory = ({
// check if user has permission to import into destination path // check if user has permission to import into destination path
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Create, ProjectPermissionActions.Create,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
const plan = await licenseService.getPlan(actorOrgId); const plan = await licenseService.getPlan(actorOrgId);
@@ -291,7 +344,7 @@ export const secretImportServiceFactory = ({
}); });
} }
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Update import" });
const [secretImportDoc] = await secretImportDAL.find({ const [secretImportDoc] = await secretImportDAL.find({
@@ -338,7 +391,7 @@ export const secretImportServiceFactory = ({
}; };
const getImports = async ({ const getImports = async ({
path, path: secretPath,
environment, environment,
projectId, projectId,
actor, actor,
@@ -355,10 +408,10 @@ export const secretImportServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" }); if (!folder) throw new BadRequestError({ message: "Folder not found", name: "Get imports" });
const secImports = await secretImportDAL.find({ folderId: folder.id }); const secImports = await secretImportDAL.find({ folderId: folder.id });
@@ -366,7 +419,7 @@ export const secretImportServiceFactory = ({
}; };
const getSecretsFromImports = async ({ const getSecretsFromImports = async ({
path, path: secretPath,
environment, environment,
projectId, projectId,
actor, actor,
@@ -383,9 +436,9 @@ export const secretImportServiceFactory = ({
); );
ForbiddenError.from(permission).throwUnlessCan( ForbiddenError.from(permission).throwUnlessCan(
ProjectPermissionActions.Read, ProjectPermissionActions.Read,
subject(ProjectPermissionSub.Secrets, { environment, secretPath: path }) subject(ProjectPermissionSub.Secrets, { environment, secretPath })
); );
const folder = await folderDAL.findBySecretPath(projectId, environment, path); const folder = await folderDAL.findBySecretPath(projectId, environment, secretPath);
if (!folder) return []; if (!folder) return [];
// this will already order by position // this will already order by position
// so anything based on this order will also be in right position // so anything based on this order will also be in right position
+2 -1
View File
@@ -151,7 +151,8 @@ export const recursivelyGetSecretPaths = ({
// Fetch all folders in env once with a single query // Fetch all folders in env once with a single query
const folders = await folderDAL.find({ const folders = await folderDAL.find({
envId: env.id envId: env.id,
isReserved: false
}); });
// Build the folder hierarchy map // Build the folder hierarchy map