UNPKG

@jfvilas/plugin-kwirth-backend

Version:
499 lines (488 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.0.1"; const MIN_KWIRTH_VERSION = "0.3.128"; class KwirthStaticData { static clusterKwirthData = /* @__PURE__ */ new Map(); } const loadNamespacePermissions = (block, logger) => { var namespacePermissions = []; if (block.has("namespacePermissions")) { logger.info(` Namespace permisson evaluation will be performed.`); var permNamespaces = block.getOptionalConfigArray("namespacePermissions"); for (var ns of permNamespaces) { var namespace = ns.keys()[0]; var identityRefs = ns.getStringArray(namespace); identityRefs = identityRefs.map((g) => g.toLowerCase()); namespacePermissions.push({ namespace, identityRefs }); } } else { logger.info(` No namespace restrictions.`); namespacePermissions = []; } return namespacePermissions; }; const loadPodRules = (config, category) => { var rules = []; for (var rule of config.getConfigArray(category)) { 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 = (block, logger) => { var clusterPodPermissions = []; if (block.has("podPermissions")) { var namespaceList = block.getConfigArray("podPermissions"); 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 will be applied.`); } return clusterPodPermissions; }; const addChannelPermissions = (channel, logger, cluster, kdata) => { var keyName = "kwirth" + channel; if (cluster.has(keyName)) { logger.info(`Load permissions for block ${channel}.`); var configBlock = cluster.getConfig(keyName); if (configBlock.has("namespacePermissions")) { logger.info(` Loading namespace permissions.`); kdata.namespacePermissions.set(channel, loadNamespacePermissions(configBlock, logger)); } else { logger.info(` No namespace permissions.`); kdata.namespacePermissions.set(channel, []); } if (configBlock.has("podPermissions")) { logger.info(` Loading pod permissions.`); kdata.podPermissions.set(channel, loadPodPermissions(configBlock, logger)); } else { logger.info(` No pod permissions.`); kdata.podPermissions.set(channel, []); } } else { logger.info(`Cluster ${cluster.getString("name")} will have no channel '${channel}' restrictions.`); kdata.namespacePermissions.set(channel, []); kdata.podPermissions.set(channel, []); } }; const loadClusters = async (logger, config) => { KwirthStaticData.clusterKwirthData.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("kwirthHome") && cluster.has("kwirthApiKey")) { var kwirthHome = cluster.getOptionalString("kwirthHome"); var kwirthApiKey = cluster.getOptionalString("kwirthApiKey"); var title = cluster.has("title") ? cluster.getString("title") : "No name"; var kwirthClusterData = { name, kwirthHome, kwirthApiKey, kwirthData: { version: "", clusterName: "", inCluster: false, namespace: "", deployment: "", lastVersion: "" }, title, namespacePermissions: /* @__PURE__ */ new Map(), podPermissions: /* @__PURE__ */ new Map(), enabled: false }; logger.info(`Kwirth for ${name} is located at ${kwirthClusterData.kwirthHome}. Testing connection...`); let enableCluster = false; try { var response = await fetch(kwirthClusterData.kwirthHome + "/config/version"); try { var data = await response.text(); try { var kwirthData = JSON.parse(data); logger.info(`Kwirth info at cluster '${kwirthClusterData.name}': ${JSON.stringify(kwirthData)}`); kwirthClusterData.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 ${kwirthClusterData.name} returned errors: ${err}`); logger.info("Returned data is:"); logger.info(data); kwirthClusterData.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 '${kwirthClusterData.name}': ${err}`); } } catch (err) { logger.info(`Kwirth access error: ${err}.`); logger.warn(`Kwirth home URL (${kwirthClusterData.kwirthHome}) at cluster '${kwirthClusterData.name}' cannot be accessed right now.`); } if (enableCluster) { addChannelPermissions("log", logger, cluster, kwirthClusterData); addChannelPermissions("alert", logger, cluster, kwirthClusterData); addChannelPermissions("metrics", logger, cluster, kwirthClusterData); KwirthStaticData.clusterKwirthData.set(name, kwirthClusterData); } else { logger.warn(`Cluster ${name} will be disabled`); } } else { logger.warn(`Cluster ${name} has no Kwirth information (kwirthHome and kwirthApiKey are missing).`); } } } logger.info("Kwirth static data has been set including following clusters:"); for (var c of KwirthStaticData.clusterKwirthData.keys()) { logger.info(" " + c); } for (var c of KwirthStaticData.clusterKwirthData.keys()) { console.log(KwirthStaticData.clusterKwirthData.get(c)); } }; const checkNamespaceAccess = (channel, cluster, podData, userEntityRef, userGroups) => { let allowedToNamespace = false; let namespacePermissions = KwirthStaticData.clusterKwirthData.get(cluster.name)?.namespacePermissions; if (namespacePermissions?.has(channel)) { let rule = namespacePermissions?.get(channel).find((ns) => ns.namespace === podData.namespace); if (rule) { if (rule.identityRefs.includes(userEntityRef.toLowerCase())) { allowedToNamespace = true; } else { var groupResult = rule.identityRefs.some((identityRef) => userGroups.includes(identityRef)); if (groupResult) { allowedToNamespace = true; } } } else { allowedToNamespace = true; } } else { console.log(`Invalid channel: ${channel}`); } 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 = (channel, cluster) => { if (cluster.podPermissions.has(channel)) { return cluster.podPermissions.get(channel); } else { console.log(`Invalid channel ${channel} for permission set`); 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; }; async function createRouter(options) { const { configSvc, loggerSvc, userInfoSvc, authSvc, httpAuthSvc, discoverySvc } = options; loggerSvc.info("Loading static config"); if (!configSvc.has("kubernetes.clusterLocatorMethods")) { loggerSvc.error(`Kwirth will not start, there is no 'clusterLocatorMethods' defined in app-config.`); throw new Error("Kwirth 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); } if (configSvc.subscribe) { configSvc.subscribe(() => { try { loggerSvc.warn("Change detected on app-config, Kwirth will update config."); loadClusters(loggerSvc, configSvc); } catch (err) { loggerSvc.error(`Errors detected reading new configuration: ${err}`); } }); } else { loggerSvc.info("Kwirth 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 clusterName of KwirthStaticData.clusterKwirthData.keys()) { var url = KwirthStaticData.clusterKwirthData.get(clusterName)?.kwirthHome; var apiKeyStr = KwirthStaticData.clusterKwirthData.get(clusterName)?.kwirthApiKey; var title = KwirthStaticData.clusterKwirthData.get(clusterName)?.title; var queryUrl = url + `/managecluster/find?label=backstage.io%2fkubernetes-id&entity=${entityName}&type=pod&data=containers`; try { var fetchResp = await fetch(queryUrl, { headers: { "Authorization": "Bearer " + apiKeyStr } }); if (fetchResp.status === 200) { var jsonResp = await fetchResp.json(); if (jsonResp) { let podData = { name: clusterName, url, title, data: jsonResp, accessKeys: /* @__PURE__ */ new Map() }; clusterList.push(podData); } } else { loggerSvc.warn(`Invalid response from cluster ${clusterName}: ${fetchResp.status}`); console.log(await fetchResp.text()); clusterList.push({ name: clusterName, url, title, data: [], accessKeys: /* @__PURE__ */ new Map() }); } } catch (err) { loggerSvc.warn(`Cannot access cluster ${clusterName} (URL: ${queryUrl}): ${err}`); clusterList.push({ name: clusterName, url, title, data: [], accessKeys: /* @__PURE__ */ new Map() }); } } return clusterList; }; const createAccessKey = async (reqScope, cluster, reqPods, userName) => { var resources = reqPods.map((podData) => `${reqScope}:${podData.namespace}::${podData.name}:`).join(","); var kwirthHome = KwirthStaticData.clusterKwirthData.get(cluster.name)?.kwirthHome; var kwirthApiKey = KwirthStaticData.clusterKwirthData.get(cluster.name)?.kwirthApiKey; var payload = { type: "bearer", resource: resources, description: `Backstage API key for user ${userName}`, expire: Date.now() + 60 * 60 * 1e3 }; var fetchResp = await fetch(kwirthHome + "/key", { method: "POST", body: JSON.stringify(payload), headers: { "Content-Type": "application/json", Authorization: "Bearer " + kwirthApiKey } }); if (fetchResp.status === 200) { var data = await fetchResp.json(); return data.accessKey; } else { loggerSvc.warn(`Invalid response asking for a key from cluster ${cluster.name}: ${fetchResp.status}`); return {}; } }; const addAccessKeys = async (channel, reqScope, foundClusters, entityName, userEntityRef, userGroups) => { if (!reqScope) { loggerSvc.info(`Invalid scope requested: ${reqScope}`); return; } var principal = userEntityRef.split(":")[1]; var username = principal.split("/")[1]; for (var foundCluster of foundClusters) { var podList = []; for (var podData of foundCluster.data) { var allowedToNamespace = checkNamespaceAccess(channel, foundCluster, podData, userEntityRef, userGroups); if (allowedToNamespace) { var clusterDef = KwirthStaticData.clusterKwirthData.get(foundCluster.name); var podPermissionSet = getPodPermissionSet(channel, clusterDef); if (!podPermissionSet) { loggerSvc.warn(`Pod permission set not found: ${channel}`); continue; } var namespaceRestricted = podPermissionSet.some((pp) => pp.namespace === podData.namespace); if (!namespaceRestricted || checkPodAccess(podData, podPermissionSet, entityName, userEntityRef, userGroups)) { podList.push(podData); } } } if (podList.length > 0) { let accessKey = await createAccessKey(reqScope, foundCluster, podList, username); foundCluster.accessKeys.set(reqScope, accessKey); } else { console.log(`No pods on podList for ${channel} and ${reqScope}`); } } }; 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 processVersion = async (_req, res) => { res.status(200).send({ version: VERSION }); }; const processAccess = async (req, res) => { if (!req.query["scopes"] || !req.query["scopes"]) { res.status(400).send(`'scopes' and 'channel' are required`); return; } let reqScopes = req.query["scopes"].toString().split(","); let reqChannel = req.query["channel"]?.toString(); const credentials = await httpAuthSvc.credentials(req, { allow: ["user"] }); const userInfo = await userInfoSvc.getUserInfo(credentials); let 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}'`); let foundClusters = await getValidClusters(req.body.metadata.name); for (var reqScopeStr of reqScopes) { var reqScope = reqScopeStr; await addAccessKeys(reqChannel, reqScope, foundClusters, req.body.metadata.name, userInfo.userEntityRef, userGroupsRefs); } for (var c of foundClusters) { c.accessKeys = JSON.stringify(Array.from(c.accessKeys.entries())); } res.status(200).send(foundClusters); }; router.post(["/access"], (req, res) => { processAccess(req, res); }); router.get(["/version"], (req, res) => { processVersion(req, res); }); return router; } const kwirthPlugin = backendPluginApi.createBackendPlugin({ pluginId: "kwirth", 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 = kwirthPlugin; //# sourceMappingURL=index.cjs.js.map