import { useEffect, useState } from "react"; import { useFormContext } from "react-hook-form"; import { faInfoCircle } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; import { Button, FilterableSelect, FormControl, Modal, ModalClose, ModalContent, TextArea } from "@app/components/v2"; import { ProjectPermissionSub } from "@app/context"; import { useGetVaultMounts, useGetVaultNamespaces, useGetVaultPolicies } from "@app/hooks/api/migration/queries"; import { TFormSchema } from "./ProjectRoleModifySection.utils"; import { analyzeVaultPolicy, PolicyBlock, PolicyLine } from "./VaultPolicyAnalyzer.utils"; import { parseVaultPolicyToInfisical } from "./VaultPolicyImportModal.utils"; import { VaultPolicyPreview } from "./VaultPolicyPreview"; type Props = { isOpen: boolean; onOpenChange: (isOpen: boolean) => void; }; type ContentProps = { onClose: () => void; }; const Content = ({ onClose }: ContentProps) => { const rootForm = useFormContext(); const [selectedNamespace, setSelectedNamespace] = useState(null); const [selectedPolicy, setSelectedPolicy] = useState(null); const [hclPolicy, setHclPolicy] = useState(""); const [shouldFetchPolicies, setShouldFetchPolicies] = useState(false); const [shouldFetchMounts, setShouldFetchMounts] = useState(false); const [analysisResult, setAnalysisResult] = useState<{ blocks: PolicyBlock[]; lines: PolicyLine[]; } | null>(null); const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces(); const { data: policies, isLoading: isLoadingPolicies } = useGetVaultPolicies( shouldFetchPolicies, selectedNamespace ?? undefined ); const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts( shouldFetchMounts, selectedNamespace ?? undefined ); // Enable fetching policies and mounts when namespace is selected useEffect(() => { if (selectedNamespace) { setShouldFetchPolicies(true); setShouldFetchMounts(true); } }, [selectedNamespace]); // Auto-populate HCL when a policy is selected useEffect(() => { if (selectedPolicy && policies) { const policy = policies.find((p) => p.name === selectedPolicy); if (policy) { setHclPolicy(policy.rules); } } }, [selectedPolicy, policies]); // Automatically analyze policy when it changes (with debouncing) useEffect(() => { if (!hclPolicy.trim() || !mounts || mounts.length === 0) { setAnalysisResult(null); return undefined; } const timeoutId = setTimeout(() => { const result = analyzeVaultPolicy(hclPolicy, mounts); setAnalysisResult(result); }, 300); // Debounce for 300ms return () => { clearTimeout(timeoutId); }; }, [hclPolicy, mounts]); const renderEmptyState = () => { if (!selectedNamespace) { return (

Select a namespace to enable preview

); } if (isLoadingMounts) { return
Loading mounts...
; } if (!mounts || mounts.length === 0) { return (

No KV mounts found

This namespace has no KV secret engines configured.

Policy translation requires KV mounts.

); } return
Enter a policy to see translation preview
; }; const handleTranslateAndApply = () => { if (!hclPolicy.trim()) { createNotification({ type: "error", text: "Please provide a Vault HCL policy" }); return; } if (!mounts || mounts.length === 0) { createNotification({ type: "error", text: "No Vault mounts found. Please ensure you have KV secret engines configured." }); return; } try { const parsedPermissions = parseVaultPolicyToInfisical(hclPolicy, mounts); if (!parsedPermissions || Object.keys(parsedPermissions).length === 0) { createNotification({ type: "warning", text: "No translatable permissions found in the policy. Ensure the policy contains KV secret paths (e.g., secret/data/*, secret/metadata/*)." }); return; } // Apply the parsed permissions to the form (Object.keys(parsedPermissions) as ProjectPermissionSub[]).forEach((subjectKey) => { const value = parsedPermissions[subjectKey]; if (!value) return; const existingValue = rootForm.getValues(`permissions.${subjectKey}`) as unknown[]; if (Array.isArray(existingValue) && existingValue.length > 0) { // Merge with existing permissions rootForm.setValue(`permissions.${subjectKey}`, [...existingValue, ...value] as never, { shouldDirty: true, shouldTouch: true, shouldValidate: true }); } else { rootForm.setValue(`permissions.${subjectKey}`, value as never, { shouldDirty: true, shouldTouch: true, shouldValidate: true }); } }); createNotification({ type: "info", text: "Vault policy translated and prefilled" }); onClose(); } catch (err) { console.error("Translation error:", err); createNotification({ type: "error", text: "Failed to translate policy. Please check the HCL format." }); } }; return ( <>
How Policy Translation Works

Policies are translated by identifying KV secret engine mounts and parsing path structures to extract environments and secret paths.

Key assumptions: The first path segment after the mount is treated as the environment (e.g., secret/data/prod/app → env: prod, path:{" "} /app). Vault capabilities and wildcards are automatically mapped to equivalent Infisical permissions and glob patterns.

<> ns.id === selectedNamespace)} onChange={(value) => { if (value && !Array.isArray(value)) { const namespace = value as { id: string; name: string }; setSelectedNamespace(namespace.name); setSelectedPolicy(null); } }} options={namespaces || []} getOptionValue={(option) => option.name} getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)} isDisabled={isLoadingNamespaces} placeholder="Select namespace..." className="w-full" />

Select the Vault namespace to fetch policies and mount information

<> p.name === selectedPolicy) : null} onChange={(value) => { if (value && !Array.isArray(value)) { const policy = value as { name: string; rules: string }; setSelectedPolicy(policy.name); } else { setSelectedPolicy(null); } }} options={policies || []} getOptionValue={(option) => option.name} getOptionLabel={(option) => option.name} isDisabled={isLoadingPolicies} placeholder="Choose a policy to import..." isClearable className="w-full" />

Select a policy to auto-populate the HCL editor below, or skip to paste your own

<>