@jfvilas/plugin-kubelog-backend
Version:
Backstage backend plugin for Kubelog
488 lines (477 loc) • 19.7 kB
JavaScript
;
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