initial updates to api v2.0.0:

-  switch access backend to access-manager-api
- change resource quota to pool based
-  simplify backend system
- various cleanup
This commit is contained in:
alu committed 2026-05-24 19:08:39 +00:00
1 parent cf47cf6c71
commit 24ed6907c7
26 files changed
+708 -1055

No files matched your search

+3 -8
View File
@@ -64,12 +64,7 @@ router.post("/ticket", async (req, res) => {
const domain = global.config.application.domain;
const userObj = global.utils.getUserObjFromUsername(params.username);
let backends = global.userManager.getBackendsByUser(userObj);
if (backends == null) {
res.status(401).send({ auth: false, error: `${params.username} not found in any ProxmoxAAS backends` });
return;
}
backends = backends.concat(["pve"]);
const backends = [global.config.handlers.users, global.config.handlers.instance];
const cm = new CookieFetcher();
const error = await cm.fetchBackends(backends, userObj, params.password);
if (error) {
@@ -107,7 +102,7 @@ router.delete("/ticket", async (req, res) => {
res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" });
}
await global.pve.closeSession(req.cookies);
await global.userManager.closeSession(req.cookies);
await global.access.closeSession(req.cookies);
res.status(200).send({ auth: false });
});
@@ -134,6 +129,6 @@ router.post("/password", async (req, res) => {
const newAttributes = {
userpassword: params.password
};
const response = await global.userManager.setUser(userObj, newAttributes, req.cookies);
const response = await global.access.setUser(userObj, newAttributes, req.cookies);
res.status(response.status).send(response);
});
+9 -17
View File
@@ -3,22 +3,6 @@ export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all groups
* responses:
* - 200: {auth: true, groups: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const groups = await global.userManager.getAllGroups(req.cookies);
res.status(200).send({ groups });
});
/**
* GET - get specific group
* request:
@@ -36,6 +20,14 @@ router.get("/:groupname", async (req, res) => {
if (!auth) {
return;
}
const group = await global.userManager.getGroup(params.groupname, req.cookies);
const groupObj = global.utils.getGroupObjFromGroupname(params.groupname);
const g = await global.access.getGroup(groupObj, req.cookies);
if (g.ok !== true) {
res.status(g.status).send(g);
return;
}
const group = g.group;
res.status(200).send({ group });
});
+76
View File
@@ -0,0 +1,76 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const checkUserInPool = global.utils.checkUserInPool;
/**
* GET - get all available cluster pools
* returns only pool IDs
* responses:
* - 200: List of pools
* - PVE error
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const pools = {};
const poolnames = await global.pve.requestPVE("/pools", "GET", { token: true });
for (const poolpartial of poolnames.data) {
const poolname = poolpartial.poolid;
const p = await global.access.getPool(poolname, req.cookies);
if (p.ok !== true) {
continue;
}
const pool = p.pool;
if (checkUserInPool(pool, userObj)) {
const resources = await global.utils.getPoolResources(req, poolname);
pool.resources = resources;
pools[poolname] = pool;
}
}
res.status(200).send({ pools });
res.end();
});
/**
* GET - get specific pool
* request:
* - poolname: name of pool to get
* responses:
* - 200: {auth: true, pool: Object}
* - 401: {auth: false}
*/
router.get("/:poolname", async (req, res) => {
const params = {
poolname: req.params.poolname
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const p = await global.access.getPool(params.poolname, req.cookies);
if (p.ok !== true) {
res.status(p.status).send(p);
return;
}
const pool = p.pool;
const resources = await global.utils.getPoolResources(req, params.poolname);
pool.resources = resources;
res.status(200).send({ pool });
});
+8 -17
View File
@@ -3,22 +3,6 @@ export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all users
* responses:
* - 200: {auth:true, users: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const users = await global.userManager.getAllUsers(req.cookies);
res.status(200).send({ users });
});
/**
* GET - get specific user
* request:
@@ -36,7 +20,14 @@ router.get("/:username", async (req, res) => {
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(params.username);
const user = await global.userManager.getUser(userObj, req.cookies);
const u = await global.access.getUser(userObj, req.cookies);
if (u.ok !== true) {
res.status(u.status).send(u);
return;
}
const user = u.user;
res.status(200).send({ user });
});
+21 -47
View File
@@ -3,7 +3,8 @@ export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
const getUserResources = global.utils.getUserResources;
const getPoolResources = global.utils.getPoolResources;
const checkUserInPool = global.utils.checkUserInPool;
const nodeRegexP = "[\\w-]+";
const typeRegexP = "qemu|lxc";
@@ -13,33 +14,6 @@ const basePath = `/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP}
global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url);
/**
* GET - get all available cluster pools
* returns only pool IDs
* responses:
* - 200: List of pools
* - PVE error
*/
router.get("/pools", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const allPools = await global.pve.requestPVE("/pools", "GET", { token: true });
if (allPools.status === 200) {
const allPoolsIDs = Array.from(allPools.data.data, (x) => x.poolid);
res.status(allPools.status).send({ pools: allPoolsIDs });
res.end();
}
else {
res.status(allPools.status).send({ error: allPools.statusText });
res.end();
}
});
/**
* GET - get all available cluster nodes
* uses existing user permissions without elevation
@@ -58,7 +32,7 @@ router.get("/nodes", async (req, res) => {
const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies });
if (allNodes.status === 200) {
const allNodesIDs = Array.from(allNodes.data.data, (x) => x.node);
const allNodesIDs = Array.from(allNodes.data, (x) => x.node);
res.status(allNodes.status).send({ nodes: allNodesIDs });
res.end();
}
@@ -89,7 +63,7 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
if (!auth) {
return;
}
const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes;
const userNodes = (await global.access.getPool(userObj, req.cookies)).cluster.nodes;
if (userNodes[params.node] !== true) { // user does not have access to the node
res.status(401).send({ auth: false, path: params.node });
res.end();
@@ -97,7 +71,7 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
}
// get remaining user resources
const userAvailPci = (await getUserResources(req, userObj)).pci.nodes[params.node]; // we assume that the node list is used. TODO support global lists
const userAvailPci = (await getPoolResources(req, userObj)).pci.nodes[params.node]; // we assume that the node list is used. TODO support global lists
if (userAvailPci === undefined) { // user has no available devices on this node, so send an empty list
res.status(200).send([]);
res.end();
@@ -201,7 +175,7 @@ router.post(`${basePath}/resources`, async (req, res) => {
request.cpu = params.proctype;
}
// check resource approval
const { approved, reason } = await approveResources(req, userObj, request, params.node);
const { approved, reason } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
res.end();
@@ -269,11 +243,11 @@ router.post(`${basePath}/create`, async (req, res) => {
if (!auth) {
return;
}
// get user db config
const user = await global.userManager.getUser(userObj, req.cookies);
// get pool config
const pool = (await global.access.getPool(params.pool, req.cookies)).pool;
const vmid = Number.parseInt(params.vmid);
const vmidMin = user.cluster.vmid.min;
const vmidMax = user.cluster.vmid.max;
const vmidMin = pool["vmid-allowed"].min;
const vmidMax = pool["vmid-allowed"].max;
// check vmid is within allowed range
if (vmid < vmidMin || vmid > vmidMax) {
res.status(500).send({ error: `Requested vmid ${vmid} is out of allowed range [${vmidMin},${vmidMax}].` });
@@ -281,14 +255,14 @@ router.post(`${basePath}/create`, async (req, res) => {
return;
}
// check node is within allowed list
if (user.cluster.nodes[params.node] !== true) {
res.status(500).send({ error: `Requested node ${params.node} is not in allowed nodes [${user.cluster.nodes}].` });
if (pool["nodes-allowed"][params.node] !== true) {
res.status(500).send({ error: `Requested node ${params.node} is not in allowed nodes [${pool["nodes-allowed"]}].` });
res.end();
return;
}
// check if pool is in user allowed pools
if (user.cluster.pools[params.pool] !== true) {
res.status(500).send({ error: `Requested pool ${params.pool} not in allowed pools [${user.pools}]` });
// check if user is in pool
if(checkUserInPool(pool, userObj) !== true) {
res.status(500).send({ error: `Requested pool ${params.pool} does not contain user ${req.cookies.username}]` });
res.end();
return;
}
@@ -301,9 +275,9 @@ router.post(`${basePath}/create`, async (req, res) => {
request.swap = Number(params.swap) * 1024 ** 2;
request[params.rootfslocation] = params.rootfssize * 1024 ** 3;
}
for (const key of Object.keys(user.templates.instances[params.type])) {
const item = user.templates.instances[params.type][key];
if (item.resource) {
for (const key of Object.keys(pool.templates.instances[params.type])) {
const item = pool.templates.instances[params.type][key];
if (item.resource.enabled) {
if (request[item.resource.name]) {
request[item.resource.name] += item.resource.amount;
}
@@ -313,7 +287,7 @@ router.post(`${basePath}/create`, async (req, res) => {
}
}
// check resource approval
const { approved, reason } = await approveResources(req, userObj, request, params.node);
const { approved, reason } = await await approveResources(req, userObj, params.node, params.pool, request);
if (!approved) {
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
res.end();
@@ -326,8 +300,8 @@ router.post(`${basePath}/create`, async (req, res) => {
memory: Number(params.memory),
pool: params.pool
};
for (const key of Object.keys(user.templates.instances[params.type])) {
action[key] = user.templates.instances[params.type][key].value;
for (const key of Object.keys(pool.templates.instances[params.type])) {
action[key] = pool.templates.instances[params.type][key].value;
}
if (params.type === "lxc") {
action.swap = params.swap;
+8 -9
View File
@@ -33,7 +33,7 @@ router.get("/", async (req, res) => {
const storage = global.config.backups.storage;
const backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
if (backups.status === 200) {
res.status(backups.status).send(backups.data.data);
res.status(backups.status).send(backups.data);
}
else {
res.status(backups.status).send({ error: backups.statusText });
@@ -72,9 +72,9 @@ router.post("/", async (req, res) => {
// check if number of backups is less than the allowed number
const storage = global.config.backups.storage;
const backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
const numBackups = backups.data.data.length;
const numBackups = backups.data.length;
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const maxAllowed = (await global.userManager.getUser(userObj, req.cookies)).cluster.backups.max;
const maxAllowed = (await global.access.getUser(userObj, req.cookies)).cluster.backups.max;
if (backups.status !== 200) {
res.status(backups.status).send({ error: backups.statusText });
return;
@@ -94,7 +94,7 @@ router.post("/", async (req, res) => {
"notes-template": params.notes
};
const result = await global.pve.requestPVE(`/nodes/${params.node}/vzdump`, "POST", { token: true }, body);
res.status(result.status).send(result.data.data);
res.status(result.status).send(result.data);
});
/**
@@ -136,7 +136,7 @@ router.post("/notes", async (req, res) => {
return;
}
let found = false;
for (const volume of backups.data.data) {
for (const volume of backups.data) {
if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true;
}
@@ -196,7 +196,7 @@ router.delete("/", async (req, res) => {
return;
}
let found = false;
for (const volume of backups.data.data) {
for (const volume of backups.data) {
if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true;
}
@@ -208,7 +208,7 @@ router.delete("/", async (req, res) => {
// found a valid backup with matching vmid and volid
const result = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content/${params.volid}?delay=5`, "DELETE", { token: true });
res.status(result.status).send(result.data.data);
res.status(result.status).send(result.data);
});
/**
@@ -248,7 +248,7 @@ router.post("/restore", async (req, res) => {
return;
}
let found = false;
for (const volume of backups.data.data) {
for (const volume of backups.data) {
if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true;
}
@@ -281,7 +281,6 @@ router.post("/restore", async (req, res) => {
}
const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/`, "POST", { token: true }, body);
console.log(result);
if (result.status === 200) {
res.status(result.status).send();
}
+9 -3
View File
@@ -145,6 +145,8 @@ router.post("/:disk/resize", async (req, res) => {
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist
@@ -157,7 +159,7 @@ router.post("/:disk/resize", async (req, res) => {
const request = {};
request[storage] = Number(params.size * 1024 ** 3); // setup request object
// check request approval
const { approved } = await approveResources(req, userObj, request, params.node);
const { approved } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Storage ${storage} could not fulfill request of size ${params.size}G.` });
res.end();
@@ -205,6 +207,8 @@ router.post("/:disk/move", async (req, res) => {
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist
@@ -220,7 +224,7 @@ router.post("/:disk/move", async (req, res) => {
request[dstStorage] = Number(size); // always decrease destination storage by size
}
// check request approval
const { approved } = await approveResources(req, userObj, request, params.node);
const { approved } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
res.end();
@@ -324,6 +328,8 @@ router.post("/:disk/create", async (req, res) => {
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// disk must not exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
if (disk) {
@@ -337,7 +343,7 @@ router.post("/:disk/create", async (req, res) => {
// setup request
request[params.storage] = Number(params.size * 1024 ** 3);
// check request approval
const { approved } = await approveResources(req, userObj, request, params.node);
const { approved } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
res.end();
+7 -3
View File
@@ -36,6 +36,8 @@ router.post("/:netid/create", async (req, res) => {
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// net interface must not exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (net) {
@@ -53,14 +55,14 @@ router.post("/:netid/create", async (req, res) => {
};
// check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const { approved } = await approveResources(req, userObj, request, params.node);
const { approved } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end();
return;
}
// setup action
const nc = (await global.userManager.getUser(userObj, req.cookies)).templates.network[params.type];
const nc = (await global.access.getUser(userObj, req.cookies)).templates.network[params.type];
const action = {};
if (params.type === "lxc") {
action[`${params.netid}`] = `name=${params.name},bridge=${nc.bridge},ip=${nc.ip},ip6=${nc.ip6},tag=${nc.vlan},type=${nc.type},rate=${params.rate}`;
@@ -105,6 +107,8 @@ router.post("/:netid/modify", async (req, res) => {
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (!net) {
@@ -117,7 +121,7 @@ router.post("/:netid/modify", async (req, res) => {
};
// check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const { approved } = await approveResources(req, userObj, request, params.node);
const { approved } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end();
+6 -2
View File
@@ -78,6 +78,8 @@ router.post("/:hostpci/modify", async (req, res) => {
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// force all functions
params.device = params.device.split(".")[0];
// device must exist to be modified
@@ -100,7 +102,7 @@ router.post("/:hostpci/modify", async (req, res) => {
return;
}
// check resource approval
const { approved } = await approveResources(req, userObj, request, params.node);
const { approved } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
res.end();
@@ -158,6 +160,8 @@ router.post("/:hostpci/create", async (req, res) => {
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// force all functions
params.device = params.device.split(".")[0];
// device must not exist to be added
@@ -173,7 +177,7 @@ router.post("/:hostpci/create", async (req, res) => {
const request = { pci: requestedDevice.device_name };
// check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const { approved } = await approveResources(req, userObj, request, params.node);
const { approved } = await approveResources(req, userObj, params.node, instance.pool, request);
if (!approved) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
res.end();
+5 -6
View File
@@ -52,7 +52,7 @@ if (schemes.hash.enabled) {
return;
}
// get current cluster resources - do not use fabric here because fabric is not always updated to changes like up/down state changes
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data.data;
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { cookies: req.cookies })).data;
// filter out just state information of resources that are needed
const state = extractClusterState(status, resourceTypes);
res.status(200).send(getObjectHash(state));
@@ -166,11 +166,10 @@ if (schemes.interrupt.enabled) {
}
else {
wsServer.handleUpgrade(req, socket, head, async (socket) => {
// get the user pools
const userObj = global.utils.getUserObjFromUsername(cookies.username);
const pools = Object.keys((await global.userManager.getUser(userObj, cookies)).cluster.pools);
// use user cookies to determine which pools they can see, lazily assume that if a user can audit a pool they are also can audit pool member state
const pools = await global.pve.requestPVE("/pools", "GET", { cookies });
// emit the connection to initialize socket
wsServer.emit("connection", socket, cookies.username, pools);
wsServer.emit("connection", socket, cookies.username, Object.keys(pools.data));
});
}
});
@@ -190,7 +189,7 @@ if (schemes.interrupt.enabled) {
return;
}
// get current cluster resources
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { token: true })).data.data;
const status = (await global.pve.requestPVE("/cluster/resources", "GET", { token: true })).data;
// filter out just state information of resources that are needed, and hash each one
const currState = extractClusterState(status, resourceTypes, true);
// get a map of users to send sync notifications
+2 -56
View File
@@ -4,60 +4,6 @@ export const router = Router({ mergeParams: true }); ;
const config = global.config;
const checkAuth = global.utils.checkAuth;
/**
* GET - get db user resource information including allocated, free, and maximum resource values along with resource metadata
* responses:
* - 200: {avail: Object, max: Object, used: Object, resources: Object}
* - 401: {auth: false}
*/
router.get("/dynamic/resources", async (req, res) => {
const params = {
username: req.cookies.username
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(params.username);
const resources = await global.utils.getUserResources(req, userObj);
res.status(200).send(resources);
});
/**
* GET - get db user configuration by key
* request:
* - key: string - user config key
* responses:
* - 200: Object
* - 401: {auth: false}
* - 401: {auth: false, error: string}
*/
router.get("/config/:key", async (req, res) => {
const params = {
key: req.params.key
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const allowKeys = ["resources", "cluster"];
if (allowKeys.includes(params.key)) {
const config = await global.userManager.getUser(userObj, req.cookies);
res.status(200).send(config[params.key]);
}
else {
res.status(401).send({ auth: false, error: `User is not authorized to access /user/config/${params.key}.` });
}
});
/**
* GET - get user accessible iso files
* response:
@@ -78,7 +24,7 @@ router.get("/vm-isos", async (req, res) => {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data.data;
const isos = content.data;
const userIsos = [];
isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, "");
@@ -108,7 +54,7 @@ router.get("/ct-templates", async (req, res) => {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data.data;
const isos = content.data;
const userIsos = [];
isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, "");