mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 04:26:33 +00:00
Improve Gitlab sync destination check and show projectId on duplicate destination
This commit is contained in:
@@ -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 };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -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}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
+12
-5
@@ -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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user