diff --git a/backend/src/server/routes/v3/external-migration-router.ts b/backend/src/server/routes/v3/external-migration-router.ts index 259a97ddb..2bca54be7 100644 --- a/backend/src/server/routes/v3/external-migration-router.ts +++ b/backend/src/server/routes/v3/external-migration-router.ts @@ -5,7 +5,10 @@ import { BadRequestError } from "@app/lib/errors"; import { writeLimit } from "@app/server/config/rateLimiter"; import { verifyAuth } from "@app/server/plugins/auth/verify-auth"; import { AuthMode } from "@app/services/auth/auth-type"; -import { VaultMappingType } from "@app/services/external-migration/external-migration-types"; +import { + ExternalMigrationProviders, + VaultMappingType +} from "@app/services/external-migration/external-migration-types"; const MB25_IN_BYTES = 26214400; @@ -81,4 +84,30 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider }); } }); + + server.route({ + method: "GET", + url: "/custom-migration-enabled/:provider", + schema: { + params: z.object({ + provider: z.nativeEnum(ExternalMigrationProviders) + }), + response: { + 200: z.object({ + enabled: z.boolean() + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const enabled = await server.services.migration.hasCustomVaultMigration({ + actorId: req.permission.id, + actor: req.permission.type, + actorOrgId: req.permission.orgId, + actorAuthMethod: req.permission.authMethod, + provider: req.params.provider + }); + return { enabled }; + } + }); }; diff --git a/backend/src/services/external-migration/external-migration-fns/vault.ts b/backend/src/services/external-migration/external-migration-fns/vault.ts index 268826a93..ca6d2b0b0 100644 --- a/backend/src/services/external-migration/external-migration-fns/vault.ts +++ b/backend/src/services/external-migration/external-migration-fns/vault.ts @@ -501,6 +501,15 @@ export const transformToInfisicalFormatKeyVaultToProjectsCustomC1 = (vaultData: }; }; +// refer to internal doc for more details on which ID's belong to which orgs. +// when its a custom migration, then it doesn't matter which mapping type is used (as of now). +export const vaultMigrationTransformMappings: Record< + string, + (vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData +> = { + "68c57ab3-cea5-41fc-ae38-e156b10c14d2": transformToInfisicalFormatKeyVaultToProjectsCustomC1 +} as const; + export const importVaultDataFn = async ( { vaultAccessToken, @@ -527,6 +536,25 @@ export const importVaultDataFn = async ( }); } + let transformFn: (vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData; + + if (mappingType === VaultMappingType.Custom) { + transformFn = vaultMigrationTransformMappings[orgId]; + + if (!transformFn) { + throw new BadRequestError({ + message: "Please contact our sales team to enable custom vault migrations." + }); + } + } else { + transformFn = transformToInfisicalFormatNamespaceToProjects; + } + + logger.info( + { orgId, mappingType }, + `[importVaultDataFn]: Running ${orgId in vaultMigrationTransformMappings ? "custom" : "default"} transform` + ); + const vaultApi = vaultFactory(gatewayService); const vaultData = await vaultApi.collectVaultData({ @@ -536,27 +564,5 @@ export const importVaultDataFn = async ( gatewayId }); - // refer to internal doc for more details on which ID's belong to which orgs. - // when its a custom migration, then it doesn't matter which mapping type is used (as of now). - const transformMappings: Record< - string, - (vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData - > = { - "68c57ab3-cea5-41fc-ae38-e156b10c14d2": transformToInfisicalFormatKeyVaultToProjectsCustomC1 - } as const; - - let transformFn: (vaultData: VaultData[], mappingType: VaultMappingType) => InfisicalImportData; - - if (orgId in transformMappings) { - transformFn = transformMappings[orgId]; - } else { - transformFn = transformToInfisicalFormatNamespaceToProjects; - } - - logger.info( - { orgId, mappingType }, - `[importVaultDataFn]: Running ${orgId in transformMappings ? "custom" : "default"} transform` - ); - return transformFn(vaultData, mappingType); }; diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index fadbd840c..73fac00b9 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -5,9 +5,20 @@ import { crypto } from "@app/lib/crypto/cryptography"; import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors"; import { TUserDALFactory } from "../user/user-dal"; -import { decryptEnvKeyDataFn, importVaultDataFn, parseEnvKeyDataFn } from "./external-migration-fns"; +import { + decryptEnvKeyDataFn, + importVaultDataFn, + parseEnvKeyDataFn, + vaultMigrationTransformMappings +} from "./external-migration-fns"; import { TExternalMigrationQueueFactory } from "./external-migration-queue"; -import { ExternalPlatforms, TImportEnvKeyDataDTO, TImportVaultDataDTO } from "./external-migration-types"; +import { + ExternalMigrationProviders, + ExternalPlatforms, + THasCustomVaultMigrationDTO, + TImportEnvKeyDataDTO, + TImportVaultDataDTO +} from "./external-migration-types"; type TExternalMigrationServiceFactoryDep = { permissionService: TPermissionServiceFactory; @@ -128,8 +139,37 @@ export const externalMigrationServiceFactory = ({ }); }; + const hasCustomVaultMigration = async ({ + actor, + actorId, + actorOrgId, + actorAuthMethod, + provider + }: THasCustomVaultMigrationDTO) => { + const { membership } = await permissionService.getOrgPermission( + actor, + actorId, + actorOrgId, + actorAuthMethod, + actorOrgId + ); + + if (membership.role !== OrgMembershipRole.Admin) { + throw new ForbiddenRequestError({ message: "Only admins can check custom migration status" }); + } + + if (provider !== ExternalMigrationProviders.Vault) { + throw new BadRequestError({ + message: "Invalid provider. Vault is the only supported provider for custom migrations." + }); + } + + return actorOrgId in vaultMigrationTransformMappings; + }; + return { importEnvKeyData, - importVaultData + importVaultData, + hasCustomVaultMigration }; }; diff --git a/backend/src/services/external-migration/external-migration-types.ts b/backend/src/services/external-migration/external-migration-types.ts index ac8ff44e2..804444172 100644 --- a/backend/src/services/external-migration/external-migration-types.ts +++ b/backend/src/services/external-migration/external-migration-types.ts @@ -4,7 +4,8 @@ import { ActorAuthMethod, ActorType } from "../auth/auth-type"; export enum VaultMappingType { Namespace = "namespace", - KeyVault = "key-vault" + KeyVault = "key-vault", + Custom = "custom" } export type InfisicalImportData = { @@ -26,6 +27,10 @@ export type TImportEnvKeyDataDTO = { encryptedJson: { nonce: string; data: string }; } & Omit; +export type THasCustomVaultMigrationDTO = { + provider: ExternalMigrationProviders; +} & Omit; + export type TImportVaultDataDTO = { vaultAccessToken: string; vaultNamespace?: string; @@ -111,3 +116,8 @@ export enum ExternalPlatforms { EnvKey = "EnvKey", Vault = "Vault" } + +export enum ExternalMigrationProviders { + Vault = "vault", + EnvKey = "env-key" +} diff --git a/frontend/src/hooks/api/migration/index.ts b/frontend/src/hooks/api/migration/index.ts index f8dd99d03..0c2adeab0 100644 --- a/frontend/src/hooks/api/migration/index.ts +++ b/frontend/src/hooks/api/migration/index.ts @@ -1 +1,2 @@ export * from "./mutations"; +export * from "./queries"; diff --git a/frontend/src/hooks/api/migration/queries.tsx b/frontend/src/hooks/api/migration/queries.tsx new file mode 100644 index 000000000..2279b0eea --- /dev/null +++ b/frontend/src/hooks/api/migration/queries.tsx @@ -0,0 +1,20 @@ +import { apiRequest } from "@app/config/request"; +import { useQuery } from "@tanstack/react-query"; +import { ExternalMigrationProviders } from "./types"; + +const externalMigrationQueryKeys = { + customMigrationAvailable: (provider: ExternalMigrationProviders) => [ + "custom-migration-available", + provider + ] +}; + +export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProviders) => { + return useQuery({ + queryKey: externalMigrationQueryKeys.customMigrationAvailable(provider), + queryFn: () => + apiRequest.get<{ enabled: boolean }>( + `/api/v3/external-migration/custom-migration-enabled/${provider}` + ) + }); +}; diff --git a/frontend/src/hooks/api/migration/types.ts b/frontend/src/hooks/api/migration/types.ts new file mode 100644 index 000000000..945f18d8e --- /dev/null +++ b/frontend/src/hooks/api/migration/types.ts @@ -0,0 +1,4 @@ +export enum ExternalMigrationProviders { + Vault = "vault", + EnvKey = "env-key" +} diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx index 137e765d2..d8ad2605c 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultPlatformModal.tsx @@ -16,6 +16,8 @@ import { } from "@app/context/OrgPermissionContext/types"; import { gatewaysQueryKeys } from "@app/hooks/api"; import { useImportVault } from "@app/hooks/api/migration/mutations"; +import { useHasCustomMigrationAvailable } from "@app/hooks/api/migration"; +import { ExternalMigrationProviders } from "@app/hooks/api/migration/types"; type Props = { id?: string; @@ -24,11 +26,13 @@ type Props = { enum VaultMappingType { KeyVault = "key-vault", - Namespace = "namespace" + Namespace = "namespace", + Custom = "custom" } const MAPPING_TYPE_MENU_ITEMS = [ { + isCustom: false, value: VaultMappingType.KeyVault, label: "Key Vaults", tooltip: ( @@ -48,6 +52,7 @@ const MAPPING_TYPE_MENU_ITEMS = [ ) }, { + isCustom: false, value: VaultMappingType.Namespace, label: "Namespaces", tooltip: ( @@ -63,10 +68,25 @@ const MAPPING_TYPE_MENU_ITEMS = [ ) + }, + { + isCustom: true, + value: VaultMappingType.Custom, + label: "Custom Migration", + tooltip: ( +
+ Custom migrations allow you to shape your Vault migration to your specific needs. Please + contact our sales team to get started with custom migrations. +
+ ) } ]; export const VaultPlatformModal = ({ onClose }: Props) => { + const { data: isCustomMigrationAvailable } = useHasCustomMigrationAvailable( + ExternalMigrationProviders.Vault + ); + const formSchema = z.object({ vaultUrl: z.string().min(1), gatewayId: z.string().optional(), @@ -230,31 +250,40 @@ export const VaultPlatformModal = ({ onClose }: Props) => { errorText={error?.message} className="flex-1" > -
+
{MAPPING_TYPE_MENU_ITEMS.map((el) => (
field.onChange(el.value)} + onClick={() => { + if (el.isCustom && !isCustomMigrationAvailable?.data?.enabled) { + return; + } + + field.onChange(el.value); + }} role="button" tabIndex={0} - onKeyDown={(e) => { - if (e.key === "Enter") { - field.onChange(el.value); - } - }} >
{el.label}
{el.tooltip && (
- +
)} @@ -272,7 +301,7 @@ export const VaultPlatformModal = ({ onClose }: Props) => { isLoading={isLoading} isDisabled={!isDirty || isSubmitting || isLoading || !isValid} > - Import data + Import Data