mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 14:27:59 +00:00
misc: resolved findings
This commit is contained in:
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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[];
|
||||||
|
|||||||
-7
@@ -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("*")
|
||||||
|
|||||||
+7
-9
@@ -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"
|
||||||
|
|||||||
+123
-30
@@ -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"
|
||||||
|
|||||||
+16
-15
@@ -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"
|
||||||
|
|||||||
Reference in New Issue
Block a user