mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-11 14:28:56 +00:00
Add warning on same destination secret sync
This commit is contained in:
@@ -53,3 +53,34 @@ export const titleCaseToCamelCase = (obj: unknown): unknown => {
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
export const deepEqual = (obj1: unknown, obj2: unknown): boolean => {
|
||||
if (obj1 === obj2) return true;
|
||||
|
||||
if (obj1 === null || obj2 === null || obj1 === undefined || obj2 === undefined) {
|
||||
return obj1 === obj2;
|
||||
}
|
||||
|
||||
if (typeof obj1 !== typeof obj2) return false;
|
||||
|
||||
if (typeof obj1 !== "object") return obj1 === obj2;
|
||||
|
||||
if (Array.isArray(obj1) !== Array.isArray(obj2)) return false;
|
||||
|
||||
if (Array.isArray(obj1)) {
|
||||
const arr1 = obj1 as unknown[];
|
||||
const arr2 = obj2 as unknown[];
|
||||
if (arr1.length !== arr2.length) return false;
|
||||
return arr1.every((val, idx) => deepEqual(val, arr2[idx]));
|
||||
}
|
||||
|
||||
const keys1 = Object.keys(obj1 as Record<string, unknown>).sort();
|
||||
const keys2 = Object.keys(obj2 as Record<string, unknown>).sort();
|
||||
|
||||
if (keys1.length !== keys2.length) return false;
|
||||
if (keys1.some((key, idx) => key !== keys2[idx])) return false;
|
||||
|
||||
return keys1.every((key) =>
|
||||
deepEqual((obj1 as Record<string, unknown>)[key], (obj2 as Record<string, unknown>)[key])
|
||||
);
|
||||
};
|
||||
|
||||
@@ -425,4 +425,39 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
|
||||
return { secretSync };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "POST",
|
||||
url: "/check-destination",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
tags: [ApiDocsTags.SecretSyncs],
|
||||
body: z.object({
|
||||
destinationConfig: z.unknown(),
|
||||
excludeSyncId: z.string().uuid().optional(),
|
||||
projectId: z.string().uuid()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({ hasDuplicate: z.boolean() })
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||
handler: async (req) => {
|
||||
const { destinationConfig, excludeSyncId, projectId } = req.body;
|
||||
|
||||
const result = await server.services.secretSync.checkDuplicateDestination(
|
||||
{
|
||||
destinationConfig,
|
||||
destination,
|
||||
excludeSyncId,
|
||||
projectId
|
||||
},
|
||||
req.permission
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
import { KeyStorePrefixes, TKeyStoreFactory } from "@app/keystore/keystore";
|
||||
import { DatabaseErrorCode } from "@app/lib/error-codes";
|
||||
import { BadRequestError, DatabaseError, NotFoundError } from "@app/lib/errors";
|
||||
import { deepEqual } from "@app/lib/fn/object";
|
||||
import { OrgServiceActor } from "@app/lib/types";
|
||||
import { TAppConnectionServiceFactory } from "@app/services/app-connection/app-connection-service";
|
||||
import { TProjectBotServiceFactory } from "@app/services/project-bot/project-bot-service";
|
||||
@@ -20,6 +21,7 @@ import { SecretSync } from "@app/services/secret-sync/secret-sync-enums";
|
||||
import { enterpriseSyncCheck, listSecretSyncOptions } from "@app/services/secret-sync/secret-sync-fns";
|
||||
import {
|
||||
SecretSyncStatus,
|
||||
TCheckDuplicateDestinationDTO,
|
||||
TCreateSecretSyncDTO,
|
||||
TDeleteSecretSyncDTO,
|
||||
TFindSecretSyncByIdDTO,
|
||||
@@ -696,6 +698,51 @@ export const secretSyncServiceFactory = ({
|
||||
return updatedSecretSync as TSecretSync;
|
||||
};
|
||||
|
||||
const checkDuplicateDestination = async (
|
||||
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
|
||||
actor: OrgServiceActor
|
||||
) => {
|
||||
const { permission } = await permissionService.getProjectPermission({
|
||||
actor: actor.type,
|
||||
actorId: actor.id,
|
||||
actorAuthMethod: actor.authMethod,
|
||||
actorOrgId: actor.orgId,
|
||||
actionProjectType: ActionProjectType.SecretManager,
|
||||
projectId
|
||||
});
|
||||
|
||||
ForbiddenError.from(permission).throwUnlessCan(
|
||||
ProjectPermissionSecretSyncActions.Read,
|
||||
ProjectPermissionSub.SecretSyncs
|
||||
);
|
||||
|
||||
if (!destinationConfig || typeof destinationConfig !== "object") {
|
||||
return { hasDuplicate: false };
|
||||
}
|
||||
|
||||
try {
|
||||
const existingSyncs = await secretSyncDAL.find({
|
||||
destination
|
||||
});
|
||||
|
||||
const duplicates = existingSyncs.filter((sync) => {
|
||||
if (sync.id === excludeSyncId) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
return deepEqual(sync.destinationConfig, destinationConfig);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
|
||||
return { hasDuplicate: duplicates.length > 0 };
|
||||
} catch (error) {
|
||||
return { hasDuplicate: false };
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
listSecretSyncOptions,
|
||||
listSecretSyncsByProjectId,
|
||||
@@ -707,6 +754,7 @@ export const secretSyncServiceFactory = ({
|
||||
deleteSecretSync,
|
||||
triggerSecretSyncSyncSecretsById,
|
||||
triggerSecretSyncImportSecretsById,
|
||||
triggerSecretSyncRemoveSecretsById
|
||||
triggerSecretSyncRemoveSecretsById,
|
||||
checkDuplicateDestination
|
||||
};
|
||||
};
|
||||
|
||||
@@ -324,6 +324,13 @@ export type TDeleteSecretSyncDTO = {
|
||||
removeSecrets: boolean;
|
||||
};
|
||||
|
||||
export type TCheckDuplicateDestinationDTO = {
|
||||
destination: SecretSync;
|
||||
destinationConfig: unknown;
|
||||
excludeSyncId?: string;
|
||||
projectId: string;
|
||||
};
|
||||
|
||||
export enum SecretSyncStatus {
|
||||
Pending = "pending",
|
||||
Running = "running",
|
||||
|
||||
Reference in New Issue
Block a user