misc: filter optimizations

This commit is contained in:
Sheen Capadngan
2025-10-16 05:08:48 +08:00
parent efb8616c63
commit a95cb63682
8 changed files with 300 additions and 47 deletions
@@ -332,6 +332,35 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
}
});
server.route({
method: "GET",
url: "/vault/auth-mounts",
config: {
rateLimit: readLimit
},
schema: {
querystring: z.object({
namespace: z.string(),
authType: z.string().optional()
}),
response: {
200: z.object({
mounts: z.array(z.object({ path: z.string(), type: z.string() }))
})
}
},
onRequest: verifyAuth([AuthMode.JWT]),
handler: async (req) => {
const mounts = await server.services.migration.getVaultAuthMounts({
actor: req.permission,
namespace: req.query.namespace,
authType: req.query.authType
});
return { mounts };
}
});
server.route({
method: "POST",
url: "/vault/import-secrets",
@@ -372,7 +401,8 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
},
schema: {
querystring: z.object({
namespace: z.string()
namespace: z.string(),
mountPath: z.string()
}),
response: {
200: z.object({
@@ -384,7 +414,8 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
handler: async (req) => {
const secretPaths = await server.services.migration.getVaultSecretPaths({
actor: req.permission,
namespace: req.query.namespace
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { secretPaths };
@@ -399,7 +430,8 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
},
schema: {
querystring: z.object({
namespace: z.string()
namespace: z.string(),
mountPath: z.string()
}),
response: {
@@ -437,7 +469,8 @@ export const registerExternalMigrationRouter = async (server: FastifyZodProvider
handler: async (req) => {
const roles = await server.services.migration.getVaultKubernetesAuthRoles({
actor: req.permission,
namespace: req.query.namespace
namespace: req.query.namespace,
mountPath: req.query.mountPath
});
return { roles };
@@ -455,7 +455,8 @@ export const listHCVaultMounts = async (
export const listHCVaultSecretPaths = async (
namespace: string,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">,
filterMountPath?: string
) => {
const instanceUrl = await getHCVaultInstanceUrl(connection);
const accessToken = await getHCVaultAccessToken(connection, gatewayService);
@@ -532,7 +533,13 @@ export const listHCVaultSecretPaths = async (
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"));
let kvMounts = mounts.filter((mount) => mount.type === "kv" || mount.type.startsWith("kv"));
// If filterMountPath is provided, filter to only that mount
if (filterMountPath) {
const normalizedFilterPath = filterMountPath.replace(/\/$/, ""); // Remove trailing slash
kvMounts = kvMounts.filter((mount) => mount.path.replace(/\/$/, "") === normalizedFilterPath);
}
// Create concurrency limiter to avoid overwhelming the Vault instance
const limiter = createConcurrencyLimiter(HC_VAULT_CONCURRENCY_LIMIT);
@@ -648,7 +655,7 @@ export const getHCVaultSecretsForPath = async (
export const getHCVaultAuthMounts = async (
namespace: string,
authType: HCVaultAuthType,
authType: HCVaultAuthType | undefined,
connection: THCVaultConnection,
gatewayService: Pick<TGatewayServiceFactory, "fnGetGatewayClientTlsByGatewayId">
): Promise<THCVaultAuthMount[]> => {
@@ -668,7 +675,8 @@ export const getHCVaultAuthMounts = async (
const authMounts: THCVaultAuthMount[] = [];
Object.entries(data.data).forEach(([path, authMethod]) => {
if (authMethod.type === authType) {
// If authType is specified, filter by it; otherwise, include all
if (!authType || authMethod.type === authType) {
authMounts.push({
path,
type: authMethod.type,
@@ -680,16 +688,17 @@ export const getHCVaultAuthMounts = async (
return authMounts;
} catch (error: unknown) {
logger.error(error, `Unable to list HC Vault ${authType} auth mounts`);
const authTypeStr = authType || "all";
logger.error(error, `Unable to list HC Vault ${authTypeStr} auth mounts`);
if (error instanceof AxiosError) {
throw new BadRequestError({
message: `Failed to list ${authType} auth mounts: ${error.message || "Unknown error"}`
message: `Failed to list ${authTypeStr} auth mounts: ${error.message || "Unknown error"}`
});
}
throw new BadRequestError({
message: `Unable to list ${authType} auth mounts from HashiCorp Vault`
message: `Unable to list ${authTypeStr} auth mounts from HashiCorp Vault`
});
}
};
@@ -444,7 +444,15 @@ export const externalMigrationServiceFactory = ({
return mounts;
};
const getVaultSecretPaths = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
const getVaultSecretPaths = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
@@ -482,7 +490,7 @@ export const externalMigrationServiceFactory = ({
credentials
} as THCVaultConnection;
const secretPaths = await listHCVaultSecretPaths(namespace, connection, gatewayService);
const secretPaths = await listHCVaultSecretPaths(namespace, connection, gatewayService, mountPath);
return secretPaths;
};
@@ -617,7 +625,66 @@ export const externalMigrationServiceFactory = ({
return deletedConfig;
};
const getVaultKubernetesAuthRoles = async ({ actor, namespace }: { actor: OrgServiceActor; namespace: string }) => {
const getVaultAuthMounts = async ({
actor,
namespace,
authType
}: {
actor: OrgServiceActor;
namespace: string;
authType?: 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 auth mounts" });
}
const vaultConfig = await vaultExternalMigrationConfigDAL.findOne({
orgId: actor.orgId,
namespace
});
if (!vaultConfig) {
throw new NotFoundError({ message: "Vault migration config not found for this namespace" });
}
if (!vaultConfig.connection) {
throw new BadRequestError({ message: "Vault migration connection is not configured for this namespace" });
}
const credentials = await decryptAppConnectionCredentials({
orgId: vaultConfig.orgId,
encryptedCredentials: vaultConfig.connection.encryptedCredentials,
kmsService,
projectId: null
});
const connection = {
...vaultConfig.connection,
credentials
} as THCVaultConnection;
const authMounts = await getHCVaultAuthMounts(namespace, authType as HCVaultAuthType, connection, gatewayService);
return authMounts;
};
const getVaultKubernetesAuthRoles = async ({
actor,
namespace,
mountPath
}: {
actor: OrgServiceActor;
namespace: string;
mountPath: string;
}) => {
const { hasRole } = await permissionService.getOrgPermission(
actor.type,
actor.id,
@@ -655,18 +722,10 @@ export const externalMigrationServiceFactory = ({
credentials
} as THCVaultConnection;
// Get all Kubernetes auth mounts for this namespace
const authMounts = await getHCVaultAuthMounts(namespace, HCVaultAuthType.Kubernetes, connection, gatewayService);
// Get roles for the specified mount path only
const roles = await getHCVaultKubernetesAuthRoles(namespace, mountPath, 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 roles;
};
return {
@@ -680,6 +739,7 @@ export const externalMigrationServiceFactory = ({
getVaultNamespaces,
getVaultPolicies,
getVaultMounts,
getVaultAuthMounts,
getVaultSecretPaths,
importVaultSecrets,
getVaultKubernetesAuthRoles