UNPKG

@jfvilas/plugin-kubelog-backend

Version:
488 lines (477 loc) 19.7 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var express = require('express'); var Router = require('express-promise-router'); var catalogClient = require('@backstage/catalog-client'); var kwirthCommon = require('@jfvilas/kwirth-common'); var backendPluginApi = require('@backstage/backend-plugin-api'); function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; } var express__default = /*#__PURE__*/_interopDefaultCompat(express); var Router__default = /*#__PURE__*/_interopDefaultCompat(Router); const VERSION = "0.11.1"; const MIN_KWIRTH_VERSION = "0.3.130"; class KubelogStaticData { static clusterKubelogData = /* @__PURE__ */ new Map(); } const loadNamespacePermissions = (logger, cluster, kdata) => { if (cluster.has("kubelogNamespacePermissions")) { logger.info(`Namespace permisson evaluation will be performed for cluster ${cluster.getString("name")}.`); var permNamespaces = cluster.getOptionalConfigArray("kubelogNamespacePermissions"); for (var ns of permNamespaces) { var namespace = ns.keys()[0]; var identityRefs = ns.getStringArray(namespace); identityRefs = identityRefs.map((g) => g.toLowerCase()); kdata.namespacePermissions.push({ namespace, identityRefs }); } } else { logger.info(`Cluster ${cluster.getString("name")} will have no namespace restrictions.`); kdata.namespacePermissions = []; } }; const loadPodRules = (config, id) => { var rules = []; for (var rule of config.getConfigArray(id)) { var podsStringArray = rule.getOptionalStringArray("pods") || [".*"]; var podsRegexArray = []; for (var expr of podsStringArray) { podsRegexArray.push(new RegExp(expr)); } var refsStringArray = rule.getOptionalStringArray("refs") || [".*"]; var refsRegexArray = []; for (var expr of refsStringArray) { refsRegexArray.push(new RegExp(expr)); } var prr = { pods: podsRegexArray, refs: refsRegexArray }; rules.push(prr); } return rules; }; const loadPodPermissions = (configKey, logger, cluster) => { var clusterPodPermissions = []; if (cluster.has(configKey)) { var namespaceList = cluster.getConfigArray(configKey); for (var ns of namespaceList) { var namespaceName = ns.keys()[0]; var podPermissions = { namespace: namespaceName }; if (ns.getConfig(namespaceName).has("allow")) { podPermissions.allow = loadPodRules(ns.getConfig(namespaceName), "allow"); if (ns.getConfig(namespaceName).has("except")) podPermissions.except = loadPodRules(ns.getConfig(namespaceName), "except"); if (ns.getConfig(namespaceName).has("deny")) podPermissions.deny = loadPodRules(ns.getConfig(namespaceName), "deny"); if (ns.getConfig(namespaceName).has("unless")) podPermissions.unless = loadPodRules(ns.getConfig(namespaceName), "unless"); } else { podPermissions.allow = []; podPermissions.allow.push({ pods: [new RegExp(".*")], refs: [new RegExp(".*")] }); } clusterPodPermissions.push(podPermissions); } } else { logger.info(`No pod permissions for ${configKey} will be applied for ${cluster.getString("name")} (everyone will be allowed).`); } return clusterPodPermissions; }; const loadClusters = async (logger, config) => { KubelogStaticData.clusterKubelogData.clear(); var locatingMethods = config.getConfigArray("kubernetes.clusterLocatorMethods"); for (var method of locatingMethods) { var clusters = method.getConfigArray("clusters"); for (var cluster of clusters) { var name = cluster.getString("name"); if (cluster.has("kubelogKwirthHome") && cluster.has("kubelogKwirthApiKey")) { var home = cluster.getOptionalString("kwirthHome") || cluster.getOptionalString("kubelogKwirthHome"); var apiKeyStr = cluster.getOptionalString("kwirthApiKey") || cluster.getOptionalString("kubelogKwirthApiKey"); var title = cluster.has("title") ? cluster.getString("title") : "No name"; var kubelogClusterData = { name, kwirthHome: home, kwirthApiKeyStr: apiKeyStr, kwirthData: { version: "", clusterName: "", inCluster: false, namespace: "", deployment: "", lastVersion: "" }, title, namespacePermissions: [], viewPermissions: [], restartPermissions: [], enabled: false }; logger.info(`Kwirth for ${name} is located at ${kubelogClusterData.kwirthHome}. Testing connection...`); let enableCluster = false; try { var response = await fetch(kubelogClusterData.kwirthHome + "/config/version"); try { var data = await response.text(); try { var kwirthData = JSON.parse(data); logger.info(`Kwirth info at cluster '${kubelogClusterData.name}': ${JSON.stringify(kwirthData)}`); kubelogClusterData.kwirthData = kwirthData; if (kwirthCommon.versionGreatOrEqualThan(kwirthData.version, MIN_KWIRTH_VERSION)) { enableCluster = true; } else { logger.error(`Unsupported Kwirth version on cluster '${name}' (${title}) [${kwirthData.version}]. Min version is ${MIN_KWIRTH_VERSION}`); } } catch (err) { logger.error(`Kwirth at cluster ${kubelogClusterData.name} returned errors: ${err}`); logger.info("Returned data is:"); logger.info(data); kubelogClusterData.kwirthData = { version: "0.0.0", clusterName: "unknown", inCluster: false, namespace: "unknown", deployment: "unknown", lastVersion: "0.0.0" }; } } catch (err) { logger.warn(`Error parsing version response from cluster '${kubelogClusterData.name}': ${err}`); } } catch (err) { logger.info(`Kwirth access error: ${err}.`); logger.warn(`Kwirth home URL (${kubelogClusterData.kwirthHome}) at cluster '${kubelogClusterData.name}' cannot be accessed right now.`); } if (enableCluster) { loadNamespacePermissions(logger, cluster, kubelogClusterData); kubelogClusterData.viewPermissions = loadPodPermissions("kubelogPodViewPermissions", logger, cluster); kubelogClusterData.restartPermissions = loadPodPermissions("kubelogPodRestartPermissions", logger, cluster); KubelogStaticData.clusterKubelogData.set(name, kubelogClusterData); } } else { logger.warn(`Cluster ${name} has no Kwirth information (kubelogHome and kubelogApiKey are missing). It will not be used for Kubelog log viewing.`); } } } console.log(KubelogStaticData.clusterKubelogData); }; var KWIRTH_SCOPE = /* @__PURE__ */ ((KWIRTH_SCOPE2) => { KWIRTH_SCOPE2[KWIRTH_SCOPE2["filter"] = 1] = "filter"; KWIRTH_SCOPE2[KWIRTH_SCOPE2["view"] = 2] = "view"; KWIRTH_SCOPE2[KWIRTH_SCOPE2["restart"] = 3] = "restart"; KWIRTH_SCOPE2[KWIRTH_SCOPE2["api"] = 4] = "api"; KWIRTH_SCOPE2[KWIRTH_SCOPE2["cluster"] = 5] = "cluster"; return KWIRTH_SCOPE2; })(KWIRTH_SCOPE || {}); const debug$1 = (a) => { if (process.env.KUBELOGDEBUG) console.log(a); }; const checkNamespaceAccess = (cluster, podData, userEntityRef, userGroups) => { var namespacePermissions = KubelogStaticData.clusterKubelogData.get(cluster.name)?.namespacePermissions; var allowedToNamespace = false; var rule = namespacePermissions?.find((ns) => ns.namespace === podData.namespace); if (rule) { debug$1("CNA rule found " + rule.namespace); if (rule.identityRefs.includes(userEntityRef.toLowerCase())) { allowedToNamespace = true; } else { var groupResult = rule.identityRefs.some((identityRef) => userGroups.includes(identityRef)); if (groupResult) { allowedToNamespace = true; } } } else { debug$1("CNA rule NOT found for " + podData.namespace); allowedToNamespace = true; } return allowedToNamespace; }; const checkPodPermissionRule = (ppr, entityName, userEntityRef, userGroups) => { var refMatch = false; for (var podNameRegex of ppr.pods) { if (podNameRegex.test(entityName)) { for (var refRegex of ppr.refs) { refMatch = refRegex.test(userEntityRef.toLowerCase()); if (refMatch) { break; } else { refMatch = userGroups.some((g) => refRegex.test(g)); if (refMatch) { break; } } } } if (refMatch) break; } return refMatch; }; const getPodPermissionSet = (reqScope, cluster) => { switch (reqScope) { case 2 /* view */: return cluster.viewPermissions; case 3 /* restart */: return cluster.restartPermissions; } return void 0; }; const checkPodAccess = (reqPod, podPermissionSet, entityName, userEntityRef, userGroups) => { for (var podPermission of podPermissionSet.filter((pp) => pp.namespace === reqPod.namespace)) { if (podPermission.allow) { var allowMatches = false; var exceptMatches = false; for (var prr of podPermission.allow) { allowMatches = checkPodPermissionRule(prr, entityName, userEntityRef, userGroups); } if (allowMatches) { if (podPermission.except) { for (var prr of podPermission.except) { exceptMatches = checkPodPermissionRule(prr, entityName, userEntityRef, userGroups); if (exceptMatches) { break; } } } } if (allowMatches && !exceptMatches) { if (podPermission.deny) { var denyMatches = false; var unlessMatches = false; for (var prr of podPermission.deny) { denyMatches = checkPodPermissionRule(prr, entityName, userEntityRef, userGroups); if (denyMatches) { break; } } if (denyMatches && podPermission.unless) { for (var prr of podPermission.unless) { unlessMatches = checkPodPermissionRule(prr, entityName, userEntityRef, userGroups); if (unlessMatches) { break; } } } if (!denyMatches || denyMatches && unlessMatches) { return true; } } else { return true; } } } else { return true; } } return false; }; const debug = (a) => { if (process.env.KUBELOGDEBUG) console.log(a); }; async function createRouter(options) { const { configSvc, loggerSvc, userInfoSvc, authSvc, httpAuthSvc, discoverySvc } = options; loggerSvc.info("Loading static config"); if (!configSvc.has("kubernetes.clusterLocatorMethods")) { loggerSvc.error(`Kueblog will not start, there is no 'clusterLocatorMethods' defined in app-config.`); throw new Error("Kueblog backend will not be available."); } try { loadClusters(loggerSvc, configSvc); } catch (err) { var txt = `Errors detected reading static configuration: ${err}`; loggerSvc.error(txt); throw new Error(txt); } loggerSvc.info("Static config loaded"); if (configSvc.subscribe) { configSvc.subscribe(() => { try { loggerSvc.warn("Change detected on app-config, Kubelog will update config."); loadClusters(loggerSvc, configSvc); } catch (err) { loggerSvc.error(`Errors detected reading new configuration: ${err}`); } }); } else { loggerSvc.info("Kubelog cannot subscribe to config changes."); } const router = Router__default.default(); router.use(express__default.default.json()); const createAuthFetchApi = (token) => { return { fetch: async (input, init) => { init = init || {}; init.headers = { ...init.headers, Authorization: `Bearer ${token}` }; return fetch(input, init); } }; }; const getValidClusters = async (entityName) => { var clusterList = []; for (const name of KubelogStaticData.clusterKubelogData.keys()) { var url = KubelogStaticData.clusterKubelogData.get(name)?.kwirthHome; var apiKeyStr = KubelogStaticData.clusterKubelogData.get(name)?.kwirthApiKeyStr; var title = KubelogStaticData.clusterKubelogData.get(name)?.title; var queryUrl = url + `/managecluster/find?label=backstage.io%2fkubernetes-id&entity=${entityName}&type=pod&data=id`; try { var fetchResp = await fetch(queryUrl, { headers: { "Authorization": "Bearer " + apiKeyStr } }); if (fetchResp.status === 200) { var jsonResp = await fetchResp.json(); if (jsonResp) clusterList.push({ name, url, title, data: jsonResp }); } else { loggerSvc.warn(`Invalid response from cluster ${name}: ${fetchResp.status}`); clusterList.push({ name, url, title, data: [] }); } } catch (err) { loggerSvc.warn(`Cannot access cluster ${name} (URL: ${queryUrl}): ${err}`); clusterList.push({ name, url, title, data: [] }); } } return clusterList; }; const setAccessKey = async (reqScope, cluster, reqPod, userName, keyName) => { var kwirthResource = `${KWIRTH_SCOPE[reqScope]}:${reqPod.namespace}::${reqPod.name}:`; var url = KubelogStaticData.clusterKubelogData.get(cluster.name)?.kwirthHome; var apiKeyStr = KubelogStaticData.clusterKubelogData.get(cluster.name)?.kwirthApiKeyStr; var payload = { type: "volatile", resource: kwirthResource, description: `Backstage API key for user ${userName} accessing pod ${reqPod.namespace}/${reqPod.name}`, expire: Date.now() + 60 * 60 * 1e3 }; var fetchResp = await fetch(url + "/key", { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", Authorization: "Bearer " + apiKeyStr } }); if (fetchResp.status === 200) { var data = await fetchResp.json(); reqPod[keyName] = data.accessKey; } else { loggerSvc.warn(`Invalid response obtaining key from cluster ${cluster.name}: ${fetchResp.status}`); } }; const addAccessKeys = async (reqScopeStr, foundClusters, entityName, userEntityRef, userGroups, keyName) => { var reqScope = KWIRTH_SCOPE[reqScopeStr]; if (!reqScope) { loggerSvc.info(`Invalid scope requested: ${reqScopeStr}`); return; } var principal = userEntityRef.split(":")[1]; var username = principal.split("/")[1]; debug("addAccesssKeys"); debug("reqScope " + reqScopeStr); for (var foundCluster of foundClusters) { debug("foundCluster " + foundCluster.name); debug("podDataLength " + foundCluster.data.length); for (var podData of foundCluster.data) { debug(">>> checkNamespaceAccess"); var allowedToNamespace = checkNamespaceAccess(foundCluster, podData, userEntityRef, userGroups); debug("<<< checkNamespaceAccess"); debug("allowedToNamespace: " + allowedToNamespace); if (allowedToNamespace) { var clusterDef = KubelogStaticData.clusterKubelogData.get(foundCluster.name); var podPermissionSet = getPodPermissionSet(reqScope, clusterDef); if (!podPermissionSet) { loggerSvc.warn(`Pod permission set not found: ${reqScope}`); continue; } var namespaceRestricted = podPermissionSet.some((pp) => pp.namespace === podData.namespace); if (!namespaceRestricted) { await setAccessKey(reqScope, foundCluster, podData, username, keyName); } else { var allowedToPod = checkPodAccess(podData, podPermissionSet, entityName, userEntityRef, userGroups); if (allowedToPod) { await setAccessKey(reqScope, foundCluster, podData, username, keyName); } } } } } }; const getUserGroups = async (userInfo) => { const { token } = await authSvc.getPluginRequestToken({ onBehalfOf: await authSvc.getOwnServiceCredentials(), targetPluginId: "catalog" }); const catalogClient$1 = new catalogClient.CatalogClient({ discoveryApi: discoverySvc, fetchApi: createAuthFetchApi(token) }); const entity = await catalogClient$1.getEntityByRef(userInfo.userEntityRef); var userGroupsRefs = []; if (entity?.spec.memberOf) userGroupsRefs = entity?.spec.memberOf; return userGroupsRefs; }; const processStart = async (req, res) => { const credentials = await httpAuthSvc.credentials(req, { allow: ["user"] }); const userInfo = await userInfoSvc.getUserInfo(credentials); var userGroupsRefs = await getUserGroups(userInfo); var foundClusters = await getValidClusters(req.body.metadata.name); await addAccessKeys("view", foundClusters, req.body.metadata.name, userInfo.userEntityRef, userGroupsRefs, "accessKey"); res.status(200).send(foundClusters); }; const processVersion = async (_req, res) => { res.status(200).send({ version: VERSION }); }; const processAccess = async (req, res) => { if (!req.query["scopes"]) { res.status(400).send(); return; } var reqScopes = req.query["scopes"].toString().split(","); const credentials = await httpAuthSvc.credentials(req, { allow: ["user"] }); const userInfo = await userInfoSvc.getUserInfo(credentials); var userGroupsRefs = await getUserGroups(userInfo); loggerSvc.info(`Checking reqScopes '${req.query["scopes"]}' scopes to pod: '${req.body.metadata.namespace + "/" + req.body.metadata.name}' for user '${userInfo.userEntityRef}'`); var foundClusters = await getValidClusters(req.body.metadata.name); debug("foundClusters"); debug(foundClusters); for (var reqScopeStr of reqScopes) { debug(""); debug(""); debug(""); debug("******************************"); debug("******** SCOPE " + reqScopeStr); debug("******************************"); await addAccessKeys(reqScopeStr, foundClusters, req.body.metadata.name, userInfo.userEntityRef, userGroupsRefs, reqScopeStr + "AccessKey"); } res.status(200).send(foundClusters); }; router.post(["/start"], (req, res) => { processStart(req, res); }); router.post(["/access"], (req, res) => { processAccess(req, res); }); router.get(["/version"], (req, res) => { processVersion(req, res); }); return router; } const kubelogPlugin = backendPluginApi.createBackendPlugin({ pluginId: "kubelog", register(env) { env.registerInit({ deps: { discovery: backendPluginApi.coreServices.discovery, config: backendPluginApi.coreServices.rootConfig, logger: backendPluginApi.coreServices.logger, auth: backendPluginApi.coreServices.auth, httpAuth: backendPluginApi.coreServices.httpAuth, httpRouter: backendPluginApi.coreServices.httpRouter, userInfo: backendPluginApi.coreServices.userInfo }, async init({ discovery, config, httpRouter, logger, auth, httpAuth, userInfo }) { httpRouter.use( await createRouter({ discoverySvc: discovery, configSvc: config, loggerSvc: logger, authSvc: auth, httpAuthSvc: httpAuth, userInfoSvc: userInfo }) ); } }); } }); exports.createRouter = createRouter; exports.default = kubelogPlugin; //# sourceMappingURL=index.cjs.js.map