diff --git a/api/test_access.http b/api/test_access.http index f2284c9..509222d 100644 --- a/api/test_access.http +++ b/api/test_access.http @@ -17,5 +17,5 @@ GET {{baseUrl}}/access/pools/ ### Get pool GET {{baseUrl}}/access/pools/{{poolname}} -### Get all pools +### Get all nodes GET {{baseUrl}}/cluster/nodes \ No newline at end of file diff --git a/api/test_instance.http b/api/test_instance.http index ec9f309..50db5d7 100644 --- a/api/test_instance.http +++ b/api/test_instance.http @@ -8,6 +8,9 @@ username={{username}} ### Get instance resources GET {{baseUrl}}/cluster/{{testvmpath}} +### Get instance backups +GET {{baseUrl}}/cluster/{{testvmpath}}/backup + ### Test create instance POST {{baseUrl}}/cluster/{{testvmpath}}/create diff --git a/src/routes/access.js b/src/routes/access.js index 4b86248..cfb605e 100644 --- a/src/routes/access.js +++ b/src/routes/access.js @@ -62,9 +62,11 @@ router.post("/ticket", async (req, res) => { password: req.body.password }; - const domain = global.config.application.domain; + // get user and user backends from config const userObj = global.utils.getUserObjFromUsername(params.username); const backends = [global.config.handlers.users, global.config.handlers.instance]; + + // fetch cookies using cookie fetcher const cm = new CookieFetcher(); const error = await cm.fetchBackends(backends, userObj, params.password); if (error) { @@ -72,6 +74,11 @@ router.post("/ticket", async (req, res) => { return; } const cookies = cm.exportCookies(); + + // get global config domain name + const domain = global.config.application.domain; + + // for each cookie, add the cookie to response and also compute the minimum across all cookies let minimumExpires = Infinity; for (const cookie of cookies) { const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow); @@ -80,6 +87,8 @@ router.post("/ticket", async (req, res) => { minimumExpires = cookie.expiresMSFromNow; } } + + // set username and auth cookie with the minimum cookie length const expiresDate = new Date(Date.now() + minimumExpires); res.cookie("username", params.username, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" }); res.cookie("auth", 1, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" }); @@ -92,15 +101,20 @@ router.post("/ticket", async (req, res) => { * - 200: {auth: false} */ router.delete("/ticket", async (req, res) => { + // must have cookies to delete, otherwise just return ok if (Object.keys(req.cookies).length === 0) { res.status(200).send({ auth: false }); return; } + + // for each cookie, set the expire date to 0 const domain = global.config.application.domain; const expire = new Date(0); for (const cookie in req.cookies) { res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" }); } + + // call close session on each backend, even if was not used await global.pve.closeSession(req.cookies); await global.access.closeSession(req.cookies); res.status(200).send({ auth: false }); diff --git a/src/routes/access/groups.js b/src/routes/access/groups.js index f0330b8..b134a8d 100644 --- a/src/routes/access/groups.js +++ b/src/routes/access/groups.js @@ -1,8 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); -const checkAuth = global.utils.checkAuth; - /** * GET - get specific group * request: @@ -16,18 +14,24 @@ router.get("/:groupname", async (req, res) => { groupname: req.params.groupname }; // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } + // attempt to parse group from groupname const groupObj = global.utils.getGroupObjFromGroupname(params.groupname); + if (groupObj == null) { + res.status(400).send({ auth: true, error:`Groupname ${params.groupname} does not match format gid-realm or gid.` }); + } + + // get group const g = await global.access.getGroup(groupObj, req.cookies); if (g.ok !== true) { - res.status(g.status).send(g); + res.status(g.status).send({ auth:true, error:g }); return; } const group = g.group; - res.status(200).send({ group }); + res.status(200).send({ auth:true, group }); }); diff --git a/src/routes/access/pools.js b/src/routes/access/pools.js index a0b8334..6c78321 100644 --- a/src/routes/access/pools.js +++ b/src/routes/access/pools.js @@ -1,9 +1,6 @@ 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 @@ -13,27 +10,30 @@ const checkUserInPool = global.utils.checkUserInPool; */ router.get("/", async (req, res) => { // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } + // get user object const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - const pools = {}; - + // get all pool names using api token const poolnames = await global.pve.requestPVE("/pools", "GET", { token: true }); + // setup pools (return value) + const pools = {}; + // for each poolname for (const poolpartial of poolnames.data) { const poolname = poolpartial.poolid; - + // get the pool const p = await global.access.getPool(poolname, req.cookies); if (p.ok !== true) { continue; } const pool = p.pool; - - if (checkUserInPool(pool, userObj)) { + // if user is in the pool, add it to pools (return value) + if (global.utils.checkUserInPool(pool, userObj)) { const resources = await global.utils.getPoolResources(req, poolname); pool.resources = resources; pools[poolname] = pool; @@ -57,20 +57,22 @@ router.get("/:poolname", async (req, res) => { poolname: req.params.poolname }; // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } + // get pool const p = await global.access.getPool(params.poolname, req.cookies); if (p.ok !== true) { - res.status(p.status).send(p); + res.status(p.status).send({ auth:true, error: p }); return; } const pool = p.pool; + // get resources const resources = await global.utils.getPoolResources(req, params.poolname); - + // append resources to pool pool.resources = resources; - res.status(200).send({ pool }); + res.status(200).send({ auth: true, pool }); }); diff --git a/src/routes/access/users.js b/src/routes/access/users.js index c17f228..73414f5 100644 --- a/src/routes/access/users.js +++ b/src/routes/access/users.js @@ -1,8 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); -const checkAuth = global.utils.checkAuth; - /** * GET - get specific user * request: @@ -16,15 +14,21 @@ router.get("/:username", async (req, res) => { username: req.params.username }; // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } - + + // attempt to parse user from username const userObj = global.utils.getUserObjFromUsername(params.username); + if (userObj == null) { + res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); + } + + // get user const u = await global.access.getUser(userObj, req.cookies); if (u.ok !== true) { - res.status(u.status).send(u); + res.status(u.status).send({ auth: true, error: u }); return; } const user = u.user; diff --git a/src/routes/cluster.js b/src/routes/cluster.js index 3999458..ecf284f 100644 --- a/src/routes/cluster.js +++ b/src/routes/cluster.js @@ -1,10 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); -const checkAuth = global.utils.checkAuth; -const approveResources = global.utils.approveResources; -const checkUserInPool = global.utils.checkUserInPool; - const nodeRegexP = "[\\w-]+"; const typeRegexP = "qemu|lxc"; const vmidRegexP = "\\d+"; @@ -23,13 +19,13 @@ global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url) */ router.get("/nodes", async (req, res) => { // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } + // get all nodes const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies }); - if (allNodes.status === 200) { const allNodesIDs = Array.from(allNodes.data, (x) => x.node); res.status(allNodes.status).send({ nodes: allNodesIDs }); @@ -60,14 +56,13 @@ router.get(`${basePath}`, async (req, res) => { // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } // get current config const instance = await global.pve.getInstance(params.node, params.vmid); - res.status(200).send(instance); }); @@ -99,11 +94,9 @@ router.post(`${basePath}/resources`, async (req, res) => { boot: req.body.boot }; - const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } @@ -119,13 +112,16 @@ router.post(`${basePath}/resources`, async (req, res) => { else if (params.type === "qemu") { request.cpu = params.proctype; } + // check resource approval - const { approved, reason } = await approveResources(req, userObj, params.node, instance.pool, request); + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); + const { approved, reason } = await global.utils.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(); return; } + // setup action const action = { cores: params.cores, memory: params.memory }; if (params.type === "lxc") { @@ -136,6 +132,7 @@ router.post(`${basePath}/resources`, async (req, res) => { action.boot = `order=${params.boot.toString().replaceAll(",", ";")};`; } const method = params.type === "qemu" ? "POST" : "PUT"; + // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); await global.pve.handleResponse(params.node, result, res); @@ -181,36 +178,40 @@ router.post(`${basePath}/create`, async (req, res) => { rootfssize: req.body.rootfssize }; - const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } + // get pool config const pool = (await global.access.getPool(params.pool, req.cookies)).pool; const vmid = Number.parseInt(params.vmid); 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}].` }); res.end(); return; } + // check node is within allowed list 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 user is in pool - if(checkUserInPool(pool, userObj) !== true) { + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); + if(global.utils.checkUserInPool(pool, userObj) !== true) { res.status(500).send({ error: `Requested pool ${params.pool} does not contain user ${req.cookies.username}]` }); res.end(); return; } + // setup request const request = { cores: Number(params.cores), @@ -231,13 +232,15 @@ router.post(`${basePath}/create`, async (req, res) => { } } } + // check resource approval - const { approved, reason } = await await approveResources(req, userObj, params.node, params.pool, request); + const { approved, reason } = await await global.utils.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(); return; } + // setup action by adding non resource values const action = { vmid: params.vmid, @@ -260,6 +263,7 @@ router.post(`${basePath}/create`, async (req, res) => { else { action.name = params.name; } + // commit action const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, action); await global.pve.handleResponse(params.node, result, res); @@ -283,12 +287,14 @@ router.delete(`${basePath}/delete`, async (req, res) => { type: req.params.type, vmid: req.params.vmid }; + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } + // commit action const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true }); await global.pve.handleResponse(params.node, result, res); diff --git a/src/routes/cluster/backup.js b/src/routes/cluster/backup.js index fdcc07d..0367ade 100644 --- a/src/routes/cluster/backup.js +++ b/src/routes/cluster/backup.js @@ -1,8 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const checkAuth = global.utils.checkAuth; - /** * GET - get backups for an instance * request: @@ -24,7 +22,7 @@ router.get("/", async (req, res) => { // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } @@ -32,11 +30,12 @@ router.get("/", async (req, res) => { // get vm backups 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); } else { - res.status(backups.status).send({ error: backups.statusText }); + res.status(backups.status).send({ auth: true, error: backups.statusText }); } }); @@ -64,22 +63,37 @@ router.post("/", async (req, res) => { // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } - // check if number of backups is less than the allowed number + // get number of currently backups used 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.length; - const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - 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; } - else if (numBackups >= maxAllowed) { + const numBackups = backups.data.length; + + // get instance + const instance = await global.pve.getInstance(params.node, params.vmid); + if (instance === null) { + res.status(400).send({ error: `failed to get instance ${params.node}/${params.vmid}` }); + return; + } + + // get pool and pool allowed nodes + const pool = await global.access.getPool(instance.pool, req.cookies); + if (!pool.ok) { + res.status(pool.status).send({ error: `failed to get pool ${pool}` }); + return; + } + const maxAllowed = pool.pool["backups-allowed"].max; + + // check if used backups is more than maximum allowed, if so exit + if (numBackups >= maxAllowed) { res.status(backups.status).send({ error: `${params.vmid} already has ${numBackups} >= ${maxAllowed} max backups allowed` }); return; } @@ -122,7 +136,7 @@ router.post("/notes", async (req, res) => { // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } @@ -146,7 +160,7 @@ router.post("/notes", async (req, res) => { return; } - // create backup using vzdump path + // modify backup notes const body = { notes: params.notes }; @@ -182,7 +196,7 @@ router.delete("/", async (req, res) => { // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } @@ -234,7 +248,7 @@ router.post("/restore", async (req, res) => { // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index 93abd33..c30ea4f 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -1,9 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); -const checkAuth = global.utils.checkAuth; -const approveResources = global.utils.approveResources; - /** * POST - detach mounted disk from instance * request: @@ -25,12 +22,14 @@ router.post("/:disk/detach", async (req, res) => { vmid: req.params.vmid, disk: req.params.disk }; + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } + // disk must exist const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); if (!disk) { @@ -38,14 +37,19 @@ router.post("/:disk/detach", async (req, res) => { res.end(); return; } + // disk cannot be unused if (params.disk.includes("unused")) { res.status(500).send({ error: `Requested disk ${params.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.` }); res.end(); return; } + + // setup detach action const action = { delete: params.disk }; const method = params.type === "qemu" ? "POST" : "PUT"; + + // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); await global.pve.handleResponse(params.node, result, res); await global.pve.syncInstance(params.node, params.vmid); @@ -75,9 +79,10 @@ router.post("/:disk/attach", async (req, res) => { source: req.body.source, mp: req.body.mp }; + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } @@ -89,6 +94,7 @@ router.post("/:disk/attach", async (req, res) => { res.end(); return; } + // target disk must be allowed according to source disk's storage options const resourceConfig = global.config.resources; if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) { @@ -96,14 +102,10 @@ router.post("/:disk/attach", async (req, res) => { res.end(); return; } + // setup action using source disk info from vm config const action = {}; - if (params.type === "qemu") { - action[params.disk] = `${disk.file}`; - } - else if (params.type === "lxc") { - action[params.disk] = `${disk.file},mp=${params.mp},backup=1`; - } + action[params.disk] = params.type === "qemu" ? `${disk.file}` : `${disk.file},mp=${params.mp},backup=1`; const method = params.type === "qemu" ? "POST" : "PUT"; // commit action @@ -137,16 +139,22 @@ router.post("/:disk/resize", async (req, res) => { size: req.body.size }; - const userObj = global.utils.getUserObjFromUsername(req.cookies.username); + // attempt to parse user from username + const userObj = global.utils.getUserObjFromUsername(params.username); + if (userObj == null) { + res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); + } // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); 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 @@ -154,17 +162,20 @@ router.post("/:disk/resize", async (req, res) => { res.end(); return; } + // setup request const storage = disk.storage; // get the storage const request = {}; request[storage] = Number(params.size * 1024 ** 3); // setup request object + // check request approval - const { approved } = await approveResources(req, userObj, params.node, instance.pool, request); + const { approved } = await global.utils.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(); return; } + // action approved, commit to action const action = { disk: params.disk, size: `+${params.size}G` }; const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action); @@ -199,11 +210,15 @@ router.post("/:disk/move", async (req, res) => { delete: req.body.delete }; - const userObj = global.utils.getUserObjFromUsername(req.cookies.username); + // attempt to parse user from username + const userObj = global.utils.getUserObjFromUsername(params.username); + if (userObj == null) { + res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); + } // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } @@ -224,7 +239,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, params.node, instance.pool, request); + const { approved } = await global.utils.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(); @@ -268,7 +283,7 @@ router.delete("/:disk/delete", async (req, res) => { }; // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } @@ -321,15 +336,23 @@ router.post("/:disk/create", async (req, res) => { size: req.body.size, iso: req.body.iso }; - const userObj = global.utils.getUserObjFromUsername(req.cookies.username); + + // attempt to parse user from username + const userObj = global.utils.getUserObjFromUsername(params.username); + if (userObj == null) { + res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); + } + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); 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,13 +360,14 @@ router.post("/:disk/create", async (req, res) => { res.end(); return; } + // setup request const request = {}; - if (!params.disk.includes("ide")) { + if (!params.disk.includes("ide")) { // ignore resource request if the type is ide (iso file) // setup request request[params.storage] = Number(params.size * 1024 ** 3); // check request approval - const { approved } = await approveResources(req, userObj, params.node, instance.pool, request); + const { approved } = await global.utils.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(); @@ -357,6 +381,7 @@ router.post("/:disk/create", async (req, res) => { return; } } + // setup action const action = {}; if (params.disk.includes("ide") && params.iso) { @@ -369,6 +394,7 @@ router.post("/:disk/create", async (req, res) => { action[params.disk] = `${params.storage}:${params.size},mp=/${params.disk}/,backup=1`; } const method = params.type === "qemu" ? "POST" : "PUT"; + // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); await global.pve.handleResponse(params.node, result, res); diff --git a/src/routes/cluster/net.js b/src/routes/cluster/net.js index 876a912..70aa079 100644 --- a/src/routes/cluster/net.js +++ b/src/routes/cluster/net.js @@ -1,9 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const checkAuth = global.utils.checkAuth; -const approveResources = global.utils.approveResources; - /** * POST - create new virtual network interface * request: @@ -30,14 +27,17 @@ router.post("/:netid/create", async (req, res) => { rate: req.body.rate, name: req.body.name }; + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); 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) { @@ -50,17 +50,21 @@ router.post("/:netid/create", async (req, res) => { res.end(); return; } + + // setup request const request = { network: Number(params.rate) }; + // check resource approval const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - const { approved } = await approveResources(req, userObj, params.node, instance.pool, request); + const { approved } = await global.utils.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.access.getUser(userObj, req.cookies)).templates.network[params.type]; const action = {}; @@ -71,6 +75,7 @@ router.post("/:netid/create", async (req, res) => { action[`${params.netid}`] = `${nc.type},bridge=${nc.bridge},tag=${nc.vlan},rate=${params.rate}`; } const method = params.type === "qemu" ? "POST" : "PUT"; + // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); await global.pve.handleResponse(params.node, result, res); @@ -101,14 +106,17 @@ router.post("/:netid/modify", async (req, res) => { netid: req.params.netid, rate: req.body.rate }; + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); 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) { @@ -116,21 +124,26 @@ router.post("/:netid/modify", async (req, res) => { res.end(); return; } + + // setup request const request = { network: Number(params.rate) - Number(net.rate) }; + // check resource approval const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - const { approved } = await approveResources(req, userObj, params.node, instance.pool, request); + const { approved } = await global.utils.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 action = {}; action[`${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`); const method = params.type === "qemu" ? "POST" : "PUT"; + // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); await global.pve.handleResponse(params.node, result, res); @@ -158,12 +171,14 @@ router.delete("/:netid/delete", async (req, res) => { vmid: req.params.vmid, netid: req.params.netid }; + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } + // net interface must already exist const net = await global.pve.getNet(params.node, params.vmid, params.netid); if (!net) { @@ -171,10 +186,13 @@ router.delete("/:netid/delete", async (req, res) => { res.end(); return; } + // setup action + const action = { delete: `${params.netid}` }; const method = params.type === "qemu" ? "POST" : "PUT"; + // commit action - const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, { delete: `${params.netid}` }); + const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); await global.pve.handleResponse(params.node, result, res); await global.pve.syncInstance(params.node, params.vmid); }); diff --git a/src/routes/cluster/pci.js b/src/routes/cluster/pci.js index 5b84d79..1d51112 100644 --- a/src/routes/cluster/pci.js +++ b/src/routes/cluster/pci.js @@ -1,10 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const checkAuth = global.utils.checkAuth; -const getPoolResources = global.utils.getPoolResources; -const approveResources = global.utils.approveResources; - /** * GET - get available pcie devices for the given node and user * request: @@ -21,8 +17,9 @@ router.get("/", async (req, res) => { type: req.params.type, vmid: req.params.vmid, }; + // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } @@ -48,7 +45,7 @@ router.get("/", async (req, res) => { } // get remaining user resources - const poolAvailPci = (await getPoolResources(req, instance.pool)).pci.nodes[params.node]; // we assume that the node list is used. TODO support global lists + const poolAvailPci = (await global.utils.getPoolResources(req, instance.pool)).pci.nodes[params.node]; // we assume that the node list is used. TODO support global lists if (poolAvailPci === undefined) { // user has no available devices on this node, so send an empty list res.status(200).send([]); res.end(); @@ -93,12 +90,14 @@ router.get("/:hostpci", async (req, res) => { vmid: req.params.vmid, hostpci: req.params.hostpci }; + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } + // get device const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci); if (!device) { @@ -135,22 +134,27 @@ router.post("/:hostpci/modify", async (req, res) => { device: req.body.device, pcie: req.body.pcie }; + // check if type is qemu if (params.type !== "qemu") { res.status(500).send({ error: "Type must be qemu (vm)." }); res.end(); return; } + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); 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 const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); if (!existingDevice) { @@ -158,6 +162,7 @@ router.post("/:hostpci/modify", async (req, res) => { res.end(); return; } + // only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci const userObj = global.utils.getUserObjFromUsername(req.cookies.username); if (existingDevice.device_bus.split(".")[0] !== params.device) { @@ -171,7 +176,7 @@ router.post("/:hostpci/modify", async (req, res) => { return; } // check resource approval - const { approved } = await approveResources(req, userObj, params.node, instance.pool, request); + const { approved } = await global.utils.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(); @@ -184,9 +189,11 @@ router.post("/:hostpci/modify", async (req, res) => { return; } } + // setup action const action = {}; action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; + // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); await global.pve.handleResponse(params.node, result, res); @@ -217,22 +224,27 @@ router.post("/:hostpci/create", async (req, res) => { device: req.body.device, pcie: req.body.pcie }; + // check if type is qemu if (params.type !== "qemu") { res.status(500).send({ error: "Type must be qemu (vm)." }); res.end(); return; } + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); 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 const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); if (existingDevice) { @@ -240,27 +252,32 @@ router.post("/:hostpci/create", async (req, res) => { res.end(); return; } + // setup request const node = await global.pve.getNode(params.node); const requestedDevice = node.devices[`${params.device}`]; const request = { pci: requestedDevice.device_name }; + // check resource approval const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - const { approved } = await approveResources(req, userObj, params.node, instance.pool, request); + const { approved } = await global.utils.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(); return; } + // check node availability if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) { res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` }); res.end(); return; } + // setup action const action = {}; action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; + // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); await global.pve.handleResponse(params.node, result, res); @@ -288,18 +305,21 @@ router.delete("/:hostpci/delete", async (req, res) => { vmid: req.params.vmid, hostpci: req.params.hostpci }; + // check if type is qemu if (params.type !== "qemu") { res.status(500).send({ error: "Type must be qemu (vm)." }); res.end(); return; } + // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; - const auth = await checkAuth(req.cookies, res, vmpath); + const auth = await global.utils.checkAuth(req.cookies, res, vmpath); if (!auth) { return; } + // check device is in instance config const device = global.pve.getDevice(params.node, params.vmid, params.hostpci); if (!device) { @@ -307,8 +327,10 @@ router.delete("/:hostpci/delete", async (req, res) => { res.end(); return; } + // setup action const action = { delete: `${params.hostpci}` }; + // commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); await global.pve.handleResponse(params.node, result, res); diff --git a/src/routes/global.js b/src/routes/global.js index 652084e..2c461fe 100644 --- a/src/routes/global.js +++ b/src/routes/global.js @@ -12,11 +12,15 @@ router.get("/config/:key", async (req, res) => { const params = { key: req.params.key }; + // check auth const auth = await checkAuth(req.cookies, res); if (!auth) { return; } + + // check if users are allowed to get the config value + // return the value if so, otherwise send unauthorized const allowKeys = ["resources"]; if (allowKeys.includes(params.key)) { const config = global.config; diff --git a/src/routes/sync.js b/src/routes/sync.js index 322b5ee..9c6fdf1 100644 --- a/src/routes/sync.js +++ b/src/routes/sync.js @@ -2,11 +2,7 @@ import { WebSocketServer } from "ws"; import * as cookie from "cookie"; import { Router } from "express"; -export const router = Router({ mergeParams: true }); ; - -const checkAuth = global.utils.checkAuth; -const getObjectHash = global.utils.getObjectHash; -const getTimeLeft = global.utils.getTimeLeft; +export const router = Router({ mergeParams: true }); // maps usernames to socket object(s) const userSocketMap = {}; @@ -47,7 +43,7 @@ if (schemes.hash.enabled) { */ router.get("/hash", async (req, res) => { // check auth - const auth = await checkAuth(req.cookies, res); + const auth = await global.utils.checkAuth(req.cookies, res); if (!auth) { return; } @@ -55,7 +51,7 @@ if (schemes.hash.enabled) { 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)); + res.status(200).send(global.utils.getObjectHash(state)); }); console.log("clientsync: enabled hash sync"); } @@ -135,7 +131,7 @@ if (schemes.interrupt.enabled) { // AND if the next event trigger is more than the new rate in the future, // restart the timer with the new rate // avoids a large requested rate preventing a faster rate from being fulfilled - else if (rate < Math.min.apply(null, Object.values(requestedRates)) && getTimeLeft(timer) > rate) { + else if (rate < Math.min.apply(null, Object.values(requestedRates)) && global.utils.getTimeLeft(timer) > rate) { clearTimeout(timer); timer = setTimeout(handleInterruptSync, rate); const time = global.process.uptime(); @@ -259,7 +255,7 @@ function extractClusterState (status, resourceTypes, hashIndividual = false) { pool: resource.pool || null }; if (hashIndividual) { - const hash = getObjectHash(state[resource.id]); + const hash = global.utils.getObjectHash(state[resource.id]); state[resource.id].hash = hash; } } diff --git a/src/utils.js b/src/utils.js index aa916e2..671a7bf 100644 --- a/src/utils.js +++ b/src/utils.js @@ -230,16 +230,13 @@ export async function getPoolResources (req, pool) { export async function approveResources (req, user, node, pool, request) { const configResources = global.config.resources; const poolResources = await getPoolResources(req, pool); - // let approved = true; const reason = {}; for (const key in request) { // if requested resource is not specified in user resources, assume it's not allowed if (!(key in poolResources)) { - // approved = false; reason[key] = { approved: false, reason: `${key} not allowed` }; continue; - // return; } // use node specific quota if there is one available, otherwise use the global resource quota @@ -252,25 +249,21 @@ export async function approveResources (req, user, node, pool, request) { // 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 !== configResources[key].whitelist) { - // approved = false; reason[key] = { approved: false, reason: `${key} ${configResources[key].whitelist ? "not in whitelist" : "in blacklist"}` }; - // return; continue; } } + // if either the requested or avail resource is not strictly a number, block - else if (typeof (resourceData.avail) !== "number" || typeof (request[key]) !== "number") { - // approved = false; + if (typeof (resourceData.avail) !== "number" || typeof (request[key]) !== "number") { reason[key] = { approved: false, reason: `expected ${key} to be a number but got ${request[key]}` }; continue; - // return; } + // if the avail resources is less than the requested resources, block - else if (resourceData.avail - request[key] < 0) { - // approved = false; + if (resourceData.avail - request[key] < 0) { reason[key] = { approved: false, reason: `${key} requested ${request[key]} which is more than ${resourceData.avail} available` }; continue; - // return; } reason[key] = { approved: true, reason: "ok" }; @@ -340,7 +333,7 @@ export function readJSONFile (path) { }; /** - * + * Parse username into user object using the uid@realm format. * @param {*} username * @returns {Object | null} user object containing userid and realm or null if username format was invalid */ @@ -357,7 +350,7 @@ export function getUserObjFromUsername (username) { } /** - * + * Parse groupname into group object using the gid-realm format. * @param {*} groupname * @returns {Object | null} user object containing groupid and realm or null if groupname format was invalid */ @@ -382,7 +375,7 @@ export function getGroupObjFromGroupname (groupname) { } /** - * + * Inspect pool object and return true if pool contains any groups which contain the user object. * @param {Object} poolObj pool data object * @param {Object} userObj user object containing id and realm * @returns {boolean} true if userObj in poolObj