mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-07 14:27:30 +00:00
feat: implemented replication to a folder strategy
This commit is contained in:
@@ -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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user