mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-06 14:27:59 +00:00
feat: add in-platform support for secret imports
This commit is contained in:
@@ -2199,7 +2199,9 @@ export const registerRoutes = async (
|
|||||||
gatewayService,
|
gatewayService,
|
||||||
kmsService,
|
kmsService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
externalMigrationConfigDAL
|
externalMigrationConfigDAL,
|
||||||
|
secretService,
|
||||||
|
auditLogService
|
||||||
});
|
});
|
||||||
|
|
||||||
// setup the communication with license key server
|
// setup the communication with license key server
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
onRequest: verifyAuth([AuthMode.JWT]),
|
||||||
handler: async (req) => {
|
handler: async (req) => {
|
||||||
const config = await server.services.migration.getExternalMigrationConfig({
|
const config = await server.services.migration.getExternalMigrationConfig({
|
||||||
platform: req.query.platform,
|
platform: req.query.platform,
|
||||||
@@ -260,4 +260,63 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
return { mounts };
|
return { mounts };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "POST",
|
||||||
|
url: "/vault/import-secrets",
|
||||||
|
config: {
|
||||||
|
rateLimit: writeLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
body: z.object({
|
||||||
|
projectId: z.string(),
|
||||||
|
environment: z.string(),
|
||||||
|
secretPath: z.string(),
|
||||||
|
vaultNamespace: z.string(),
|
||||||
|
vaultSecretPath: z.string()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
message: z.string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
await server.services.migration.importVaultSecrets({
|
||||||
|
actor: req.permission,
|
||||||
|
auditLogInfo: req.auditLogInfo,
|
||||||
|
...req.body
|
||||||
|
});
|
||||||
|
|
||||||
|
return { message: "Successfully imported vault secrets" };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/vault/secret-paths",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
namespace: z.string().optional()
|
||||||
|
}),
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
secretPaths: z.string().array()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const secretPaths = await server.services.migration.getVaultSecretPaths({
|
||||||
|
actor: req.permission,
|
||||||
|
namespace: req.query.namespace
|
||||||
|
});
|
||||||
|
|
||||||
|
return { secretPaths };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -363,3 +363,210 @@ export const listHCVaultMounts = async (
|
|||||||
|
|
||||||
return mounts;
|
return mounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const listHCVaultSecretPaths = async (
|
||||||
|
connection: THCVaultConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
namespace?: string
|
||||||
|
) => {
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
|
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||||
|
|
||||||
|
if (namespace && connection.credentials.namespace) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Namespace cannot be specified when namespace is already set in the connection credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetNamespace = namespace || connection.credentials.namespace;
|
||||||
|
|
||||||
|
const getPaths = async (mountPath: string, secretPath: string, kvVersion: "1" | "2"): Promise<string[] | null> => {
|
||||||
|
try {
|
||||||
|
let path: string;
|
||||||
|
if (kvVersion === "2") {
|
||||||
|
// For KV v2: /v1/{mount}/metadata/{path}?list=true
|
||||||
|
path = secretPath ? `${mountPath}/metadata/${secretPath}` : `${mountPath}/metadata`;
|
||||||
|
} else {
|
||||||
|
// For KV v1: /v1/{mount}/{path}?list=true
|
||||||
|
path = secretPath ? `${mountPath}/${secretPath}` : mountPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await requestWithHCVaultGateway<{
|
||||||
|
data: {
|
||||||
|
keys: string[];
|
||||||
|
};
|
||||||
|
}>(connection, gatewayService, {
|
||||||
|
url: `${instanceUrl}/v1/${path}?list=true`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data.keys;
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof AxiosError && error.response?.status === 404) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Recursive function to get all secret paths in a mount
|
||||||
|
const recursivelyGetAllPaths = async (
|
||||||
|
mountPath: string,
|
||||||
|
kvVersion: "1" | "2",
|
||||||
|
currentPath: string = ""
|
||||||
|
): Promise<string[]> => {
|
||||||
|
const paths = await getPaths(mountPath, currentPath, kvVersion);
|
||||||
|
|
||||||
|
if (paths === null || paths.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const allSecrets: string[] = [];
|
||||||
|
|
||||||
|
// Process paths sequentially to maintain tree traversal order
|
||||||
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
|
for (const path of paths) {
|
||||||
|
const cleanPath = path.endsWith("/") ? path.slice(0, -1) : path;
|
||||||
|
const fullItemPath = currentPath ? `${currentPath}/${cleanPath}` : cleanPath;
|
||||||
|
|
||||||
|
if (path.endsWith("/")) {
|
||||||
|
// it's a folder so we recurse into it
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const subSecrets = await recursivelyGetAllPaths(mountPath, kvVersion, fullItemPath);
|
||||||
|
allSecrets.push(...subSecrets);
|
||||||
|
} else {
|
||||||
|
// it's a secret so we add it to our results
|
||||||
|
allSecrets.push(`${mountPath}/${fullItemPath}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allSecrets;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get all mounts
|
||||||
|
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
|
||||||
|
|
||||||
|
// Filter for KV mounts (kv, kv-v1, kv-v2)
|
||||||
|
const kvMounts = mounts.filter((mount) => mount.type === "kv" || mount.type.startsWith("kv"));
|
||||||
|
|
||||||
|
// Collect all secret paths from all KV mounts in parallel
|
||||||
|
const allSecretPathsArrays = await Promise.all(
|
||||||
|
kvMounts.map(async (mount) => {
|
||||||
|
const kvVersion = mount.version === "2" ? "2" : "1";
|
||||||
|
const cleanMountPath = mount.path.replace(/\/$/, ""); // Remove trailing slash
|
||||||
|
return recursivelyGetAllPaths(cleanMountPath, kvVersion);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// Flatten the arrays into a single array
|
||||||
|
const allSecretPaths = allSecretPathsArrays.flat();
|
||||||
|
|
||||||
|
return allSecretPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHCVaultSecretsForPath = async (
|
||||||
|
connection: THCVaultConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
||||||
|
namespace: string,
|
||||||
|
secretPath: string
|
||||||
|
) => {
|
||||||
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
|
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||||
|
|
||||||
|
if (connection.credentials.namespace && connection.credentials.namespace !== namespace) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Specified namespace does not match the namespace in the connection credentials"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const targetNamespace = namespace || connection.credentials.namespace;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract mount and path from the secretPath
|
||||||
|
// secretPath format: {mount}/{path}
|
||||||
|
const pathParts = secretPath.split("/");
|
||||||
|
const mountPath = pathParts[0];
|
||||||
|
const actualPath = pathParts.slice(1).join("/");
|
||||||
|
|
||||||
|
if (!mountPath || !actualPath) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Invalid secret path format. Expected format: {mount}/{path}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get mounts to determine KV version
|
||||||
|
const mounts = await listHCVaultMounts(connection, gatewayService, namespace);
|
||||||
|
const mount = mounts.find((m) => m.path.replace(/\/$/, "") === mountPath);
|
||||||
|
|
||||||
|
if (!mount) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Mount '${mountPath}' not found in HashiCorp Vault`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const kvVersion = mount.version === "2" ? "2" : "1";
|
||||||
|
|
||||||
|
// Fetch secrets based on KV version
|
||||||
|
if (kvVersion === "2") {
|
||||||
|
// For KV v2: /v1/{mount}/data/{path}
|
||||||
|
const { data } = await requestWithHCVaultGateway<{
|
||||||
|
data: {
|
||||||
|
data: Record<string, string>; // KV v2 has nested data structure
|
||||||
|
metadata: {
|
||||||
|
created_time: string;
|
||||||
|
deletion_time: string;
|
||||||
|
destroyed: boolean;
|
||||||
|
version: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}>(connection, gatewayService, {
|
||||||
|
url: `${instanceUrl}/v1/${mountPath}/data/${actualPath}`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For KV v1: /v1/{mount}/{path}
|
||||||
|
const { data } = await requestWithHCVaultGateway<{
|
||||||
|
data: Record<string, string>; // KV v1 has flat data structure
|
||||||
|
lease_duration: number;
|
||||||
|
lease_id: string;
|
||||||
|
renewable: boolean;
|
||||||
|
}>(connection, gatewayService, {
|
||||||
|
url: `${instanceUrl}/v1/${mountPath}/${actualPath}`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.data;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, "Unable to fetch secrets from HC Vault path");
|
||||||
|
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to fetch secrets: ${error.message || "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof BadRequestError) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to fetch secrets from HashiCorp Vault"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,10 @@
|
|||||||
import { OrgMembershipRole } from "@app/db/schemas";
|
import { OrgMembershipRole } from "@app/db/schemas";
|
||||||
|
import {
|
||||||
|
AuditLogInfo,
|
||||||
|
EventType,
|
||||||
|
SecretApprovalEvent,
|
||||||
|
TAuditLogServiceFactory
|
||||||
|
} from "@app/ee/services/audit-log/audit-log-types";
|
||||||
import { TGatewayServiceFactory } from "@app/ee/services/gateway/gateway-service";
|
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";
|
||||||
@@ -9,12 +15,16 @@ import { AppConnection } from "../app-connection/app-connection-enums";
|
|||||||
import { decryptAppConnectionCredentials } from "../app-connection/app-connection-fns";
|
import { decryptAppConnectionCredentials } from "../app-connection/app-connection-fns";
|
||||||
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
|
import { TAppConnectionServiceFactory } from "../app-connection/app-connection-service";
|
||||||
import {
|
import {
|
||||||
|
getHCVaultSecretsForPath,
|
||||||
listHCVaultMounts,
|
listHCVaultMounts,
|
||||||
listHCVaultNamespaces,
|
listHCVaultNamespaces,
|
||||||
listHCVaultPolicies,
|
listHCVaultPolicies,
|
||||||
|
listHCVaultSecretPaths,
|
||||||
THCVaultConnection
|
THCVaultConnection
|
||||||
} from "../app-connection/hc-vault";
|
} from "../app-connection/hc-vault";
|
||||||
import { TKmsServiceFactory } from "../kms/kms-service";
|
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 { TUserDALFactory } from "../user/user-dal";
|
||||||
import { TExternalMigrationConfigDALFactory } from "./external-migration-config-dal";
|
import { TExternalMigrationConfigDALFactory } from "./external-migration-config-dal";
|
||||||
import {
|
import {
|
||||||
@@ -35,6 +45,8 @@ import {
|
|||||||
|
|
||||||
type TExternalMigrationServiceFactoryDep = {
|
type TExternalMigrationServiceFactoryDep = {
|
||||||
permissionService: TPermissionServiceFactory;
|
permissionService: TPermissionServiceFactory;
|
||||||
|
secretService: TSecretServiceFactory;
|
||||||
|
auditLogService: Pick<TAuditLogServiceFactory, "createAuditLog">;
|
||||||
externalMigrationQueue: TExternalMigrationQueueFactory;
|
externalMigrationQueue: TExternalMigrationQueueFactory;
|
||||||
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
appConnectionService: Pick<TAppConnectionServiceFactory, "connectAppConnectionById">;
|
||||||
externalMigrationConfigDAL: Pick<TExternalMigrationConfigDALFactory, "create" | "upsert" | "findOne" | "transaction">;
|
externalMigrationConfigDAL: Pick<TExternalMigrationConfigDALFactory, "create" | "upsert" | "findOne" | "transaction">;
|
||||||
@@ -50,6 +62,8 @@ export const externalMigrationServiceFactory = ({
|
|||||||
externalMigrationQueue,
|
externalMigrationQueue,
|
||||||
userDAL,
|
userDAL,
|
||||||
gatewayService,
|
gatewayService,
|
||||||
|
secretService,
|
||||||
|
auditLogService,
|
||||||
appConnectionService,
|
appConnectionService,
|
||||||
externalMigrationConfigDAL,
|
externalMigrationConfigDAL,
|
||||||
kmsService
|
kmsService
|
||||||
@@ -371,6 +385,143 @@ export const externalMigrationServiceFactory = ({
|
|||||||
return mounts;
|
return mounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVaultSecretPaths = async ({ actor, namespace }: { actor: OrgServiceActor; namespace?: string }) => {
|
||||||
|
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 secret paths" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultConfig = await externalMigrationConfigDAL.findOne({
|
||||||
|
orgId: actor.orgId,
|
||||||
|
platform: ExternalMigrationProviders.Vault
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!vaultConfig) {
|
||||||
|
throw new NotFoundError({ message: "Vault migration config not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 secretPaths = await listHCVaultSecretPaths(connection, gatewayService, namespace);
|
||||||
|
|
||||||
|
return secretPaths;
|
||||||
|
};
|
||||||
|
|
||||||
|
const importVaultSecrets = async ({
|
||||||
|
actor,
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
vaultNamespace,
|
||||||
|
vaultSecretPath,
|
||||||
|
auditLogInfo
|
||||||
|
}: {
|
||||||
|
actor: OrgServiceActor;
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
vaultNamespace: string;
|
||||||
|
vaultSecretPath: string;
|
||||||
|
auditLogInfo: AuditLogInfo;
|
||||||
|
}) => {
|
||||||
|
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 import vault secrets" });
|
||||||
|
}
|
||||||
|
|
||||||
|
const vaultConfig = await externalMigrationConfigDAL.findOne({
|
||||||
|
orgId: actor.orgId,
|
||||||
|
platform: ExternalMigrationProviders.Vault
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!vaultConfig) {
|
||||||
|
throw new NotFoundError({ message: "Vault migration config not found" });
|
||||||
|
}
|
||||||
|
|
||||||
|
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 vaultSecrets = await getHCVaultSecretsForPath(connection, gatewayService, vaultNamespace, vaultSecretPath);
|
||||||
|
|
||||||
|
const secretOperation = await secretService.createManySecretsRaw({
|
||||||
|
actorId: actor.id,
|
||||||
|
actor: actor.type,
|
||||||
|
actorAuthMethod: actor.authMethod,
|
||||||
|
actorOrgId: actor.orgId,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
projectId,
|
||||||
|
secrets: Object.entries(vaultSecrets).map(([secretKey, secretValue]) => ({
|
||||||
|
secretKey,
|
||||||
|
secretValue
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
if (secretOperation.type === SecretProtectionType.Approval) {
|
||||||
|
await auditLogService.createAuditLog({
|
||||||
|
projectId,
|
||||||
|
...auditLogInfo,
|
||||||
|
event: {
|
||||||
|
type: EventType.SECRET_APPROVAL_REQUEST,
|
||||||
|
metadata: {
|
||||||
|
committedBy: secretOperation.approval.committerUserId,
|
||||||
|
secretApprovalRequestId: secretOperation.approval.id,
|
||||||
|
secretApprovalRequestSlug: secretOperation.approval.slug,
|
||||||
|
secretPath,
|
||||||
|
environment,
|
||||||
|
secrets: Object.entries(vaultSecrets).map(([secretKey]) => ({
|
||||||
|
secretKey
|
||||||
|
})),
|
||||||
|
eventType: SecretApprovalEvent.CreateMany
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { approval: secretOperation.approval };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
importEnvKeyData,
|
importEnvKeyData,
|
||||||
importVaultData,
|
importVaultData,
|
||||||
@@ -379,6 +530,8 @@ export const externalMigrationServiceFactory = ({
|
|||||||
getExternalMigrationConfig,
|
getExternalMigrationConfig,
|
||||||
getVaultNamespaces,
|
getVaultNamespaces,
|
||||||
getVaultPolicies,
|
getVaultPolicies,
|
||||||
getVaultMounts
|
getVaultMounts,
|
||||||
|
getVaultSecretPaths,
|
||||||
|
importVaultSecrets
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
import { dashboardKeys } from "@app/hooks/api/dashboard/queries";
|
||||||
|
import { secretKeys } from "@app/hooks/api/secrets/queries";
|
||||||
|
|
||||||
import { projectKeys } from "../projects";
|
import { projectKeys } from "../projects";
|
||||||
import { externalMigrationQueryKeys } from "./queries";
|
import { externalMigrationQueryKeys } from "./queries";
|
||||||
import { ExternalMigrationProviders, TExternalMigrationConfig } from "./types";
|
import {
|
||||||
|
ExternalMigrationProviders,
|
||||||
|
TExternalMigrationConfig,
|
||||||
|
TImportVaultSecretsDTO
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const useImportEnvKey = () => {
|
export const useImportEnvKey = () => {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
@@ -90,3 +96,27 @@ export const useUpdateExternalMigrationConfig = (platform: ExternalMigrationProv
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useImportVaultSecrets = () => {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
return useMutation<{ message: string }, object, TImportVaultSecretsDTO>({
|
||||||
|
mutationFn: async (dto) => {
|
||||||
|
const { data } = await apiRequest.post<{ message: string }>(
|
||||||
|
"/api/v3/external-migration/vault/import-secrets",
|
||||||
|
dto
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
onSuccess: (_, { projectId, environment, secretPath }) => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: dashboardKeys.all() });
|
||||||
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: secretKeys.getProjectSecret({
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
secretPath
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export const externalMigrationQueryKeys = {
|
|||||||
config: (platform: string) => ["external-migration-config", { platform }],
|
config: (platform: string) => ["external-migration-config", { platform }],
|
||||||
vaultNamespaces: () => ["vault-namespaces"],
|
vaultNamespaces: () => ["vault-namespaces"],
|
||||||
vaultPolicies: () => ["vault-policies"],
|
vaultPolicies: () => ["vault-policies"],
|
||||||
vaultMounts: () => ["vault-mounts"]
|
vaultMounts: () => ["vault-mounts"],
|
||||||
|
vaultSecretPaths: () => ["vault-secret-paths"]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProviders) => {
|
export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProviders) => {
|
||||||
@@ -88,3 +89,21 @@ export const useGetVaultMounts = (enabled = true, namespace?: string) => {
|
|||||||
enabled
|
enabled
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetVaultSecretPaths = (enabled = true, namespace?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: externalMigrationQueryKeys.vaultSecretPaths(),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
secretPaths: string[];
|
||||||
|
}>("/api/v3/external-migration/vault/secret-paths", {
|
||||||
|
params: {
|
||||||
|
namespace
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.secretPaths;
|
||||||
|
},
|
||||||
|
enabled
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -11,3 +11,11 @@ export type TExternalMigrationConfig = {
|
|||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type TImportVaultSecretsDTO = {
|
||||||
|
projectId: string;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
vaultNamespace: string;
|
||||||
|
vaultSecretPath: string;
|
||||||
|
};
|
||||||
|
|||||||
+81
-7
@@ -77,6 +77,8 @@ import {
|
|||||||
fetchDashboardProjectSecretsByKeys
|
fetchDashboardProjectSecretsByKeys
|
||||||
} from "@app/hooks/api/dashboard/queries";
|
} from "@app/hooks/api/dashboard/queries";
|
||||||
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
import { UsedBySecretSyncs } from "@app/hooks/api/dashboard/types";
|
||||||
|
import { useGetExternalMigrationConfig, useImportVaultSecrets } from "@app/hooks/api/migration";
|
||||||
|
import { ExternalMigrationProviders } from "@app/hooks/api/migration/types";
|
||||||
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
import { secretApprovalRequestKeys } from "@app/hooks/api/secretApprovalRequest/queries";
|
||||||
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
import { PendingAction } from "@app/hooks/api/secretFolders/types";
|
||||||
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
import { fetchProjectSecrets, secretKeys } from "@app/hooks/api/secrets/queries";
|
||||||
@@ -98,6 +100,7 @@ import { CreateDynamicSecretForm } from "./CreateDynamicSecretForm";
|
|||||||
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
import { CreateSecretImportForm } from "./CreateSecretImportForm";
|
||||||
import { FolderForm } from "./FolderForm";
|
import { FolderForm } from "./FolderForm";
|
||||||
import { MoveSecretsModal } from "./MoveSecretsModal";
|
import { MoveSecretsModal } from "./MoveSecretsModal";
|
||||||
|
import { VaultSecretImportModal } from "./VaultSecretImportModal";
|
||||||
|
|
||||||
type TParsedEnv = { value: string; comments: string[]; secretPath?: string; secretKey: string }[];
|
type TParsedEnv = { value: string; comments: string[]; secretPath?: string; secretKey: string }[];
|
||||||
type TParsedFolderEnv = Record<
|
type TParsedFolderEnv = Record<
|
||||||
@@ -171,7 +174,8 @@ export const ActionBar = ({
|
|||||||
"upgradePlan",
|
"upgradePlan",
|
||||||
"replicateFolder",
|
"replicateFolder",
|
||||||
"confirmUpload",
|
"confirmUpload",
|
||||||
"requestAccess"
|
"requestAccess",
|
||||||
|
"importFromVault"
|
||||||
] as const);
|
] as const);
|
||||||
const isProtectedBranch = Boolean(protectedBranchPolicyName);
|
const isProtectedBranch = Boolean(protectedBranchPolicyName);
|
||||||
const { subscription } = useSubscription();
|
const { subscription } = useSubscription();
|
||||||
@@ -185,6 +189,7 @@ export const ActionBar = ({
|
|||||||
const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({
|
const { mutateAsync: createSecretBatch, isPending: isCreatingSecrets } = useCreateSecretBatch({
|
||||||
options: { onSuccess: undefined }
|
options: { onSuccess: undefined }
|
||||||
});
|
});
|
||||||
|
const { mutateAsync: importVaultSecrets } = useImportVaultSecrets();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const { addPendingChange } = useBatchModeActions();
|
const { addPendingChange } = useBatchModeActions();
|
||||||
|
|
||||||
@@ -193,6 +198,8 @@ export const ActionBar = ({
|
|||||||
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
const isMultiSelectActive = Boolean(Object.keys(selectedSecrets).length);
|
||||||
|
|
||||||
const { permission } = useProjectPermission();
|
const { permission } = useProjectPermission();
|
||||||
|
const { data: vaultConfig } = useGetExternalMigrationConfig(ExternalMigrationProviders.Vault);
|
||||||
|
const hasVaultConnection = Boolean(vaultConfig?.connectionId);
|
||||||
|
|
||||||
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
const handleFolderCreate = async (folderName: string, description: string | null) => {
|
||||||
try {
|
try {
|
||||||
@@ -663,6 +670,33 @@ export const ActionBar = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleVaultImport = async (vaultPath: string, namespace: string) => {
|
||||||
|
try {
|
||||||
|
await importVaultSecrets({
|
||||||
|
projectId,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
vaultNamespace: namespace,
|
||||||
|
vaultSecretPath: vaultPath
|
||||||
|
});
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: "Successfully imported secrets from HashiCorp Vault"
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Vault import error:", err);
|
||||||
|
const error = err as AxiosError<{ message?: string }>;
|
||||||
|
const errorMessage =
|
||||||
|
error.response?.data?.message || "Failed to import secrets from Vault. Please try again.";
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: errorMessage
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const isTableFiltered =
|
const isTableFiltered =
|
||||||
Object.values(filter.tags).some(Boolean) || Object.values(filter.include).some(Boolean);
|
Object.values(filter.tags).some(Boolean) || Object.values(filter.include).some(Boolean);
|
||||||
|
|
||||||
@@ -784,8 +818,8 @@ export const ActionBar = ({
|
|||||||
{Boolean(filteredTags) && <Badge>{filteredTags} Applied</Badge>}
|
{Boolean(filteredTags) && <Badge>{filteredTags} Applied</Badge>}
|
||||||
</div>
|
</div>
|
||||||
</DropdownSubMenuTrigger>
|
</DropdownSubMenuTrigger>
|
||||||
<DropdownSubMenuContent className="max-h-80 thin-scrollbar overflow-y-auto rounded-l-none">
|
<DropdownSubMenuContent className="thin-scrollbar max-h-80 overflow-y-auto rounded-l-none">
|
||||||
<DropdownMenuLabel className="sticky top-0 bg-mineshaft-900">
|
<DropdownMenuLabel className="bg-mineshaft-900 sticky top-0">
|
||||||
<div className="flex w-full items-center justify-between">
|
<div className="flex w-full items-center justify-between">
|
||||||
<span>Filter by Secret Tags</span>
|
<span>Filter by Secret Tags</span>
|
||||||
<Tooltip content="Matches secrets with one or more of the applied tags">
|
<Tooltip content="Matches secrets with one or more of the applied tags">
|
||||||
@@ -926,7 +960,7 @@ export const ActionBar = ({
|
|||||||
<IconButton
|
<IconButton
|
||||||
ariaLabel="add-folder-or-import"
|
ariaLabel="add-folder-or-import"
|
||||||
variant="outline_bg"
|
variant="outline_bg"
|
||||||
className="rounded-l-none bg-mineshaft-600 p-3"
|
className="bg-mineshaft-600 rounded-l-none p-3"
|
||||||
>
|
>
|
||||||
<FontAwesomeIcon icon={faAngleDown} />
|
<FontAwesomeIcon icon={faAngleDown} />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
@@ -1059,6 +1093,39 @@ export const ActionBar = ({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</ProjectPermissionCan>
|
</ProjectPermissionCan>
|
||||||
|
{hasVaultConnection && (
|
||||||
|
<ProjectPermissionCan
|
||||||
|
I={ProjectPermissionActions.Create}
|
||||||
|
a={subject(ProjectPermissionSub.Secrets, {
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
secretName: "*",
|
||||||
|
secretTags: ["*"]
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{(isAllowed) => (
|
||||||
|
<Button
|
||||||
|
leftIcon={
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Vault.png"
|
||||||
|
alt="HashiCorp Vault"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={() => {
|
||||||
|
handlePopUpOpen("importFromVault");
|
||||||
|
handlePopUpClose("misc");
|
||||||
|
}}
|
||||||
|
isDisabled={!isAllowed}
|
||||||
|
variant="outline_bg"
|
||||||
|
className="h-10 text-left"
|
||||||
|
isFullWidth
|
||||||
|
>
|
||||||
|
Add from HashiCorp Vault
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</ProjectPermissionCan>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -1070,11 +1137,11 @@ export const ActionBar = ({
|
|||||||
isMultiSelectActive && "h-16"
|
isMultiSelectActive && "h-16"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="mt-3.5 flex items-center rounded-md border border-mineshaft-600 bg-mineshaft-800 px-4 py-2 text-bunker-300">
|
<div className="border-mineshaft-600 bg-mineshaft-800 text-bunker-300 mt-3.5 flex items-center rounded-md border px-4 py-2">
|
||||||
<div className="mr-2 text-sm">{Object.keys(selectedSecrets).length} Selected</div>
|
<div className="mr-2 text-sm">{Object.keys(selectedSecrets).length} Selected</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mr-auto text-xs text-mineshaft-400 underline-offset-2 hover:text-mineshaft-200 hover:underline"
|
className="text-mineshaft-400 hover:text-mineshaft-200 mr-auto text-xs underline-offset-2 hover:underline"
|
||||||
onClick={resetSelectedSecret}
|
onClick={resetSelectedSecret}
|
||||||
>
|
>
|
||||||
Unselect All
|
Unselect All
|
||||||
@@ -1261,7 +1328,7 @@ export const ActionBar = ({
|
|||||||
onOpenChange={(open) => handlePopUpToggle("requestAccess", open)}
|
onOpenChange={(open) => handlePopUpToggle("requestAccess", open)}
|
||||||
>
|
>
|
||||||
<ModalContent title="Access Restricted">
|
<ModalContent title="Access Restricted">
|
||||||
<p className="mb-2 text-bunker-300">You do not have permission to perform this action.</p>
|
<p className="text-bunker-300 mb-2">You do not have permission to perform this action.</p>
|
||||||
<p className="text-bunker-300">Request access to perform this action in this folder.</p>
|
<p className="text-bunker-300">Request access to perform this action in this folder.</p>
|
||||||
<div className="mt-8 flex items-center gap-4">
|
<div className="mt-8 flex items-center gap-4">
|
||||||
<ModalClose asChild>
|
<ModalClose asChild>
|
||||||
@@ -1277,6 +1344,13 @@ export const ActionBar = ({
|
|||||||
</div>
|
</div>
|
||||||
</ModalContent>
|
</ModalContent>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<VaultSecretImportModal
|
||||||
|
isOpen={popUp.importFromVault.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handlePopUpToggle("importFromVault", isOpen)}
|
||||||
|
environment={environment}
|
||||||
|
secretPath={secretPath}
|
||||||
|
onImport={handleVaultImport}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+199
@@ -0,0 +1,199 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { faInfoCircle } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
useGetVaultMounts,
|
||||||
|
useGetVaultNamespaces,
|
||||||
|
useGetVaultSecretPaths
|
||||||
|
} from "@app/hooks/api/migration/queries";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
onImport: (vaultPath: string, namespace: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentProps = {
|
||||||
|
onClose: () => void;
|
||||||
|
environment: string;
|
||||||
|
secretPath: string;
|
||||||
|
onImport: (vaultPath: string, namespace: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = ({ onClose, environment, secretPath, onImport }: ContentProps) => {
|
||||||
|
const [selectedNamespace, setSelectedNamespace] = useState<string>("default");
|
||||||
|
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||||
|
const [shouldFetchPaths, setShouldFetchPaths] = useState(false);
|
||||||
|
const [shouldFetchMounts, setShouldFetchMounts] = useState(false);
|
||||||
|
|
||||||
|
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
|
||||||
|
const {
|
||||||
|
data: secretPaths,
|
||||||
|
isLoading: isLoadingPaths,
|
||||||
|
refetch: refetchPaths
|
||||||
|
} = useGetVaultSecretPaths(shouldFetchPaths, selectedNamespace);
|
||||||
|
const {
|
||||||
|
data: mounts,
|
||||||
|
isLoading: isLoadingMounts,
|
||||||
|
refetch: refetchMounts
|
||||||
|
} = useGetVaultMounts(shouldFetchMounts, selectedNamespace);
|
||||||
|
|
||||||
|
// Enable fetching paths and mounts when namespace is selected
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedNamespace) {
|
||||||
|
setShouldFetchPaths(true);
|
||||||
|
setShouldFetchMounts(true);
|
||||||
|
}
|
||||||
|
}, [selectedNamespace]);
|
||||||
|
|
||||||
|
const handleImport = () => {
|
||||||
|
if (!selectedPath) {
|
||||||
|
createNotification({ type: "error", text: "Please select a Vault secret path to import" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mounts || mounts.length === 0) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "No Vault mounts found. Please ensure you have KV secret engines configured."
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onImport(selectedPath, selectedNamespace);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="bg-primary/10 text-mineshaft-200 mb-4 rounded-md p-3 text-sm">
|
||||||
|
<div className="flex items-start gap-2">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-primary mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<div className="mb-2">
|
||||||
|
<strong>Import Secrets from HashiCorp Vault</strong>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1.5 text-xs leading-relaxed">
|
||||||
|
<p>
|
||||||
|
Select a Vault namespace and secret path to import secrets into the current
|
||||||
|
environment (<code className="text-xs">{environment}</code>) at path{" "}
|
||||||
|
<code className="text-xs">{secretPath}</code>.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<strong>Note:</strong> Existing secrets with the same key will be overwritten.
|
||||||
|
Secrets will be imported from the selected Vault path.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormControl
|
||||||
|
label="Namespace"
|
||||||
|
className="mb-4"
|
||||||
|
tooltipText="Select the Vault namespace containing the secrets you want to import."
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<FilterableSelect
|
||||||
|
value={namespaces?.find((ns) => ns.name === selectedNamespace)}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value && !Array.isArray(value)) {
|
||||||
|
const namespace = value as { id: string; name: string };
|
||||||
|
setSelectedNamespace(namespace.name);
|
||||||
|
setSelectedPath(null);
|
||||||
|
// Refetch paths and mounts when namespace changes
|
||||||
|
refetchPaths();
|
||||||
|
refetchMounts();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={namespaces || []}
|
||||||
|
getOptionValue={(option) => option.name}
|
||||||
|
getOptionLabel={(option) => option.name}
|
||||||
|
isDisabled={isLoadingNamespaces}
|
||||||
|
placeholder="Select namespace..."
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-mineshaft-400 mt-1 text-xs">
|
||||||
|
Select the Vault namespace to fetch available secret paths
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Vault Secret Path" className="mb-6">
|
||||||
|
<>
|
||||||
|
<FilterableSelect
|
||||||
|
value={selectedPath ? { path: selectedPath } : null}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value && !Array.isArray(value)) {
|
||||||
|
setSelectedPath((value as { path: string }).path);
|
||||||
|
} else {
|
||||||
|
setSelectedPath(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={(secretPaths || []).map((path) => ({ path }))}
|
||||||
|
getOptionValue={(option) => option.path}
|
||||||
|
getOptionLabel={(option) => option.path}
|
||||||
|
isDisabled={isLoadingPaths || !secretPaths?.length}
|
||||||
|
placeholder="Select a Vault path to import..."
|
||||||
|
isClearable
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-mineshaft-400 mt-1 text-xs">
|
||||||
|
Choose a secret path from your Vault namespace to import into Infisical
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="mt-8 flex space-x-4">
|
||||||
|
<Button
|
||||||
|
onClick={handleImport}
|
||||||
|
isDisabled={!selectedPath || isLoadingMounts || isLoadingPaths}
|
||||||
|
>
|
||||||
|
Import Secrets
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VaultSecretImportModal = ({
|
||||||
|
isOpen,
|
||||||
|
onOpenChange,
|
||||||
|
environment,
|
||||||
|
secretPath,
|
||||||
|
onImport
|
||||||
|
}: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
title="Import from HashiCorp Vault"
|
||||||
|
subTitle="Select a Vault namespace and secret path to import secrets into the current environment and folder."
|
||||||
|
className="max-w-2xl"
|
||||||
|
>
|
||||||
|
<Content
|
||||||
|
onClose={() => onOpenChange(false)}
|
||||||
|
environment={environment}
|
||||||
|
secretPath={secretPath}
|
||||||
|
onImport={onImport}
|
||||||
|
/>
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user