From 79ff152a67b757b6b1652a8985d9b9d6c1e55afc Mon Sep 17 00:00:00 2001 From: Sheen Capadngan Date: Wed, 15 Oct 2025 19:09:16 +0800 Subject: [PATCH] misc: resolved findings --- .../hc-vault/hc-vault-connection-fns.ts | 171 ++++++++++-------- frontend/src/hooks/api/migration/queries.tsx | 12 +- .../IdentityKubernetesAuthForm.tsx | 7 - .../VaultKubernetesAuthImportModal.tsx | 16 +- .../components/VaultPolicyImportModal.tsx | 153 +++++++++++++--- .../ActionBar/VaultSecretImportModal.tsx | 31 ++-- 6 files changed, 244 insertions(+), 146 deletions(-) diff --git a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts index 84574cfb0..f1c65a9f4 100644 --- a/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts +++ b/backend/src/services/app-connection/hc-vault/hc-vault-connection-fns.ts @@ -199,15 +199,11 @@ export const listHCVaultPolicies = async ( const instanceUrl = await getHCVaultInstanceUrl(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService); - if (connection.credentials.namespace && connection.credentials.namespace !== namespace) { - throw new BadRequestError({ - message: "Specified namespace does not match the namespace in the connection credentials" - }); - } - try { const { data: listData } = await requestWithHCVaultGateway<{ - policies: string[]; + data: { + policies: string[]; + }; }>(connection, gatewayService, { url: `${instanceUrl}/v1/sys/policy`, method: "GET", @@ -217,14 +213,16 @@ export const listHCVaultPolicies = async ( } }); - const policyNames = listData.policies || []; + const policyNames = listData.data.policies || []; const policies = await Promise.all( policyNames.map(async (policyName) => { try { const { data: policyData } = await requestWithHCVaultGateway<{ - name: string; - rules: string; + data: { + name: string; + rules: string; + }; }>(connection, gatewayService, { url: `${instanceUrl}/v1/sys/policy/${policyName}`, method: "GET", @@ -235,8 +233,8 @@ export const listHCVaultPolicies = async ( }); return { - name: policyData.name, - rules: policyData.rules + name: policyData.data.name, + rules: policyData.data.rules }; } catch (error: unknown) { logger.error(error, `Unable to fetch policy details for ${policyName}`); @@ -271,50 +269,95 @@ export const listHCVaultNamespaces = async ( const instanceUrl = await getHCVaultInstanceUrl(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService); - try { - const { data } = await requestWithHCVaultGateway<{ - data: { - keys: string[]; - key_info?: { - [key: string]: { - id: string; - path: string; - custom_metadata?: Record; + const currentNamespace = connection.credentials.namespace || "/"; + + // Helper function to fetch namespaces at a specific path + const fetchNamespacesAtPath = async (namespacePath: string): Promise => { + try { + const { data } = await requestWithHCVaultGateway<{ + data: { + keys: string[]; + key_info?: { + [key: string]: { + id: string; + path: string; + custom_metadata?: Record; + }; }; }; - }; - }>(connection, gatewayService, { - url: `${instanceUrl}/v1/sys/namespaces`, - method: "LIST", - headers: { - "X-Vault-Token": accessToken, - ...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) - } - }); + }>(connection, gatewayService, { + url: `${instanceUrl}/v1/sys/namespaces?list=true`, + method: "GET", + headers: { + "X-Vault-Token": accessToken, + "X-Vault-Namespace": namespacePath + } + }); - // Transform using key_info if available, otherwise fall back to keys array - const namespaces = (data.data.keys || []).map((namespaceKey) => { - const keyInfo = data.data.key_info?.[namespaceKey]; - return { - id: keyInfo?.id || namespaceKey.replace(/\/$/, ""), // Use Vault's ID if available, otherwise use the key - name: namespaceKey.replace(/\/$/, "") // Remove trailing slash for display - }; + return data.data.keys || []; + } catch (error: unknown) { + if (error instanceof AxiosError && error.response?.status === 404) { + // No child namespaces at this path + return null; + } + throw error; + } + }; + + // Recursive function to get all namespaces at all depths + const recursivelyGetAllNamespaces = async (parentPath: string): Promise => { + const childKeys = await fetchNamespacesAtPath(parentPath); + + if (childKeys === null || childKeys.length === 0) { + return []; + } + + const allNamespaces: string[] = []; + + // Process namespaces sequentially to maintain order + // eslint-disable-next-line no-restricted-syntax + for (const namespaceKey of childKeys) { + // Remove trailing slash from the key + const cleanNamespaceKey = namespaceKey.replace(/\/$/, ""); + + // Build the full path + let fullNamespacePath: string; + if (parentPath === "/") { + fullNamespacePath = cleanNamespaceKey; + } else { + fullNamespacePath = `${parentPath}/${cleanNamespaceKey}`; + } + + // Add this namespace to our results + allNamespaces.push(fullNamespacePath); + + // Recursively fetch child namespaces + // eslint-disable-next-line no-await-in-loop + const childNamespaces = await recursivelyGetAllNamespaces(fullNamespacePath); + allNamespaces.push(...childNamespaces); + } + + return allNamespaces; + }; + + try { + // Get all namespaces starting from currentNamespace + const childNamespaces = await recursivelyGetAllNamespaces(currentNamespace); + + // Build the result array with full paths + const namespaces = childNamespaces.map((path) => ({ + id: path, + name: path + })); + + // Always include the current/root namespace + namespaces.unshift({ + id: currentNamespace, + name: currentNamespace }); return namespaces; } catch (error: unknown) { - // 404 means namespaces endpoint doesn't exist (Vault Community Edition) - // Return empty array to gracefully degrade - if (error instanceof AxiosError && error.response?.status === 404) { - logger.info("Namespaces endpoint not available (likely Vault Community Edition). Returning empty list."); - return [ - { - id: "default", - name: "default" - } - ]; - } - logger.error(error, "Unable to list HC Vault namespaces"); if (error instanceof AxiosError) { @@ -337,12 +380,6 @@ export const listHCVaultMounts = async ( const instanceUrl = await getHCVaultInstanceUrl(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService); - if (namespace && connection.credentials.namespace) { - throw new BadRequestError({ - message: "Namespace cannot be specified when namespace is already set in the connection credentials" - }); - } - const targetNamespace = namespace || connection.credentials.namespace; const { data } = await requestWithHCVaultGateway(connection, gatewayService, { @@ -375,12 +412,6 @@ export const listHCVaultSecretPaths = async ( const instanceUrl = await getHCVaultInstanceUrl(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService); - if (connection.credentials.namespace && connection.credentials.namespace !== namespace) { - throw new BadRequestError({ - message: "Specified namespace does not match the namespace in the connection credentials" - }); - } - const getPaths = async (mountPath: string, secretPath: string, kvVersion: "1" | "2"): Promise => { try { let path: string; @@ -479,12 +510,6 @@ export const getHCVaultSecretsForPath = async ( const instanceUrl = await getHCVaultInstanceUrl(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService); - if (connection.credentials.namespace && connection.credentials.namespace !== namespace) { - throw new BadRequestError({ - message: "Specified namespace does not match the namespace in the connection credentials" - }); - } - try { // Extract mount and path from the secretPath // secretPath format: {mount}/{path} @@ -579,12 +604,6 @@ export const getHCVaultAuthMounts = async ( const instanceUrl = await getHCVaultInstanceUrl(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService); - if (connection.credentials.namespace && connection.credentials.namespace !== namespace) { - throw new BadRequestError({ - message: "Specified namespace does not match the namespace in the connection credentials" - }); - } - try { const { data } = await requestWithHCVaultGateway(connection, gatewayService, { url: `${instanceUrl}/v1/sys/auth`, @@ -633,12 +652,6 @@ export const getHCVaultKubernetesAuthRoles = async ( const instanceUrl = await getHCVaultInstanceUrl(connection); const accessToken = await getHCVaultAccessToken(connection, gatewayService); - if (connection.credentials.namespace && connection.credentials.namespace !== namespace) { - throw new BadRequestError({ - message: "Specified namespace does not match the namespace in the connection credentials" - }); - } - // Remove trailing slash from mount path const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath; diff --git a/frontend/src/hooks/api/migration/queries.tsx b/frontend/src/hooks/api/migration/queries.tsx index f257ea005..ab0816d3e 100644 --- a/frontend/src/hooks/api/migration/queries.tsx +++ b/frontend/src/hooks/api/migration/queries.tsx @@ -15,9 +15,9 @@ export const externalMigrationQueryKeys = { ], config: (platform: string) => ["external-migration-config", { platform }], vaultNamespaces: () => ["vault-namespaces"], - vaultPolicies: () => ["vault-policies"], - vaultMounts: () => ["vault-mounts"], - vaultSecretPaths: () => ["vault-secret-paths"], + vaultPolicies: (namespace?: string) => ["vault-policies", namespace], + vaultMounts: (namespace?: string) => ["vault-mounts", namespace], + vaultSecretPaths: (namespace?: string) => ["vault-secret-paths", namespace], vaultKubernetesAuthRoles: (namespace?: string) => ["vault-kubernetes-auth-roles", namespace] }; @@ -61,7 +61,7 @@ export const useGetVaultNamespaces = () => { export const useGetVaultPolicies = (enabled = true, namespace?: string) => { return useQuery({ - queryKey: externalMigrationQueryKeys.vaultPolicies(), + queryKey: externalMigrationQueryKeys.vaultPolicies(namespace), queryFn: async () => { const { data } = await apiRequest.get<{ policies: Array<{ name: string; rules: string }>; @@ -79,7 +79,7 @@ export const useGetVaultPolicies = (enabled = true, namespace?: string) => { export const useGetVaultMounts = (enabled = true, namespace?: string) => { return useQuery({ - queryKey: externalMigrationQueryKeys.vaultMounts(), + queryKey: externalMigrationQueryKeys.vaultMounts(namespace), queryFn: async () => { const { data } = await apiRequest.get<{ mounts: Array<{ path: string; type: string; version: string | null }>; @@ -97,7 +97,7 @@ export const useGetVaultMounts = (enabled = true, namespace?: string) => { export const useGetVaultSecretPaths = (enabled = true, namespace?: string) => { return useQuery({ - queryKey: externalMigrationQueryKeys.vaultSecretPaths(), + queryKey: externalMigrationQueryKeys.vaultSecretPaths(namespace), queryFn: async () => { const { data } = await apiRequest.get<{ secretPaths: string[]; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 51be8a5d5..52bb4d808 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -212,13 +212,6 @@ export const IdentityKubernetesAuthForm = ({ shouldValidate: true }); - if (role.config.token_reviewer_jwt) { - setValue("tokenReviewerJwt", role.config.token_reviewer_jwt, { - shouldDirty: true, - shouldTouch: true - }); - } - if (role.bound_service_account_names?.length > 0) { // In Vault, "*" means allow all; in Infisical, empty field means allow any const allowedNames = role.bound_service_account_names.includes("*") diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/VaultKubernetesAuthImportModal.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/VaultKubernetesAuthImportModal.tsx index e56047450..f31921b90 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/VaultKubernetesAuthImportModal.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/VaultKubernetesAuthImportModal.tsx @@ -27,16 +27,15 @@ type ContentProps = { }; const Content = ({ onClose, onImport }: ContentProps) => { - const [selectedNamespace, setSelectedNamespace] = useState("default"); + const [selectedNamespace, setSelectedNamespace] = useState(null); const [selectedRole, setSelectedRole] = useState(null); const [shouldFetchRoles, setShouldFetchRoles] = useState(false); const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces(); - const { - data: roles, - isLoading: isLoadingRoles, - refetch: refetchRoles - } = useGetVaultKubernetesAuthRoles(shouldFetchRoles, selectedNamespace); + const { data: roles, isLoading: isLoadingRoles } = useGetVaultKubernetesAuthRoles( + shouldFetchRoles, + selectedNamespace ?? undefined + ); useEffect(() => { if (selectedNamespace) { @@ -72,13 +71,11 @@ const Content = ({ onClose, onImport }: ContentProps) => { const namespace = value as { id: string; name: string }; setSelectedNamespace(namespace.name); setSelectedRole(null); - // Refetch roles when namespace changes - refetchRoles(); } }} options={namespaces || []} getOptionValue={(option) => option.name} - getOptionLabel={(option) => option.name} + getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)} isDisabled={isLoadingNamespaces} placeholder="Select namespace..." className="w-full" @@ -132,6 +129,7 @@ export const VaultKubernetesAuthImportModal = ({ isOpen, onOpenChange, onImport return ( { +): { + 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 }; + return { environment: null, secretPath: null, mount: null, isWildcardMount: false }; } // Remove mount prefix and any trailing slash @@ -103,7 +163,23 @@ const parseVaultPath = ( } } - return { environment, secretPath, mount }; + 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 @@ -115,6 +191,9 @@ const parseVaultPolicyToInfisical = ( const secretsPermissions: SecretPermissionRule[] = []; const foldersPermissions: FolderPermissionRule[] = []; + const seenSecretRules = new Set(); + const seenFolderRules = new Set(); + try { // Remove comments from HCL before parsing const cleanedPolicy = hclPolicy @@ -135,14 +214,16 @@ const parseVaultPolicyToInfisical = ( .map((c) => c.trim().replace(/["'\s]/g, "")) // Remove quotes, spaces, newlines .filter((c) => c.length > 0); // Filter out empty strings - // Parse the Vault path using mount information + // 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"; - const isDataPath = isKvV2 ? path.includes("/data/") : true; // KV v1 = all paths are data paths - const isMetadataPath = isKvV2 ? path.includes("/metadata/") : false; // KV v1 has no metadata endpoint + // 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 @@ -154,7 +235,8 @@ const parseVaultPolicyToInfisical = ( actions[ProjectPermissionSecretActions.DescribeSecret] = true; actions[ProjectPermissionSecretActions.ReadValue] = true; } - if (capabilities.includes("update")) actions[ProjectPermissionSecretActions.Edit] = true; + if (capabilities.includes("update") || capabilities.includes("patch")) + actions[ProjectPermissionSecretActions.Edit] = true; if (capabilities.includes("delete")) actions[ProjectPermissionSecretActions.Delete] = true; @@ -179,7 +261,7 @@ const parseVaultPolicyToInfisical = ( } // Add secret path condition with glob support - if (secretPath) { + 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) @@ -195,17 +277,25 @@ const parseVaultPolicyToInfisical = ( }); } - secretsPermissions.push({ + 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")) actions[ProjectPermissionActions.Edit] = 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) { @@ -229,7 +319,7 @@ const parseVaultPolicyToInfisical = ( } // Add secret path condition for folders with glob support - if (secretPath) { + if (secretPath && secretPath !== "/*") { // Convert Vault '+' wildcard to glob '*' const globPath = secretPath.replace(/\+/g, "*"); const hasWildcard = globPath.includes("*"); @@ -242,10 +332,17 @@ const parseVaultPolicyToInfisical = ( }); } - foldersPermissions.push({ + const newRule = { ...actions, conditions - }); + }; + + // Check for duplicates before adding + const ruleKey = createPermissionRuleKey(newRule); + if (!seenFolderRules.has(ruleKey)) { + seenFolderRules.add(ruleKey); + foldersPermissions.push(newRule); + } } } } @@ -269,23 +366,21 @@ const parseVaultPolicyToInfisical = ( const Content = ({ onClose }: ContentProps) => { const rootForm = useFormContext(); - const [selectedNamespace, setSelectedNamespace] = useState("default"); + 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 { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces(); - const { - data: policies, - isLoading: isLoadingPolicies, - refetch: refetchPolicies - } = useGetVaultPolicies(shouldFetchPolicies, selectedNamespace); - const { - data: mounts, - isLoading: isLoadingMounts, - refetch: refetchMounts - } = useGetVaultMounts(shouldFetchMounts, selectedNamespace); + 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(() => { @@ -414,19 +509,17 @@ const Content = ({ onClose }: ContentProps) => { > <> ns.name === selectedNamespace)} + value={namespaces?.find((ns) => ns.id === selectedNamespace)} onChange={(value) => { if (value && !Array.isArray(value)) { const namespace = value as { id: string; name: string }; setSelectedNamespace(namespace.name); - // Refetch policies and mounts when namespace changes - refetchPolicies(); - refetchMounts(); + setSelectedPolicy(null); } }} options={namespaces || []} getOptionValue={(option) => option.name} - getOptionLabel={(option) => option.name} + getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)} isDisabled={isLoadingNamespaces} placeholder="Select namespace..." className="w-full" diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/VaultSecretImportModal.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/VaultSecretImportModal.tsx index b691c5418..7386f74be 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/VaultSecretImportModal.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/VaultSecretImportModal.tsx @@ -33,22 +33,20 @@ type ContentProps = { }; const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) => { - const [selectedNamespace, setSelectedNamespace] = useState("default"); + const [selectedNamespace, setSelectedNamespace] = useState(null); const [selectedPath, setSelectedPath] = useState(null); const [shouldFetchPaths, setShouldFetchPaths] = useState(false); const [shouldFetchMounts, setShouldFetchMounts] = useState(false); const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces(); - const { - data: secretPaths, - isLoading: isLoadingPaths, - refetch: refetchPaths - } = useGetVaultSecretPaths(shouldFetchPaths, selectedNamespace); - const { - data: mounts, - isLoading: isLoadingMounts, - refetch: refetchMounts - } = useGetVaultMounts(shouldFetchMounts, selectedNamespace); + const { data: secretPaths, isLoading: isLoadingPaths } = useGetVaultSecretPaths( + shouldFetchPaths, + selectedNamespace ?? undefined + ); + const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts( + shouldFetchMounts, + selectedNamespace ?? undefined + ); // Enable fetching paths and mounts when namespace is selected useEffect(() => { @@ -64,6 +62,11 @@ const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) = return; } + if (!selectedNamespace) { + createNotification({ type: "error", text: "Please select a namespace" }); + return; + } + if (!mounts || mounts.length === 0) { createNotification({ type: "error", @@ -109,14 +112,11 @@ const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) = const namespace = value as { id: string; name: string }; setSelectedNamespace(namespace.name); setSelectedPath(null); - // Refetch paths and mounts when namespace changes - refetchPaths(); - refetchMounts(); } }} options={namespaces || []} getOptionValue={(option) => option.name} - getOptionLabel={(option) => option.name} + getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)} isDisabled={isLoadingNamespaces} placeholder="Select namespace..." className="w-full" @@ -179,6 +179,7 @@ export const VaultSecretImportModal = ({ return (