misc: resolved findings

This commit is contained in:
Sheen Capadngan
2025-10-15 19:09:16 +08:00
parent d3b4ef6088
commit 79ff152a67
6 changed files with 244 additions and 146 deletions
@@ -199,15 +199,11 @@ export const listHCVaultPolicies = async (
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); 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 { try {
const { data: listData } = await requestWithHCVaultGateway<{ const { data: listData } = await requestWithHCVaultGateway<{
policies: string[]; data: {
policies: string[];
};
}>(connection, gatewayService, { }>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/policy`, url: `${instanceUrl}/v1/sys/policy`,
method: "GET", method: "GET",
@@ -217,14 +213,16 @@ export const listHCVaultPolicies = async (
} }
}); });
const policyNames = listData.policies || []; const policyNames = listData.data.policies || [];
const policies = await Promise.all( const policies = await Promise.all(
policyNames.map(async (policyName) => { policyNames.map(async (policyName) => {
try { try {
const { data: policyData } = await requestWithHCVaultGateway<{ const { data: policyData } = await requestWithHCVaultGateway<{
name: string; data: {
rules: string; name: string;
rules: string;
};
}>(connection, gatewayService, { }>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/policy/${policyName}`, url: `${instanceUrl}/v1/sys/policy/${policyName}`,
method: "GET", method: "GET",
@@ -235,8 +233,8 @@ export const listHCVaultPolicies = async (
}); });
return { return {
name: policyData.name, name: policyData.data.name,
rules: policyData.rules rules: policyData.data.rules
}; };
} catch (error: unknown) { } catch (error: unknown) {
logger.error(error, `Unable to fetch policy details for ${policyName}`); logger.error(error, `Unable to fetch policy details for ${policyName}`);
@@ -271,50 +269,95 @@ export const listHCVaultNamespaces = async (
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); const accessToken = await getHCVaultAccessToken(connection, gatewayService);
try { const currentNamespace = connection.credentials.namespace || "/";
const { data } = await requestWithHCVaultGateway<{
data: { // Helper function to fetch namespaces at a specific path
keys: string[]; const fetchNamespacesAtPath = async (namespacePath: string): Promise<string[] | null> => {
key_info?: { try {
[key: string]: { const { data } = await requestWithHCVaultGateway<{
id: string; data: {
path: string; keys: string[];
custom_metadata?: Record<string, unknown>; key_info?: {
[key: string]: {
id: string;
path: string;
custom_metadata?: Record<string, unknown>;
};
}; };
}; };
}; }>(connection, gatewayService, {
}>(connection, gatewayService, { url: `${instanceUrl}/v1/sys/namespaces?list=true`,
url: `${instanceUrl}/v1/sys/namespaces`, method: "GET",
method: "LIST", headers: {
headers: { "X-Vault-Token": accessToken,
"X-Vault-Token": accessToken, "X-Vault-Namespace": namespacePath
...(connection.credentials.namespace ? { "X-Vault-Namespace": connection.credentials.namespace } : {}) }
} });
});
// Transform using key_info if available, otherwise fall back to keys array return data.data.keys || [];
const namespaces = (data.data.keys || []).map((namespaceKey) => { } catch (error: unknown) {
const keyInfo = data.data.key_info?.[namespaceKey]; if (error instanceof AxiosError && error.response?.status === 404) {
return { // No child namespaces at this path
id: keyInfo?.id || namespaceKey.replace(/\/$/, ""), // Use Vault's ID if available, otherwise use the key return null;
name: namespaceKey.replace(/\/$/, "") // Remove trailing slash for display }
}; throw error;
}
};
// Recursive function to get all namespaces at all depths
const recursivelyGetAllNamespaces = async (parentPath: string): Promise<string[]> => {
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; return namespaces;
} catch (error: unknown) { } 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"); logger.error(error, "Unable to list HC Vault namespaces");
if (error instanceof AxiosError) { if (error instanceof AxiosError) {
@@ -337,12 +380,6 @@ export const listHCVaultMounts = async (
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); 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 targetNamespace = namespace || connection.credentials.namespace;
const { data } = await requestWithHCVaultGateway<THCVaultMountResponse>(connection, gatewayService, { const { data } = await requestWithHCVaultGateway<THCVaultMountResponse>(connection, gatewayService, {
@@ -375,12 +412,6 @@ export const listHCVaultSecretPaths = async (
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); 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<string[] | null> => { const getPaths = async (mountPath: string, secretPath: string, kvVersion: "1" | "2"): Promise<string[] | null> => {
try { try {
let path: string; let path: string;
@@ -479,12 +510,6 @@ export const getHCVaultSecretsForPath = async (
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); 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 { try {
// Extract mount and path from the secretPath // Extract mount and path from the secretPath
// secretPath format: {mount}/{path} // secretPath format: {mount}/{path}
@@ -579,12 +604,6 @@ export const getHCVaultAuthMounts = async (
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); 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 { try {
const { data } = await requestWithHCVaultGateway<THCVaultAuthMountResponse>(connection, gatewayService, { const { data } = await requestWithHCVaultGateway<THCVaultAuthMountResponse>(connection, gatewayService, {
url: `${instanceUrl}/v1/sys/auth`, url: `${instanceUrl}/v1/sys/auth`,
@@ -633,12 +652,6 @@ export const getHCVaultKubernetesAuthRoles = async (
const instanceUrl = await getHCVaultInstanceUrl(connection); const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService); 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 // Remove trailing slash from mount path
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath; const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
+6 -6
View File
@@ -15,9 +15,9 @@ export const externalMigrationQueryKeys = {
], ],
config: (platform: string) => ["external-migration-config", { platform }], config: (platform: string) => ["external-migration-config", { platform }],
vaultNamespaces: () => ["vault-namespaces"], vaultNamespaces: () => ["vault-namespaces"],
vaultPolicies: () => ["vault-policies"], vaultPolicies: (namespace?: string) => ["vault-policies", namespace],
vaultMounts: () => ["vault-mounts"], vaultMounts: (namespace?: string) => ["vault-mounts", namespace],
vaultSecretPaths: () => ["vault-secret-paths"], vaultSecretPaths: (namespace?: string) => ["vault-secret-paths", namespace],
vaultKubernetesAuthRoles: (namespace?: string) => ["vault-kubernetes-auth-roles", namespace] vaultKubernetesAuthRoles: (namespace?: string) => ["vault-kubernetes-auth-roles", namespace]
}; };
@@ -61,7 +61,7 @@ export const useGetVaultNamespaces = () => {
export const useGetVaultPolicies = (enabled = true, namespace?: string) => { export const useGetVaultPolicies = (enabled = true, namespace?: string) => {
return useQuery({ return useQuery({
queryKey: externalMigrationQueryKeys.vaultPolicies(), queryKey: externalMigrationQueryKeys.vaultPolicies(namespace),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{
policies: Array<{ name: string; rules: string }>; policies: Array<{ name: string; rules: string }>;
@@ -79,7 +79,7 @@ export const useGetVaultPolicies = (enabled = true, namespace?: string) => {
export const useGetVaultMounts = (enabled = true, namespace?: string) => { export const useGetVaultMounts = (enabled = true, namespace?: string) => {
return useQuery({ return useQuery({
queryKey: externalMigrationQueryKeys.vaultMounts(), queryKey: externalMigrationQueryKeys.vaultMounts(namespace),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{
mounts: Array<{ path: string; type: string; version: string | null }>; 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) => { export const useGetVaultSecretPaths = (enabled = true, namespace?: string) => {
return useQuery({ return useQuery({
queryKey: externalMigrationQueryKeys.vaultSecretPaths(), queryKey: externalMigrationQueryKeys.vaultSecretPaths(namespace),
queryFn: async () => { queryFn: async () => {
const { data } = await apiRequest.get<{ const { data } = await apiRequest.get<{
secretPaths: string[]; secretPaths: string[];
@@ -212,13 +212,6 @@ export const IdentityKubernetesAuthForm = ({
shouldValidate: true 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) { if (role.bound_service_account_names?.length > 0) {
// In Vault, "*" means allow all; in Infisical, empty field means allow any // In Vault, "*" means allow all; in Infisical, empty field means allow any
const allowedNames = role.bound_service_account_names.includes("*") const allowedNames = role.bound_service_account_names.includes("*")
@@ -27,16 +27,15 @@ type ContentProps = {
}; };
const Content = ({ onClose, onImport }: ContentProps) => { const Content = ({ onClose, onImport }: ContentProps) => {
const [selectedNamespace, setSelectedNamespace] = useState<string>("default"); const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
const [selectedRole, setSelectedRole] = useState<VaultKubernetesAuthRole | null>(null); const [selectedRole, setSelectedRole] = useState<VaultKubernetesAuthRole | null>(null);
const [shouldFetchRoles, setShouldFetchRoles] = useState(false); const [shouldFetchRoles, setShouldFetchRoles] = useState(false);
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces(); const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
const { const { data: roles, isLoading: isLoadingRoles } = useGetVaultKubernetesAuthRoles(
data: roles, shouldFetchRoles,
isLoading: isLoadingRoles, selectedNamespace ?? undefined
refetch: refetchRoles );
} = useGetVaultKubernetesAuthRoles(shouldFetchRoles, selectedNamespace);
useEffect(() => { useEffect(() => {
if (selectedNamespace) { if (selectedNamespace) {
@@ -72,13 +71,11 @@ const Content = ({ onClose, onImport }: ContentProps) => {
const namespace = value as { id: string; name: string }; const namespace = value as { id: string; name: string };
setSelectedNamespace(namespace.name); setSelectedNamespace(namespace.name);
setSelectedRole(null); setSelectedRole(null);
// Refetch roles when namespace changes
refetchRoles();
} }
}} }}
options={namespaces || []} options={namespaces || []}
getOptionValue={(option) => option.name} getOptionValue={(option) => option.name}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)}
isDisabled={isLoadingNamespaces} isDisabled={isLoadingNamespaces}
placeholder="Select namespace..." placeholder="Select namespace..."
className="w-full" className="w-full"
@@ -132,6 +129,7 @@ export const VaultKubernetesAuthImportModal = ({ isOpen, onOpenChange, onImport
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent <ModalContent
bodyClassName="overflow-visible"
title="Load Kubernetes Auth from HashiCorp Vault" title="Load Kubernetes Auth from HashiCorp Vault"
subTitle="Load Kubernetes authentication configuration from your Vault instance. The auth method and role settings will be automatically translated and prefilled in the form." subTitle="Load Kubernetes authentication configuration from your Vault instance. The auth method and role settings will be automatically translated and prefilled in the form."
className="max-w-2xl" className="max-w-2xl"
@@ -52,13 +52,73 @@ type FolderPermissionRule = ArrayElement<
const parseVaultPath = ( const parseVaultPath = (
vaultPath: string, vaultPath: string,
mounts: VaultMount[] mounts: VaultMount[]
): { environment: string | null; secretPath: string | null; mount: VaultMount | null } => { ): {
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 // Find the matching mount for this path
// Sort by path length (longest first) to match most specific mount // Sort by path length (longest first) to match most specific mount
const sortedMounts = [...mounts].sort((a, b) => b.path.length - a.path.length); const sortedMounts = [...mounts].sort((a, b) => b.path.length - a.path.length);
const mount = sortedMounts.find((m) => vaultPath.startsWith(m.path)); const mount = sortedMounts.find((m) => vaultPath.startsWith(m.path));
if (!mount) { 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 // 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 // HCL parser for Vault policies - converts Vault HCL to Infisical permissions
@@ -115,6 +191,9 @@ const parseVaultPolicyToInfisical = (
const secretsPermissions: SecretPermissionRule[] = []; const secretsPermissions: SecretPermissionRule[] = [];
const foldersPermissions: FolderPermissionRule[] = []; const foldersPermissions: FolderPermissionRule[] = [];
const seenSecretRules = new Set<string>();
const seenFolderRules = new Set<string>();
try { try {
// Remove comments from HCL before parsing // Remove comments from HCL before parsing
const cleanedPolicy = hclPolicy const cleanedPolicy = hclPolicy
@@ -135,14 +214,16 @@ const parseVaultPolicyToInfisical = (
.map((c) => c.trim().replace(/["'\s]/g, "")) // Remove quotes, spaces, newlines .map((c) => c.trim().replace(/["'\s]/g, "")) // Remove quotes, spaces, newlines
.filter((c) => c.length > 0); // Filter out empty strings .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); const { environment, secretPath, mount } = parseVaultPath(path, mounts);
// Only process KV (Key-Value) mounts // Only process KV (Key-Value) mounts
if (mount && (mount.type === "kv" || mount.type === "generic")) { if (mount && (mount.type === "kv" || mount.type === "generic")) {
const isKvV2 = mount.version === "2"; const isKvV2 = mount.version === "2";
const isDataPath = isKvV2 ? path.includes("/data/") : true; // KV v1 = all paths are data paths // For KV v2: explicit metadata paths are metadata, explicit data paths or paths without prefix are data
const isMetadataPath = isKvV2 ? path.includes("/metadata/") : false; // KV v1 has no metadata endpoint // 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) { if (isDataPath && !isMetadataPath) {
// Data paths map to secret permissions // Data paths map to secret permissions
@@ -154,7 +235,8 @@ const parseVaultPolicyToInfisical = (
actions[ProjectPermissionSecretActions.DescribeSecret] = true; actions[ProjectPermissionSecretActions.DescribeSecret] = true;
actions[ProjectPermissionSecretActions.ReadValue] = 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")) if (capabilities.includes("delete"))
actions[ProjectPermissionSecretActions.Delete] = true; actions[ProjectPermissionSecretActions.Delete] = true;
@@ -179,7 +261,7 @@ const parseVaultPolicyToInfisical = (
} }
// Add secret path condition with glob support // Add secret path condition with glob support
if (secretPath) { if (secretPath && secretPath !== "/*") {
// Convert Vault wildcards to picomatch glob patterns // Convert Vault wildcards to picomatch glob patterns
// Vault '*' = match within segment, picomatch '**' = match across segments // Vault '*' = match within segment, picomatch '**' = match across segments
// Vault '+' = single segment, convert to '*' (note: slightly more permissive) // Vault '+' = single segment, convert to '*' (note: slightly more permissive)
@@ -195,17 +277,25 @@ const parseVaultPolicyToInfisical = (
}); });
} }
secretsPermissions.push({ const newRule = {
...actions, ...actions,
conditions conditions
}); };
// Check for duplicates before adding
const ruleKey = createPermissionRuleKey(newRule);
if (!seenSecretRules.has(ruleKey)) {
seenSecretRules.add(ruleKey);
secretsPermissions.push(newRule);
}
} }
} else if (isMetadataPath) { } else if (isMetadataPath) {
// Metadata paths map to folder permissions // Metadata paths map to folder permissions
const actions: { [key: string]: boolean } = {}; const actions: { [key: string]: boolean } = {};
if (capabilities.includes("create")) actions[ProjectPermissionActions.Create] = true; 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 (capabilities.includes("delete")) actions[ProjectPermissionActions.Delete] = true;
if (Object.keys(actions).length > 0) { if (Object.keys(actions).length > 0) {
@@ -229,7 +319,7 @@ const parseVaultPolicyToInfisical = (
} }
// Add secret path condition for folders with glob support // Add secret path condition for folders with glob support
if (secretPath) { if (secretPath && secretPath !== "/*") {
// Convert Vault '+' wildcard to glob '*' // Convert Vault '+' wildcard to glob '*'
const globPath = secretPath.replace(/\+/g, "*"); const globPath = secretPath.replace(/\+/g, "*");
const hasWildcard = globPath.includes("*"); const hasWildcard = globPath.includes("*");
@@ -242,10 +332,17 @@ const parseVaultPolicyToInfisical = (
}); });
} }
foldersPermissions.push({ const newRule = {
...actions, ...actions,
conditions 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 Content = ({ onClose }: ContentProps) => {
const rootForm = useFormContext<TFormSchema>(); const rootForm = useFormContext<TFormSchema>();
const [selectedNamespace, setSelectedNamespace] = useState<string>("default"); const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
const [selectedPolicy, setSelectedPolicy] = useState<string | null>(null); const [selectedPolicy, setSelectedPolicy] = useState<string | null>(null);
const [hclPolicy, setHclPolicy] = useState<string>(""); const [hclPolicy, setHclPolicy] = useState<string>("");
const [shouldFetchPolicies, setShouldFetchPolicies] = useState(false); const [shouldFetchPolicies, setShouldFetchPolicies] = useState(false);
const [shouldFetchMounts, setShouldFetchMounts] = useState(false); const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces(); const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
const { const { data: policies, isLoading: isLoadingPolicies } = useGetVaultPolicies(
data: policies, shouldFetchPolicies,
isLoading: isLoadingPolicies, selectedNamespace ?? undefined
refetch: refetchPolicies );
} = useGetVaultPolicies(shouldFetchPolicies, selectedNamespace); const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts(
const { shouldFetchMounts,
data: mounts, selectedNamespace ?? undefined
isLoading: isLoadingMounts, );
refetch: refetchMounts
} = useGetVaultMounts(shouldFetchMounts, selectedNamespace);
// Enable fetching policies and mounts when namespace is selected // Enable fetching policies and mounts when namespace is selected
useEffect(() => { useEffect(() => {
@@ -414,19 +509,17 @@ const Content = ({ onClose }: ContentProps) => {
> >
<> <>
<FilterableSelect <FilterableSelect
value={namespaces?.find((ns) => ns.name === selectedNamespace)} value={namespaces?.find((ns) => ns.id === selectedNamespace)}
onChange={(value) => { onChange={(value) => {
if (value && !Array.isArray(value)) { if (value && !Array.isArray(value)) {
const namespace = value as { id: string; name: string }; const namespace = value as { id: string; name: string };
setSelectedNamespace(namespace.name); setSelectedNamespace(namespace.name);
// Refetch policies and mounts when namespace changes setSelectedPolicy(null);
refetchPolicies();
refetchMounts();
} }
}} }}
options={namespaces || []} options={namespaces || []}
getOptionValue={(option) => option.name} getOptionValue={(option) => option.name}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)}
isDisabled={isLoadingNamespaces} isDisabled={isLoadingNamespaces}
placeholder="Select namespace..." placeholder="Select namespace..."
className="w-full" className="w-full"
@@ -33,22 +33,20 @@ type ContentProps = {
}; };
const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) => { const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) => {
const [selectedNamespace, setSelectedNamespace] = useState<string>("default"); const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
const [selectedPath, setSelectedPath] = useState<string | null>(null); const [selectedPath, setSelectedPath] = useState<string | null>(null);
const [shouldFetchPaths, setShouldFetchPaths] = useState(false); const [shouldFetchPaths, setShouldFetchPaths] = useState(false);
const [shouldFetchMounts, setShouldFetchMounts] = useState(false); const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces(); const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
const { const { data: secretPaths, isLoading: isLoadingPaths } = useGetVaultSecretPaths(
data: secretPaths, shouldFetchPaths,
isLoading: isLoadingPaths, selectedNamespace ?? undefined
refetch: refetchPaths );
} = useGetVaultSecretPaths(shouldFetchPaths, selectedNamespace); const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts(
const { shouldFetchMounts,
data: mounts, selectedNamespace ?? undefined
isLoading: isLoadingMounts, );
refetch: refetchMounts
} = useGetVaultMounts(shouldFetchMounts, selectedNamespace);
// Enable fetching paths and mounts when namespace is selected // Enable fetching paths and mounts when namespace is selected
useEffect(() => { useEffect(() => {
@@ -64,6 +62,11 @@ const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) =
return; return;
} }
if (!selectedNamespace) {
createNotification({ type: "error", text: "Please select a namespace" });
return;
}
if (!mounts || mounts.length === 0) { if (!mounts || mounts.length === 0) {
createNotification({ createNotification({
type: "error", type: "error",
@@ -109,14 +112,11 @@ const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) =
const namespace = value as { id: string; name: string }; const namespace = value as { id: string; name: string };
setSelectedNamespace(namespace.name); setSelectedNamespace(namespace.name);
setSelectedPath(null); setSelectedPath(null);
// Refetch paths and mounts when namespace changes
refetchPaths();
refetchMounts();
} }
}} }}
options={namespaces || []} options={namespaces || []}
getOptionValue={(option) => option.name} getOptionValue={(option) => option.name}
getOptionLabel={(option) => option.name} getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)}
isDisabled={isLoadingNamespaces} isDisabled={isLoadingNamespaces}
placeholder="Select namespace..." placeholder="Select namespace..."
className="w-full" className="w-full"
@@ -179,6 +179,7 @@ export const VaultSecretImportModal = ({
return ( return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}> <Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<ModalContent <ModalContent
bodyClassName="overflow-visible"
title="Import from HashiCorp Vault" title="Import from HashiCorp Vault"
subTitle="Select a Vault namespace and secret path to import secrets into the current environment and folder." subTitle="Select a Vault namespace and secret path to import secrets into the current environment and folder."
className="max-w-2xl" className="max-w-2xl"