diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/VaultPolicyImportModal.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/VaultPolicyImportModal.tsx index fb17d6332..62f04e770 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/VaultPolicyImportModal.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/VaultPolicyImportModal.tsx @@ -13,11 +13,7 @@ import { ModalContent, TextArea } from "@app/components/v2"; -import { ProjectPermissionActions, ProjectPermissionSub } from "@app/context"; -import { - PermissionConditionOperators, - ProjectPermissionSecretActions -} from "@app/context/ProjectPermissionContext/types"; +import { ProjectPermissionSub } from "@app/context"; import { useGetVaultMounts, useGetVaultNamespaces, @@ -25,6 +21,7 @@ import { } from "@app/hooks/api/migration/queries"; import { TFormSchema } from "./ProjectRoleModifySection.utils"; +import { parseVaultPolicyToInfisical } from "./VaultPolicyImportModal.utils"; type Props = { isOpen: boolean; @@ -35,335 +32,6 @@ type ContentProps = { onClose: () => void; }; -type VaultMount = { path: string; type: string; version: string | null }; - -// Extract array element type helper -type ArrayElement = T extends (infer U)[] ? U : never; - -// Extract permission rule types from the form schema -type SecretPermissionRule = ArrayElement< - NonNullable[ProjectPermissionSub.Secrets] ->; -type FolderPermissionRule = ArrayElement< - NonNullable[ProjectPermissionSub.SecretFolders] ->; - -// Helper to parse Vault path and extract environment and secret path -const parseVaultPath = ( - vaultPath: string, - mounts: VaultMount[] -): { - environment: string | null; - secretPath: string | null; - mount: VaultMount | null; - isWildcardMount: boolean; -} => { - // Check if path starts with wildcard mount (e.g., "*/data/*") - const isWildcardMount = vaultPath.startsWith("*/") || vaultPath.startsWith("+/"); - - if (isWildcardMount) { - // For wildcard mounts, extract everything after the wildcard prefix - let remainingPath = vaultPath.slice(2); // Remove "*/" or "+/" - if (remainingPath.startsWith("/")) remainingPath = remainingPath.slice(1); - - let environment: string | null = null; - let secretPath: string | null = null; - let isDataPath = false; - let isMetadataPath = false; - - // Check for KV v2 data/ or metadata/ prefix - if (remainingPath.startsWith("data/")) { - isDataPath = true; - remainingPath = remainingPath.slice(5); // Remove "data/" - } else if (remainingPath.startsWith("metadata/")) { - isMetadataPath = true; - remainingPath = remainingPath.slice(9); // Remove "metadata/" - } - - // Split remaining path into segments - const segments = remainingPath.split("/").filter(Boolean); - - if (segments.length > 0) { - // Special case: if the only segment is a wildcard, treat it as matching everything - if (segments.length === 1 && (segments[0] === "*" || segments[0] === "+")) { - environment = "*"; // Match all environments - secretPath = "/*"; // Match all paths - } else { - // First segment is the environment - [environment] = segments; - - // Remaining segments form the secret path - if (segments.length > 1) { - secretPath = `/${segments.slice(1).join("/")}`; - } else { - secretPath = "/"; - } - } - } - - // For wildcard mounts, return a synthetic mount object - // We'll use this to determine if it's KV v2 (has data/metadata paths) - const syntheticMount: VaultMount = { - path: "*", - type: "kv", - version: isDataPath || isMetadataPath ? "2" : "1" - }; - - return { environment, secretPath, mount: syntheticMount, isWildcardMount: true }; - } - - // Original logic for non-wildcard paths - // Find the matching mount for this path - // Sort by path length (longest first) to match most specific mount - const sortedMounts = [...mounts].sort((a, b) => b.path.length - a.path.length); - const mount = sortedMounts.find((m) => vaultPath.startsWith(m.path)); - if (!mount) { - return { environment: null, secretPath: null, mount: null, isWildcardMount: false }; - } - - // Remove mount prefix and any trailing slash - let remainingPath = vaultPath.slice(mount.path.length); - if (remainingPath.startsWith("/")) remainingPath = remainingPath.slice(1); - - // For KV v2, paths have format: data/{environment}/{path} or metadata/{environment}/{path} - // For KV v1, paths have format: {environment}/{path} - const isKvV2 = mount.version === "2" || mount.type === "kv"; - - let environment: string | null = null; - let secretPath: string | null = null; - - if (isKvV2) { - // Remove data/ or metadata/ prefix for KV v2 - if (remainingPath.startsWith("data/")) { - remainingPath = remainingPath.slice(5); - } else if (remainingPath.startsWith("metadata/")) { - remainingPath = remainingPath.slice(9); - } - } - - // Split remaining path into segments - const segments = remainingPath.split("/").filter(Boolean); - - if (segments.length > 0) { - // Special case: if the only segment is a wildcard, treat it as a path wildcard - if (segments.length === 1 && (segments[0] === "*" || segments[0] === "+")) { - environment = null; // No specific environment - secretPath = "/*"; // Match all paths - } else { - // First segment is treated as the environment - // (wildcards in environment will be handled with $GLOB operator later) - [environment] = segments; - - // Remaining segments form the secret path - if (segments.length > 1) { - secretPath = `/${segments.slice(1).join("/")}`; - } else { - secretPath = "/"; - } - } - } - - return { environment, secretPath, mount, isWildcardMount: false }; -}; - -// Helper to create a unique key for deduplication of permission rules -const createPermissionRuleKey = (rule: SecretPermissionRule | FolderPermissionRule): string => { - const actions = Object.entries(rule) - .filter(([key]) => key !== "conditions") - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => `${key}:${value}`) - .join("|"); - - const conditions = (rule.conditions || []) - .map((c) => `${c.lhs}${c.operator}${c.rhs}`) - .sort() - .join("|"); - - return `${actions}::${conditions}`; -}; - -// HCL parser for Vault policies - converts Vault HCL to Infisical permissions -const parseVaultPolicyToInfisical = ( - hclPolicy: string, - mounts: VaultMount[] -): Partial => { - const permissions: Partial = {}; - const secretsPermissions: SecretPermissionRule[] = []; - const foldersPermissions: FolderPermissionRule[] = []; - - const seenSecretRules = new Set(); - const seenFolderRules = new Set(); - - try { - // Remove comments from HCL before parsing - const cleanedPolicy = hclPolicy - .split("\n") - .map((line) => line.replace(/#.*$/, "").trim()) // Remove # comments - .filter((line) => line.length > 0) // Remove empty lines - .join(" "); // Join into single line for easier parsing - - // Match path blocks with flexible whitespace handling - const pathRegex = /path\s+"([^"]+)"\s*\{[^}]*capabilities\s*=\s*\[([^\]]+)\][^}]*\}/gi; - let match = pathRegex.exec(cleanedPolicy); - - while (match !== null) { - const [, path, capabilitiesStr] = match; - // Split by comma and clean up each capability (handles newlines, extra spaces, quotes) - const capabilities = capabilitiesStr - .split(",") - .map((c) => c.trim().replace(/["'\s]/g, "")) // Remove quotes, spaces, newlines - .filter((c) => c.length > 0); // Filter out empty strings - - // Parse the Vault path - handles both regular and wildcard mount paths - const { environment, secretPath, mount } = parseVaultPath(path, mounts); - - // Only process KV (Key-Value) mounts - if (mount && (mount.type === "kv" || mount.type === "generic")) { - const isKvV2 = mount.version === "2"; - // For KV v2: explicit metadata paths are metadata, explicit data paths or paths without prefix are data - // For KV v1: no metadata endpoint exists, everything is data - const isMetadataPath = isKvV2 ? path.includes("/metadata/") : false; - const isDataPath = !isMetadataPath; // Everything that's not metadata is a data path - - if (isDataPath && !isMetadataPath) { - // Data paths map to secret permissions - const actions: { [key: string]: boolean } = {}; - - if (capabilities.includes("create")) - actions[ProjectPermissionSecretActions.Create] = true; - if (capabilities.includes("read")) { - actions[ProjectPermissionSecretActions.DescribeSecret] = true; - actions[ProjectPermissionSecretActions.ReadValue] = true; - } - if (capabilities.includes("update") || capabilities.includes("patch")) - actions[ProjectPermissionSecretActions.Edit] = true; - if (capabilities.includes("delete")) - actions[ProjectPermissionSecretActions.Delete] = true; - - if (Object.keys(actions).length > 0) { - const conditions: Array<{ lhs: string; operator: string; rhs: string }> = []; - - // Add environment condition with glob support if it contains wildcards - if (environment) { - // Convert Vault '+' to glob '*' for environment matching - const globEnv = environment.replace(/\+/g, "*"); - // Skip condition if it's just '*' (matches everything = no restriction) - if (globEnv !== "*") { - const hasWildcard = globEnv.includes("*"); - conditions.push({ - lhs: "environment", - operator: hasWildcard - ? PermissionConditionOperators.$GLOB - : PermissionConditionOperators.$EQ, - rhs: globEnv - }); - } - } - - // Add secret path condition with glob support - if (secretPath && secretPath !== "/*") { - // Convert Vault wildcards to picomatch glob patterns - // Vault '*' = match within segment, picomatch '**' = match across segments - // Vault '+' = single segment, convert to '*' (note: slightly more permissive) - const globPath = secretPath.replace(/\+/g, "*"); - // Check if we need glob operator - const hasWildcard = globPath.includes("*"); - conditions.push({ - lhs: "secretPath", - operator: hasWildcard - ? PermissionConditionOperators.$GLOB - : PermissionConditionOperators.$EQ, - rhs: globPath - }); - } - - const newRule = { - ...actions, - conditions - }; - - // Check for duplicates before adding - const ruleKey = createPermissionRuleKey(newRule); - if (!seenSecretRules.has(ruleKey)) { - seenSecretRules.add(ruleKey); - secretsPermissions.push(newRule); - } - } - } else if (isMetadataPath) { - // Metadata paths map to folder permissions - const actions: { [key: string]: boolean } = {}; - - if (capabilities.includes("create")) actions[ProjectPermissionActions.Create] = true; - if (capabilities.includes("update") || capabilities.includes("patch")) - actions[ProjectPermissionActions.Edit] = true; - if (capabilities.includes("delete")) actions[ProjectPermissionActions.Delete] = true; - - if (Object.keys(actions).length > 0) { - const conditions: Array<{ lhs: string; operator: string; rhs: string }> = []; - - // Add environment condition with glob support if it contains wildcards - if (environment) { - // Convert Vault '+' to glob '*' for environment matching - const globEnv = environment.replace(/\+/g, "*"); - // Skip condition if it's just '*' (matches everything = no restriction) - if (globEnv !== "*") { - const hasWildcard = globEnv.includes("*"); - conditions.push({ - lhs: "environment", - operator: hasWildcard - ? PermissionConditionOperators.$GLOB - : PermissionConditionOperators.$EQ, - rhs: globEnv - }); - } - } - - // Add secret path condition for folders with glob support - if (secretPath && secretPath !== "/*") { - // Convert Vault '+' wildcard to glob '*' - const globPath = secretPath.replace(/\+/g, "*"); - const hasWildcard = globPath.includes("*"); - conditions.push({ - lhs: "secretPath", - operator: hasWildcard - ? PermissionConditionOperators.$GLOB - : PermissionConditionOperators.$EQ, - rhs: globPath - }); - } - - const newRule = { - ...actions, - conditions - }; - - // Check for duplicates before adding - const ruleKey = createPermissionRuleKey(newRule); - if (!seenFolderRules.has(ruleKey)) { - seenFolderRules.add(ruleKey); - foldersPermissions.push(newRule); - } - } - } - } - - match = pathRegex.exec(cleanedPolicy); - } - - if (secretsPermissions.length > 0) { - permissions[ProjectPermissionSub.Secrets] = secretsPermissions; - } - - if (foldersPermissions.length > 0) { - permissions[ProjectPermissionSub.SecretFolders] = foldersPermissions; - } - } catch (err) { - console.error("Error parsing HCL policy:", err); - } - - return permissions; -}; - const Content = ({ onClose }: ContentProps) => { const rootForm = useFormContext(); const [selectedNamespace, setSelectedNamespace] = useState(null); @@ -462,8 +130,8 @@ const Content = ({ onClose }: ContentProps) => { }); createNotification({ - type: "success", - text: "Policy translated and applied successfully" + type: "info", + text: "Vault policy translated and prefilled" }); onClose(); @@ -505,7 +173,7 @@ const Content = ({ onClose }: ContentProps) => { <> = T extends (infer U)[] ? U : never; + +export type SecretPermissionRule = ArrayElement< + NonNullable[ProjectPermissionSub.Secrets] +>; + +export type FolderPermissionRule = ArrayElement< + NonNullable[ProjectPermissionSub.SecretFolders] +>; + +type ParsedVaultPath = { + environment: string | null; + secretPath: string | null; + mount: VaultMount | null; + isWildcardMount: boolean; +}; + +// ============================================================================ +// Path Parsing +// ============================================================================ + +/** + * Parses a Vault policy path to extract mount, environment, and secret path. + * + * Handles three types of path patterns: + * 1. Global wildcards: "*" or "+" → matches all mounts, environments, paths + * 2. Wildcard mounts: "* /data/prod/*" → matches all mounts with specific path + * 3. Regular paths: "secret/data/prod/api-keys" → specific mount and path + * + * For KV v2 mounts: + * - data/ paths → secret operations (read, write values) + * - metadata/ paths → folder operations (create, delete folders) + * + * Path structure after mount: + * - KV v2: [data|metadata]/{environment}/{secretPath} + * - KV v1: {environment}/{secretPath} + */ +export const parseVaultPath = (vaultPath: string, mounts: VaultMount[]): ParsedVaultPath => { + // Case 1: Global wildcard (e.g., "*" or "+") - matches everything + if (vaultPath === "*" || vaultPath === "+") { + const syntheticMount: VaultMount = { + path: "*", + type: "kv", + version: "1" // Default to v1 for global wildcards + }; + return { + environment: "*", + secretPath: "/*", + mount: syntheticMount, + isWildcardMount: true + }; + } + + // Case 2: Wildcard mount (e.g., "*/data/*") - matches any mount with pattern + const isWildcardMount = vaultPath.startsWith("*/") || vaultPath.startsWith("+/"); + + if (isWildcardMount) { + let remainingPath = vaultPath.slice(2); // Remove "*/" or "+/" + if (remainingPath.startsWith("/")) remainingPath = remainingPath.slice(1); + + let environment: string | null = null; + let secretPath: string | null = null; + let isDataPath = false; + let isMetadataPath = false; + + // Check for KV v2 data/ or metadata/ prefix + if (remainingPath.startsWith("data/")) { + isDataPath = true; + remainingPath = remainingPath.slice(5); + } else if (remainingPath.startsWith("metadata/")) { + isMetadataPath = true; + remainingPath = remainingPath.slice(9); + } + + // Parse remaining segments + const segments = remainingPath.split("/").filter(Boolean); + + if (segments.length > 0) { + if (segments.length === 1 && (segments[0] === "*" || segments[0] === "+")) { + environment = "*"; + secretPath = "/*"; + } else { + [environment] = segments; + secretPath = segments.length > 1 ? `/${segments.slice(1).join("/")}` : "/"; + } + } + + // Create synthetic mount based on detected version + const syntheticMount: VaultMount = { + path: "*", + type: "kv", + version: isDataPath || isMetadataPath ? "2" : "1" + }; + + return { environment, secretPath, mount: syntheticMount, isWildcardMount: true }; + } + + // Case 3: Regular path (e.g., "secret/data/prod/api-keys") + // Find matching mount (longest path first for most specific match) + const sortedMounts = [...mounts].sort((a, b) => b.path.length - a.path.length); + const mount = sortedMounts.find((m) => vaultPath.startsWith(m.path)); + + if (!mount) { + return { environment: null, secretPath: null, mount: null, isWildcardMount: false }; + } + + // Remove mount prefix + let remainingPath = vaultPath.slice(mount.path.length); + if (remainingPath.startsWith("/")) remainingPath = remainingPath.slice(1); + + const isKvV2 = mount.version === "2" || mount.type === "kv"; + + // For KV v2, remove data/ or metadata/ prefix + if (isKvV2) { + if (remainingPath.startsWith("data/")) { + remainingPath = remainingPath.slice(5); + } else if (remainingPath.startsWith("metadata/")) { + remainingPath = remainingPath.slice(9); + } + } + + // Parse environment and secret path + const segments = remainingPath.split("/").filter(Boolean); + let environment: string | null = null; + let secretPath: string | null = null; + + if (segments.length > 0) { + if (segments.length === 1 && (segments[0] === "*" || segments[0] === "+")) { + // Single wildcard segment + environment = null; + secretPath = "/*"; + } else { + // First segment is the environment + [environment] = segments; + // Remaining segments form the secret path + secretPath = segments.length > 1 ? `/${segments.slice(1).join("/")}` : "/"; + } + } + + return { environment, secretPath, mount, isWildcardMount: false }; +}; + +// ============================================================================ +// Capability Mapping +// ============================================================================ + +/** + * Maps Vault capabilities to Infisical secret actions. + * + * Mapping: + * - create → Create + * - list → DescribeSecret (view metadata without values) + * - read → DescribeSecret + ReadValue (full access) + * - update/patch → Edit + * - delete → Delete + */ +const mapVaultCapabilitiesToSecretActions = (capabilities: string[]): Record => { + const actions: Record = {}; + + if (capabilities.includes("create")) { + actions[ProjectPermissionSecretActions.Create] = true; + } + if (capabilities.includes("list")) { + actions[ProjectPermissionSecretActions.DescribeSecret] = true; + } + if (capabilities.includes("read")) { + actions[ProjectPermissionSecretActions.DescribeSecret] = true; + actions[ProjectPermissionSecretActions.ReadValue] = true; + } + if (capabilities.includes("update") || capabilities.includes("patch")) { + actions[ProjectPermissionSecretActions.Edit] = true; + } + if (capabilities.includes("delete")) { + actions[ProjectPermissionSecretActions.Delete] = true; + } + + return actions; +}; + +/** + * Maps Vault capabilities to Infisical folder actions. + * + * Mapping: + * - create → Create + * - update/patch → Edit + * - delete → Delete + * + * Note: 'list' is not mapped for folders as it's handled at the secret level + */ +const mapVaultCapabilitiesToFolderActions = (capabilities: string[]): Record => { + const actions: Record = {}; + + if (capabilities.includes("create")) { + actions[ProjectPermissionActions.Create] = true; + } + if (capabilities.includes("update") || capabilities.includes("patch")) { + actions[ProjectPermissionActions.Edit] = true; + } + if (capabilities.includes("delete")) { + actions[ProjectPermissionActions.Delete] = true; + } + + return actions; +}; + +// ============================================================================ +// Condition Building +// ============================================================================ + +type PermissionCondition = { + lhs: string; + operator: string; + rhs: string; +}; + +/** + * Converts Vault wildcard patterns to Infisical glob patterns. + * - Vault '+' → picomatch '*' (matches single segment) + * - Vault '*' → picomatch '**' (matches any depth) + */ +const convertVaultWildcardToGlob = (vaultPattern: string): string => { + // Use a placeholder to avoid replacing + twice + // Step 1: Replace + with a placeholder + let result = vaultPattern.replace(/\+/g, "__PLUS__"); + // Step 2: Replace * with ** + result = result.replace(/\*/g, "**"); + // Step 3: Replace placeholder with * + result = result.replace(/__PLUS__/g, "*"); + return result; +}; + +/** + * Builds permission conditions for environment and secret path filtering. + * Returns empty array if no restrictions are needed (matches everything). + */ +const buildConditions = ( + environment: string | null, + secretPath: string | null +): PermissionCondition[] => { + const conditions: PermissionCondition[] = []; + + // Add environment condition if present and not matching everything + if (environment) { + const globEnv = convertVaultWildcardToGlob(environment); + // Skip if matches everything (Vault * becomes **) + if (globEnv !== "**") { + const hasWildcard = globEnv.includes("*"); + conditions.push({ + lhs: "environment", + operator: hasWildcard + ? PermissionConditionOperators.$GLOB + : PermissionConditionOperators.$EQ, + rhs: globEnv + }); + } + } + + // Add secret path condition if present and not matching everything + if (secretPath && secretPath !== "/*") { + const globPath = convertVaultWildcardToGlob(secretPath); + // After conversion, /* becomes /** which matches everything + if (globPath !== "/**") { + const hasWildcard = globPath.includes("*"); + conditions.push({ + lhs: "secretPath", + operator: hasWildcard + ? PermissionConditionOperators.$GLOB + : PermissionConditionOperators.$EQ, + rhs: globPath + }); + } + } + + return conditions; +}; + +// ============================================================================ +// Rule Deduplication +// ============================================================================ + +/** + * Creates a unique key for deduplication of permission rules. + * Combines all actions and conditions into a single string identifier. + */ +const createPermissionRuleKey = (rule: SecretPermissionRule | FolderPermissionRule): string => { + const actions = Object.entries(rule) + .filter(([key]) => key !== "conditions") + .sort(([a], [b]) => a.localeCompare(b)) + .map(([key, value]) => `${key}:${value}`) + .join("|"); + + const conditions = (rule.conditions || []) + .map((c) => `${c.lhs}${c.operator}${c.rhs}`) + .sort() + .join("|"); + + return `${actions}::${conditions}`; +}; + +/** + * Adds a permission rule to the list if it's not a duplicate. + */ +const addPermissionRuleIfUnique = ( + rule: T, + rulesList: T[], + seenRules: Set +): void => { + const ruleKey = createPermissionRuleKey(rule); + if (!seenRules.has(ruleKey)) { + seenRules.add(ruleKey); + rulesList.push(rule); + } +}; + +// ============================================================================ +// Main Parser +// ============================================================================ + +/** + * Parses Vault HCL policy and converts it to Infisical permissions. + * + * Process: + * 1. Clean HCL (remove comments, whitespace) + * 2. Extract path blocks with regex + * 3. For each path: + * - Parse to extract mount, environment, and secret path + * - Determine if it's a data path (secrets) or metadata path (folders) + * - Map Vault capabilities to Infisical actions + * - Build conditions for environment and path filtering + * - Create permission rule and add if unique + * + * @param hclPolicy - Raw Vault HCL policy string + * @param mounts - List of Vault mounts to match paths against + * @returns Parsed permissions object ready for Infisical role creation + */ +export const parseVaultPolicyToInfisical = ( + hclPolicy: string, + mounts: VaultMount[] +): Partial => { + const secretsPermissions: SecretPermissionRule[] = []; + const foldersPermissions: FolderPermissionRule[] = []; + + const seenSecretRules = new Set(); + const seenFolderRules = new Set(); + + try { + // Step 1: Clean HCL policy - remove comments and extra whitespace + const cleanedPolicy = hclPolicy + .split("\n") + .map((line) => line.replace(/#.*$/, "").trim()) + .filter((line) => line.length > 0) + .join(" "); + + // Step 2: Extract path blocks using regex + const pathRegex = /path\s+"([^"]+)"\s*\{[^}]*capabilities\s*=\s*\[([^\]]+)\][^}]*\}/gi; + let match = pathRegex.exec(cleanedPolicy); + + // Step 3: Process each path block + while (match !== null) { + const [, path, capabilitiesStr] = match; + + // Parse capabilities list + const capabilities = capabilitiesStr + .split(",") + .map((c) => c.trim().replace(/["'\s]/g, "")) + .filter((c) => c.length > 0); + + // Parse the Vault path + const { environment, secretPath, mount } = parseVaultPath(path, mounts); + + // Only process KV (Key-Value) secret engines + if (mount && (mount.type === "kv" || mount.type === "generic")) { + const isKvV2 = mount.version === "2"; + const isMetadata = isKvV2 && path.includes("/metadata/"); + + if (isMetadata) { + // Metadata paths → Folder permissions only (KV v2 metadata endpoint) + const actions = mapVaultCapabilitiesToFolderActions(capabilities); + if (Object.keys(actions).length > 0) { + const conditions = buildConditions(environment, secretPath); + addPermissionRuleIfUnique( + { ...actions, conditions }, + foldersPermissions, + seenFolderRules + ); + } + } else { + // Data paths → Both secret AND folder permissions (KV v1 and v2 data paths) + // Users need both to fully manage secrets and their containing folders + const conditions = buildConditions(environment, secretPath); + + // Create secret permissions + const secretActions = mapVaultCapabilitiesToSecretActions(capabilities); + if (Object.keys(secretActions).length > 0) { + addPermissionRuleIfUnique( + { ...secretActions, conditions }, + secretsPermissions, + seenSecretRules + ); + } + + // Create folder permissions for create/update/delete capabilities + const folderActions = mapVaultCapabilitiesToFolderActions(capabilities); + if (Object.keys(folderActions).length > 0) { + addPermissionRuleIfUnique( + { ...folderActions, conditions }, + foldersPermissions, + seenFolderRules + ); + } + } + } + + match = pathRegex.exec(cleanedPolicy); + } + } catch (err) { + console.error("Error parsing HCL policy:", err); + } + + // Build final permissions object + const permissions: Partial = {}; + if (secretsPermissions.length > 0) { + permissions[ProjectPermissionSub.Secrets] = secretsPermissions; + } + if (foldersPermissions.length > 0) { + permissions[ProjectPermissionSub.SecretFolders] = foldersPermissions; + } + + return permissions; +};