@jfvilas/plugin-kwirth-backend
Version:
Backstage backend plugin for Kwirth plugins
499 lines (488 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.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