diff --git a/backend/src/@types/knex.d.ts b/backend/src/@types/knex.d.ts index 8efcce2b7..619f7f92d 100644 --- a/backend/src/@types/knex.d.ts +++ b/backend/src/@types/knex.d.ts @@ -83,9 +83,6 @@ import { TExternalKms, TExternalKmsInsert, TExternalKmsUpdate, - TExternalMigrationConfigs, - TExternalMigrationConfigsInsert, - TExternalMigrationConfigsUpdate, TFolderCheckpointResources, TFolderCheckpointResourcesInsert, TFolderCheckpointResourcesUpdate, @@ -521,6 +518,9 @@ import { TUsers, TUsersInsert, TUsersUpdate, + TVaultExternalMigrationConfigs, + TVaultExternalMigrationConfigsInsert, + TVaultExternalMigrationConfigsUpdate, TWebhooks, TWebhooksInsert, TWebhooksUpdate, @@ -1348,10 +1348,10 @@ declare module "knex/types/tables" { TAdditionalPrivilegesInsert, TAdditionalPrivilegesUpdate >; - [TableName.ExternalMigrationConfig]: KnexOriginal.CompositeTableType< - TExternalMigrationConfigs, - TExternalMigrationConfigsInsert, - TExternalMigrationConfigsUpdate + [TableName.VaultExternalMigrationConfig]: KnexOriginal.CompositeTableType< + TVaultExternalMigrationConfigs, + TVaultExternalMigrationConfigsInsert, + TVaultExternalMigrationConfigsUpdate >; } } diff --git a/backend/src/db/migrations/20251013104547_add-in-platform-external-migration.ts b/backend/src/db/migrations/20251013104547_add-in-platform-external-migration.ts index f9a2adf35..e5f74a0e0 100644 --- a/backend/src/db/migrations/20251013104547_add-in-platform-external-migration.ts +++ b/backend/src/db/migrations/20251013104547_add-in-platform-external-migration.ts @@ -4,25 +4,26 @@ import { TableName } from "../schemas"; import { createOnUpdateTrigger, dropOnUpdateTrigger } from "../utils"; export async function up(knex: Knex): Promise { - if (!(await knex.schema.hasTable(TableName.ExternalMigrationConfig))) { - await knex.schema.createTable(TableName.ExternalMigrationConfig, (t) => { + if (!(await knex.schema.hasTable(TableName.VaultExternalMigrationConfig))) { + await knex.schema.createTable(TableName.VaultExternalMigrationConfig, (t) => { t.uuid("id", { primaryKey: true }).defaultTo(knex.fn.uuid()); t.uuid("orgId").notNullable(); t.foreign("orgId").references("id").inTable(TableName.Organization).onDelete("CASCADE"); - t.string("platform").notNullable(); + + t.string("namespace").notNullable(); t.uuid("connectionId"); t.foreign("connectionId").references("id").inTable(TableName.AppConnection); t.timestamps(true, true, true); - t.unique(["orgId", "platform"]); + t.unique(["orgId", "namespace"]); }); - await createOnUpdateTrigger(knex, TableName.ExternalMigrationConfig); + await createOnUpdateTrigger(knex, TableName.VaultExternalMigrationConfig); } } export async function down(knex: Knex): Promise { - await knex.schema.dropTableIfExists(TableName.ExternalMigrationConfig); - await dropOnUpdateTrigger(knex, TableName.ExternalMigrationConfig); + await knex.schema.dropTableIfExists(TableName.VaultExternalMigrationConfig); + await dropOnUpdateTrigger(knex, TableName.VaultExternalMigrationConfig); } diff --git a/backend/src/db/schemas/external-migration-configs.ts b/backend/src/db/schemas/external-migration-configs.ts deleted file mode 100644 index c781d92b3..000000000 --- a/backend/src/db/schemas/external-migration-configs.ts +++ /dev/null @@ -1,23 +0,0 @@ -// Code generated by automation script, DO NOT EDIT. -// Automated by pulling database and generating zod schema -// To update. Just run npm run generate:schema -// Written by akhilmhdh. - -import { z } from "zod"; - -import { TImmutableDBKeys } from "./models"; - -export const ExternalMigrationConfigsSchema = z.object({ - id: z.string().uuid(), - orgId: z.string().uuid(), - platform: z.string(), - connectionId: z.string().uuid().nullable().optional(), - createdAt: z.date(), - updatedAt: z.date() -}); - -export type TExternalMigrationConfigs = z.infer; -export type TExternalMigrationConfigsInsert = Omit, TImmutableDBKeys>; -export type TExternalMigrationConfigsUpdate = Partial< - Omit, TImmutableDBKeys> ->; diff --git a/backend/src/db/schemas/index.ts b/backend/src/db/schemas/index.ts index 19eac5882..6c718d097 100644 --- a/backend/src/db/schemas/index.ts +++ b/backend/src/db/schemas/index.ts @@ -25,7 +25,6 @@ export * from "./dynamic-secrets"; export * from "./external-certificate-authorities"; export * from "./external-group-org-role-mappings"; export * from "./external-kms"; -export * from "./external-migration-configs"; export * from "./folder-checkpoint-resources"; export * from "./folder-checkpoints"; export * from "./folder-commit-changes"; @@ -177,5 +176,6 @@ export * from "./user-aliases"; export * from "./user-encryption-keys"; export * from "./user-group-membership"; export * from "./users"; +export * from "./vault-external-migration-configs"; export * from "./webhooks"; export * from "./workflow-integrations"; diff --git a/backend/src/db/schemas/models.ts b/backend/src/db/schemas/models.ts index 2e8ddf5c2..f7291a70f 100644 --- a/backend/src/db/schemas/models.ts +++ b/backend/src/db/schemas/models.ts @@ -205,7 +205,7 @@ export enum TableName { PamAccount = "pam_accounts", PamSession = "pam_sessions", - ExternalMigrationConfig = "external_migration_configs" + VaultExternalMigrationConfig = "vault_external_migration_configs" } export type TImmutableDBKeys = "id" | "createdAt" | "updatedAt" | "commitId"; diff --git a/backend/src/db/schemas/vault-external-migration-configs.ts b/backend/src/db/schemas/vault-external-migration-configs.ts new file mode 100644 index 000000000..310166669 --- /dev/null +++ b/backend/src/db/schemas/vault-external-migration-configs.ts @@ -0,0 +1,26 @@ +// Code generated by automation script, DO NOT EDIT. +// Automated by pulling database and generating zod schema +// To update. Just run npm run generate:schema +// Written by akhilmhdh. + +import { z } from "zod"; + +import { TImmutableDBKeys } from "./models"; + +export const VaultExternalMigrationConfigsSchema = z.object({ + id: z.string().uuid(), + orgId: z.string().uuid(), + namespace: z.string(), + connectionId: z.string().uuid().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() +}); + +export type TVaultExternalMigrationConfigs = z.infer; +export type TVaultExternalMigrationConfigsInsert = Omit< + z.input, + TImmutableDBKeys +>; +export type TVaultExternalMigrationConfigsUpdate = Partial< + Omit, TImmutableDBKeys> +>; diff --git a/backend/src/server/routes/index.ts b/backend/src/server/routes/index.ts index 66ff0f395..add5eaf8b 100644 --- a/backend/src/server/routes/index.ts +++ b/backend/src/server/routes/index.ts @@ -174,9 +174,9 @@ import { cmekServiceFactory } from "@app/services/cmek/cmek-service"; import { convertorServiceFactory } from "@app/services/convertor/convertor-service"; import { externalGroupOrgRoleMappingDALFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-dal"; import { externalGroupOrgRoleMappingServiceFactory } from "@app/services/external-group-org-role-mapping/external-group-org-role-mapping-service"; -import { externalMigrationConfigDALFactory } from "@app/services/external-migration/external-migration-config-dal"; import { externalMigrationQueueFactory } from "@app/services/external-migration/external-migration-queue"; import { externalMigrationServiceFactory } from "@app/services/external-migration/external-migration-service"; +import { vaultExternalMigrationConfigDALFactory } from "@app/services/external-migration/vault-external-migration-config-dal"; import { folderCheckpointDALFactory } from "@app/services/folder-checkpoint/folder-checkpoint-dal"; import { folderCheckpointResourcesDALFactory } from "@app/services/folder-checkpoint-resources/folder-checkpoint-resources-dal"; import { folderCommitDALFactory } from "@app/services/folder-commit/folder-commit-dal"; @@ -534,7 +534,7 @@ export const registerRoutes = async ( const membershipRoleDAL = membershipRoleDALFactory(db); const roleDAL = roleDALFactory(db); - const externalMigrationConfigDAL = externalMigrationConfigDALFactory(db); + const vaultExternalMigrationConfigDAL = vaultExternalMigrationConfigDALFactory(db); const eventBusService = eventBusFactory(server.redis); const sseService = sseServiceFactory(eventBusService, server.redis); @@ -2199,7 +2199,7 @@ export const registerRoutes = async ( gatewayService, kmsService, appConnectionService, - externalMigrationConfigDAL, + vaultExternalMigrationConfigDAL, secretService, auditLogService }); diff --git a/backend/src/server/routes/v3/external-migration-router.ts b/backend/src/server/routes/v3/external-migration-router.ts index 83b7a8275..9afbded12 100644 --- a/backend/src/server/routes/v3/external-migration-router.ts +++ b/backend/src/server/routes/v3/external-migration-router.ts @@ -117,33 +117,64 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider server.route({ method: "GET", - url: "/config", + url: "/vault/configs", config: { rateLimit: readLimit }, schema: { - querystring: z.object({ - platform: z.nativeEnum(ExternalMigrationProviders) - }), response: { 200: z.object({ - config: z + configs: z .object({ id: z.string(), orgId: z.string(), - platform: z.string(), + namespace: z.string(), connectionId: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date() }) - .nullable() + .array() }) } }, onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const config = await server.services.migration.getExternalMigrationConfig({ - platform: req.query.platform, + const configs = await server.services.migration.getVaultExternalMigrationConfigs({ + actor: req.permission + }); + + return { configs }; + } + }); + + server.route({ + method: "POST", + url: "/vault/configs", + config: { + rateLimit: writeLimit + }, + schema: { + body: z.object({ + connectionId: z.string(), + namespace: z.string() + }), + response: { + 200: z.object({ + config: z.object({ + id: z.string(), + orgId: z.string(), + namespace: z.string(), + connectionId: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const config = await server.services.migration.createVaultExternalMigration({ + ...req.body, actor: req.permission }); @@ -153,21 +184,24 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider server.route({ method: "PUT", - url: "/config", + url: "/vault/configs/:id", config: { rateLimit: writeLimit }, schema: { + params: z.object({ + id: z.string() + }), body: z.object({ - connectionId: z.string().nullable(), - platform: z.nativeEnum(ExternalMigrationProviders) + connectionId: z.string(), + namespace: z.string() }), response: { 200: z.object({ config: z.object({ id: z.string(), orgId: z.string(), - platform: z.string(), + namespace: z.string(), connectionId: z.string().nullable().optional(), createdAt: z.date(), updatedAt: z.date() @@ -175,12 +209,48 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider }) } }, - onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]), + onRequest: verifyAuth([AuthMode.JWT]), handler: async (req) => { - const config = await server.services.migration.configureExternalMigration({ + const config = await server.services.migration.updateVaultExternalMigration({ + id: req.params.id, ...req.body, actor: req.permission }); + + return { config }; + } + }); + + server.route({ + method: "DELETE", + url: "/vault/configs/:id", + config: { + rateLimit: writeLimit + }, + schema: { + params: z.object({ + id: z.string() + }), + response: { + 200: z.object({ + config: z.object({ + id: z.string(), + orgId: z.string(), + namespace: z.string(), + connectionId: z.string().nullable().optional(), + createdAt: z.date(), + updatedAt: z.date() + }) + }) + } + }, + onRequest: verifyAuth([AuthMode.JWT]), + handler: async (req) => { + const config = await server.services.migration.deleteVaultExternalMigration({ + id: req.params.id, + actor: req.permission + }); + return { config }; } }); @@ -243,7 +313,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider }, schema: { querystring: z.object({ - namespace: z.string().optional() + namespace: z.string() }), response: { 200: z.object({ diff --git a/backend/src/services/external-migration/external-migration-service.ts b/backend/src/services/external-migration/external-migration-service.ts index d1489a347..da475a012 100644 --- a/backend/src/services/external-migration/external-migration-service.ts +++ b/backend/src/services/external-migration/external-migration-service.ts @@ -20,7 +20,6 @@ import { getHCVaultSecretsForPath, HCVaultAuthType, listHCVaultMounts, - listHCVaultNamespaces, listHCVaultPolicies, listHCVaultSecretPaths, THCVaultConnection @@ -29,7 +28,6 @@ import { TKmsServiceFactory } from "../kms/kms-service"; import { TSecretServiceFactory } from "../secret/secret-service"; import { SecretProtectionType } from "../secret/secret-types"; import { TUserDALFactory } from "../user/user-dal"; -import { TExternalMigrationConfigDALFactory } from "./external-migration-config-dal"; import { decryptEnvKeyDataFn, importVaultDataFn, @@ -40,12 +38,15 @@ import { TExternalMigrationQueueFactory } from "./external-migration-queue"; import { ExternalMigrationProviders, ExternalPlatforms, - TConfigureExternalMigrationDTO, + TCreateVaultExternalMigrationDTO, + TDeleteVaultExternalMigrationDTO, THasCustomVaultMigrationDTO, TImportEnvKeyDataDTO, TImportVaultDataDTO, + TUpdateVaultExternalMigrationDTO, VaultImportStatus } from "./external-migration-types"; +import { TVaultExternalMigrationConfigDALFactory } from "./vault-external-migration-config-dal"; type TExternalMigrationServiceFactoryDep = { permissionService: TPermissionServiceFactory; @@ -53,7 +54,10 @@ type TExternalMigrationServiceFactoryDep = { auditLogService: Pick; externalMigrationQueue: TExternalMigrationQueueFactory; appConnectionService: Pick; - externalMigrationConfigDAL: Pick; + vaultExternalMigrationConfigDAL: Pick< + TVaultExternalMigrationConfigDALFactory, + "create" | "findOne" | "transaction" | "find" | "updateById" | "deleteById" | "findById" + >; userDAL: Pick; gatewayService: Pick; kmsService: Pick; @@ -69,7 +73,7 @@ export const externalMigrationServiceFactory = ({ secretService, auditLogService, appConnectionService, - externalMigrationConfigDAL, + vaultExternalMigrationConfigDAL, kmsService }: TExternalMigrationServiceFactoryDep) => { const importEnvKeyData = async ({ @@ -208,7 +212,35 @@ export const externalMigrationServiceFactory = ({ return actorOrgId in vaultMigrationTransformMappings; }; - const configureExternalMigration = async ({ platform, connectionId, actor }: TConfigureExternalMigrationDTO) => { + const validateVaultExternalMigrationConnection = async ({ + connection, + namespace + }: { + connection: THCVaultConnection; + namespace: string; + }) => { + // Allow root namespace access when no namespace is configured on the connection + const isRootAccess = namespace === "root" || namespace === "/"; + const hasNoNamespace = connection.credentials.namespace === undefined; + + if (hasNoNamespace && isRootAccess) { + // Skip validation for root access with no configured namespace + } else if (connection.credentials.namespace !== namespace) { + throw new BadRequestError({ message: "Namespace value does not match the namespace of the connection" }); + } + + try { + await listHCVaultPolicies(namespace, connection, gatewayService); + await listHCVaultSecretPaths(namespace, connection, gatewayService); + await listHCVaultMounts(connection, gatewayService); + } catch (error) { + throw new BadRequestError({ + message: `Failed to establish namespace confiugration. ${error instanceof Error ? error.message : "Unknown error"}` + }); + } + }; + + const createVaultExternalMigration = async ({ namespace, connectionId, actor }: TCreateVaultExternalMigrationDTO) => { const { hasRole } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -218,19 +250,22 @@ export const externalMigrationServiceFactory = ({ ); if (!hasRole(OrgMembershipRole.Admin)) { - throw new ForbiddenRequestError({ message: "Only admins can configure external migration" }); + throw new ForbiddenRequestError({ message: "Only admins can configure vault external migration" }); } - if (connectionId) { - if (platform === ExternalMigrationProviders.Vault) { - await appConnectionService.connectAppConnectionById(AppConnection.HCVault, connectionId, actor); - } else { - throw new BadRequestError({ message: "Invalid platform" }); - } - } + const connection = await appConnectionService.connectAppConnectionById( + AppConnection.HCVault, + connectionId, + actor + ); - const config = await externalMigrationConfigDAL.upsert({ - platform, + await validateVaultExternalMigrationConnection({ + connection, + namespace + }); + + const config = await vaultExternalMigrationConfigDAL.create({ + namespace, connectionId, orgId: actor.orgId }); @@ -238,7 +273,12 @@ export const externalMigrationServiceFactory = ({ return config; }; - const getExternalMigrationConfig = async ({ platform, actor }: { platform: string; actor: OrgServiceActor }) => { + const updateVaultExternalMigration = async ({ + id, + namespace, + connectionId, + actor + }: TUpdateVaultExternalMigrationDTO) => { const { hasRole } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -248,19 +288,48 @@ export const externalMigrationServiceFactory = ({ ); if (!hasRole(OrgMembershipRole.Admin)) { - throw new ForbiddenRequestError({ message: "Only admins can view external migration config" }); + throw new ForbiddenRequestError({ message: "Only admins can update vault external migration" }); } - const config = await externalMigrationConfigDAL.findOne({ - orgId: actor.orgId, - platform + if (connectionId) { + const connection = await appConnectionService.connectAppConnectionById( + AppConnection.HCVault, + connectionId, + actor + ); + + await validateVaultExternalMigrationConnection({ + connection, + namespace + }); + } + + const config = await vaultExternalMigrationConfigDAL.updateById(id, { + namespace, + connectionId }); - if (!config) { - throw new NotFoundError({ message: "External migration config not found" }); + return config; + }; + + const getVaultExternalMigrationConfigs = async ({ actor }: { actor: OrgServiceActor }) => { + const { hasRole } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + if (!hasRole(OrgMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "Only admins can view vault external migration configs" }); } - return config; + const configs = await vaultExternalMigrationConfigDAL.find({ + orgId: actor.orgId + }); + + return configs; }; const getVaultNamespaces = async ({ actor }: { actor: OrgServiceActor }) => { @@ -276,32 +345,18 @@ export const externalMigrationServiceFactory = ({ throw new ForbiddenRequestError({ message: "Only admins can view vault namespaces" }); } - const vaultConfig = await externalMigrationConfigDAL.findOne({ - orgId: actor.orgId, - platform: ExternalMigrationProviders.Vault + // Get all configured namespaces for this org + const vaultConfigs = await vaultExternalMigrationConfigDAL.find({ + orgId: actor.orgId }); - if (!vaultConfig) { - throw new BadRequestError({ message: "Vault migration config not found" }); - } + // Return the configured namespaces as an array of objects with id and name + // where both id and name are the namespace path + const namespaces = vaultConfigs.map((config) => ({ + id: config.namespace, + name: config.namespace + })); - if (!vaultConfig.connection) { - throw new BadRequestError({ message: "Vault migration connection is not configured" }); - } - - const credentials = await decryptAppConnectionCredentials({ - orgId: vaultConfig.orgId, - encryptedCredentials: vaultConfig.connection.encryptedCredentials, - kmsService, - projectId: null - }); - - const connection = { - ...vaultConfig.connection, - credentials - } as THCVaultConnection; - - const namespaces = await listHCVaultNamespaces(connection, gatewayService); return namespaces; }; @@ -318,17 +373,17 @@ export const externalMigrationServiceFactory = ({ throw new ForbiddenRequestError({ message: "Only admins can view vault policies" }); } - const vaultConfig = await externalMigrationConfigDAL.findOne({ + const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({ orgId: actor.orgId, - platform: ExternalMigrationProviders.Vault + namespace }); if (!vaultConfig) { - throw new NotFoundError({ message: "Vault migration config not found" }); + throw new NotFoundError({ message: "Vault migration config not found for this namespace" }); } if (!vaultConfig.connection) { - throw new BadRequestError({ message: "Vault migration connection is not configured" }); + throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" }); } const credentials = await decryptAppConnectionCredentials({ @@ -347,7 +402,7 @@ export const externalMigrationServiceFactory = ({ return policies; }; - const getVaultMounts = async ({ actor, namespace }: { actor: OrgServiceActor; namespace?: string }) => { + const getVaultMounts = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { const { hasRole } = await permissionService.getOrgPermission( actor.type, actor.id, @@ -360,17 +415,17 @@ export const externalMigrationServiceFactory = ({ throw new ForbiddenRequestError({ message: "Only admins can view vault mounts" }); } - const vaultConfig = await externalMigrationConfigDAL.findOne({ + const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({ orgId: actor.orgId, - platform: ExternalMigrationProviders.Vault + namespace }); if (!vaultConfig) { - throw new NotFoundError({ message: "Vault migration config not found" }); + throw new NotFoundError({ message: "Vault migration config not found for this namespace" }); } if (!vaultConfig.connection) { - throw new BadRequestError({ message: "Vault migration connection is not configured" }); + throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" }); } const credentials = await decryptAppConnectionCredentials({ @@ -402,17 +457,17 @@ export const externalMigrationServiceFactory = ({ throw new ForbiddenRequestError({ message: "Only admins can view vault secret paths" }); } - const vaultConfig = await externalMigrationConfigDAL.findOne({ + const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({ orgId: actor.orgId, - platform: ExternalMigrationProviders.Vault + namespace }); if (!vaultConfig) { - throw new NotFoundError({ message: "Vault migration config not found" }); + throw new NotFoundError({ message: "Vault migration config not found for this namespace" }); } if (!vaultConfig.connection) { - throw new BadRequestError({ message: "Vault migration connection is not configured" }); + throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" }); } const credentials = await decryptAppConnectionCredentials({ @@ -461,17 +516,17 @@ export const externalMigrationServiceFactory = ({ throw new ForbiddenRequestError({ message: "Only admins can import vault secrets" }); } - const vaultConfig = await externalMigrationConfigDAL.findOne({ + const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({ orgId: actor.orgId, - platform: ExternalMigrationProviders.Vault + namespace: vaultNamespace }); if (!vaultConfig) { - throw new NotFoundError({ message: "Vault migration config not found" }); + throw new NotFoundError({ message: "Vault migration config not found for this namespace" }); } if (!vaultConfig.connection) { - throw new BadRequestError({ message: "Vault migration connection is not configured" }); + throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" }); } const credentials = await decryptAppConnectionCredentials({ @@ -534,6 +589,34 @@ export const externalMigrationServiceFactory = ({ } }; + const deleteVaultExternalMigration = async ({ id, actor }: TDeleteVaultExternalMigrationDTO) => { + const { hasRole } = await permissionService.getOrgPermission( + actor.type, + actor.id, + actor.orgId, + actor.authMethod, + actor.orgId + ); + + if (!hasRole(OrgMembershipRole.Admin)) { + throw new ForbiddenRequestError({ message: "Only admins can delete vault external migration configs" }); + } + + const config = await vaultExternalMigrationConfigDAL.findById(id); + + if (!config) { + throw new NotFoundError({ message: "Vault migration config not found" }); + } + + if (config.orgId !== actor.orgId) { + throw new ForbiddenRequestError({ message: "Config does not belong to this organization" }); + } + + const deletedConfig = await vaultExternalMigrationConfigDAL.deleteById(id); + + return deletedConfig; + }; + const getVaultKubernetesAuthRoles = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => { const { hasRole } = await permissionService.getOrgPermission( actor.type, @@ -547,17 +630,17 @@ export const externalMigrationServiceFactory = ({ throw new ForbiddenRequestError({ message: "Only admins can view vault Kubernetes auth roles" }); } - const vaultConfig = await externalMigrationConfigDAL.findOne({ + const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({ orgId: actor.orgId, - platform: ExternalMigrationProviders.Vault + namespace }); if (!vaultConfig) { - throw new NotFoundError({ message: "Vault migration config not found" }); + throw new NotFoundError({ message: "Vault migration config not found for this namespace" }); } if (!vaultConfig.connection) { - throw new BadRequestError({ message: "Vault migration connection is not configured" }); + throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" }); } const credentials = await decryptAppConnectionCredentials({ @@ -590,8 +673,10 @@ export const externalMigrationServiceFactory = ({ importEnvKeyData, importVaultData, hasCustomVaultMigration, - configureExternalMigration, - getExternalMigrationConfig, + createVaultExternalMigration, + getVaultExternalMigrationConfigs, + updateVaultExternalMigration, + deleteVaultExternalMigration, getVaultNamespaces, getVaultPolicies, getVaultMounts, diff --git a/backend/src/services/external-migration/external-migration-types.ts b/backend/src/services/external-migration/external-migration-types.ts index da2d04771..548f807f0 100644 --- a/backend/src/services/external-migration/external-migration-types.ts +++ b/backend/src/services/external-migration/external-migration-types.ts @@ -127,8 +127,20 @@ export enum VaultImportStatus { ApprovalRequired = "approval_required" } -export type TConfigureExternalMigrationDTO = { - platform: ExternalMigrationProviders; +export type TCreateVaultExternalMigrationDTO = { + namespace: string; + connectionId: string; + actor: OrgServiceActor; +}; + +export type TUpdateVaultExternalMigrationDTO = { + id: string; + namespace: string; connectionId: string | null; actor: OrgServiceActor; }; + +export type TDeleteVaultExternalMigrationDTO = { + id: string; + actor: OrgServiceActor; +}; diff --git a/backend/src/services/external-migration/external-migration-config-dal.ts b/backend/src/services/external-migration/vault-external-migration-config-dal.ts similarity index 71% rename from backend/src/services/external-migration/external-migration-config-dal.ts rename to backend/src/services/external-migration/vault-external-migration-config-dal.ts index 64874a25e..34ff8757a 100644 --- a/backend/src/services/external-migration/external-migration-config-dal.ts +++ b/backend/src/services/external-migration/vault-external-migration-config-dal.ts @@ -1,39 +1,26 @@ import { Knex } from "knex"; import { TDbClient } from "@app/db"; -import { TableName, TExternalMigrationConfigsInsert } from "@app/db/schemas"; +import { TableName } from "@app/db/schemas"; import { DatabaseError } from "@app/lib/errors"; import { buildFindFilter, ormify, prependTableNameToFindFilter, selectAllTableCols } from "@app/lib/knex"; -export type TExternalMigrationConfigDALFactory = ReturnType; +export type TVaultExternalMigrationConfigDALFactory = ReturnType; -export const externalMigrationConfigDALFactory = (db: TDbClient) => { - const orm = ormify(db, TableName.ExternalMigrationConfig); +export const vaultExternalMigrationConfigDALFactory = (db: TDbClient) => { + const orm = ormify(db, TableName.VaultExternalMigrationConfig); - const upsert = async (data: TExternalMigrationConfigsInsert, tx?: Knex) => { + const findOne = async (filter: { orgId: string; namespace: string }, tx?: Knex) => { try { - const [doc] = await (tx || db)(TableName.ExternalMigrationConfig) - .insert(data) - .onConflict(["orgId", "platform"]) - .merge() - .returning("*"); - return doc; - } catch (error) { - throw new DatabaseError({ error, name: "UpsertExternalMigrationConfig" }); - } - }; - - const findOne = async (filter: { orgId: string; platform: string }, tx?: Knex) => { - try { - const result = await (tx || db?.replicaNode?.() || db)(TableName.ExternalMigrationConfig) + const result = await (tx || db?.replicaNode?.() || db)(TableName.VaultExternalMigrationConfig) .leftJoin( TableName.AppConnection, `${TableName.AppConnection}.id`, - `${TableName.ExternalMigrationConfig}.connectionId` + `${TableName.VaultExternalMigrationConfig}.connectionId` ) /* eslint-disable @typescript-eslint/no-misused-promises */ - .where(buildFindFilter(prependTableNameToFindFilter(TableName.ExternalMigrationConfig, filter))) - .select(selectAllTableCols(TableName.ExternalMigrationConfig)) + .where(buildFindFilter(prependTableNameToFindFilter(TableName.VaultExternalMigrationConfig, filter))) + .select(selectAllTableCols(TableName.VaultExternalMigrationConfig)) .select( db.ref("id").withSchema(TableName.AppConnection).as("appConnectionId"), db.ref("name").withSchema(TableName.AppConnection).as("appConnectionName"), @@ -76,5 +63,5 @@ export const externalMigrationConfigDALFactory = (db: TDbClient) => { } }; - return { ...orm, upsert, findOne }; + return { ...orm, findOne }; }; diff --git a/frontend/src/hooks/api/migration/mutations.tsx b/frontend/src/hooks/api/migration/mutations.tsx index 53c6c2fb4..00d3148ef 100644 --- a/frontend/src/hooks/api/migration/mutations.tsx +++ b/frontend/src/hooks/api/migration/mutations.tsx @@ -6,12 +6,7 @@ import { secretKeys } from "@app/hooks/api/secrets/queries"; import { projectKeys } from "../projects"; import { externalMigrationQueryKeys } from "./queries"; -import { - ExternalMigrationProviders, - TExternalMigrationConfig, - TImportVaultSecretsDTO, - VaultImportStatus -} from "./types"; +import { TImportVaultSecretsDTO, TVaultExternalMigrationConfig, VaultImportStatus } from "./types"; export const useImportEnvKey = () => { const queryClient = useQueryClient(); @@ -75,29 +70,6 @@ export const useImportVault = () => { }); }; -export const useUpdateExternalMigrationConfig = (platform: ExternalMigrationProviders) => { - const queryClient = useQueryClient(); - - return useMutation({ - mutationFn: async ({ connectionId }: { connectionId: string | null }) => { - const { data } = await apiRequest.put<{ config: TExternalMigrationConfig }>( - "/api/v3/external-migration/config", - { - connectionId, - platform - } - ); - - return data.config; - }, - onSuccess: () => { - queryClient.invalidateQueries({ - queryKey: externalMigrationQueryKeys.config(platform) - }); - } - }); -}; - export const useImportVaultSecrets = () => { const queryClient = useQueryClient(); @@ -121,3 +93,73 @@ export const useImportVaultSecrets = () => { } }); }; + +export const useCreateVaultExternalMigrationConfig = () => { + const queryClient = useQueryClient(); + + return useMutation< + TVaultExternalMigrationConfig, + Error, + { connectionId: string; namespace: string } + >({ + mutationFn: async ({ connectionId, namespace }) => { + const { data } = await apiRequest.post<{ config: TVaultExternalMigrationConfig }>( + "/api/v3/external-migration/vault/configs", + { + connectionId, + namespace + } + ); + return data.config; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: externalMigrationQueryKeys.vaultConfigs() + }); + } + }); +}; + +export const useUpdateVaultExternalMigrationConfig = () => { + const queryClient = useQueryClient(); + + return useMutation< + TVaultExternalMigrationConfig, + Error, + { id: string; connectionId: string; namespace: string } + >({ + mutationFn: async ({ id, connectionId, namespace }) => { + const { data } = await apiRequest.put<{ config: TVaultExternalMigrationConfig }>( + `/api/v3/external-migration/vault/configs/${id}`, + { + connectionId, + namespace + } + ); + return data.config; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: externalMigrationQueryKeys.vaultConfigs() + }); + } + }); +}; + +export const useDeleteVaultExternalMigrationConfig = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ id }) => { + const { data } = await apiRequest.delete<{ config: TVaultExternalMigrationConfig }>( + `/api/v3/external-migration/vault/configs/${id}` + ); + return data.config; + }, + onSuccess: () => { + queryClient.invalidateQueries({ + queryKey: externalMigrationQueryKeys.vaultConfigs() + }); + } + }); +}; diff --git a/frontend/src/hooks/api/migration/queries.tsx b/frontend/src/hooks/api/migration/queries.tsx index ab0816d3e..8c982e3e0 100644 --- a/frontend/src/hooks/api/migration/queries.tsx +++ b/frontend/src/hooks/api/migration/queries.tsx @@ -4,7 +4,7 @@ import { apiRequest } from "@app/config/request"; import { ExternalMigrationProviders, - TExternalMigrationConfig, + TVaultExternalMigrationConfig, VaultKubernetesAuthRole } from "./types"; @@ -13,7 +13,7 @@ export const externalMigrationQueryKeys = { "custom-migration-available", provider ], - config: (platform: string) => ["external-migration-config", { platform }], + vaultConfigs: () => ["vault-external-migration-configs"], vaultNamespaces: () => ["vault-namespaces"], vaultPolicies: (namespace?: string) => ["vault-policies", namespace], vaultMounts: (namespace?: string) => ["vault-mounts", namespace], @@ -31,19 +31,15 @@ export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProvid }); }; -export const useGetExternalMigrationConfig = (platform: string) => { +export const useGetVaultExternalMigrationConfigs = () => { return useQuery({ - queryKey: externalMigrationQueryKeys.config(platform), + queryKey: externalMigrationQueryKeys.vaultConfigs(), queryFn: async () => { - const { data } = await apiRequest.get<{ config: TExternalMigrationConfig | null }>( - "/api/v3/external-migration/config", - { - params: { platform } - } + const { data } = await apiRequest.get<{ configs: TVaultExternalMigrationConfig[] }>( + "/api/v3/external-migration/vault/configs" ); - return data.config; - }, - enabled: Boolean(platform) + return data.configs; + } }); }; diff --git a/frontend/src/hooks/api/migration/types.ts b/frontend/src/hooks/api/migration/types.ts index 71fdecfbd..e83d5d5d7 100644 --- a/frontend/src/hooks/api/migration/types.ts +++ b/frontend/src/hooks/api/migration/types.ts @@ -8,10 +8,10 @@ export enum VaultImportStatus { ApprovalRequired = "approval_required" } -export type TExternalMigrationConfig = { +export type TVaultExternalMigrationConfig = { id: string; orgId: string; - platform: string; + namespace: string; connectionId: string | null; createdAt: string; updatedAt: string; diff --git a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx index 52bb4d808..bc010e633 100644 --- a/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx +++ b/frontend/src/pages/organization/AccessManagementPage/components/OrgIdentityTab/components/IdentitySection/IdentityKubernetesAuthForm.tsx @@ -37,11 +37,8 @@ import { IdentityKubernetesAuthTokenReviewMode, IdentityTrustedIp } from "@app/hooks/api/identities/types"; -import { useGetExternalMigrationConfig } from "@app/hooks/api/migration/queries"; -import { - ExternalMigrationProviders, - VaultKubernetesAuthRole -} from "@app/hooks/api/migration/types"; +import { useGetVaultExternalMigrationConfigs } from "@app/hooks/api/migration/queries"; +import { VaultKubernetesAuthRole } from "@app/hooks/api/migration/types"; import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp"; import { IdentityFormTab } from "./types"; @@ -130,8 +127,8 @@ export const IdentityKubernetesAuthForm = ({ const { popUp, handlePopUpToggle: handleImportPopUpToggle } = usePopUp([ "importFromVault" ] as const); - const { data: vaultConfig } = useGetExternalMigrationConfig(ExternalMigrationProviders.Vault); - const hasVaultConnection = Boolean(vaultConfig?.connectionId); + const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs(); + const hasVaultConnection = vaultConfigs.some((config) => config.connectionId); const { control, 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 c375b03b3..9e8b46826 100644 --- a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultConnectionSection.tsx @@ -1,100 +1,188 @@ -import { useMemo } from "react"; +import { useState } from "react"; +import { faEdit, faPlus, faTrash } from "@fortawesome/free-solid-svg-icons"; +import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { createNotification } from "@app/components/notifications"; -import { FilterableSelect, FormControl } from "@app/components/v2"; -import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { + Button, + DeleteActionModal, + EmptyState, + Table, + TableContainer, + TableSkeleton, + TBody, + Td, + Th, + THead, + Tr +} from "@app/components/v2"; import { useListAppConnections } from "@app/hooks/api/appConnections/queries"; import { - useGetExternalMigrationConfig, - useUpdateExternalMigrationConfig + useDeleteVaultExternalMigrationConfig, + useGetVaultExternalMigrationConfigs } from "@app/hooks/api/migration"; -import { ExternalMigrationProviders } from "@app/hooks/api/migration/types"; +import { TVaultExternalMigrationConfig } from "@app/hooks/api/migration/types"; + +import { VaultNamespaceConfigModal } from "./VaultNamespaceConfigModal"; export const VaultConnectionSection = () => { - const { data: appConnections = [], isPending: isLoadingConnections } = useListAppConnections(); + const [selectedConfig, setSelectedConfig] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false); + const [configToDelete, setConfigToDelete] = useState(null); - const vaultConnections = useMemo( - () => appConnections.filter((conn) => conn.app === AppConnection.HCVault), - [appConnections] - ); + const { data: configs = [], isPending: isLoadingConfigs } = useGetVaultExternalMigrationConfigs(); + const { data: appConnections = [] } = useListAppConnections(); + const { mutateAsync: deleteConfig } = useDeleteVaultExternalMigrationConfig(); - const { data: currentConfig, isPending: isLoadingConfig } = useGetExternalMigrationConfig( - ExternalMigrationProviders.Vault - ); + const handleEdit = (config: TVaultExternalMigrationConfig) => { + setSelectedConfig(config); + setIsModalOpen(true); + }; - const { mutateAsync: updateConfig, isPending: isUpdating } = useUpdateExternalMigrationConfig( - ExternalMigrationProviders.Vault - ); + const handleAdd = () => { + setSelectedConfig(null); + setIsModalOpen(true); + }; + + const handleDeleteClick = (config: TVaultExternalMigrationConfig) => { + setConfigToDelete(config); + setIsDeleteModalOpen(true); + }; + + const handleDeleteConfirm = async () => { + if (!configToDelete) return; - const handleConnectionChange = async ( - selectedConnection: { id: string; name: string } | null - ) => { try { - await updateConfig({ - connectionId: selectedConnection?.id || null - }); - + await deleteConfig({ id: configToDelete.id }); createNotification({ type: "success", - text: "Vault connection updated successfully" + text: "Namespace configuration deleted successfully" }); + setIsDeleteModalOpen(false); + setConfigToDelete(null); } catch (error) { - console.error("Failed to update vault connection:", error); + console.error("Failed to delete namespace config:", error); createNotification({ type: "error", - text: "Failed to update vault connection" + text: "Failed to delete namespace configuration" }); } }; - const selectedConnection = useMemo(() => { - if (!currentConfig?.connectionId) return null; - return vaultConnections.find((conn) => conn.id === currentConfig.connectionId) || null; - }, [currentConfig?.connectionId, vaultConnections]); - - const isLoading = isLoadingConnections || isLoadingConfig; + const getConnectionName = (connectionId: string | null) => { + if (!connectionId) return "None"; + const connection = appConnections.find((conn) => conn.id === connectionId); + return connection?.name || "Unknown"; + }; return (
-
- HashiCorp Vault logo -
-

HashiCorp Vault

-

- Enable in-platform migration tooling for policy imports and secret engine migrations -

-
-
- -
- - { - handleConnectionChange(newValue as { id: string; name: string } | null); - }} - isLoading={isLoading} - isDisabled={isUpdating} - options={vaultConnections} - placeholder="Select connection..." - getOptionLabel={(option) => option.name} - getOptionValue={(option) => option.id} - isClearable +
+
+ HashiCorp Vault logo - +
+

HashiCorp Vault

+

+ Enable in-platform migration tooling for policy imports and secret engine migrations +

+
+
+
-

- Select an existing App Connection to enable in-platform migration features. Manage + + + + + + + + + + {isLoadingConfigs && ( + + )} + {!isLoadingConfigs && configs.length === 0 && ( + + + + )} + {!isLoadingConfigs && + configs.map((config) => ( + + + + + + ))} + +
NamespaceConnection +
+ +

+ Add a namespace configuration to enable in-platform migration features. +

+
+
{config.namespace}{getConnectionName(config.connectionId)} +
+ + +
+
+
+ +

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

+ + { + setIsModalOpen(open); + if (!open) setSelectedConfig(null); + }} + editConfig={selectedConfig || undefined} + /> + + { + setIsDeleteModalOpen(open); + if (!open) setConfigToDelete(null); + }} + deleteKey="confirm" + onDeleteApproved={handleDeleteConfirm} + />
); }; diff --git a/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultNamespaceConfigModal.tsx b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultNamespaceConfigModal.tsx new file mode 100644 index 000000000..43696f569 --- /dev/null +++ b/frontend/src/pages/organization/SettingsPage/components/ExternalMigrationsTab/components/VaultNamespaceConfigModal.tsx @@ -0,0 +1,187 @@ +import { useEffect, useMemo } from "react"; +import { Controller, useForm } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; + +import { createNotification } from "@app/components/notifications"; +import { + Button, + FilterableSelect, + FormControl, + Input, + Modal, + ModalContent +} from "@app/components/v2"; +import { AppConnection } from "@app/hooks/api/appConnections/enums"; +import { useListAppConnections } from "@app/hooks/api/appConnections/queries"; +import { + useCreateVaultExternalMigrationConfig, + useUpdateVaultExternalMigrationConfig +} from "@app/hooks/api/migration"; +import { TVaultExternalMigrationConfig } from "@app/hooks/api/migration/types"; + +const schema = z.object({ + namespace: z.string().min(1, "Namespace is required"), + connectionId: z.string().min(1, "Connection is required") +}); + +type FormData = z.infer; + +type Props = { + isOpen: boolean; + onOpenChange: (isOpen: boolean) => void; + editConfig?: TVaultExternalMigrationConfig; +}; + +export const VaultNamespaceConfigModal = ({ isOpen, onOpenChange, editConfig }: Props) => { + const isEdit = Boolean(editConfig); + + const { data: appConnections = [], isPending: isLoadingConnections } = useListAppConnections(); + + const vaultConnections = useMemo( + () => appConnections.filter((conn) => conn.app === AppConnection.HCVault), + [appConnections] + ); + + const { mutateAsync: createConfig, isPending: isCreating } = + useCreateVaultExternalMigrationConfig(); + const { mutateAsync: updateConfig, isPending: isUpdating } = + useUpdateVaultExternalMigrationConfig(); + + const { + control, + handleSubmit, + reset, + formState: { errors, isSubmitting } + } = useForm({ + resolver: zodResolver(schema), + defaultValues: { + namespace: "", + connectionId: "" + } + }); + + // Reset form when editConfig changes or modal opens + useEffect(() => { + if (isOpen) { + reset({ + namespace: editConfig?.namespace || "", + connectionId: editConfig?.connectionId || "" + }); + } + }, [isOpen, editConfig, reset]); + + const onFormSubmit = async (data: FormData) => { + try { + if (isEdit && editConfig) { + await updateConfig({ + id: editConfig.id, + namespace: data.namespace, + connectionId: data.connectionId + }); + createNotification({ + type: "success", + text: "Namespace configuration updated successfully" + }); + } else { + await createConfig({ + namespace: data.namespace, + connectionId: data.connectionId + }); + createNotification({ + type: "success", + text: "Namespace configuration created successfully" + }); + } + reset(); + onOpenChange(false); + } catch (error) { + console.error("Failed to save namespace config:", error); + createNotification({ + type: "error", + text: `Failed to ${isEdit ? "update" : "create"} namespace configuration` + }); + } + }; + + const handleClose = () => { + reset(); + onOpenChange(false); + }; + + return ( + + +
+ ( + + + + )} + /> + + { + const selectedConnection = vaultConnections.find((conn) => conn.id === field.value); + + return ( + + { + const singleValue = Array.isArray(newValue) ? newValue[0] : newValue; + if (singleValue && "id" in singleValue) { + field.onChange(singleValue.id); + } else { + field.onChange(""); + } + }} + isLoading={isLoadingConnections} + options={vaultConnections} + placeholder="Select connection..." + getOptionLabel={(option) => option.name} + getOptionValue={(option) => option.id} + /> + + ); + }} + /> + +
+ + +
+ +
+
+ ); +}; diff --git a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx index c7932fc97..bf581f7e2 100644 --- a/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx +++ b/frontend/src/pages/project/RoleDetailsBySlugPage/components/AddPoliciesButton.tsx @@ -12,8 +12,7 @@ import { import { useOrgPermission } from "@app/context"; import { OrgMembershipRole } from "@app/helpers/roles"; import { usePopUp } from "@app/hooks"; -import { useGetExternalMigrationConfig } from "@app/hooks/api/migration"; -import { ExternalMigrationProviders } from "@app/hooks/api/migration/types"; +import { useGetVaultExternalMigrationConfigs } from "@app/hooks/api/migration"; import { ProjectType } from "@app/hooks/api/projects/types"; import { PolicySelectionModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicySelectionModal"; import { PolicyTemplateModal } from "@app/pages/project/RoleDetailsBySlugPage/components/PolicyTemplateModal"; @@ -33,9 +32,8 @@ export const AddPoliciesButton = ({ isDisabled, projectType }: Props) => { ] as const); const { hasOrgRole } = useOrgPermission(); - const { data: vaultConfig } = useGetExternalMigrationConfig(ExternalMigrationProviders.Vault); - - const hasVaultConnection = Boolean(vaultConfig?.connectionId); + const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs(); + const hasVaultConnection = vaultConfigs.some((config) => config.connectionId); const isOrgAdmin = hasOrgRole(OrgMembershipRole.Admin); const isVaultImportDisabled = isDisabled || !isOrgAdmin; diff --git a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx index 68172b2d5..0bf85a15e 100644 --- a/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx +++ b/frontend/src/pages/secret-manager/SecretDashboardPage/components/ActionBar/ActionBar.tsx @@ -77,8 +77,11 @@ import { fetchDashboardProjectSecretsByKeys } from "@app/hooks/api/dashboard/queries"; import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types"; -import { useGetExternalMigrationConfig, useImportVaultSecrets } from "@app/hooks/api/migration"; -import { ExternalMigrationProviders, VaultImportStatus } from "@app/hooks/api/migration/types"; +import { + useGetVaultExternalMigrationConfigs, + useImportVaultSecrets +} from "@app/hooks/api/migration"; +import { VaultImportStatus } from "@app/hooks/api/migration/types"; import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries"; import { PendingAction } from "@app/hooks/api/secretFolders/types"; import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries"; @@ -198,8 +201,8 @@ export const ActionBar = ({ const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length); const { permission } = useProjectPermission(); - const { data: vaultConfig } = useGetExternalMigrationConfig(ExternalMigrationProviders.Vault); - const hasVaultConnection = Boolean(vaultConfig?.connectionId); + const { data: vaultConfigs = [] } = useGetVaultExternalMigrationConfigs(); + const hasVaultConnection = vaultConfigs.some((config) => config.connectionId); const handleFolderCreate = async (folderName: string, description: string | null) => { try {