add interface for generic backends, add interfaces for DB and AUTH type backends, implement basic user password change method
361 lines
14 KiB
JavaScript
361 lines
14 KiB
JavaScript
import { createHash } from "crypto";
|
|
import path from "path";
|
|
import url from "url";
|
|
import * as fs from "fs";
|
|
import { readFileSync } from "fs";
|
|
import { exit } from "process";
|
|
|
|
/**
|
|
* Check if a user is authorized to access a specified vm, or the cluster in general.
|
|
* @param {Object} cookies user auth cookies.
|
|
* @param {Object} res ProxmoxAAS API response object, used to send auth error responses.
|
|
* @param {string} vmpath vm path to check. Optional, if null then the general /version path is used.
|
|
* @returns {boolean} true if the user is authorized to access the specific vm or cluster in general, false otheriwse.
|
|
*/
|
|
export async function checkAuth (cookies, res, vmpath = null) {
|
|
let auth = false;
|
|
|
|
if (global.db.getUser(cookies.username) === null) {
|
|
auth = false;
|
|
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in localdb.` });
|
|
res.end();
|
|
return false;
|
|
}
|
|
|
|
if (vmpath) {
|
|
const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies });
|
|
auth = result.status === 200;
|
|
}
|
|
else { // if no path is specified, then do a simple authentication
|
|
const result = await global.pve.requestPVE("/version", "GET", { cookies });
|
|
auth = result.status === 200;
|
|
}
|
|
|
|
if (!auth) {
|
|
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." });
|
|
res.end();
|
|
}
|
|
return auth;
|
|
}
|
|
|
|
/**
|
|
* Get the full config of an instance, including searching disk information.
|
|
* @param {Object} req ProxmoxAAS API request object.
|
|
* @param {Object} instance to get config as object containing node, type, and id.
|
|
* @param {Array} diskprefixes Array containing prefixes for disks.
|
|
* @returns
|
|
*/
|
|
async function getFullInstanceConfig (req, instance, diskprefixes) {
|
|
const config = (await global.pve.requestPVE(`/nodes/${instance.node}/${instance.type}/${instance.vmid}/config`, "GET", { cookies: req.cookies })).data.data;
|
|
// fetch all instance disk and device data concurrently
|
|
const promises = [];
|
|
const mappings = [];
|
|
for (const key in config) {
|
|
if (diskprefixes.some(prefix => key.startsWith(prefix))) {
|
|
promises.push(global.pve.getDiskInfo(instance.node, config, key));
|
|
mappings.push(key);
|
|
}
|
|
else if (key.startsWith("hostpci")) {
|
|
promises.push(global.pve.getDeviceInfo(instance.node, config[key].split(",")[0]));
|
|
mappings.push(key);
|
|
}
|
|
}
|
|
const results = await Promise.all(promises);
|
|
results.forEach((e, i) => {
|
|
const key = mappings[i];
|
|
config[key] = e;
|
|
});
|
|
config.node = instance.node;
|
|
return config;
|
|
}
|
|
|
|
/**
|
|
* Get all configs for every instance owned by the user. Uses the expanded config data from getFullInstanceConfig.
|
|
* @param {Object} req ProxmoxAAS API request object.
|
|
* @param {Object} dbResources data about application resources, to indicate which resources are tracked.
|
|
* @returns {Object} k-v pairs of resource name and used amounts
|
|
*/
|
|
async function getAllInstanceConfigs (req, diskprefixes) {
|
|
// get the basic resources list
|
|
const resources = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data;
|
|
|
|
// filter resources by their type, we only want lxc and qemu
|
|
const instances = [];
|
|
for (const resource of resources) {
|
|
if (resource.type === "lxc" || resource.type === "qemu") {
|
|
instances.push(resource);
|
|
}
|
|
}
|
|
|
|
// get all instance configs, also include detailed disk and device info
|
|
const promises = [];
|
|
const mappings = [];
|
|
for (let i = 0; i < instances.length; i++) {
|
|
const instance = instances[i];
|
|
const config = getFullInstanceConfig(req, instance, diskprefixes);
|
|
promises.push(config);
|
|
mappings.push(i);
|
|
}
|
|
const configs = await Promise.all(promises);
|
|
|
|
return configs;
|
|
}
|
|
|
|
/**
|
|
* Get user resource data including used, available, and maximum resources.
|
|
* @param {Object} req ProxmoxAAS API request object.
|
|
* @param {string} username of user to get resource data.
|
|
* @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user.
|
|
*/
|
|
export async function getUserResources (req, username) {
|
|
const dbResources = global.config.resources;
|
|
const userResources = global.db.getUser(username).resources;
|
|
|
|
// setup disk prefixes object
|
|
const diskprefixes = [];
|
|
for (const resourceName of Object.keys(dbResources)) {
|
|
if (dbResources[resourceName].type === "storage") {
|
|
for (const diskPrefix of dbResources[resourceName].disks) {
|
|
diskprefixes.push(diskPrefix);
|
|
}
|
|
}
|
|
}
|
|
|
|
// setup the user resource object with used and avail for each resource and each resource pool
|
|
// also add a total counter for each resource (only used for display, not used to check requests)
|
|
for (const resourceName of Object.keys(userResources)) {
|
|
if (dbResources[resourceName].type === "list") {
|
|
userResources[resourceName].total = [];
|
|
userResources[resourceName].global.forEach((e) => {
|
|
e.used = 0;
|
|
e.avail = e.max;
|
|
const index = userResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match);
|
|
if (index === -1) {
|
|
userResources[resourceName].total.push(structuredClone(e));
|
|
}
|
|
else {
|
|
userResources[resourceName].total[index].max += e.max;
|
|
userResources[resourceName].total[index].avail += e.avail;
|
|
}
|
|
});
|
|
for (const nodeName of Object.keys(userResources[resourceName].nodes)) {
|
|
userResources[resourceName].nodes[nodeName].forEach((e) => {
|
|
e.used = 0;
|
|
e.avail = e.max;
|
|
const index = userResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match);
|
|
if (index === -1) {
|
|
userResources[resourceName].total.push(structuredClone(e));
|
|
}
|
|
else {
|
|
userResources[resourceName].total[index].max += e.max;
|
|
userResources[resourceName].total[index].avail += e.avail;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
else {
|
|
const total = {
|
|
max: 0,
|
|
used: 0,
|
|
avail: 0
|
|
};
|
|
userResources[resourceName].global.used = 0;
|
|
userResources[resourceName].global.avail = userResources[resourceName].global.max;
|
|
total.max += userResources[resourceName].global.max;
|
|
total.avail += userResources[resourceName].global.avail;
|
|
for (const nodeName of Object.keys(userResources[resourceName].nodes)) {
|
|
userResources[resourceName].nodes[nodeName].used = 0;
|
|
userResources[resourceName].nodes[nodeName].avail = userResources[resourceName].nodes[nodeName].max;
|
|
total.max += userResources[resourceName].nodes[nodeName].max;
|
|
total.avail += userResources[resourceName].nodes[nodeName].avail;
|
|
}
|
|
userResources[resourceName].total = total;
|
|
}
|
|
}
|
|
|
|
const configs = await getAllInstanceConfigs(req, diskprefixes);
|
|
|
|
for (const config of configs) {
|
|
const nodeName = config.node;
|
|
for (const resourceName of Object.keys(config)) {
|
|
// numeric resource type
|
|
if (resourceName in dbResources && dbResources[resourceName].type === "numeric") {
|
|
const val = Number(config[resourceName]);
|
|
// if the instance's node is restricted by this resource, add it to the instance's used value
|
|
if (nodeName in userResources[resourceName].nodes) {
|
|
userResources[resourceName].nodes[nodeName].used += val;
|
|
userResources[resourceName].nodes[nodeName].avail -= val;
|
|
}
|
|
// otherwise add the resource to the global pool
|
|
else {
|
|
userResources[resourceName].global.used += val;
|
|
userResources[resourceName].global.avail -= val;
|
|
}
|
|
userResources[resourceName].total.used += val;
|
|
userResources[resourceName].total.avail -= val;
|
|
}
|
|
else if (diskprefixes.some(prefix => resourceName.startsWith(prefix))) {
|
|
const diskInfo = config[resourceName];
|
|
if (diskInfo) { // only count if disk exists
|
|
const val = Number(diskInfo.size);
|
|
const storage = diskInfo.storage;
|
|
// if the instance's node is restricted by this resource, add it to the instance's used value
|
|
if (nodeName in userResources[storage].nodes) {
|
|
userResources[storage].nodes[nodeName].used += val;
|
|
userResources[storage].nodes[nodeName].avail -= val;
|
|
}
|
|
// otherwise add the resource to the global pool
|
|
else {
|
|
userResources[storage].global.used += val;
|
|
userResources[storage].global.avail -= val;
|
|
}
|
|
userResources[storage].total.used += val;
|
|
userResources[storage].total.avail -= val;
|
|
}
|
|
}
|
|
else if (resourceName.startsWith("net") && config[resourceName].includes("rate=")) { // only count net instances with a rate limit
|
|
const val = Number(config[resourceName].split("rate=")[1].split(",")[0]);
|
|
// if the instance's node is restricted by this resource, add it to the instance's used value
|
|
if (nodeName in userResources.network.nodes) {
|
|
userResources.network.nodes[nodeName].used += val;
|
|
userResources.network.nodes[nodeName].avail -= val;
|
|
}
|
|
// otherwise add the resource to the global pool
|
|
else {
|
|
userResources.network.global.used += val;
|
|
userResources.network.global.avail -= val;
|
|
}
|
|
userResources.network.total.used += val;
|
|
userResources.network.total.avail -= val;
|
|
}
|
|
else if (resourceName.startsWith("hostpci")) {
|
|
const deviceInfo = config[resourceName];
|
|
if (deviceInfo) { // only count if device exists
|
|
const deviceName = deviceInfo.device_name;
|
|
// if the instance's node is restricted by this resource, add it to the instance's used value
|
|
if (nodeName in userResources.pci.nodes) {
|
|
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => deviceName.includes(availEelement.match));
|
|
userResources.pci.nodes[nodeName][index].used++;
|
|
userResources.pci.nodes[nodeName][index].avail--;
|
|
}
|
|
// otherwise add the resource to the global pool
|
|
else {
|
|
const index = userResources.pci.global.findIndex((availEelement) => deviceName.includes(availEelement.match));
|
|
userResources.pci.global[index].used++;
|
|
userResources.pci.global[index].avail--;
|
|
}
|
|
const index = userResources.pci.total.findIndex((availEelement) => deviceName.includes(availEelement.match));
|
|
userResources.pci.total[index].used++;
|
|
userResources.pci.total[index].avail--;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return userResources;
|
|
}
|
|
|
|
/**
|
|
* Check approval for user requesting additional resources. Generally, subtracts the request from available resources and ensures request can be fulfilled by the available resources.
|
|
* @param {Object} req ProxmoxAAS API request object.
|
|
* @param {string} username of user requesting additional resources.
|
|
* @param {Object} request k-v pairs of resources and requested amounts
|
|
* @returns {boolean} true if the available resources can fullfill the requested resources, false otherwise.
|
|
*/
|
|
export async function approveResources (req, username, request, node) {
|
|
const dbResources = global.config.resources;
|
|
const userResources = await getUserResources(req, username);
|
|
let approved = true;
|
|
Object.keys(request).every((key) => {
|
|
// if requested resource is not specified in user resources, assume it's not allowed
|
|
if (!(key in userResources)) {
|
|
approved = false;
|
|
return false;
|
|
}
|
|
|
|
const inNode = node in userResources[key].nodes;
|
|
const resourceData = inNode ? userResources[key].nodes[node] : userResources[key].global;
|
|
|
|
// if the resource type is list, check if the requested resource exists in the list
|
|
if (dbResources[key].type === "list") {
|
|
const index = resourceData.findIndex((availElement) => request[key].includes(availElement.match));
|
|
// if no matching resource when index == -1, then remaining is -1 otherwise use the remaining value
|
|
const avail = index === -1 ? false : resourceData[index].avail > 0;
|
|
if (avail !== dbResources[key].whitelist) {
|
|
approved = false;
|
|
return false;
|
|
}
|
|
}
|
|
// if either the requested or avail resource is NaN, block
|
|
else if (isNaN(resourceData.avail) || isNaN(request[key])) {
|
|
approved = false;
|
|
return false;
|
|
}
|
|
// if the avail resources is less than the requested resources, block
|
|
else if (resourceData.avail - request[key] < 0) {
|
|
approved = false;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
});
|
|
return approved; // if all requested resources pass, allow
|
|
}
|
|
|
|
/**
|
|
* Get the hash value of an object with data values.
|
|
* @param {Object} object to be hashed.
|
|
* @param {string} alg algorithm used to get digest.
|
|
* @param {string} format format of digest.
|
|
* @returns {string} digest of hash function.
|
|
*/
|
|
export function getObjectHash (object, alg = "sha256", format = "hex") {
|
|
const hash = createHash(alg);
|
|
hash.update(JSON.stringify(object, Object.keys(object).sort()));
|
|
return hash.digest(format);
|
|
}
|
|
|
|
/**
|
|
* Get the time remaining of scheduler timeout object.
|
|
* @param {Object} timeout object to get time reamining.
|
|
* @returns {number} milliseconds remaining until next event.
|
|
*/
|
|
export function getTimeLeft (timeout) {
|
|
return Math.ceil((timeout._idleStart + timeout._idleTimeout - (global.process.uptime() * 1000)));
|
|
}
|
|
|
|
/**
|
|
* Recursively import routes from target folder.
|
|
* @param {Object} router or app object.
|
|
* @param {string} baseroute API route for each imported module.
|
|
* @param {string} target folder to import modules.
|
|
* @param {string} from source folder of calling module, optional for imports from the same base directory.
|
|
*/
|
|
export function recursiveImportRoutes (router, baseroute, target, from = import.meta.url) {
|
|
const thisPath = path.dirname(url.fileURLToPath(import.meta.url));
|
|
const fromPath = path.relative(".", path.dirname(url.fileURLToPath(from)));
|
|
const targetPath = path.relative(".", `${fromPath}/${target}`);
|
|
const baseImportPath = path.relative(thisPath, targetPath);
|
|
const files = fs.readdirSync(targetPath);
|
|
files.forEach((file) => {
|
|
if (file.endsWith(".js")) {
|
|
const path = `./${baseImportPath}/${file}`;
|
|
const route = `${baseroute}/${file.replace(".js", "")}`;
|
|
import(path).then((module) => {
|
|
router.use(route, module.router);
|
|
});
|
|
console.log(`routes: loaded ${path} as ${route}`);
|
|
}
|
|
});
|
|
}
|
|
|
|
export function readJSONFile (path) {
|
|
try {
|
|
return JSON.parse(readFileSync(path));
|
|
}
|
|
catch (e) {
|
|
console.log(`error: ${path} was not found.`);
|
|
exit(1);
|
|
}
|
|
};
|