Improve Gitlab sync destination check and show projectId on duplicate destination

This commit is contained in:
Carlos Monastyrski
2025-10-03 01:00:30 -03:00
parent 76976ab85a
commit fb1d5ca257
7 changed files with 112 additions and 28 deletions
@@ -440,7 +440,10 @@ export const registerSyncSecretsEndpoints = <T extends TSecretSync, I extends TS
projectId: z.string().uuid() projectId: z.string().uuid()
}), }),
response: { response: {
200: z.object({ hasDuplicate: z.boolean() }) 200: z.object({
hasDuplicate: z.boolean(),
duplicateProjectId: z.string().uuid().optional()
})
} }
}, },
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
@@ -713,7 +713,16 @@ export const secretSyncServiceFactory = ({
case SecretSync.DigitalOceanAppPlatform: case SecretSync.DigitalOceanAppPlatform:
return ["appName"]; return ["appName"];
case SecretSync.GitLab: case SecretSync.GitLab:
return ["projectName", "shouldProtectSecrets", "shouldMaskSecrets", "shouldHideSecrets"]; return [
"projectName",
"shouldProtectSecrets",
"shouldMaskSecrets",
"shouldHideSecrets",
"targetEnvironment",
"groupName",
"groupId",
"projectId"
];
case SecretSync.Heroku: case SecretSync.Heroku:
return ["appName"]; return ["appName"];
case SecretSync.Netlify: case SecretSync.Netlify:
@@ -733,6 +742,34 @@ export const secretSyncServiceFactory = ({
} }
}; };
const handleSpecialCaseDuplicateCheck = (
destination: SecretSync,
existingConfig: Record<string, unknown>,
newConfig: Record<string, unknown>
): boolean => {
switch (destination) {
case SecretSync.GitLab: {
const existingTargetEnv = existingConfig.targetEnvironment as string | undefined;
const newTargetEnv = newConfig.targetEnvironment as string | undefined;
// If either has wildcard '*', it conflicts with any targetEnvironment
if (existingTargetEnv === "*" || newTargetEnv === "*") {
return true;
}
return (
existingTargetEnv === newTargetEnv &&
((newConfig.scope as string) === "group"
? existingConfig.groupId === newConfig.groupId
: existingConfig.projectId === newConfig.projectId)
);
}
default:
// For other sync types, no special handling needed
return true;
}
};
const checkDuplicateDestination = async ( const checkDuplicateDestination = async (
{ destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO, { destination, destinationConfig, excludeSyncId, projectId }: TCheckDuplicateDestinationDTO,
actor: OrgServiceActor actor: OrgServiceActor
@@ -753,7 +790,7 @@ export const secretSyncServiceFactory = ({
); );
if (!destinationConfig || Object.keys(destinationConfig).length === 0) { if (!destinationConfig || Object.keys(destinationConfig).length === 0) {
return { hasDuplicate: false }; return { hasDuplicate: false, duplicateProjectId: undefined };
} }
try { try {
@@ -765,15 +802,27 @@ export const secretSyncServiceFactory = ({
} }
try { try {
return deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields); const baseFieldsMatch = deepEqualSkipFields(sync.destinationConfig, destinationConfig, skipFields);
if (baseFieldsMatch) {
return handleSpecialCaseDuplicateCheck(
destination,
sync.destinationConfig as Record<string, unknown>,
destinationConfig
);
}
return false;
} catch { } catch {
return false; return false;
} }
}); });
return { hasDuplicate: duplicates.length > 0 }; const hasDuplicate = duplicates.length > 0;
return {
hasDuplicate,
duplicateProjectId: hasDuplicate ? duplicates[0].projectId : undefined
};
} catch (error) { } catch (error) {
return { hasDuplicate: false }; return { hasDuplicate: false, duplicateProjectId: undefined };
} }
}; };
@@ -5,13 +5,15 @@ type Props = {
onOpenChange: (isOpen: boolean) => void; onOpenChange: (isOpen: boolean) => void;
onConfirm: () => void; onConfirm: () => void;
isLoading?: boolean; isLoading?: boolean;
duplicateProjectId?: string;
}; };
export const DuplicateDestinationConfirmationModal = ({ export const DuplicateDestinationConfirmationModal = ({
isOpen, isOpen,
onOpenChange, onOpenChange,
onConfirm, onConfirm,
isLoading isLoading,
duplicateProjectId
}: Props) => { }: Props) => {
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
@@ -21,6 +23,14 @@ export const DuplicateDestinationConfirmationModal = ({
Another secret sync in your organization is already configured with the same Another secret sync in your organization is already configured with the same
destination. Proceeding may cause conflicts or overwrite existing data. destination. Proceeding may cause conflicts or overwrite existing data.
</p> </p>
{duplicateProjectId && (
<p className="mt-2 text-xs text-mineshaft-400">
Duplicate found in project ID:{" "}
<code className="rounded bg-mineshaft-600 px-1 py-0.5 text-mineshaft-200">
{duplicateProjectId}
</code>
</p>
)}
<p className="mt-2">Are you sure you want to continue?</p> <p className="mt-2">Are you sure you want to continue?</p>
</div> </div>
@@ -44,8 +44,9 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
const [destinationConfigToCheck, setDestinationConfigToCheck] = useState<unknown>(null); const [destinationConfigToCheck, setDestinationConfigToCheck] = useState<unknown>(null);
const [checkDuplicateEnabled, setCheckDuplicateEnabled] = useState(false); const [checkDuplicateEnabled, setCheckDuplicateEnabled] = useState(false);
const [storedDuplicateProjectId, setStoredDuplicateProjectId] = useState<string | undefined>();
const { data: hasDuplicate, isLoading: isCheckingDuplicate } = useCheckDuplicateDestination( const { data: duplicateData, isLoading: isCheckingDuplicate } = useCheckDuplicateDestination(
secretSync.destination, secretSync.destination,
destinationConfigToCheck, destinationConfigToCheck,
secretSync.projectId, secretSync.projectId,
@@ -84,7 +85,8 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
useEffect(() => { useEffect(() => {
if (checkDuplicateEnabled && !isCheckingDuplicate && destinationConfigToCheck) { if (checkDuplicateEnabled && !isCheckingDuplicate && destinationConfigToCheck) {
if (hasDuplicate) { if (duplicateData?.hasDuplicate) {
setStoredDuplicateProjectId(duplicateData.duplicateProjectId);
setShowDuplicateConfirmation(true); setShowDuplicateConfirmation(true);
} else if (pendingFormData) { } else if (pendingFormData) {
performUpdate(pendingFormData); performUpdate(pendingFormData);
@@ -96,7 +98,8 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
}, [ }, [
checkDuplicateEnabled, checkDuplicateEnabled,
isCheckingDuplicate, isCheckingDuplicate,
hasDuplicate, duplicateData?.hasDuplicate,
duplicateData?.duplicateProjectId,
destinationConfigToCheck, destinationConfigToCheck,
pendingFormData, pendingFormData,
performUpdate performUpdate
@@ -197,9 +200,15 @@ export const EditSecretSyncForm = ({ secretSync, fields, onComplete }: Props) =>
<DuplicateDestinationConfirmationModal <DuplicateDestinationConfirmationModal
isOpen={showDuplicateConfirmation} isOpen={showDuplicateConfirmation}
onOpenChange={setShowDuplicateConfirmation} onOpenChange={(open) => {
setShowDuplicateConfirmation(open);
if (!open) {
setStoredDuplicateProjectId(undefined);
}
}}
onConfirm={handleConfirmDuplicate} onConfirm={handleConfirmDuplicate}
isLoading={updateSecretSync.isPending} isLoading={updateSecretSync.isPending}
duplicateProjectId={storedDuplicateProjectId}
/> />
</> </>
); );
@@ -67,7 +67,7 @@ export const SecretSyncReviewFields = () => {
const destinationName = SECRET_SYNC_MAP[destination].name; const destinationName = SECRET_SYNC_MAP[destination].name;
const { hasDuplicate, isChecking } = useDuplicateDestinationCheck({ const { hasDuplicate, duplicateProjectId, isChecking } = useDuplicateDestinationCheck({
destination, destination,
projectId: currentProject?.id || "", projectId: currentProject?.id || "",
enabled: true, enabled: true,
@@ -192,10 +192,17 @@ export const SecretSyncReviewFields = () => {
<div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 px-3 py-2"> <div className="mb-2 flex items-start rounded-md border border-yellow-600 bg-yellow-900/20 px-3 py-2">
<div className="flex text-sm text-yellow-100"> <div className="flex text-sm text-yellow-100">
<FontAwesomeIcon icon={faWarning} className="mr-2 mt-1 text-yellow-600" /> <FontAwesomeIcon icon={faWarning} className="mr-2 mt-1 text-yellow-600" />
<p> <div>
Another secret sync in your organization is already configured with the same <p>
destination. This may lead to conflicts or unexpected behavior. Another secret sync in your organization is already configured with the same
</p> destination. This may lead to conflicts or unexpected behavior.
</p>
{duplicateProjectId && (
<p className="mt-1 text-xs text-yellow-200">
Duplicate found in project ID: <code className="rounded bg-yellow-800/50 px-1 py-0.5">{duplicateProjectId}</code>
</p>
)}
</div>
</div> </div>
</div> </div>
)} )}
+15 -10
View File
@@ -103,23 +103,28 @@ export const useCheckDuplicateDestination = (
projectId: string, projectId: string,
excludeSyncId?: string, excludeSyncId?: string,
options?: Omit< options?: Omit<
UseQueryOptions<boolean, unknown, boolean, ReturnType<typeof secretSyncKeys.duplicateCheck>>, UseQueryOptions<
{ hasDuplicate: boolean; duplicateProjectId?: string },
unknown,
{ hasDuplicate: boolean; duplicateProjectId?: string },
ReturnType<typeof secretSyncKeys.duplicateCheck>
>,
"queryKey" | "queryFn" "queryKey" | "queryFn"
> >
) => { ) => {
return useQuery({ return useQuery({
queryKey: secretSyncKeys.duplicateCheck(destination, destinationConfig, excludeSyncId), queryKey: secretSyncKeys.duplicateCheck(destination, destinationConfig, excludeSyncId),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.post<{ hasDuplicate: boolean }>( const { data } = await apiRequest.post<{
`/api/v1/secret-syncs/${destination}/check-destination`, hasDuplicate: boolean;
{ duplicateProjectId?: string;
destinationConfig, }>(`/api/v1/secret-syncs/${destination}/check-destination`, {
excludeSyncId, destinationConfig,
projectId excludeSyncId,
} projectId
); });
return data.hasDuplicate; return data;
}, },
enabled: Boolean(destinationConfig) && Object.keys(destinationConfig || {}).length > 0, enabled: Boolean(destinationConfig) && Object.keys(destinationConfig || {}).length > 0,
...options ...options
@@ -30,7 +30,7 @@ export const useDuplicateDestinationCheck = ({
const shouldCheck = enabled && hasValidConfig; const shouldCheck = enabled && hasValidConfig;
const { const {
data: hasDuplicate, data: duplicateData,
isLoading, isLoading,
error, error,
refetch refetch
@@ -41,7 +41,8 @@ export const useDuplicateDestinationCheck = ({
}); });
return { return {
hasDuplicate: shouldCheck ? Boolean(hasDuplicate) : false, hasDuplicate: shouldCheck ? Boolean(duplicateData?.hasDuplicate) : false,
duplicateProjectId: duplicateData?.duplicateProjectId,
isChecking: shouldCheck && isLoading, isChecking: shouldCheck && isLoading,
hasError: Boolean(error), hasError: Boolean(error),
hasValidConfig, hasValidConfig,