mirror of
https://github.com/Infisical/infisical.git
synced 2026-01-09 23:48:05 -05:00
ldap (ad) migration support
This commit is contained in:
@@ -574,4 +574,49 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
|
||||
server.route({
|
||||
method: "GET",
|
||||
url: "/vault/ldap-roles",
|
||||
config: {
|
||||
rateLimit: readLimit
|
||||
},
|
||||
schema: {
|
||||
querystring: z.object({
|
||||
namespace: z.string(),
|
||||
mountPath: z.string()
|
||||
}),
|
||||
response: {
|
||||
200: z.object({
|
||||
roles: z.array(
|
||||
z.object({
|
||||
name: z.string(),
|
||||
mountPath: z.string(),
|
||||
default_ttl: z.number().nullish(),
|
||||
max_ttl: z.number().nullish(),
|
||||
creation_ldif: z.string().nullish(),
|
||||
deletion_ldif: z.string().nullish(),
|
||||
rollback_ldif: z.string().nullish(),
|
||||
username_template: z.string().nullish(),
|
||||
config: z.object({
|
||||
binddn: z.string(),
|
||||
url: z.string(),
|
||||
certificate: z.string().nullish()
|
||||
})
|
||||
})
|
||||
)
|
||||
})
|
||||
}
|
||||
},
|
||||
onRequest: verifyAuth([AuthMode.JWT]),
|
||||
handler: async (req) => {
|
||||
const roles = await server.services.migration.getVaultLdapRoles({
|
||||
actor: req.permission,
|
||||
namespace: req.query.namespace,
|
||||
mountPath: req.query.mountPath
|
||||
});
|
||||
|
||||
return { roles };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -25,6 +25,8 @@ import {
|
||||
THCVaultKubernetesAuthRoleWithConfig,
|
||||
THCVaultKubernetesRole,
|
||||
THCVaultKubernetesSecretsConfig,
|
||||
THCVaultLdapConfig,
|
||||
THCVaultLdapRole,
|
||||
THCVaultMount,
|
||||
THCVaultMountResponse
|
||||
} from "./hc-vault-connection-types";
|
||||
@@ -1071,3 +1073,103 @@ export const getHCVaultDatabaseRoles = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
export const getHCVaultLdapRoles = async (
|
||||
namespace: string,
|
||||
mountPath: string,
|
||||
connection: THCVaultConnection,
|
||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||
): Promise<THCVaultLdapRole[]> => {
|
||||
// Remove trailing slash from mount path
|
||||
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
||||
|
||||
try {
|
||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||
|
||||
// 1. Get the LDAP secrets engine configuration for this mount
|
||||
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultLdapConfig }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/config`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const ldapConfig = configResponse.data;
|
||||
|
||||
// 2. List all dynamic roles in this mount
|
||||
let roleNames: string[] = [];
|
||||
try {
|
||||
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
|
||||
connection,
|
||||
gatewayService,
|
||||
{
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/role?list=true`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
}
|
||||
);
|
||||
roleNames = roleListResponse.data.keys || [];
|
||||
} catch (error) {
|
||||
if (isVault404Error(error)) return [];
|
||||
throw error;
|
||||
}
|
||||
|
||||
if (!roleNames || roleNames.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 3. Fetch details for each role with concurrency control
|
||||
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
|
||||
|
||||
const roleDetailsPromises = roleNames.map((roleName) =>
|
||||
limiter(async () => {
|
||||
const { data: roleResponse } = await requestWithHCVaultGateway<{
|
||||
data: {
|
||||
default_ttl?: number;
|
||||
max_ttl?: number;
|
||||
creation_ldif?: string;
|
||||
deletion_ldif?: string;
|
||||
rollback_ldif?: string;
|
||||
username_template?: string;
|
||||
};
|
||||
}>(connection, gatewayService, {
|
||||
url: `${instanceUrl}/v1/${cleanMountPath}/role/${roleName}`,
|
||||
method: "GET",
|
||||
headers: {
|
||||
"X-Vault-Token": accessToken,
|
||||
"X-Vault-Namespace": namespace
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
name: roleName,
|
||||
default_ttl: roleResponse.data.default_ttl,
|
||||
max_ttl: roleResponse.data.max_ttl,
|
||||
creation_ldif: roleResponse.data.creation_ldif,
|
||||
deletion_ldif: roleResponse.data.deletion_ldif,
|
||||
rollback_ldif: roleResponse.data.rollback_ldif,
|
||||
username_template: roleResponse.data.username_template,
|
||||
config: ldapConfig,
|
||||
mountPath: cleanMountPath
|
||||
} as THCVaultLdapRole;
|
||||
})
|
||||
);
|
||||
|
||||
return await Promise.all(roleDetailsPromises);
|
||||
} catch (error: unknown) {
|
||||
logger.error(error, "Unable to list HC Vault LDAP secrets engine roles");
|
||||
throw new BadRequestError({
|
||||
message: `Failed to list LDAP secrets engine roles: ${getVaultErrorMessage(error, "Unknown error")}`
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -139,3 +139,21 @@ export type THCVaultDatabaseRole = {
|
||||
config: THCVaultDatabaseConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
export type THCVaultLdapConfig = {
|
||||
binddn: string;
|
||||
url: string;
|
||||
certificate?: string;
|
||||
};
|
||||
|
||||
export type THCVaultLdapRole = {
|
||||
name: string;
|
||||
default_ttl?: number;
|
||||
max_ttl?: number;
|
||||
creation_ldif?: string;
|
||||
deletion_ldif?: string;
|
||||
rollback_ldif?: string;
|
||||
username_template?: string;
|
||||
config: THCVaultLdapConfig;
|
||||
mountPath: string;
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
getHCVaultDatabaseRoles,
|
||||
getHCVaultKubernetesAuthRoles,
|
||||
getHCVaultKubernetesRoles,
|
||||
getHCVaultLdapRoles,
|
||||
getHCVaultSecretsForPath,
|
||||
HCVaultAuthType,
|
||||
listHCVaultMounts,
|
||||
@@ -600,6 +601,19 @@ export const externalMigrationServiceFactory = ({
|
||||
return getHCVaultDatabaseRoles(namespace, mountPath, connection, gatewayService);
|
||||
};
|
||||
|
||||
const getVaultLdapRoles = async ({
|
||||
actor,
|
||||
namespace,
|
||||
mountPath
|
||||
}: {
|
||||
actor: OrgServiceActor;
|
||||
namespace: string;
|
||||
mountPath: string;
|
||||
}) => {
|
||||
const connection = await getVaultConnectionForNamespace(actor, namespace, "get LDAP roles");
|
||||
return getHCVaultLdapRoles(namespace, mountPath, connection, gatewayService);
|
||||
};
|
||||
|
||||
return {
|
||||
importEnvKeyData,
|
||||
importVaultData,
|
||||
@@ -616,6 +630,7 @@ export const externalMigrationServiceFactory = ({
|
||||
importVaultSecrets,
|
||||
getVaultKubernetesAuthRoles,
|
||||
getVaultKubernetesRoles,
|
||||
getVaultDatabaseRoles
|
||||
getVaultDatabaseRoles,
|
||||
getVaultLdapRoles
|
||||
};
|
||||
};
|
||||
|
||||
@@ -7,7 +7,8 @@ import {
|
||||
TVaultExternalMigrationConfig,
|
||||
VaultDatabaseRole,
|
||||
VaultKubernetesAuthRole,
|
||||
VaultKubernetesRole
|
||||
VaultKubernetesRole,
|
||||
VaultLdapRole
|
||||
} from "./types";
|
||||
|
||||
export const externalMigrationQueryKeys = {
|
||||
@@ -43,6 +44,11 @@ export const externalMigrationQueryKeys = {
|
||||
"vault-database-roles",
|
||||
namespace,
|
||||
mountPath
|
||||
],
|
||||
vaultLdapRoles: (namespace?: string, mountPath?: string) => [
|
||||
"vault-ldap-roles",
|
||||
namespace,
|
||||
mountPath
|
||||
]
|
||||
};
|
||||
|
||||
@@ -238,3 +244,26 @@ export const useGetVaultDatabaseRoles = (
|
||||
enabled: enabled && !!namespace && !!mountPath
|
||||
});
|
||||
};
|
||||
|
||||
export const useGetVaultLdapRoles = (enabled = true, namespace?: string, mountPath?: string) => {
|
||||
return useQuery({
|
||||
queryKey: externalMigrationQueryKeys.vaultLdapRoles(namespace, mountPath),
|
||||
queryFn: async () => {
|
||||
if (!namespace || !mountPath) {
|
||||
throw new Error("Both namespace and mountPath are required");
|
||||
}
|
||||
|
||||
const { data } = await apiRequest.get<{
|
||||
roles: VaultLdapRole[];
|
||||
}>("/api/v3/external-migration/vault/ldap-roles", {
|
||||
params: {
|
||||
namespace,
|
||||
mountPath
|
||||
}
|
||||
});
|
||||
|
||||
return data.roles;
|
||||
},
|
||||
enabled: enabled && !!namespace && !!mountPath
|
||||
});
|
||||
};
|
||||
|
||||
@@ -89,3 +89,19 @@ export type VaultDatabaseRole = {
|
||||
plugin_name: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type VaultLdapRole = {
|
||||
name: string;
|
||||
mountPath: string;
|
||||
default_ttl?: number;
|
||||
max_ttl?: number;
|
||||
creation_ldif?: string;
|
||||
deletion_ldif?: string;
|
||||
rollback_ldif?: string;
|
||||
username_template?: string;
|
||||
config: {
|
||||
binddn: string;
|
||||
url: string;
|
||||
certificate?: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { useState } from "react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import ms from "ms";
|
||||
import { z } from "zod";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
|
||||
import { TtlFormLabel } from "@app/components/features";
|
||||
import {
|
||||
Button,
|
||||
@@ -15,9 +18,13 @@ import {
|
||||
} from "@app/components/v2";
|
||||
import { useCreateDynamicSecret } from "@app/hooks/api";
|
||||
import { DynamicSecretProviders } from "@app/hooks/api/dynamicSecret/types";
|
||||
import { VaultLdapRole } from "@app/hooks/api/migration/types";
|
||||
import { ProjectEnv } from "@app/hooks/api/types";
|
||||
import { slugSchema } from "@app/lib/schemas";
|
||||
|
||||
import { LoadFromVaultBanner } from "./components/LoadFromVaultBanner";
|
||||
import { VaultLdapImportModal } from "./VaultLdapImportModal";
|
||||
|
||||
enum CredentialType {
|
||||
Dynamic = "dynamic",
|
||||
Static = "static"
|
||||
@@ -98,6 +105,8 @@ export const LdapInputForm = ({
|
||||
environments,
|
||||
isSingleEnvironmentMode
|
||||
}: Props) => {
|
||||
const [isVaultImportModalOpen, setIsVaultImportModalOpen] = useState(false);
|
||||
|
||||
const {
|
||||
control,
|
||||
formState: { isSubmitting },
|
||||
@@ -126,6 +135,64 @@ export const LdapInputForm = ({
|
||||
|
||||
const createDynamicSecret = useCreateDynamicSecret();
|
||||
|
||||
const handleVaultImport = (role: VaultLdapRole) => {
|
||||
try {
|
||||
setValue("name", role.name);
|
||||
|
||||
if (role.config.url) {
|
||||
setValue("provider.url", role.config.url);
|
||||
}
|
||||
|
||||
if (role.config.binddn) {
|
||||
setValue("provider.binddn", role.config.binddn);
|
||||
}
|
||||
|
||||
if (role.config.certificate) {
|
||||
setValue("provider.ca", role.config.certificate);
|
||||
}
|
||||
|
||||
// Set credential type to Dynamic if creation_ldif is present
|
||||
if (role.creation_ldif) {
|
||||
setValue("provider.credentialType", CredentialType.Dynamic);
|
||||
setValue("provider.creationLdif", role.creation_ldif);
|
||||
|
||||
if (role.deletion_ldif) {
|
||||
setValue("provider.revocationLdif", role.deletion_ldif);
|
||||
}
|
||||
|
||||
if (role.rollback_ldif) {
|
||||
setValue("provider.rollbackLdif", role.rollback_ldif);
|
||||
}
|
||||
}
|
||||
|
||||
// Set TTLs
|
||||
if (role.default_ttl) {
|
||||
const defaultTTL = `${role.default_ttl}s`;
|
||||
setValue("defaultTTL", defaultTTL);
|
||||
}
|
||||
|
||||
if (role.max_ttl) {
|
||||
const maxTTL = `${role.max_ttl}s`;
|
||||
setValue("maxTTL", maxTTL);
|
||||
}
|
||||
|
||||
// Set username template
|
||||
if (role.username_template) {
|
||||
setValue("usernameTemplate", role.username_template);
|
||||
}
|
||||
|
||||
createNotification({
|
||||
type: "info",
|
||||
text: "Configuration loaded successfully from HashiCorp Vault"
|
||||
});
|
||||
} catch {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Failed to load configuration from HashiCorp Vault"
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateDynamicSecret = async ({
|
||||
name,
|
||||
maxTTL,
|
||||
@@ -155,6 +222,7 @@ export const LdapInputForm = ({
|
||||
return (
|
||||
<form onSubmit={handleSubmit(handleCreateDynamicSecret)} autoComplete="off">
|
||||
<div>
|
||||
<LoadFromVaultBanner onClick={() => setIsVaultImportModalOpen(true)} />
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="grow">
|
||||
<Controller
|
||||
@@ -425,6 +493,11 @@ export const LdapInputForm = ({
|
||||
Cancel
|
||||
</Button>
|
||||
</div>
|
||||
<VaultLdapImportModal
|
||||
isOpen={isVaultImportModalOpen}
|
||||
onOpenChange={setIsVaultImportModalOpen}
|
||||
onImport={handleVaultImport}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
|
||||
import { createNotification } from "@app/components/notifications";
|
||||
import {
|
||||
Button,
|
||||
FilterableSelect,
|
||||
FormControl,
|
||||
Modal,
|
||||
ModalClose,
|
||||
ModalContent
|
||||
} from "@app/components/v2";
|
||||
import {
|
||||
useGetVaultLdapRoles,
|
||||
useGetVaultMounts,
|
||||
useGetVaultNamespaces
|
||||
} from "@app/hooks/api/migration/queries";
|
||||
import { VaultLdapRole } from "@app/hooks/api/migration/types";
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (isOpen: boolean) => void;
|
||||
onImport: (role: VaultLdapRole) => void;
|
||||
};
|
||||
|
||||
type ContentProps = {
|
||||
onClose: () => void;
|
||||
onImport: (role: VaultLdapRole) => void;
|
||||
};
|
||||
|
||||
const Content = ({ onClose, onImport }: ContentProps) => {
|
||||
const [selectedNamespace, setSelectedNamespace] = useState<string | null>(null);
|
||||
const [selectedMountPath, setSelectedMountPath] = useState<string | null>(null);
|
||||
const [selectedRole, setSelectedRole] = useState<VaultLdapRole | null>(null);
|
||||
const [shouldFetchRoles, setShouldFetchRoles] = useState(false);
|
||||
const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
|
||||
|
||||
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
|
||||
const { data: roles, isLoading: isLoadingRoles } = useGetVaultLdapRoles(
|
||||
shouldFetchRoles,
|
||||
selectedNamespace ?? undefined,
|
||||
selectedMountPath ?? undefined
|
||||
);
|
||||
const { data: mounts, isLoading: isLoadingMounts } = useGetVaultMounts(
|
||||
shouldFetchMounts,
|
||||
selectedNamespace ?? undefined
|
||||
);
|
||||
|
||||
// Filter to only show LDAP mounts
|
||||
const ldapMounts = mounts?.filter((mount) => mount.type === "ldap");
|
||||
|
||||
// Enable fetching mounts when namespace is selected
|
||||
useEffect(() => {
|
||||
if (selectedNamespace) {
|
||||
setShouldFetchMounts(true);
|
||||
}
|
||||
}, [selectedNamespace]);
|
||||
|
||||
// Enable fetching roles when both namespace and mount path are selected
|
||||
useEffect(() => {
|
||||
if (selectedNamespace && selectedMountPath) {
|
||||
setShouldFetchRoles(true);
|
||||
} else {
|
||||
setShouldFetchRoles(false);
|
||||
}
|
||||
}, [selectedNamespace, selectedMountPath]);
|
||||
|
||||
const handleImport = () => {
|
||||
if (!selectedRole) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "Please select a Vault LDAP role to load"
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!selectedNamespace) {
|
||||
createNotification({ type: "error", text: "Please select a namespace" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (!mounts || mounts.length === 0) {
|
||||
createNotification({
|
||||
type: "error",
|
||||
text: "No Vault mounts found. Please ensure you have LDAP secrets engine configured."
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
onImport(selectedRole);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 rounded-md bg-primary/10 p-3 text-sm text-mineshaft-200">
|
||||
<div className="flex items-start gap-2">
|
||||
<FontAwesomeIcon icon={faInfoCircle} className="mt-0.5 text-primary" />
|
||||
<div className="space-y-1.5 text-xs leading-relaxed">
|
||||
<p>
|
||||
Select an LDAP secrets engine role from Vault to pre-fill the form with its
|
||||
configuration including connection details, LDIF statements, TTL settings, etc.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormControl
|
||||
label="Namespace"
|
||||
className="mb-4"
|
||||
tooltipText="Select the Vault namespace containing the LDAP secrets engine."
|
||||
>
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={namespaces?.find((ns) => ns.name === selectedNamespace)}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const namespace = value as { id: string; name: string };
|
||||
setSelectedNamespace(namespace.name);
|
||||
setSelectedMountPath(null);
|
||||
setSelectedRole(null);
|
||||
}
|
||||
}}
|
||||
options={namespaces || []}
|
||||
getOptionValue={(option) => option.name}
|
||||
getOptionLabel={(option) => (option.name === "/" ? "root" : option.name)}
|
||||
isDisabled={isLoadingNamespaces}
|
||||
placeholder="Select namespace..."
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Select the Vault namespace to fetch available LDAP secrets engines
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl
|
||||
label="LDAP Secrets Engine"
|
||||
className="mb-4"
|
||||
tooltipText="Select the LDAP secrets engine mount to fetch available roles."
|
||||
>
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={ldapMounts?.find((mount) => mount.path === selectedMountPath)}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
const mount = value as { path: string; type: string; version: string | null };
|
||||
setSelectedMountPath(mount.path.replace(/\/$/, "")); // Remove trailing slash
|
||||
setSelectedRole(null);
|
||||
}
|
||||
}}
|
||||
options={ldapMounts || []}
|
||||
getOptionValue={(option) => option.path}
|
||||
getOptionLabel={(option) => option.path.replace(/\/$/, "")}
|
||||
isDisabled={isLoadingMounts || !ldapMounts?.length}
|
||||
placeholder="Select LDAP secrets engine..."
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Choose an LDAP secrets engine mount to list available roles
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<FormControl label="LDAP Role" className="mb-6">
|
||||
<>
|
||||
<FilterableSelect
|
||||
value={selectedRole}
|
||||
onChange={(value) => {
|
||||
if (value && !Array.isArray(value)) {
|
||||
setSelectedRole(value as VaultLdapRole);
|
||||
} else {
|
||||
setSelectedRole(null);
|
||||
}
|
||||
}}
|
||||
options={roles || []}
|
||||
getOptionValue={(option) => option.name}
|
||||
getOptionLabel={(option) => option.name}
|
||||
isDisabled={isLoadingRoles || !roles?.length || !selectedMountPath}
|
||||
placeholder={
|
||||
!selectedMountPath ? "Select a mount path first..." : "Select a role to load..."
|
||||
}
|
||||
isClearable
|
||||
className="w-full"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-mineshaft-400">
|
||||
Choose an LDAP role from the selected mount to load its configuration
|
||||
</p>
|
||||
</>
|
||||
</FormControl>
|
||||
|
||||
<div className="mt-8 flex space-x-4">
|
||||
<Button
|
||||
onClick={handleImport}
|
||||
isDisabled={!selectedRole || isLoadingMounts || isLoadingRoles}
|
||||
>
|
||||
Load Configuration
|
||||
</Button>
|
||||
<ModalClose asChild>
|
||||
<Button colorSchema="secondary" variant="plain">
|
||||
Cancel
|
||||
</Button>
|
||||
</ModalClose>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const VaultLdapImportModal = ({ isOpen, onOpenChange, onImport }: Props) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||
<ModalContent
|
||||
bodyClassName="overflow-visible"
|
||||
title="Load from HashiCorp Vault"
|
||||
subTitle="Select an LDAP secrets engine role to load its configuration."
|
||||
className="max-w-2xl"
|
||||
>
|
||||
<Content onClose={() => onOpenChange(false)} onImport={onImport} />
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user