mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 07:28:21 +00:00
feat: add in-platform migrator for kubernetes auth
This commit is contained in:
@@ -215,7 +215,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
namespace: z.string().optional()
|
namespace: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -301,7 +301,7 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
},
|
},
|
||||||
schema: {
|
schema: {
|
||||||
querystring: z.object({
|
querystring: z.object({
|
||||||
namespace: z.string().optional()
|
namespace: z.string()
|
||||||
}),
|
}),
|
||||||
response: {
|
response: {
|
||||||
200: z.object({
|
200: z.object({
|
||||||
@@ -319,4 +319,57 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
|
|||||||
return { secretPaths };
|
return { secretPaths };
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
server.route({
|
||||||
|
method: "GET",
|
||||||
|
url: "/vault/auth-roles/kubernetes",
|
||||||
|
config: {
|
||||||
|
rateLimit: readLimit
|
||||||
|
},
|
||||||
|
schema: {
|
||||||
|
querystring: z.object({
|
||||||
|
namespace: z.string()
|
||||||
|
}),
|
||||||
|
|
||||||
|
response: {
|
||||||
|
200: z.object({
|
||||||
|
roles: z.array(
|
||||||
|
z.object({
|
||||||
|
name: z.string(),
|
||||||
|
mountPath: z.string(),
|
||||||
|
bound_service_account_names: z.array(z.string()),
|
||||||
|
bound_service_account_namespaces: z.array(z.string()),
|
||||||
|
token_ttl: z.number().optional(),
|
||||||
|
token_max_ttl: z.number().optional(),
|
||||||
|
token_policies: z.array(z.string()).optional(),
|
||||||
|
token_bound_cidrs: z.array(z.string()).optional(),
|
||||||
|
token_explicit_max_ttl: z.number().optional(),
|
||||||
|
token_no_default_policy: z.boolean().optional(),
|
||||||
|
token_num_uses: z.number().optional(),
|
||||||
|
token_period: z.number().optional(),
|
||||||
|
token_type: z.string().optional(),
|
||||||
|
audience: z.string().optional(),
|
||||||
|
alias_name_source: z.string().optional(),
|
||||||
|
config: z.object({
|
||||||
|
kubernetes_host: z.string(),
|
||||||
|
kubernetes_ca_cert: z.string().optional(),
|
||||||
|
issuer: z.string().optional(),
|
||||||
|
disable_iss_validation: z.boolean().optional(),
|
||||||
|
disable_local_ca_jwt: z.boolean().optional()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onRequest: verifyAuth([AuthMode.JWT, AuthMode.IDENTITY_ACCESS_TOKEN]),
|
||||||
|
handler: async (req) => {
|
||||||
|
const roles = await server.services.migration.getVaultKubernetesAuthRoles({
|
||||||
|
actor: req.permission,
|
||||||
|
namespace: req.query.namespace
|
||||||
|
});
|
||||||
|
|
||||||
|
return { roles };
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ export enum HCVaultConnectionMethod {
|
|||||||
AccessToken = "access-token",
|
AccessToken = "access-token",
|
||||||
AppRole = "app-role"
|
AppRole = "app-role"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum HCVaultAuthType {
|
||||||
|
Kubernetes = "kubernetes"
|
||||||
|
}
|
||||||
|
|||||||
@@ -12,10 +12,15 @@ import { logger } from "@app/lib/logger";
|
|||||||
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
import { blockLocalAndPrivateIpAddresses } from "@app/lib/validator";
|
||||||
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
import { AppConnection } from "@app/services/app-connection/app-connection-enums";
|
||||||
|
|
||||||
import { HCVaultConnectionMethod } from "./hc-vault-connection-enums";
|
import { HCVaultAuthType, HCVaultConnectionMethod } from "./hc-vault-connection-enums";
|
||||||
import {
|
import {
|
||||||
|
THCVaultAuthMount,
|
||||||
|
THCVaultAuthMountResponse,
|
||||||
THCVaultConnection,
|
THCVaultConnection,
|
||||||
THCVaultConnectionConfig,
|
THCVaultConnectionConfig,
|
||||||
|
THCVaultKubernetesAuthConfig,
|
||||||
|
THCVaultKubernetesAuthRole,
|
||||||
|
THCVaultKubernetesAuthRoleWithConfig,
|
||||||
THCVaultMount,
|
THCVaultMount,
|
||||||
THCVaultMountResponse
|
THCVaultMountResponse
|
||||||
} from "./hc-vault-connection-types";
|
} from "./hc-vault-connection-types";
|
||||||
@@ -187,21 +192,19 @@ export const validateHCVaultConnectionCredentials = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const listHCVaultPolicies = async (
|
export const listHCVaultPolicies = async (
|
||||||
|
namespace: string,
|
||||||
connection: THCVaultConnection,
|
connection: THCVaultConnection,
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
namespace?: string
|
|
||||||
) => {
|
) => {
|
||||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||||
|
|
||||||
if (namespace && connection.credentials.namespace) {
|
if (connection.credentials.namespace && connection.credentials.namespace !== namespace) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Namespace cannot be specified when namespace is already set in the connection credentials"
|
message: "Specified namespace does not match the namespace in the connection credentials"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetNamespace = namespace || connection.credentials.namespace;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: listData } = await requestWithHCVaultGateway<{
|
const { data: listData } = await requestWithHCVaultGateway<{
|
||||||
policies: string[];
|
policies: string[];
|
||||||
@@ -210,7 +213,7 @@ export const listHCVaultPolicies = async (
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Vault-Token": accessToken,
|
"X-Vault-Token": accessToken,
|
||||||
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
"X-Vault-Namespace": namespace
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -227,7 +230,7 @@ export const listHCVaultPolicies = async (
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Vault-Token": accessToken,
|
"X-Vault-Token": accessToken,
|
||||||
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
"X-Vault-Namespace": namespace
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -365,21 +368,19 @@ export const listHCVaultMounts = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const listHCVaultSecretPaths = async (
|
export const listHCVaultSecretPaths = async (
|
||||||
|
namespace: string,
|
||||||
connection: THCVaultConnection,
|
connection: THCVaultConnection,
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
namespace?: string
|
|
||||||
) => {
|
) => {
|
||||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||||
|
|
||||||
if (namespace && connection.credentials.namespace) {
|
if (connection.credentials.namespace && connection.credentials.namespace !== namespace) {
|
||||||
throw new BadRequestError({
|
throw new BadRequestError({
|
||||||
message: "Namespace cannot be specified when namespace is already set in the connection credentials"
|
message: "Specified namespace does not match the namespace in the connection credentials"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetNamespace = namespace || connection.credentials.namespace;
|
|
||||||
|
|
||||||
const getPaths = async (mountPath: string, secretPath: string, kvVersion: "1" | "2"): Promise<string[] | null> => {
|
const getPaths = async (mountPath: string, secretPath: string, kvVersion: "1" | "2"): Promise<string[] | null> => {
|
||||||
try {
|
try {
|
||||||
let path: string;
|
let path: string;
|
||||||
@@ -400,7 +401,7 @@ export const listHCVaultSecretPaths = async (
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Vault-Token": accessToken,
|
"X-Vault-Token": accessToken,
|
||||||
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
"X-Vault-Namespace": namespace
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -470,10 +471,10 @@ export const listHCVaultSecretPaths = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getHCVaultSecretsForPath = async (
|
export const getHCVaultSecretsForPath = async (
|
||||||
connection: THCVaultConnection,
|
|
||||||
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
|
|
||||||
namespace: string,
|
namespace: string,
|
||||||
secretPath: string
|
secretPath: string,
|
||||||
|
connection: THCVaultConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
) => {
|
) => {
|
||||||
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
const instanceUrl = await getHCVaultInstanceUrl(connection);
|
||||||
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
|
||||||
@@ -484,8 +485,6 @@ export const getHCVaultSecretsForPath = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetNamespace = namespace || connection.credentials.namespace;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract mount and path from the secretPath
|
// Extract mount and path from the secretPath
|
||||||
// secretPath format: {mount}/{path}
|
// secretPath format: {mount}/{path}
|
||||||
@@ -529,7 +528,7 @@ export const getHCVaultSecretsForPath = async (
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Vault-Token": accessToken,
|
"X-Vault-Token": accessToken,
|
||||||
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
"X-Vault-Namespace": namespace
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -547,7 +546,7 @@ export const getHCVaultSecretsForPath = async (
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
headers: {
|
headers: {
|
||||||
"X-Vault-Token": accessToken,
|
"X-Vault-Token": accessToken,
|
||||||
...(targetNamespace ? { "X-Vault-Namespace": targetNamespace } : {})
|
"X-Vault-Namespace": namespace
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -570,3 +569,156 @@ export const getHCVaultSecretsForPath = async (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getHCVaultAuthMounts = async (
|
||||||
|
namespace: string,
|
||||||
|
authType: HCVaultAuthType,
|
||||||
|
connection: THCVaultConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
): Promise<THCVaultAuthMount[]> => {
|
||||||
|
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { data } = await requestWithHCVaultGateway<THCVaultAuthMountResponse>(connection, gatewayService, {
|
||||||
|
url: `${instanceUrl}/v1/sys/auth`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
"X-Vault-Namespace": namespace
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const authMounts: THCVaultAuthMount[] = [];
|
||||||
|
|
||||||
|
Object.entries(data.data).forEach(([path, authMethod]) => {
|
||||||
|
if (authMethod.type === authType) {
|
||||||
|
authMounts.push({
|
||||||
|
path,
|
||||||
|
type: authMethod.type,
|
||||||
|
description: authMethod.description,
|
||||||
|
accessor: authMethod.accessor
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return authMounts;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, `Unable to list HC Vault ${authType} auth mounts`);
|
||||||
|
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to list ${authType} auth mounts: ${error.message || "Unknown error"}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Unable to list ${authType} auth mounts from HashiCorp Vault`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getHCVaultKubernetesAuthRoles = async (
|
||||||
|
namespace: string,
|
||||||
|
mountPath: string,
|
||||||
|
connection: THCVaultConnection,
|
||||||
|
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
|
||||||
|
): Promise<THCVaultKubernetesAuthRoleWithConfig[]> => {
|
||||||
|
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"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove trailing slash from mount path
|
||||||
|
const cleanMountPath = mountPath.endsWith("/") ? mountPath.slice(0, -1) : mountPath;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 1. Get the Kubernetes auth configuration for this mount
|
||||||
|
const { data: configResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthConfig }>(
|
||||||
|
connection,
|
||||||
|
gatewayService,
|
||||||
|
{
|
||||||
|
url: `${instanceUrl}/v1/auth/${cleanMountPath}/config`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
"X-Vault-Namespace": namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const kubernetesConfig = configResponse.data;
|
||||||
|
|
||||||
|
// 2. List all roles in this mount
|
||||||
|
const { data: roleListResponse } = await requestWithHCVaultGateway<{ data: { keys: string[] } }>(
|
||||||
|
connection,
|
||||||
|
gatewayService,
|
||||||
|
{
|
||||||
|
url: `${instanceUrl}/v1/auth/${cleanMountPath}/role`,
|
||||||
|
method: "LIST",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
"X-Vault-Namespace": namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const roleNames = roleListResponse.data.keys;
|
||||||
|
|
||||||
|
if (!roleNames || roleNames.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Fetch details for each role
|
||||||
|
const roleDetailsPromises = roleNames.map(async (roleName) => {
|
||||||
|
const { data: roleResponse } = await requestWithHCVaultGateway<{ data: THCVaultKubernetesAuthRole }>(
|
||||||
|
connection,
|
||||||
|
gatewayService,
|
||||||
|
{
|
||||||
|
url: `${instanceUrl}/v1/auth/${cleanMountPath}/role/${roleName}`,
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
"X-Vault-Token": accessToken,
|
||||||
|
"X-Vault-Namespace": namespace
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4. Merge the role with the config
|
||||||
|
return {
|
||||||
|
...roleResponse.data,
|
||||||
|
name: roleName,
|
||||||
|
config: kubernetesConfig,
|
||||||
|
mountPath: cleanMountPath
|
||||||
|
} as THCVaultKubernetesAuthRoleWithConfig;
|
||||||
|
});
|
||||||
|
|
||||||
|
const roles = await Promise.all(roleDetailsPromises);
|
||||||
|
|
||||||
|
return roles;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
logger.error(error, "Unable to list HC Vault Kubernetes auth roles");
|
||||||
|
|
||||||
|
if (error instanceof AxiosError) {
|
||||||
|
const errorMessage =
|
||||||
|
(error.response?.data as { errors?: string[] })?.errors?.[0] || error.message || "Unknown error";
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: `Failed to list Kubernetes auth roles: ${errorMessage}`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new BadRequestError({
|
||||||
|
message: "Unable to list Kubernetes auth roles from HashiCorp Vault"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -39,3 +39,59 @@ export type THCVaultMount = {
|
|||||||
type: string;
|
type: string;
|
||||||
version?: string | null;
|
version?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type THCVaultAuthMountResponse = {
|
||||||
|
data: {
|
||||||
|
[key: string]: {
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
accessor: string;
|
||||||
|
config: {
|
||||||
|
default_lease_ttl: number;
|
||||||
|
max_lease_ttl: number;
|
||||||
|
force_no_cache: boolean;
|
||||||
|
};
|
||||||
|
local: boolean;
|
||||||
|
seal_wrap: boolean;
|
||||||
|
external_entropy_access: boolean;
|
||||||
|
options: Record<string, string> | null;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultAuthMount = {
|
||||||
|
path: string;
|
||||||
|
type: string;
|
||||||
|
description: string;
|
||||||
|
accessor: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultKubernetesAuthConfig = {
|
||||||
|
kubernetes_host: string;
|
||||||
|
kubernetes_ca_cert?: string;
|
||||||
|
issuer?: string;
|
||||||
|
disable_iss_validation?: boolean;
|
||||||
|
disable_local_ca_jwt?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultKubernetesAuthRole = {
|
||||||
|
name: string;
|
||||||
|
bound_service_account_names: string[];
|
||||||
|
bound_service_account_namespaces: string[];
|
||||||
|
token_ttl?: number;
|
||||||
|
token_max_ttl?: number;
|
||||||
|
token_policies?: string[];
|
||||||
|
token_bound_cidrs?: string[];
|
||||||
|
token_explicit_max_ttl?: number;
|
||||||
|
token_no_default_policy?: boolean;
|
||||||
|
token_num_uses?: number;
|
||||||
|
token_period?: number;
|
||||||
|
token_type?: string;
|
||||||
|
audience?: string;
|
||||||
|
alias_name_source?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type THCVaultKubernetesAuthRoleWithConfig = THCVaultKubernetesAuthRole & {
|
||||||
|
config: THCVaultKubernetesAuthConfig;
|
||||||
|
mountPath: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ 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 {
|
||||||
|
getHCVaultAuthMounts,
|
||||||
|
getHCVaultKubernetesAuthRoles,
|
||||||
getHCVaultSecretsForPath,
|
getHCVaultSecretsForPath,
|
||||||
|
HCVaultAuthType,
|
||||||
listHCVaultMounts,
|
listHCVaultMounts,
|
||||||
listHCVaultNamespaces,
|
listHCVaultNamespaces,
|
||||||
listHCVaultPolicies,
|
listHCVaultPolicies,
|
||||||
@@ -301,7 +304,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
return namespaces;
|
return namespaces;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVaultPolicies = async ({ actor, namespace }: { actor: OrgServiceActor; namespace?: string }) => {
|
const getVaultPolicies = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
|
||||||
const { hasRole } = await permissionService.getOrgPermission(
|
const { hasRole } = await permissionService.getOrgPermission(
|
||||||
actor.type,
|
actor.type,
|
||||||
actor.id,
|
actor.id,
|
||||||
@@ -339,7 +342,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
credentials
|
credentials
|
||||||
} as THCVaultConnection;
|
} as THCVaultConnection;
|
||||||
|
|
||||||
const policies = await listHCVaultPolicies(connection, gatewayService, namespace);
|
const policies = await listHCVaultPolicies(namespace, connection, gatewayService);
|
||||||
return policies;
|
return policies;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -385,7 +388,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
return mounts;
|
return mounts;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getVaultSecretPaths = async ({ actor, namespace }: { actor: OrgServiceActor; namespace?: string }) => {
|
const getVaultSecretPaths = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
|
||||||
const { hasRole } = await permissionService.getOrgPermission(
|
const { hasRole } = await permissionService.getOrgPermission(
|
||||||
actor.type,
|
actor.type,
|
||||||
actor.id,
|
actor.id,
|
||||||
@@ -423,7 +426,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
credentials
|
credentials
|
||||||
} as THCVaultConnection;
|
} as THCVaultConnection;
|
||||||
|
|
||||||
const secretPaths = await listHCVaultSecretPaths(connection, gatewayService, namespace);
|
const secretPaths = await listHCVaultSecretPaths(namespace, connection, gatewayService);
|
||||||
|
|
||||||
return secretPaths;
|
return secretPaths;
|
||||||
};
|
};
|
||||||
@@ -482,7 +485,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
credentials
|
credentials
|
||||||
} as THCVaultConnection;
|
} as THCVaultConnection;
|
||||||
|
|
||||||
const vaultSecrets = await getHCVaultSecretsForPath(connection, gatewayService, vaultNamespace, vaultSecretPath);
|
const vaultSecrets = await getHCVaultSecretsForPath(vaultNamespace, vaultSecretPath, connection, gatewayService);
|
||||||
|
|
||||||
const secretOperation = await secretService.createManySecretsRaw({
|
const secretOperation = await secretService.createManySecretsRaw({
|
||||||
actorId: actor.id,
|
actorId: actor.id,
|
||||||
@@ -522,6 +525,58 @@ export const externalMigrationServiceFactory = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getVaultKubernetesAuthRoles = 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 Kubernetes auth roles" });
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Get all Kubernetes auth mounts for this namespace
|
||||||
|
const authMounts = await getHCVaultAuthMounts(namespace, HCVaultAuthType.Kubernetes, connection, gatewayService);
|
||||||
|
|
||||||
|
// For each mount, get all roles with their configuration
|
||||||
|
const allRolesPromises = authMounts.map(async (mount) => {
|
||||||
|
const roles = await getHCVaultKubernetesAuthRoles(namespace, mount.path, connection, gatewayService);
|
||||||
|
return roles;
|
||||||
|
});
|
||||||
|
|
||||||
|
const rolesPerMount = await Promise.all(allRolesPromises);
|
||||||
|
|
||||||
|
return rolesPerMount.flat();
|
||||||
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
importEnvKeyData,
|
importEnvKeyData,
|
||||||
importVaultData,
|
importVaultData,
|
||||||
@@ -532,6 +587,7 @@ export const externalMigrationServiceFactory = ({
|
|||||||
getVaultPolicies,
|
getVaultPolicies,
|
||||||
getVaultMounts,
|
getVaultMounts,
|
||||||
getVaultSecretPaths,
|
getVaultSecretPaths,
|
||||||
importVaultSecrets
|
importVaultSecrets,
|
||||||
|
getVaultKubernetesAuthRoles
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { useQuery } from "@tanstack/react-query";
|
|||||||
|
|
||||||
import { apiRequest } from "@app/config/request";
|
import { apiRequest } from "@app/config/request";
|
||||||
|
|
||||||
import { ExternalMigrationProviders, TExternalMigrationConfig } from "./types";
|
import {
|
||||||
|
ExternalMigrationProviders,
|
||||||
|
TExternalMigrationConfig,
|
||||||
|
VaultKubernetesAuthRole
|
||||||
|
} from "./types";
|
||||||
|
|
||||||
export const externalMigrationQueryKeys = {
|
export const externalMigrationQueryKeys = {
|
||||||
customMigrationAvailable: (provider: ExternalMigrationProviders) => [
|
customMigrationAvailable: (provider: ExternalMigrationProviders) => [
|
||||||
@@ -13,7 +17,8 @@ export const externalMigrationQueryKeys = {
|
|||||||
vaultNamespaces: () => ["vault-namespaces"],
|
vaultNamespaces: () => ["vault-namespaces"],
|
||||||
vaultPolicies: () => ["vault-policies"],
|
vaultPolicies: () => ["vault-policies"],
|
||||||
vaultMounts: () => ["vault-mounts"],
|
vaultMounts: () => ["vault-mounts"],
|
||||||
vaultSecretPaths: () => ["vault-secret-paths"]
|
vaultSecretPaths: () => ["vault-secret-paths"],
|
||||||
|
vaultKubernetesAuthRoles: (namespace?: string) => ["vault-kubernetes-auth-roles", namespace]
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProviders) => {
|
export const useHasCustomMigrationAvailable = (provider: ExternalMigrationProviders) => {
|
||||||
@@ -107,3 +112,21 @@ export const useGetVaultSecretPaths = (enabled = true, namespace?: string) => {
|
|||||||
enabled
|
enabled
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const useGetVaultKubernetesAuthRoles = (enabled = true, namespace?: string) => {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: externalMigrationQueryKeys.vaultKubernetesAuthRoles(namespace),
|
||||||
|
queryFn: async () => {
|
||||||
|
const { data } = await apiRequest.get<{
|
||||||
|
roles: VaultKubernetesAuthRole[];
|
||||||
|
}>("/api/v3/external-migration/vault/auth-roles/kubernetes", {
|
||||||
|
params: {
|
||||||
|
namespace
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return data.roles;
|
||||||
|
},
|
||||||
|
enabled
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
@@ -19,3 +19,28 @@ export type TImportVaultSecretsDTO = {
|
|||||||
vaultNamespace: string;
|
vaultNamespace: string;
|
||||||
vaultSecretPath: string;
|
vaultSecretPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type VaultKubernetesAuthRole = {
|
||||||
|
name: string;
|
||||||
|
bound_service_account_names: string[];
|
||||||
|
bound_service_account_namespaces: string[];
|
||||||
|
token_ttl?: number;
|
||||||
|
token_max_ttl?: number;
|
||||||
|
token_policies?: string[];
|
||||||
|
token_bound_cidrs?: string[];
|
||||||
|
token_explicit_max_ttl?: number;
|
||||||
|
token_no_default_policy?: boolean;
|
||||||
|
token_num_uses?: number;
|
||||||
|
token_period?: number;
|
||||||
|
token_type?: string;
|
||||||
|
audience?: string;
|
||||||
|
alias_name_source?: string;
|
||||||
|
mountPath: string;
|
||||||
|
config: {
|
||||||
|
kubernetes_host: string;
|
||||||
|
kubernetes_ca_cert?: string;
|
||||||
|
issuer?: string;
|
||||||
|
disable_iss_validation?: boolean;
|
||||||
|
disable_local_ca_jwt?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
+155
-6
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
import { Controller, useFieldArray, useForm } from "react-hook-form";
|
||||||
import { faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
import { faInfoCircle, faPlus, faXmark } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
@@ -37,9 +37,15 @@ import {
|
|||||||
IdentityKubernetesAuthTokenReviewMode,
|
IdentityKubernetesAuthTokenReviewMode,
|
||||||
IdentityTrustedIp
|
IdentityTrustedIp
|
||||||
} from "@app/hooks/api/identities/types";
|
} from "@app/hooks/api/identities/types";
|
||||||
import { UsePopUpState } from "@app/hooks/usePopUp";
|
import { useGetExternalMigrationConfig } from "@app/hooks/api/migration/queries";
|
||||||
|
import {
|
||||||
|
ExternalMigrationProviders,
|
||||||
|
VaultKubernetesAuthRole
|
||||||
|
} from "@app/hooks/api/migration/types";
|
||||||
|
import { usePopUp, UsePopUpState } from "@app/hooks/usePopUp";
|
||||||
|
|
||||||
import { IdentityFormTab } from "./types";
|
import { IdentityFormTab } from "./types";
|
||||||
|
import { VaultKubernetesAuthImportModal } from "./VaultKubernetesAuthImportModal";
|
||||||
|
|
||||||
const schema = z
|
const schema = z
|
||||||
.object({
|
.object({
|
||||||
@@ -121,6 +127,12 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
enabled: isUpdate
|
enabled: isUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { popUp, handlePopUpToggle: handleImportPopUpToggle } = usePopUp([
|
||||||
|
"importFromVault"
|
||||||
|
] as const);
|
||||||
|
const { data: vaultConfig } = useGetExternalMigrationConfig(ExternalMigrationProviders.Vault);
|
||||||
|
const hasVaultConnection = Boolean(vaultConfig?.connectionId);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
control,
|
control,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -192,6 +204,106 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
}
|
}
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
const handleImportFromVault = (role: VaultKubernetesAuthRole) => {
|
||||||
|
try {
|
||||||
|
setValue("kubernetesHost", role.config.kubernetes_host, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true,
|
||||||
|
shouldValidate: true
|
||||||
|
});
|
||||||
|
|
||||||
|
if (role.config.token_reviewer_jwt) {
|
||||||
|
setValue("tokenReviewerJwt", role.config.token_reviewer_jwt, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.bound_service_account_names?.length > 0) {
|
||||||
|
// In Vault, "*" means allow all; in Infisical, empty field means allow any
|
||||||
|
const allowedNames = role.bound_service_account_names.includes("*")
|
||||||
|
? ""
|
||||||
|
: role.bound_service_account_names.join(", ");
|
||||||
|
setValue("allowedNames", allowedNames, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.bound_service_account_namespaces?.length > 0) {
|
||||||
|
// In Vault, "*" means allow all; in Infisical, empty field means allow any
|
||||||
|
const allowedNamespaces = role.bound_service_account_namespaces.includes("*")
|
||||||
|
? ""
|
||||||
|
: role.bound_service_account_namespaces.join(", ");
|
||||||
|
setValue("allowedNamespaces", allowedNamespaces, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.token_ttl !== undefined) {
|
||||||
|
setValue("accessTokenTTL", String(role.token_ttl), {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.token_max_ttl !== undefined) {
|
||||||
|
setValue("accessTokenMaxTTL", String(role.token_max_ttl), {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.token_num_uses !== undefined) {
|
||||||
|
setValue("accessTokenNumUsesLimit", String(role.token_num_uses), {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.audience) {
|
||||||
|
setValue("allowedAudience", role.audience, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role.config.kubernetes_ca_cert) {
|
||||||
|
setValue("caCert", role.config.kubernetes_ca_cert, {
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
subscription?.ipAllowlisting &&
|
||||||
|
role.token_bound_cidrs &&
|
||||||
|
role.token_bound_cidrs.length > 0
|
||||||
|
) {
|
||||||
|
setValue(
|
||||||
|
"accessTokenTrustedIps",
|
||||||
|
role.token_bound_cidrs.map((cidr) => ({ ipAddress: cidr })),
|
||||||
|
{
|
||||||
|
shouldDirty: true,
|
||||||
|
shouldTouch: true
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
createNotification({
|
||||||
|
type: "success",
|
||||||
|
text: `Successfully imported Kubernetes auth configuration from Vault role: ${role.name}`
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Import error:", err);
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Failed to import Kubernetes auth configuration"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const onFormSubmit = async ({
|
const onFormSubmit = async ({
|
||||||
kubernetesHost,
|
kubernetesHost,
|
||||||
tokenReviewerJwt,
|
tokenReviewerJwt,
|
||||||
@@ -301,6 +413,28 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
<Tab value={IdentityFormTab.Advanced}>Advanced</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanel value={IdentityFormTab.Configuration}>
|
<TabPanel value={IdentityFormTab.Configuration}>
|
||||||
|
{hasVaultConnection && !isUpdate && (
|
||||||
|
<div className="bg-primary/10 border-primary/30 mb-4 flex items-center justify-between rounded-md border p-3">
|
||||||
|
<div className="flex items-start gap-2 text-sm">
|
||||||
|
<FontAwesomeIcon icon={faInfoCircle} className="text-primary mt-0.5" />
|
||||||
|
<span className="text-mineshaft-200">Load values from HashiCorp Vault</span>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="outline_bg"
|
||||||
|
size="xs"
|
||||||
|
leftIcon={
|
||||||
|
<img
|
||||||
|
src="/images/integrations/Vault.png"
|
||||||
|
alt="HashiCorp Vault"
|
||||||
|
className="h-4 w-4"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
onClick={() => handleImportPopUpToggle("importFromVault", true)}
|
||||||
|
>
|
||||||
|
Load from Vault
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="flex w-full items-center gap-2">
|
<div className="flex w-full items-center gap-2">
|
||||||
<div className="w-full flex-1">
|
<div className="w-full flex-1">
|
||||||
<OrgPermissionCan
|
<OrgPermissionCan
|
||||||
@@ -343,7 +477,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="w-full border border-mineshaft-500"
|
className="border-mineshaft-500 w-full border"
|
||||||
dropdownContainerClassName="max-w-none"
|
dropdownContainerClassName="max-w-none"
|
||||||
isLoading={isGatewayLoading}
|
isLoading={isGatewayLoading}
|
||||||
placeholder="Default: Internet Gateway"
|
placeholder="Default: Internet Gateway"
|
||||||
@@ -407,6 +541,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
placeholder="https://my-example-k8s-api-host.com"
|
placeholder="https://my-example-k8s-api-host.com"
|
||||||
type="text"
|
type="text"
|
||||||
value={field.value || ""}
|
value={field.value || ""}
|
||||||
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
@@ -425,7 +560,7 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
|
tooltipText="Optional JWT token for accessing Kubernetes TokenReview API. If provided, this long-lived token will be used to validate service account tokens during authentication. If omitted, the client's own JWT will be used instead, which requires the client to have the system:auth-delegator ClusterRole binding."
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="" type="password" />
|
<Input {...field} placeholder="" type="password" autoComplete="new-password" />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -441,7 +576,12 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
|
tooltipText="A comma-separated list of trusted namespaces that service accounts must belong to authenticate with Infisical."
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="namespaceA, namespaceB" type="text" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="namespaceA, namespaceB"
|
||||||
|
type="text"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -456,7 +596,11 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
tooltipText="An optional comma-separated list of trusted service account names that are allowed to authenticate with Infisical. Leave empty to allow any service account."
|
||||||
errorText={error?.message}
|
errorText={error?.message}
|
||||||
>
|
>
|
||||||
<Input {...field} placeholder="service-account-1-name, service-account-1-name" />
|
<Input
|
||||||
|
{...field}
|
||||||
|
placeholder="service-account-1-name, service-account-1-name"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -628,6 +772,11 @@ export const IdentityKubernetesAuthForm = ({
|
|||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
<VaultKubernetesAuthImportModal
|
||||||
|
isOpen={popUp.importFromVault.isOpen}
|
||||||
|
onOpenChange={(isOpen) => handleImportPopUpToggle("importFromVault", isOpen)}
|
||||||
|
onImport={handleImportFromVault}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
+143
@@ -0,0 +1,143 @@
|
|||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { createNotification } from "@app/components/notifications";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
FilterableSelect,
|
||||||
|
FormControl,
|
||||||
|
Modal,
|
||||||
|
ModalClose,
|
||||||
|
ModalContent
|
||||||
|
} from "@app/components/v2";
|
||||||
|
import {
|
||||||
|
useGetVaultKubernetesAuthRoles,
|
||||||
|
useGetVaultNamespaces
|
||||||
|
} from "@app/hooks/api/migration/queries";
|
||||||
|
import { VaultKubernetesAuthRole } from "@app/hooks/api/migration/types";
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
isOpen: boolean;
|
||||||
|
onOpenChange: (isOpen: boolean) => void;
|
||||||
|
onImport: (role: VaultKubernetesAuthRole) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ContentProps = {
|
||||||
|
onClose: () => void;
|
||||||
|
onImport: (role: VaultKubernetesAuthRole) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Content = ({ onClose, onImport }: ContentProps) => {
|
||||||
|
const [selectedNamespace, setSelectedNamespace] = useState<string>("default");
|
||||||
|
const [selectedRole, setSelectedRole] = useState<VaultKubernetesAuthRole | null>(null);
|
||||||
|
const [shouldFetchRoles, setShouldFetchRoles] = useState(false);
|
||||||
|
|
||||||
|
const { data: namespaces, isLoading: isLoadingNamespaces } = useGetVaultNamespaces();
|
||||||
|
const {
|
||||||
|
data: roles,
|
||||||
|
isLoading: isLoadingRoles,
|
||||||
|
refetch: refetchRoles
|
||||||
|
} = useGetVaultKubernetesAuthRoles(shouldFetchRoles, selectedNamespace);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedNamespace) {
|
||||||
|
setShouldFetchRoles(true);
|
||||||
|
}
|
||||||
|
}, [selectedNamespace]);
|
||||||
|
|
||||||
|
const handleImportAndApply = () => {
|
||||||
|
if (!selectedRole) {
|
||||||
|
createNotification({
|
||||||
|
type: "error",
|
||||||
|
text: "Please select a Kubernetes role to load"
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
onImport(selectedRole);
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<FormControl
|
||||||
|
label="Namespace"
|
||||||
|
className="mb-4"
|
||||||
|
tooltipText="Select the Vault namespace containing the Kubernetes auth configuration."
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<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);
|
||||||
|
setSelectedRole(null);
|
||||||
|
// Refetch roles when namespace changes
|
||||||
|
refetchRoles();
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
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 Kubernetes auth roles
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormControl label="Kubernetes Role" className="mb-6">
|
||||||
|
<>
|
||||||
|
<FilterableSelect
|
||||||
|
value={selectedRole}
|
||||||
|
onChange={(value) => {
|
||||||
|
if (value && !Array.isArray(value)) {
|
||||||
|
setSelectedRole(value as VaultKubernetesAuthRole);
|
||||||
|
} else {
|
||||||
|
setSelectedRole(null);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
options={roles || []}
|
||||||
|
getOptionValue={(option) => option.name}
|
||||||
|
getOptionLabel={(option) => `${option.name} (${option.mountPath})`}
|
||||||
|
isDisabled={isLoadingRoles || !roles?.length}
|
||||||
|
placeholder="Select a Kubernetes role to load..."
|
||||||
|
isClearable
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
<p className="text-mineshaft-400 mt-1 text-xs">
|
||||||
|
Select the Kubernetes role to load configuration from
|
||||||
|
</p>
|
||||||
|
</>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<div className="mt-8 flex space-x-4">
|
||||||
|
<Button onClick={handleImportAndApply} isDisabled={!selectedRole || isLoadingRoles}>
|
||||||
|
Load
|
||||||
|
</Button>
|
||||||
|
<ModalClose asChild>
|
||||||
|
<Button colorSchema="secondary" variant="plain">
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</ModalClose>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VaultKubernetesAuthImportModal = ({ isOpen, onOpenChange, onImport }: Props) => {
|
||||||
|
return (
|
||||||
|
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
|
||||||
|
<ModalContent
|
||||||
|
title="Load Kubernetes Auth from HashiCorp Vault"
|
||||||
|
subTitle="Load Kubernetes authentication configuration from your Vault instance. The auth method and role settings will be automatically translated and prefilled in the form."
|
||||||
|
className="max-w-2xl"
|
||||||
|
>
|
||||||
|
<Content onClose={() => onOpenChange(false)} onImport={onImport} />
|
||||||
|
</ModalContent>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user