diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index 638d57511..4192ffcd5 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -8,7 +8,8 @@ import { import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service"; import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types"; import { crypto } from "@app/lib/crypto/cryptography"; -import { BadRequestError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; +import { DatabaseErrorCode } from "@app/lib/error-codes"; +import { BadRequestError, DatabaseError, ForbiddenRequestError, NotFoundError } from "@app/lib/errors"; import { OrgServiceActor } from "@app/lib/types"; import { AppConnection } from "../app-connection/app-connection-enums"; @@ -231,8 +232,13 @@ export const externalMigrationServiceFactory = ({ try { await listHCVaultPolicies(namespace, connection, gatewayService); - await listHCVaultSecretPaths(namespace, connection, gatewayService); - await listHCVaultMounts(connection, gatewayService); + await getHCVaultAuthMounts(namespace, HCVaultAuthType.Kubernetes, connection, gatewayService); + + const mounts = await listHCVaultMounts(connection, gatewayService); + const sampleKvMount = mounts.find((mount) => mount.type === "kv"); + if (sampleKvMount) { + await listHCVaultSecretPaths(namespace, connection, gatewayService, sampleKvMount.path); + } } catch (error) { throw new BadRequestError({ message: `Failed to establish namespace confiugration. ${error instanceof Error ? error.message : "Unknown error"}` @@ -264,13 +270,26 @@ export const externalMigrationServiceFactory = ({ namespace }); - const config = await vaultExternalMigrationConfigDAL.create({ - namespace, - connectionId, - orgId: actor.orgId - }); + try { + const config = await vaultExternalMigrationConfigDAL.create({ + namespace, + connectionId, + orgId: actor.orgId + }); - return config; + return config; + } catch (error) { + if ( + error instanceof DatabaseError && + (error.error as { code: string })?.code === DatabaseErrorCode.UniqueViolation + ) { + throw new BadRequestError({ + message: `Vault external migration already exists for this namespace` + }); + } + + throw error; + } }; const updateVaultExternalMigration = async ({ diff --git a/backend/src/services/external-migration/external-migration-types.ts b/backend/src/services/external-migration/external-migration-types.ts index 548f807f0..49e5bfa88 100644 --- a/backend/src/services/external-migration/external-migration-types.ts +++ b/backend/src/services/external-migration/external-migration-types.ts @@ -124,7 +124,7 @@ export enum ExternalMigrationProviders { export enum VaultImportStatus { Imported = "imported", - ApprovalRequired = "approval_required" + ApprovalRequired = "approval-required" } export type TCreateVaultExternalMigrationDTO = { diff --git a/docs/documentation/platform/external-migrations/vault.mdx b/docs/documentation/platform/external-migrations/vault.mdx index 4fd3fee6d..e267cc5e9 100644 --- a/docs/documentation/platform/external-migrations/vault.mdx +++ b/docs/documentation/platform/external-migrations/vault.mdx @@ -33,7 +33,7 @@ This migration approach lets you set up a connection to your Vault instance once In your Vault instance, create a policy that allows Infisical to read your secrets, policies, and authentication configurations. This policy grants read-only access and doesn't allow Infisical to modify anything in Vault. - ```python + ```hcl # System endpoints - for listing namespaces, policies, mounts, and auth methods path "sys/namespaces" { capabilities = ["list"] @@ -162,7 +162,17 @@ The authentication settings (service accounts, TTL, policies, etc.) will be auto #### Import and Translate Access Control Policies -When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions: +When configuring project role-based access control, you can import Vault HCL policies and automatically translate them to Infisical permissions. + + + Policy translation is best-effort and provides a starting point based on your + Vault configuration. The translated permissions should be reviewed and + adjusted as needed since Vault and Infisical have different access control + models. Infisical will analyze path patterns and capabilities to suggest + equivalent permissions. + + +**To import and translate a policy:** 1. Navigate to your project, then go to **Access Control > Roles** and create or edit a role 2. In the policy configuration, click **"Add from HashiCorp Vault"** @@ -177,12 +187,16 @@ When configuring project role-based access control, you can import Vault HCL pol 5. Review the automatically translated Infisical permissions 6. Make any adjustments and save -**How policy translation works:** - -- Vault path patterns are analyzed to identify KV secret engines and environments -- Vault capabilities (`read`, `list`, `create`, etc.) are mapped to Infisical permissions -- Wildcards in paths are converted to glob patterns -- Secret paths are preserved for granular access control + + **How policy translation works:** + + - Vault path patterns are analyzed to identify KV secret engines and environments + - Vault capabilities (`read`,`list`, `create`, etc.) are mapped to Infisical permissions + - Wildcards in paths are converted to glob patterns + - Secret paths are preserved for granular access control + + Always review the translated permissions carefully, as Vault's capability-based model may not map 1:1 with Infisical's permission structure. + --- @@ -219,7 +233,7 @@ Before starting the bulk import, you need to decide how your Vault structure wil In your Vault instance, create a policy that allows Infisical to read all secrets and metadata. This policy grants read-only access. - ```python + ```hcl # Allow listing secret engines/mounts path "sys/mounts" { capabilities = ["read", "list"] diff --git a/frontend/src/hooks/api/migration/types.ts b/frontend/src/hooks/api/migration/types.ts index e83d5d5d7..f4303ea26 100644 --- a/frontend/src/hooks/api/migration/types.ts +++ b/frontend/src/hooks/api/migration/types.ts @@ -5,7 +5,7 @@ export enum ExternalMigrationProviders { export enum VaultImportStatus { Imported = "imported", - ApprovalRequired = "approval_required" + ApprovalRequired = "approval-required" } export type TVaultExternalMigrationConfig = { diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx index e44b43dcd..c91e9c24c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx @@ -1,6 +1,7 @@ import { useState } from "react"; import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; +import { Link } from "@tanstack/react-router"; import { createNotification } from "@app/components/notifications"; import { @@ -162,7 +163,14 @@ export const VaultConnectionSection = () => {

Configure namespace-specific connections to enable in-platform migration features. Manage - connections in the App Connections section. + connections in the{" "} + + App Connections + {" "} + section.

{ } // Apply the parsed permissions to the form - Object.entries(parsedPermissions).forEach(([subject, value]) => { + (Object.keys(parsedPermissions) as ProjectPermissionSub[]).forEach((subjectKey) => { + const value = parsedPermissions[subjectKey]; if (!value) return; - const subjectKey = subject as ProjectPermissionSub; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const existingValue = rootForm.getValues(`permissions.${subjectKey}`) as any; + const existingValue = rootForm.getValues(`permissions.${subjectKey}`) as unknown[]; if (Array.isArray(existingValue) && existingValue.length > 0) { // Merge with existing permissions - rootForm.setValue( - `permissions.${subjectKey}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error - [...existingValue, ...value], - { - shouldDirty: true, - shouldTouch: true, - shouldValidate: true - } - ); + rootForm.setValue(`permissions.${subjectKey}`, [...existingValue, ...value] as never, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true + }); } else { - rootForm.setValue( - `permissions.${subjectKey}`, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore-error - value, - { - shouldDirty: true, - shouldTouch: true, - shouldValidate: true - } - ); + rootForm.setValue(`permissions.${subjectKey}`, value as never, { + shouldDirty: true, + shouldTouch: true, + shouldValidate: true + }); } });