mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 22:27:22 +00:00
feat(vault-migration): gateway support & kv v1 support
This commit is contained in:
@@ -1746,7 +1746,8 @@ export const registerRoutes = async (
|
|||||||
const migrationService = externalMigrationServiceFactory({
|
const migrationService = externalMigrationServiceFactory({
|
||||||
externalMigrationQueue,
|
externalMigrationQueue,
|
||||||
userDAL,
|
userDAL,
|
||||||
permissionService
|
permissionService,
|
||||||
|
gatewayService
|
||||||
});
|
});
|
||||||
|
|
||||||
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
|
const externalGroupOrgRoleMappingService = externalGroupOrgRoleMappingServiceFactory({
|
||||||
|
|||||||
@@ -66,7 +66,8 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
vaultAccessToken: z.string(),
|
vaultAccessToken: z.string(),
|
||||||
vaultNamespace: z.string().trim().optional(),
|
vaultNamespace: z.string().trim().optional(),
|
||||||
vaultUrl: z.string(),
|
vaultUrl: z.string(),
|
||||||
mappingType: z.nativeEnum(VaultMappingType)
|
mappingType: z.nativeEnum(VaultMappingType),
|
||||||
|
gatewayId: z.string().optional()
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
|
|||||||
@@ -1,12 +1,21 @@
|
|||||||
|
import https from "node:https";
|
||||||
|
|
||||||
import axios, { AxiosInstance } from "axios";
|
import axios, { AxiosInstance } from "axios";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { BadRequestError } from "@app/lib/errors";
|
import { BadRequestError } from "@app/lib/errors";
|
||||||
|
import { GatewayProxyProtocol, withGatewayProxy } from "@app/lib/gateway";
|
||||||
import { logger } from "@app/lib/logger";
|
import { logger } from "@app/lib/logger";
|
||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
|
|
||||||
import { InfisicalImportData, VaultMappingType } from "../external-migration-types";
|
import { InfisicalImportData, VaultMappingType } from "../external-migration-types";
|
||||||
|
|
||||||
|
enum KvVersion {
|
||||||
|
V1 = "1",
|
||||||
|
V2 = "2"
|
||||||
|
}
|
||||||
|
|
||||||
type VaultData = {
|
type VaultData = {
|
||||||
namespace: string;
|
namespace: string;
|
||||||
mount: string;
|
mount: string;
|
||||||
@@ -14,7 +23,42 @@ type VaultData = {
|
|||||||
secretData: Record<string, string>;
|
secretData: Record<string, string>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const vaultFactory = () => {
|
const vaultFactory = (gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">) => {
|
||||||
|
const $gatewayProxyWrapper = async <T>(
|
||||||
|
inputs: {
|
||||||
|
gatewayId: string;
|
||||||
|
targetHost?: string;
|
||||||
|
targetPort?: number;
|
||||||
|
},
|
||||||
|
gatewayCallback: (host: string, port: number, httpsAgent?: https.Agent) => Promise<T>
|
||||||
|
): Promise<T> => {
|
||||||
|
const relayDetails = await gatewayService.fnGetGatewayClientTlsByGatewayId(inputs.gatewayId);
|
||||||
|
const [relayHost, relayPort] = relayDetails.relayAddress.split(":");
|
||||||
|
|
||||||
|
const callbackResult = await withGatewayProxy(
|
||||||
|
async (port, httpsAgent) => {
|
||||||
|
const res = await gatewayCallback("http://localhost", port, httpsAgent);
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: GatewayProxyProtocol.Http,
|
||||||
|
targetHost: inputs.targetHost,
|
||||||
|
targetPort: inputs.targetPort,
|
||||||
|
relayHost,
|
||||||
|
relayPort: Number(relayPort),
|
||||||
|
identityId: relayDetails.identityId,
|
||||||
|
orgId: relayDetails.orgId,
|
||||||
|
tlsOptions: {
|
||||||
|
ca: relayDetails.certChain,
|
||||||
|
cert: relayDetails.certificate,
|
||||||
|
key: relayDetails.privateKey.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return callbackResult;
|
||||||
|
};
|
||||||
|
|
||||||
const getMounts = async (request: AxiosInstance) => {
|
const getMounts = async (request: AxiosInstance) => {
|
||||||
const response = await request
|
const response = await request
|
||||||
.get<{
|
.get<{
|
||||||
@@ -31,11 +75,24 @@ const vaultFactory = () => {
|
|||||||
|
|
||||||
const getPaths = async (
|
const getPaths = async (
|
||||||
request: AxiosInstance,
|
request: AxiosInstance,
|
||||||
{ mountPath, secretPath = "" }: { mountPath: string; secretPath?: string }
|
{ mountPath, secretPath = "" }: { mountPath: string; secretPath?: string },
|
||||||
|
kvVersion: KvVersion
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
// For KV v2: /v1/{mount}/metadata/{path}?list=true
|
if (kvVersion === KvVersion.V2) {
|
||||||
const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
|
// For KV v2: /v1/{mount}/metadata/{path}?list=true
|
||||||
|
const path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
|
||||||
|
const response = await request.get<{
|
||||||
|
data: {
|
||||||
|
keys: string[];
|
||||||
|
};
|
||||||
|
}>(`/v1/${path}?list=true`);
|
||||||
|
|
||||||
|
return response.data.data.keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv version v1: /v1/{mount}?list=true
|
||||||
|
const path = secretPath ? `${mountPath}/${secretPath}` : mountPath;
|
||||||
const response = await request.get<{
|
const response = await request.get<{
|
||||||
data: {
|
data: {
|
||||||
keys: string[];
|
keys: string[];
|
||||||
@@ -56,21 +113,42 @@ const vaultFactory = () => {
|
|||||||
|
|
||||||
const getSecrets = async (
|
const getSecrets = async (
|
||||||
request: AxiosInstance,
|
request: AxiosInstance,
|
||||||
{ mountPath, secretPath }: { mountPath: string; secretPath: string }
|
{ mountPath, secretPath }: { mountPath: string; secretPath: string },
|
||||||
|
kvVersion: KvVersion
|
||||||
) => {
|
) => {
|
||||||
// For KV v2: /v1/{mount}/data/{path}
|
if (kvVersion === KvVersion.V2) {
|
||||||
|
// For KV v2: /v1/{mount}/data/{path}
|
||||||
|
const response = await request
|
||||||
|
.get<{
|
||||||
|
data: {
|
||||||
|
data: Record<string, string>; // KV v2 has nested data structure
|
||||||
|
metadata: {
|
||||||
|
created_time: string;
|
||||||
|
deletion_time: string;
|
||||||
|
destroyed: boolean;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>(`/v1/${mountPath}/data/${secretPath}`)
|
||||||
|
.catch((err) => {
|
||||||
|
if (axios.isAxiosError(err)) {
|
||||||
|
logger.error(err.response?.data, "External migration: Failed to get Vault secret");
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// kv version v1
|
||||||
|
|
||||||
const response = await request
|
const response = await request
|
||||||
.get<{
|
.get<{
|
||||||
data: {
|
data: Record<string, string>; // KV v1 has flat data structure
|
||||||
data: Record<string, string>; // KV v2 has nested data structure
|
lease_duration: number;
|
||||||
metadata: {
|
lease_id: string;
|
||||||
created_time: string;
|
renewable: boolean;
|
||||||
deletion_time: string;
|
}>(`/v1/${mountPath}/${secretPath}`)
|
||||||
destroyed: boolean;
|
|
||||||
version: number;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}>(`/v1/${mountPath}/data/${secretPath}`)
|
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (axios.isAxiosError(err)) {
|
if (axios.isAxiosError(err)) {
|
||||||
logger.error(err.response?.data, "External migration: Failed to get Vault secret");
|
logger.error(err.response?.data, "External migration: Failed to get Vault secret");
|
||||||
@@ -78,7 +156,7 @@ const vaultFactory = () => {
|
|||||||
throw err;
|
throw err;
|
||||||
});
|
});
|
||||||
|
|
||||||
return response.data.data.data;
|
return response.data.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
// helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1)
|
// helper function to check if a mount is KV v2 (will be useful if we add support for Vault KV v1)
|
||||||
@@ -89,9 +167,10 @@ const vaultFactory = () => {
|
|||||||
const recursivelyGetAllPaths = async (
|
const recursivelyGetAllPaths = async (
|
||||||
request: AxiosInstance,
|
request: AxiosInstance,
|
||||||
mountPath: string,
|
mountPath: string,
|
||||||
|
kvVersion: KvVersion,
|
||||||
currentPath: string = ""
|
currentPath: string = ""
|
||||||
): Promise<string[]> => {
|
): Promise<string[]> => {
|
||||||
const paths = await getPaths(request, { mountPath, secretPath: currentPath });
|
const paths = await getPaths(request, { mountPath, secretPath: currentPath }, kvVersion);
|
||||||
|
|
||||||
if (paths === null || paths.length === 0) {
|
if (paths === null || paths.length === 0) {
|
||||||
return [];
|
return [];
|
||||||
@@ -105,7 +184,7 @@ const vaultFactory = () => {
|
|||||||
|
|
||||||
if (path.endsWith("/")) {
|
if (path.endsWith("/")) {
|
||||||
// it's a folder so we recurse into it
|
// it's a folder so we recurse into it
|
||||||
const subSecrets = await recursivelyGetAllPaths(request, mountPath, fullItemPath);
|
const subSecrets = await recursivelyGetAllPaths(request, mountPath, kvVersion, fullItemPath);
|
||||||
allSecrets.push(...subSecrets);
|
allSecrets.push(...subSecrets);
|
||||||
} else {
|
} else {
|
||||||
// it's a secret so we add it to our results
|
// it's a secret so we add it to our results
|
||||||
@@ -119,60 +198,93 @@ const vaultFactory = () => {
|
|||||||
async function collectVaultData({
|
async function collectVaultData({
|
||||||
baseUrl,
|
baseUrl,
|
||||||
namespace,
|
namespace,
|
||||||
accessToken
|
accessToken,
|
||||||
|
gatewayId
|
||||||
}: {
|
}: {
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
namespace?: string;
|
namespace?: string;
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
|
gatewayId?: string;
|
||||||
}): Promise<VaultData[]> {
|
}): Promise<VaultData[]> {
|
||||||
const request = axios.create({
|
const getData = async (host: string, port?: number, httpsAgent?: https.Agent) => {
|
||||||
baseURL: baseUrl,
|
const allData: VaultData[] = [];
|
||||||
headers: {
|
|
||||||
"X-Vault-Token": accessToken,
|
const request = axios.create({
|
||||||
...(namespace ? { "X-Vault-Namespace": namespace } : {})
|
baseURL: port ? `${host}:${port}` : host,
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(namespace ? { "X-Vault-Namespace": namespace } : {})
|
||||||
|
},
|
||||||
|
httpsAgent
|
||||||
|
});
|
||||||
|
|
||||||
|
// Get all mounts in this namespace
|
||||||
|
const mounts = await getMounts(request);
|
||||||
|
|
||||||
|
for (const mount of Object.keys(mounts)) {
|
||||||
|
if (!mount.endsWith("/")) {
|
||||||
|
delete mounts[mount];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
const allData: VaultData[] = [];
|
for await (const [mountPath, mountInfo] of Object.entries(mounts)) {
|
||||||
|
// skip non-KV mounts
|
||||||
|
if (!mountInfo.type.startsWith("kv")) {
|
||||||
|
// eslint-disable-next-line no-continue
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Get all mounts in this namespace
|
const kvVersion = mountInfo.options?.version === "2" ? KvVersion.V2 : KvVersion.V1;
|
||||||
const mounts = await getMounts(request);
|
|
||||||
|
|
||||||
for (const mount of Object.keys(mounts)) {
|
// get all paths in this mount
|
||||||
if (!mount.endsWith("/")) {
|
const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`, kvVersion);
|
||||||
delete mounts[mount];
|
|
||||||
|
const cleanMountPath = mountPath.replace(/\/$/, "");
|
||||||
|
|
||||||
|
for await (const secretPath of paths) {
|
||||||
|
// get the actual secret data
|
||||||
|
const secretData = await getSecrets(
|
||||||
|
request,
|
||||||
|
{
|
||||||
|
mountPath: cleanMountPath,
|
||||||
|
secretPath: secretPath.replace(`${cleanMountPath}/`, "")
|
||||||
|
},
|
||||||
|
kvVersion
|
||||||
|
);
|
||||||
|
|
||||||
|
allData.push({
|
||||||
|
namespace: namespace || "",
|
||||||
|
mount: mountPath.replace(/\/$/, ""),
|
||||||
|
path: secretPath.replace(`${cleanMountPath}/`, ""),
|
||||||
|
secretData
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return allData;
|
||||||
|
};
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (gatewayId) {
|
||||||
|
const url = new URL(baseUrl);
|
||||||
|
|
||||||
|
const { port, protocol, hostname } = url;
|
||||||
|
const cleanedProtocol = protocol.slice(0, -1);
|
||||||
|
|
||||||
|
data = await $gatewayProxyWrapper(
|
||||||
|
{
|
||||||
|
gatewayId,
|
||||||
|
targetHost: `${cleanedProtocol}://${hostname}`,
|
||||||
|
targetPort: port ? Number(port) : 8200 // 8200, default port for Vault self-hosted/dedicated
|
||||||
|
},
|
||||||
|
getData
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
data = await getData(baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
for await (const [mountPath, mountInfo] of Object.entries(mounts)) {
|
return data;
|
||||||
// skip non-KV mounts
|
|
||||||
if (!mountInfo.type.startsWith("kv")) {
|
|
||||||
// eslint-disable-next-line no-continue
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// get all paths in this mount
|
|
||||||
const paths = await recursivelyGetAllPaths(request, `${mountPath.replace(/\/$/, "")}`);
|
|
||||||
|
|
||||||
const cleanMountPath = mountPath.replace(/\/$/, "");
|
|
||||||
|
|
||||||
for await (const secretPath of paths) {
|
|
||||||
// get the actual secret data
|
|
||||||
const secretData = await getSecrets(request, {
|
|
||||||
mountPath: cleanMountPath,
|
|
||||||
secretPath: secretPath.replace(`${cleanMountPath}/`, "")
|
|
||||||
});
|
|
||||||
|
|
||||||
allData.push({
|
|
||||||
namespace: namespace || "",
|
|
||||||
mount: mountPath.replace(/\/$/, ""),
|
|
||||||
path: secretPath.replace(`${cleanMountPath}/`, ""),
|
|
||||||
secretData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return allData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -296,17 +408,22 @@ export const transformToInfisicalFormatNamespaceToProjects = (
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
export const importVaultDataFn = async ({
|
export const importVaultDataFn = async (
|
||||||
vaultAccessToken,
|
{
|
||||||
vaultNamespace,
|
vaultAccessToken,
|
||||||
vaultUrl,
|
vaultNamespace,
|
||||||
mappingType
|
vaultUrl,
|
||||||
}: {
|
mappingType,
|
||||||
vaultAccessToken: string;
|
gatewayId
|
||||||
vaultNamespace?: string;
|
}: {
|
||||||
vaultUrl: string;
|
vaultAccessToken: string;
|
||||||
mappingType: VaultMappingType;
|
vaultNamespace?: string;
|
||||||
}) => {
|
vaultUrl: string;
|
||||||
|
mappingType: VaultMappingType;
|
||||||
|
gatewayId?: string;
|
||||||
|
},
|
||||||
|
{ gatewayService }: { gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId"> }
|
||||||
|
) => {
|
||||||
await blockLocalAndPrivateIpAddresses(vaultUrl);
|
await blockLocalAndPrivateIpAddresses(vaultUrl);
|
||||||
|
|
||||||
if (mappingType === VaultMappingType.Namespace && !vaultNamespace) {
|
if (mappingType === VaultMappingType.Namespace && !vaultNamespace) {
|
||||||
@@ -315,12 +432,13 @@ export const importVaultDataFn = async ({
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const vaultApi = vaultFactory();
|
const vaultApi = vaultFactory(gatewayService);
|
||||||
|
|
||||||
const vaultData = await vaultApi.collectVaultData({
|
const vaultData = await vaultApi.collectVaultData({
|
||||||
accessToken: vaultAccessToken,
|
accessToken: vaultAccessToken,
|
||||||
baseUrl: vaultUrl,
|
baseUrl: vaultUrl,
|
||||||
namespace: vaultNamespace
|
namespace: vaultNamespace,
|
||||||
|
gatewayId
|
||||||
});
|
});
|
||||||
|
|
||||||
const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType);
|
const infisicalData = transformToInfisicalFormatNamespaceToProjects(vaultData, mappingType);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { OrgMembershipRole } from "@app/db/schemas";
|
import { OrgMembershipRole } from "@app/db/schemas";
|
||||||
|
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
||||||
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
import { TPermissionServiceFactory } from "@app/ee/services/permission/permission-service-types";
|
||||||
import { crypto } from "@app/lib/crypto/cryptography";
|
import { crypto } from "@app/lib/crypto/cryptography";
|
||||||
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
import { BadRequestError, ForbiddenRequestError } from "@app/lib/errors";
|
||||||
@@ -12,6 +13,7 @@ type TExternalMigrationServiceFactoryDep = {
|
|||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
externalMigrationQueue: TExternalMigrationQueueFactory;
|
externalMigrationQueue: TExternalMigrationQueueFactory;
|
||||||
userDAL: Pick<TUserDALFactory, "findById">;
|
userDAL: Pick<TUserDALFactory, "findById">;
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
|
export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrationServiceFactory>;
|
||||||
@@ -19,7 +21,8 @@ export type TExternalMigrationServiceFactory = ReturnType<typeof externalMigrati
|
|||||||
export const externalMigrationServiceFactory = ({
|
export const externalMigrationServiceFactory = ({
|
||||||
permissionService,
|
permissionService,
|
||||||
externalMigrationQueue,
|
externalMigrationQueue,
|
||||||
userDAL
|
userDAL,
|
||||||
|
gatewayService
|
||||||
}: TExternalMigrationServiceFactoryDep) => {
|
}: TExternalMigrationServiceFactoryDep) => {
|
||||||
const importEnvKeyData = async ({
|
const importEnvKeyData = async ({
|
||||||
decryptionKey,
|
decryptionKey,
|
||||||
@@ -72,6 +75,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
mappingType,
|
mappingType,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
|
gatewayId,
|
||||||
actor,
|
actor,
|
||||||
actorId,
|
actorId,
|
||||||
actorOrgId,
|
actorOrgId,
|
||||||
@@ -91,12 +95,18 @@ export const externalMigrationServiceFactory = ({
|
|||||||
|
|
||||||
const user = await userDAL.findById(actorId);
|
const user = await userDAL.findById(actorId);
|
||||||
|
|
||||||
const vaultData = await importVaultDataFn({
|
const vaultData = await importVaultDataFn(
|
||||||
vaultAccessToken,
|
{
|
||||||
vaultNamespace,
|
vaultAccessToken,
|
||||||
vaultUrl,
|
vaultNamespace,
|
||||||
mappingType
|
vaultUrl,
|
||||||
});
|
mappingType,
|
||||||
|
gatewayId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
gatewayService
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const stringifiedJson = JSON.stringify({
|
const stringifiedJson = JSON.stringify({
|
||||||
data: vaultData,
|
data: vaultData,
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export type TImportVaultDataDTO = {
|
|||||||
vaultNamespace?: string;
|
vaultNamespace?: string;
|
||||||
mappingType: VaultMappingType;
|
mappingType: VaultMappingType;
|
||||||
vaultUrl: string;
|
vaultUrl: string;
|
||||||
|
gatewayId?: string;
|
||||||
} & Omit<TOrgPermission, "orgId">;
|
} & Omit<TOrgPermission, "orgId">;
|
||||||
|
|
||||||
export type TImportInfisicalDataCreate = {
|
export type TImportInfisicalDataCreate = {
|
||||||
|
|||||||
@@ -46,18 +46,21 @@ export const useImportVault = () => {
|
|||||||
vaultAccessToken,
|
vaultAccessToken,
|
||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
mappingType
|
mappingType,
|
||||||
|
gatewayId
|
||||||
}: {
|
}: {
|
||||||
vaultAccessToken: string;
|
vaultAccessToken: string;
|
||||||
vaultNamespace?: string;
|
vaultNamespace?: string;
|
||||||
vaultUrl: string;
|
vaultUrl: string;
|
||||||
mappingType: string;
|
mappingType: string;
|
||||||
|
gatewayId?: string;
|
||||||
}) => {
|
}) => {
|
||||||
await apiRequest.post("/api/v3/external-migration/vault/", {
|
await apiRequest.post("/api/v3/external-migration/vault/", {
|
||||||
vaultAccessToken,
|
vaultAccessToken,
|
||||||
vaultNamespace,
|
vaultNamespace,
|
||||||
vaultUrl,
|
vaultUrl,
|
||||||
mappingType
|
mappingType,
|
||||||
|
gatewayId
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
+73
-11
@@ -6,9 +6,16 @@ import { twMerge } from "tailwind-merge";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { createNotification } from "@app/components/notifications";
|
import { createNotification } from "@app/components/notifications";
|
||||||
import { Button, FormControl, Input, Tooltip } from "@app/components/v2";
|
import { Button, FormControl, Input, Select, SelectItem, Tooltip } from "@app/components/v2";
|
||||||
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
import { NoticeBannerV2 } from "@app/components/v2/NoticeBannerV2/NoticeBannerV2";
|
||||||
import { useImportVault } from "@app/hooks/api/migration/mutations";
|
import { useImportVault } from "@app/hooks/api/migration/mutations";
|
||||||
|
import { OrgPermissionCan } from "@app/components/permissions";
|
||||||
|
import {
|
||||||
|
OrgGatewayPermissionActions,
|
||||||
|
OrgPermissionSubjects
|
||||||
|
} from "@app/context/OrgPermissionContext/types";
|
||||||
|
import { useQuery } from "@tanstack/react-query";
|
||||||
|
import { gatewaysQueryKeys } from "@app/hooks/api";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -62,36 +69,32 @@ const MAPPING_TYPE_MENU_ITEMS = [
|
|||||||
export const VaultPlatformModal = ({ onClose }: Props) => {
|
export const VaultPlatformModal = ({ onClose }: Props) => {
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
vaultUrl: z.string().min(1),
|
vaultUrl: z.string().min(1),
|
||||||
|
gatewayId: z.string().optional(),
|
||||||
vaultNamespace: z.string().trim().optional(),
|
vaultNamespace: z.string().trim().optional(),
|
||||||
vaultAccessToken: z.string().min(1),
|
vaultAccessToken: z.string().min(1),
|
||||||
mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault)
|
mappingType: z.nativeEnum(VaultMappingType).default(VaultMappingType.KeyVault)
|
||||||
});
|
});
|
||||||
type TFormData = z.infer<typeof formSchema>;
|
type TFormData = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
|
const { data: gateways, isPending: isGatewayLoading } = useQuery(gatewaysQueryKeys.list());
|
||||||
const { mutateAsync: importVault } = useImportVault();
|
const { mutateAsync: importVault } = useImportVault();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
reset,
|
reset,
|
||||||
formState: { isLoading, isDirty, isSubmitting, isValid, errors }
|
formState: { isLoading, isDirty, isSubmitting, isValid }
|
||||||
} = useForm<TFormData>({
|
} = useForm<TFormData>({
|
||||||
resolver: zodResolver(formSchema)
|
resolver: zodResolver(formSchema)
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log({
|
|
||||||
isSubmitting,
|
|
||||||
isLoading,
|
|
||||||
isValid,
|
|
||||||
errors
|
|
||||||
});
|
|
||||||
|
|
||||||
const onSubmit = async (data: TFormData) => {
|
const onSubmit = async (data: TFormData) => {
|
||||||
await importVault({
|
await importVault({
|
||||||
vaultAccessToken: data.vaultAccessToken,
|
vaultAccessToken: data.vaultAccessToken,
|
||||||
vaultNamespace: data.vaultNamespace,
|
vaultNamespace: data.vaultNamespace,
|
||||||
vaultUrl: data.vaultUrl,
|
vaultUrl: data.vaultUrl,
|
||||||
mappingType: data.mappingType
|
mappingType: data.mappingType,
|
||||||
|
...(data.gatewayId && { gatewayId: data.gatewayId })
|
||||||
});
|
});
|
||||||
createNotification({
|
createNotification({
|
||||||
title: "Import started",
|
title: "Import started",
|
||||||
@@ -110,11 +113,70 @@ export const VaultPlatformModal = ({ onClose }: Props) => {
|
|||||||
The Vault migration currently supports importing static secrets from Vault
|
The Vault migration currently supports importing static secrets from Vault
|
||||||
Dedicated/Self-Hosted.
|
Dedicated/Self-Hosted.
|
||||||
<div className="mt-2 text-xs opacity-80">
|
<div className="mt-2 text-xs opacity-80">
|
||||||
Currently only KV Secret Engine V2 is supported for Vault migrations.
|
Currently only KV Secret Engine is supported for Vault migrations.
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
</NoticeBannerV2>
|
</NoticeBannerV2>
|
||||||
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
<form onSubmit={handleSubmit(onSubmit)} autoComplete="off">
|
||||||
|
<div className="w-full flex-1">
|
||||||
|
<OrgPermissionCan
|
||||||
|
I={OrgGatewayPermissionActions.AttachGateways}
|
||||||
|
a={OrgPermissionSubjects.Gateway}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Controller
|
||||||
|
control={control}
|
||||||
|
name="gatewayId"
|
||||||
|
defaultValue=""
|
||||||
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
|
<FormControl
|
||||||
|
isError={Boolean(error?.message)}
|
||||||
|
errorText={error?.message}
|
||||||
|
label="Gateway"
|
||||||
|
isOptional
|
||||||
|
>
|
||||||
|
<Tooltip
|
||||||
|
isDisabled={isAllowed}
|
||||||
|
content="Restricted access. You don't have permission to attach gateways to resources."
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Select
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
value={value as string}
|
||||||
|
onValueChange={(v) => {
|
||||||
|
if (v !== "") {
|
||||||
|
onChange(v);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="w-full border border-mineshaft-500"
|
||||||
|
dropdownContainerClassName="max-w-none"
|
||||||
|
isLoading={isGatewayLoading}
|
||||||
|
placeholder="Default: Internet Gateway"
|
||||||
|
position="popper"
|
||||||
|
>
|
||||||
|
<SelectItem
|
||||||
|
value={undefined as unknown as string}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(undefined);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Internet Gateway
|
||||||
|
</SelectItem>
|
||||||
|
{gateways?.map((el) => (
|
||||||
|
<SelectItem value={el.id} key={el.id}>
|
||||||
|
{el.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</FormControl>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</OrgPermissionCan>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="vaultUrl"
|
name="vaultUrl"
|
||||||
|
|||||||
Reference in New Issue
Block a user