From 24ed6907c7b7e5a86846dc17dd32baadcec90a85 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sun, 24 May 2026 19:08:39 +0000 Subject: [PATCH 01/13] 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 --- .gitignore | 3 +- api/test_access.http | 21 +++ api/test_instance.http | 33 ++++ api/test_version.http | 2 + config/template.config.json | 119 ++----------- config/template.localdb.json | 143 ---------------- dev_config/eslint.config.mjs | 3 +- package.json | 2 +- src/backends/access_manager.js | 163 ++++++++++++++++++ src/backends/backends.js | 300 +++++++++++++-------------------- src/backends/localdb.js | 115 ------------- src/backends/paasldap.js | 184 -------------------- src/backends/pve.js | 146 +++++++++------- src/main.js | 9 - src/routes/access.js | 11 +- src/routes/access/groups.js | 26 +-- src/routes/access/pools.js | 76 +++++++++ src/routes/access/users.js | 25 +-- src/routes/cluster.js | 68 +++----- src/routes/cluster/backup.js | 17 +- src/routes/cluster/disk.js | 12 +- src/routes/cluster/net.js | 10 +- src/routes/cluster/pci.js | 8 +- src/routes/sync.js | 11 +- src/routes/user.js | 58 +------ src/utils.js | 198 +++++++++++++--------- 26 files changed, 708 insertions(+), 1055 deletions(-) create mode 100644 api/test_access.http create mode 100644 api/test_instance.http create mode 100644 api/test_version.http delete mode 100644 config/template.localdb.json create mode 100644 src/backends/access_manager.js delete mode 100644 src/backends/localdb.js delete mode 100644 src/backends/paasldap.js create mode 100644 src/routes/access/pools.js diff --git a/.gitignore b/.gitignore index e8142b2..6e7e310 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ **/node_modules **/localdb.json **/docs -**/config.json \ No newline at end of file +**/config.json +.vscode/settings.json \ No newline at end of file diff --git a/api/test_access.http b/api/test_access.http new file mode 100644 index 0000000..f2284c9 --- /dev/null +++ b/api/test_access.http @@ -0,0 +1,21 @@ +### Get ticket +POST {{baseUrl}}/access/ticket +Content-Type: application/x-www-form-urlencoded + +username={{username}} +&password={{password}} + +### Get user +GET {{baseUrl}}/access/users/{{username}} + +### Get group +GET {{baseUrl}}/access/groups/{{groupname}} + +### Get all pools +GET {{baseUrl}}/access/pools/ + +### Get pool +GET {{baseUrl}}/access/pools/{{poolname}} + +### Get all pools +GET {{baseUrl}}/cluster/nodes \ No newline at end of file diff --git a/api/test_instance.http b/api/test_instance.http new file mode 100644 index 0000000..ec9f309 --- /dev/null +++ b/api/test_instance.http @@ -0,0 +1,33 @@ +### Get ticket +POST {{baseUrl}}/access/ticket +Content-Type: application/x-www-form-urlencoded + +username={{username}} +&password={{password}} + +### Get instance resources +GET {{baseUrl}}/cluster/{{testvmpath}} + +### Test create instance + +POST {{baseUrl}}/cluster/{{testvmpath}}/create +Content-Type: application/x-www-form-urlencoded + +name=testvm +&pool={{poolname}} +&cores=8 +&memory=8192 + +### Test fail create instance + +POST {{baseUrl}}/cluster/{{testvmpath}}/create +Content-Type: application/x-www-form-urlencoded + +name=testvm +&pool={{poolname}} +&cores=9999 +&memory=8192 + +### Test delete instance + +DELETE {{baseUrl}}/cluster/{{testvmpath}}/delete \ No newline at end of file diff --git a/api/test_version.http b/api/test_version.http new file mode 100644 index 0000000..25e2a0d --- /dev/null +++ b/api/test_version.http @@ -0,0 +1,2 @@ +### Get version +GET {{baseUrl}}/version \ No newline at end of file diff --git a/config/template.config.json b/config/template.config.json index 55d2bba..af1ff14 100644 --- a/config/template.config.json +++ b/config/template.config.json @@ -1,4 +1,9 @@ { + "application": { + "hostname": "paas.mydomain.example", + "domain": "mydomain.example", + "listenPort": 8081 + }, "backends": { "pve": { "import": "pve.js", @@ -17,44 +22,16 @@ } } }, - "localdb": { - "import": "localdb.js", + "access_manager": { + "import": "access_manager.js", "config": { - "dbfile": "localdb.json" - } - }, - "paasldap": { - "import": "paasldap.js", - "config": { - "url": "http://paasldap.mydomain.example", - "realm": "ldap" + "url": "http://localhost:8083" } } }, "handlers": { - "instance": { - "pve": "pve" - }, - "users": { - "realm": { - "pve": [ - "localdb" - ], - "ldap": [ - "localdb", - "paasldap" - ] - }, - "any": [ - "localdb", - "paasldap" - ] - } - }, - "application": { - "hostname": "paas.mydomain.example", - "domain": "mydomain.example", - "listenPort": 8081 + "instance": "pve", + "users": ["access_manager"] }, "useriso": { "node": "examplenode1", @@ -160,81 +137,5 @@ "enabled": true } } - }, - "defaultuser": { - "resources": { - "cpu": { - "global": [], - "nodes": {} - }, - "cores": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "memory": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "swap": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "local": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "cephpl": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "network": { - "global": { - "max": 0 - }, - "nodes": {} - }, - "pci": { - "global": [], - "nodes": {} - } - }, - "nodes": [], - "cluster": { - "vmid": { - "min": -1, - "max": -1 - }, - "pool": "" - }, - "templates": { - "instances": { - "lxc": {}, - "qemu": {} - } - }, - "network": { - "lxc": { - "type": "veth", - "bridge": "vmbr0", - "vlan": 10, - "ip": "dhcp", - "ip6": "dhcp" - }, - "qemu": { - "type": "virtio", - "bridge": "vmbr0", - "vlan": 10 - } - } } } \ No newline at end of file diff --git a/config/template.localdb.json b/config/template.localdb.json deleted file mode 100644 index 7be43de..0000000 --- a/config/template.localdb.json +++ /dev/null @@ -1,143 +0,0 @@ -{ - "users": { - "exampleuser@auth": { - "resources": { - "cpu": { - "global": [ - { - "match": "kvm64", - "name": "kvm64", - "max": 1 - }, - { - "match": "host", - "name": "host", - "max": 1 - } - ], - "nodes": {} - }, - "cores": { - "global": { - "max": 128 - }, - "nodes": {} - }, - "memory": { - "global": { - "max": 137438953472 - }, - "nodes": {} - }, - "swap": { - "global": { - "max": 137438953472 - }, - "nodes": {} - }, - "local": { - "global": { - "max": 1099511627776 - }, - "nodes": {} - }, - "cephpl": { - "global": { - "max": 1099511627776 - }, - "nodes": {} - }, - "network": { - "global": { - "max": 100000 - }, - "nodes": {} - }, - "pci": { - "global": [], - "nodes": { - "example-node-0": [ - { - "match": "[device 1]", - "name": "Device 1", - "max": 1 - }, - { - "match": "[device 2]", - "name": "Device 2", - "max": 1 - } - ] - } - } - }, - "cluster": { - "admin": false, - "nodes": { - "example-node-0": true, - "example-node-1": true, - "example-node-2": true - }, - "vmid": { - "min": 100, - "max": 199 - }, - "pools": { - "example-pool-1": true, - "example-pool-2": true - }, - "backups": { - "max": 5 - } - }, - "templates": { - "instances": { - "lxc": { - "net0": { - "value": "name=eth0,bridge=vmbr0,ip=dhcp,ip6=dhcp,tag=10,type=veth,rate=1000", - "resource": { - "name": "network", - "amount": 1000 - } - } - }, - "qemu": { - "cpu": { - "value": "host", - "resource": null - }, - "machine": { - "value": "q35", - "resource": null - }, - "net0": { - "value": "virtio,bridge=vmbr0,tag=10,rate=1000", - "resource": { - "name": "network", - "amount": 1000 - } - }, - "scsihw": { - "value": "virtio-scsi-single", - "resource": null - } - } - }, - "network": { - "lxc": { - "type": "veth", - "bridge": "vmbr0", - "vlan": 10, - "ip": "dhcp", - "ip6": "dhcp" - }, - "qemu": { - "type": "virtio", - "bridge": "vmbr0", - "vlan": 10 - } - } - } - } - } -} \ No newline at end of file diff --git a/dev_config/eslint.config.mjs b/dev_config/eslint.config.mjs index 49bfa56..ee766d2 100644 --- a/dev_config/eslint.config.mjs +++ b/dev_config/eslint.config.mjs @@ -25,6 +25,7 @@ export default defineConfig([{ "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" - }] + }], + "prefer-const": ["error"] } }]); diff --git a/package.json b/package.json index 92a5e56..b131064 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "proxmoxaas-api", - "version": "1.0.0", + "version": "2.0.0", "description": "REST API for ProxmoxAAS", "main": "src/main.js", "type": "module", diff --git a/src/backends/access_manager.js b/src/backends/access_manager.js new file mode 100644 index 0000000..2143652 --- /dev/null +++ b/src/backends/access_manager.js @@ -0,0 +1,163 @@ +import axios from "axios"; +import { ACCESS_BACKEND } from "./backends.js"; +import * as setCookie from "set-cookie-parser"; + +export default class ACCESS_MANAGER_API extends ACCESS_BACKEND { + #apiURL = null; + + constructor (config) { + super(); + this.#apiURL = config.url; + } + + /** + * Send HTTP request to paas-LDAP API. + * @param {*} path HTTP path, prepended with the paas-LDAP API base url + * @param {*} method HTTP method + * @param {*} auth HTTP auth cookies + * @param {*} body body parameters and data to be sent. Optional. + * @returns {Object} HTTP response object + */ + async #requestAPI (path, method, auth = null, body = null) { + const url = `${this.#apiURL}${path}`; + const content = { + method, + mode: "cors", + credentials: "include", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + data: body + }; + + if (auth) { + content.headers.Cookie = `PAASAccessManagerTicket=${auth.PAASAccessManagerTicket};`; + } + + try { + const result = await axios.request(url, content); + return { + ok: result.status === 200, + status: result.status, + data: result.data, + headers: result.headers + }; + } + catch (error) { + console.log(`access: error ocuured in access.requestAPI: ${error}`); + const result = error.response; + result.ok = result.status === 200; + return result; + } + } + + async openSession (user, password) { + const credentials = { username: `${user.id}@${user.realm}`, password }; + const response = await this.#requestAPI("/ticket", "POST", null, credentials); + if (response.ok) { + const cookies = setCookie.parse(response.headers["set-cookie"]); + cookies.forEach((e) => { + e.expiresMSFromNow = e.expires - Date.now(); + }); + return { + ok: true, + status: response.status, + message: "", + cookies + }; + } + else { + return { + ok: false, + status: response.status, + message: response.data.error, + cookies: [] + }; + } + } + + async closeSession(tokens) { + const response = await this.#requestAPI("/ticket", "DELETE", tokens); + return response; + } + + async addUser (user, attributes, params) {} + + async getUser (user, params) { + const response = await this.#requestAPI(`/users/${user.id}@${user.realm}`, "GET", params); + if (response.ok) { // if ok, return user data + return { + ok: true, + status: response.status, + user: response.data.user + }; + } + else { // else return null + return { + ok: false, + status: response.status, + message: response.data.error + }; + } + } + + async setUser (user, attributes, params) {} + + async delUser (user, params) {} + + async addGroup (group, attributes, params) {} + + async getGroup (group, params) { + const response = await this.#requestAPI(`/groups/${group.id}-${group.realm}`, "GET", params); + if (response.ok) { // if ok, return user data + return { + ok: true, + status: response.status, + group: response.data.group + }; + } + else { // else return null + return { + ok: false, + status: response.status, + message: response.data.error + }; + } + } + + async setGroup (group, attributes, params) {} + + async delGroup (group, attributes, params) {} + + async addUserToGroup (user, group, params) {} + + async delUserFromGroup (user, group, params) {} + + async addPool (pool, attributes, params) {} + + async getPool (pool, params) { + const response = await this.#requestAPI(`/pools/${pool}`, "GET", params); + if (response.ok) { // if ok, return user data + return { + ok: true, + status: response.status, + pool: response.data.pool + }; + } + else { // else return null + return { + ok: false, + status: response.status, + message: response.data.error + }; + } + } + + async setPool (pool, attributes, params) {} + + async delPool (pool, params) {} + + async addGroupToPool (group, pool, params) {} + + async delGroupFromPool (group, pool, params) {} +} \ No newline at end of file diff --git a/src/backends/backends.js b/src/backends/backends.js index 1830183..e9f9bf0 100644 --- a/src/backends/backends.js +++ b/src/backends/backends.js @@ -17,43 +17,10 @@ export default async () => { global.backends[name] = new Backend(config); console.log(`backends: initialized backend ${name} from ${importPath}`); } - global.pve = global.backends[global.config.handlers.instance.pve]; - global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users); + global.pve = global.backends[global.config.handlers.instance]; + global.access = global.backends[global.config.handlers.users]; }; -/** - * Interface for all backend types. Contains only two methods for opening and closing a session with the backend. - * Users will recieve tokens from all backends when first authenticating and will delete tokens when logging out. - */ -class BACKEND { - /** - * Opens a session with the backend and creates session tokens if needed - * @param {{id: string, realm: string}} user object containing id and realm - * @param {string} password - * @returns {{ok: boolean, status: number, message: string, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value - */ - openSession (user, password) { - return { - ok: true, - status: 200, - message: "", - cookies: [] - }; - } - - /** - * Closes an opened session with the backend if needed - * @param {{name: string, value: string}[]} token list of session token objects with token name and value, may include irrelevant tokens for a specific backend - * @returns {boolean} true if session was closed successfully, false otherwise - */ - closeSession (tokens) { - return { - ok: true, - status: 200 - }; - } -} - export class AtomicChange { constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) { this.valid = valid; @@ -76,10 +43,43 @@ export function doNothingCallback (delta) { } /** - * Interface for backend types that store/interact with user & group data. + * Interface for all backend types. Contains only two methods for opening and closing a session with the backend. + * Users will recieve tokens from all backends when first authenticating and will delete tokens when logging out. + */ +export class BACKEND { + /** + * Opens a session with the backend and creates session tokens if needed + * @param {{id: string, realm: string}} user object containing id and realm + * @param {string} password + * @returns {{ok: boolean, status: number, message: string, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value + */ + async openSession (user, password) { + return { + ok: true, + status: 200, + message: "", + cookies: [] + }; + } + + /** + * Closes an opened session with the backend if needed + * @param {{name: string, value: string}[]} token list of session token objects with token name and value, may include irrelevant tokens for a specific backend + * @returns {boolean} true if session was closed successfully, false otherwise + */ + async closeSession (tokens) { + return { + ok: true, + status: 200 + }; + } +} + +/** + * Interface for backend types that store/interact with user, group, and pool data. * Not all backends need to implement all interface methods. */ -class USER_BACKEND extends BACKEND { +export class ACCESS_BACKEND extends BACKEND { /** * Validate an add user operation with the following parameters. * Returns whether the change is valid and a delta object to be used in the operation. @@ -88,7 +88,7 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies * @returns {AtomicChange} atomic change object */ - addUser (user, attributes, params) {} + async addUser (user, attributes, params) {} /** * Get user from backend @@ -96,14 +96,7 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies * @returns {Object} containing user data from this backend, null if user does not exist */ - getUser (user, params) {} - - /** - * Get all users from backend - * @param {Object} params authentication params, usually req.cookies - * @returns {Array} containing each user data from this backend - */ - getAllUsers (params) {} + async getUser (user, params) {} /** * Validate a set user operation with the following parameters. @@ -113,7 +106,7 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies * @returns {AtomicChange} atomic change object */ - setUser (user, attributes, params) {} + async setUser (user, attributes, params) {} /** * Validate a delete user operation with the following parameters. @@ -122,7 +115,7 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies * @returns {AtomicChange} atomic change object */ - delUser (user, params) {} + async delUser (user, params) {} /** * Validate an add group operation with the following parameters. @@ -132,22 +125,15 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies * @returns {AtomicChange} atomic change object */ - addGroup (group, attributes, params) {} + async addGroup (group, attributes, params) {} /** * Get group from backend - * @param {{id: string}} group + * @param {{id: string, realm: string}} group * @param {Object} params authentication params, usually req.cookies * @returns {Object} containing group data from this backend, null if user does not exist */ - getGroup (group, params) {} - - /** - * Get all users from backend - * @param {Object} params authentication params, usually req.cookies - * @returns {Array} containing each group data from this backend - */ - getAllGroups (params) {} + async getGroup (group, params) {} /** * Validate a set group operation with the following parameters. @@ -157,35 +143,93 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies * @returns {AtomicChange} atomic change object */ - setGroup (group, attributes, params) {} + async setGroup (group, attributes, params) {} + /** * Validate a del group operation with the following parameters. * Returns whether the change is valid and a delta object to be used in the operation. * @param {{id: string, realm: string}} group * @param {Object} params authentication params, usually req.cookies - * @returns {AtomicChange} atomic change object + * @returns {AtomicChange} atomic change object */ - delGroup (group, attributes, params) {} + async delGroup (group, attributes, params) {} /** * Validate an add user to group operation with the following parameters. * Returns whether the change is valid and a delta object to be used in the operation. * @param {{id: string, realm: string}} user - * @param {{id: string}} group + * @param {{id: string, realm: string}} group * @param {Object} params authentication params, usually req.cookies * @returns {AtomicChange} atomic change object */ - addUserToGroup (user, group, params) {} + async addUserToGroup (user, group, params) {} /** * Validate a remove user from group operation with the following parameters. * Returns whether the change is valid and a delta object to be used in the operation. * @param {{id: string, realm: string}} user - * @param {{id: string}} group + * @param {{id: string, realm: string}} group * @param {Object} params authentication params, usually req.cookies * @returns {AtomicChange} atomic change object */ - delUserFromGroup (user, group, params) {} + async delUserFromGroup (user, group, params) {} + + /** + * Validate an add pool operation with the following parameters. + * Returns whether the change is valid and a delta object to be used in the operation. + * @param {{id: string, realm: string}} pool + * @param {Object} attributes pool attributes + * @param {Object} params authentication params, usually req.cookies + * @returns {AtomicChange} atomic change object + */ + async addPool (pool, attributes, params) {} + + /** + * Get pool from backend + * @param {string} pool + * @param {Object} params authentication params, usually req.cookies + * @returns {Object} containing pool data from this backend, null if poll does not exist + */ + async getPool (pool, params) {} + + /** + * Validate a set pool operation with the following parameters. + * Returns whether the change is valid and a delta object to be used in the operation. + * @param {string} pool + * @param {Object} attributes pool attributes + * @param {Object} params authentication params, usually req.cookies + * @returns {AtomicChange} atomic change object + */ + async setPool (pool, attributes, params) {} + + /** + * Validate a del pool operation with the following parameters. + * Returns whether the change is valid and a delta object to be used in the operation. + * @param {string} pool + * @param {Object} params authentication params, usually req.cookies + * @returns {AtomicChange} atomic change object + */ + async delPool (pool, params) {} + + /** + * Validate an add group to pool operation with the following parameters. + * Returns whether the change is valid and a delta object to be used in the operation. + * @param {{id: string, realm: string}} group + * @param {string} pool + * @param {Object} params authentication params, usually req.cookies + * @returns {AtomicChange} atomic change object + */ + async addGroupToPool (group, pool, params) {} + + /** + * Validate a remove group from pool operation with the following parameters. + * Returns whether the change is valid and a delta object to be used in the operation. + * @param {{id: string, realm: string}} group + * @param {string} pool + * @param {Object} params authentication params, usually req.cookies + * @returns {AtomicChange} atomic change object + */ + async delGroupFromPool (group, pool, params) {} } /** @@ -198,13 +242,13 @@ export class PVE_BACKEND extends BACKEND { * @param {string} node node id * @returns {} */ - getNode (node) {} + async getNode (node) {} /** * Send a signal to synchronize a node after some change has been made. * * @param {string} node node id */ - syncNode (node) {} + async syncNode (node) {} /** * Get and return instance data. @@ -213,14 +257,14 @@ export class PVE_BACKEND extends BACKEND { * @param {string} type instance type * @param {string} vmid instance id */ - getInstance (node, type, instance) {} + async getInstance (node, type, instance) {} /** * Send a signal to synchronize an instance after some change has been made. * @param {string} node node id * @param {string} instance instance id */ - syncInstance (node, instance) {} + async syncInstance (node, instance) {} /** * Get meta data for a specific disk. Adds info that is not normally available in a instance's config. @@ -250,122 +294,10 @@ export class PVE_BACKEND extends BACKEND { async getDevice (node, instance, deviceid) {} /** - * Get user resource data including used, available, and maximum resources. - * @param {{id: string, realm: string}} user object of user to get resource data. + * Get pool resource data including used, available, and maximum resources. + * @param {string} pool * @param {Object} cookies object containing k-v store of cookies * @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user. */ - getUserResources (user, cookies) {} -} - -/** - * Interface for user database backends. - */ -export class DB_BACKEND extends USER_BACKEND {} - -/** - * Interface for user auth backends. - */ -export class AUTH_BACKEND extends USER_BACKEND {} - -/** - * Interface combining all user backends into a single interface - * Calling methods will also call sub handler methods - */ -class USER_BACKEND_MANAGER extends USER_BACKEND { - #config = null; - - constructor (config) { - super(); - this.#config = config; - } - - getBackendsByUser (user) { - if (user != null) { - return this.#config.realm[user.realm]; - } - else { - return null; - } - } - - addUser (user, attributes, params) {} - - async getUser (user, params) { - let userData = {}; - for (const backend of this.#config.realm[user.realm]) { - const backendData = await global.backends[backend].getUser(user, params); - if (backendData) { - userData = { ...backendData, ...userData }; - } - } - return userData; - } - - async getAllUsers (params) { - const userData = {}; - for (const backend of this.#config.any) { - const backendData = await global.backends[backend].getAllUsers(params); - if (backendData) { - for (const user of Object.keys(backendData)) { - userData[user] = { ...backendData[user], ...userData[user] }; - } - } - } - return userData; - } - - async setUser (user, attributes, params) { - const atomicChanges = []; - for (const backend of this.#config.realm[user.realm]) { - const atomicChange = await global.backends[backend].setUser(user, attributes, params); - if (atomicChange.valid === false) { // if any fails, preemptively exit - return atomicChange.status; - } - atomicChanges.push(atomicChange); // queue callback into array - } - const response = { - ok: true, - status: 200, - message: "", - allResponses: [] - }; - for (const atomicChange of atomicChanges) { - const atomicResponse = await atomicChange.commit(); - if (atomicResponse.ok === false) { - response.ok = false; - response.status = atomicResponse.status; - response.message = atomicResponse.message; - } - response.allResponses.push(); // execute callback - } - return response; - } - - delUser (user, params) {} - - addGroup (group, attributes, params) {} - - getGroup (group, params) {} - - async getAllGroups (params) { - const groupData = {}; - for (const backend of this.#config.any) { - const backendData = await global.backends[backend].getAllGroups(params); - if (backendData) { - for (const group of Object.keys(backendData)) { - groupData[group] = { ...backendData[group], ...groupData[group] }; - } - } - } - return groupData; - } - - setGroup (group, attributes, params) {} - - delGroup (group, params) {} - - addUserToGroup (user, group, params) {} - - delUserFromGroup (user, group, params) {} + async getPoolResources (user, cookies) {} } diff --git a/src/backends/localdb.js b/src/backends/localdb.js deleted file mode 100644 index 6437e17..0000000 --- a/src/backends/localdb.js +++ /dev/null @@ -1,115 +0,0 @@ -import { readFileSync, writeFileSync } from "fs"; -import { exit } from "process"; -import { AtomicChange, DB_BACKEND, doNothingCallback } from "./backends.js"; - -export default class LocalDB extends DB_BACKEND { - #path = null; - #data = null; - //#defaultuser = null; - - constructor (config) { - super(); - const path = config.dbfile; - try { - this.#path = path; - this.#load(); - //this.#defaultuser = global.config.defaultuser; - } - catch { - console.log(`error: ${path} was not found. Please follow the directions in the README to initialize localdb.json.`); - exit(1); - } - } - - /** - * Load db from local file system. Reads from file path store in path. - */ - #load () { - this.#data = JSON.parse(readFileSync(this.#path)); - } - - /** - * Save db to local file system. Saves to file path stored in path. - */ - #save () { - writeFileSync(this.#path, JSON.stringify(this.#data)); - } - - addUser (user, attributes, params) {} - - getUser (user, params) { - const requestedUser = `${user.id}@${user.realm}`; - const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token - // user can access a user's db data if they are an admin OR are requesting own data - const authorized = this.#data.users[requestingUser].cluster.admin || requestingUser === requestedUser; - if (authorized && this.#data.users[requestedUser]) { - return this.#data.users[requestedUser]; - } - else { - return null; - } - } - - async getAllUsers (params) { - const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token - if (this.#data.users[requestingUser].cluster.admin === true) { - return this.#data.users; - } - else { - return null; - } - } - - setUser (user, attributes, params) { - if (attributes.resources && attributes.cluster && attributes.templates) { - const username = `${user.id}@${user.realm}`; - if (this.#data.users[username]) { - if (this.#data.users[params.username] && this.#data.users[params.username].cluster.admin) { - return new AtomicChange(false, - { - username, - attributes: { - resources: attributes.resources, - cluster: attributes.cluster, - templates: attributes.templates - } - }, - (delta) => { - this.#data.users[delta.username] = delta.attributes; - this.#save(); - return { ok: true, status: 200, message: "" }; - }, - { ok: true, status: 200, message: "" } - ); - } - else { - return new AtomicChange(false, {}, doNothingCallback, { ok: false, status: 401, message: `${params.username} is not an admin user in localdb` }); - } - } - else { - return new AtomicChange(false, {}, doNothingCallback, { ok: false, status: 400, message: `${username} was not found in localdb` }); - } - } - else { - return new AtomicChange(true, {}, doNothingCallback, null); - } - } - - delUser (user, params) {} - - // group methods not implemented because db backend does not store groups - addGroup (group, atrributes, params) {} - getGroup (group, params) {} - getAllGroups (params) { - return null; - } - - setGroup (group, attributes, params) {} - delGroup (group, params) {} - - // assume that adding to group also adds to group's pool - addUserToGroup (user, group, params) {} - - // assume that adding to group also adds to group's pool - delUserFromGroup (user, group, params) {} -} diff --git a/src/backends/paasldap.js b/src/backends/paasldap.js deleted file mode 100644 index d2779a0..0000000 --- a/src/backends/paasldap.js +++ /dev/null @@ -1,184 +0,0 @@ -import axios from "axios"; -import { AtomicChange, AUTH_BACKEND, doNothingCallback } from "./backends.js"; -import * as setCookie from "set-cookie-parser"; - -export default class PAASLDAP extends AUTH_BACKEND { - #url = null; - #realm = null; - - constructor (config) { - super(); - this.#url = config.url; - this.#realm = config.realm; - } - - /** - * Send HTTP request to paas-LDAP API. - * @param {*} path HTTP path, prepended with the paas-LDAP API base url - * @param {*} method HTTP method - * @param {*} body body parameters and data to be sent. Optional. - * @returns {Object} HTTP response object - */ - async #request (path, method, auth = null, body = null) { - const url = `${this.#url}${path}`; - const content = { - method, - mode: "cors", - credentials: "include", - headers: { - "Content-Type": "application/x-www-form-urlencoded" - }, - data: body - }; - - if (auth) { - content.headers.Cookie = `PAASLDAPAuthTicket=${auth.PAASLDAPAuthTicket};`; - } - - try { - const result = await axios.request(url, content); - result.ok = result.status === 200; - return result; - } - catch (error) { - const result = error.response; - result.ok = result.status === 200; - return result; - } - } - - #handleGenericReturn (res) { - return { - ok: res.ok, - status: res.status, - message: res.ok ? "" : res.data.error - }; - } - - async openSession (user, password) { - const username = user.id; - const content = { username, password }; - const result = await this.#request("/ticket", "POST", null, content); - if (result.ok) { - const cookies = setCookie.parse(result.headers["set-cookie"]); - cookies.forEach((e) => { - e.expiresMSFromNow = e.expires - Date.now(); - }); - return { - ok: true, - status: result.status, - message: "", - cookies - }; - } - else { - return { - ok: false, - status: result.status, - message: result.data.error, - cookies: [] - }; - } - } - - async addUser (user, attributes, params) {} - - async getUser (user, params) { - if (!params) { // params required, do nothing if params are missing - return null; - } - const res = await this.#request(`/users/${user.id}`, "GET", params); - if (res.ok) { // if ok, return user data - return res.data.user; - } - else { // else return null - return null; - } - } - - async getAllUsers (params) { - if (!params) { - return null; - } - const res = await this.#request("/users", "GET", params); - if (res.ok) { // if ok, return user data - const users = res.data.users; - const usersFormatted = {}; - // label each user object by user@realm - for (const user of users) { - usersFormatted[`${user.attributes.uid}@${this.#realm}`] = user; - } - return usersFormatted; - } - else { // else return null - return null; - } - } - - async setUser (user, attributes, params) { - if (!attributes.userpassword && !attributes.cn && attributes.sn) { - return new AtomicChange(true, {}, doNothingCallback, null); // change has no ldap attributes - } - const ldapAttributes = {}; - if (attributes.userpassword) { - ldapAttributes.userpassword = attributes.userpassword; - } - if (attributes.cn) { - ldapAttributes.cn = attributes.cn; - } - if (attributes.sn) { - ldapAttributes.sn = attributes.sn; - } - return new AtomicChange( - true, - { - user, - ldapAttributes, - params - }, - async (delta) => { - const res = await this.#request(`/users/${delta.user.id}`, "POST", delta.params, delta.ldapAttributes); - return this.#handleGenericReturn(res); - }, - { ok: true, status: 200, message: "" } - ); - } - - async delUser (user, params) {} - - async addGroup (group, attributes, params) {} - - async getGroup (group, params) { - return await this.#request(`/groups/${group.id}`, "GET", params); - } - - async getAllGroups (params) { - if (!params) { - return null; - } - const res = await this.#request("/groups", "GET", params); - if (res.ok) { // if ok, return user data - const groups = res.data.groups; - const groupsFormatted = {}; - // label each user object by user@realm - for (const group of groups) { - groupsFormatted[`${group.attributes.cn}@${this.#realm}`] = group; - } - return groupsFormatted; - } - else { // else return null - return null; - } - } - - async setGroup (group, attributes, params) { - // not implemented, LDAP groups do not have any attributes to change - return new AtomicChange(true, {}, doNothingCallback, null); ; - } - - async delGroup (group, params) {} - - async addUserToGroup (user, group, params) {} - - async delUserFromGroup (user, group, params) {} -} diff --git a/src/backends/pve.js b/src/backends/pve.js index 6af5230..5ec3e99 100644 --- a/src/backends/pve.js +++ b/src/backends/pve.js @@ -26,8 +26,8 @@ export default class PVE extends PVE_BACKEND { cookies: [] }; } - const ticket = response.data.data.ticket; - const csrftoken = response.data.data.CSRFPreventionToken; + const ticket = response.data.ticket; + const csrftoken = response.data.CSRFPreventionToken; return { ok: true, status: response.status, @@ -66,73 +66,39 @@ export default class PVE extends PVE_BACKEND { data: body }; - if (auth && auth.cookies) { + if (auth && auth.cookies) { // user cookie credentials content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken; content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; CSRFPreventionToken=${auth.cookies.CSRFPreventionToken}`; } - else if (auth && auth.token) { + else if (auth && auth.token) { // upgraded request as api const token = this.#pveAPIToken; content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`; } - else if (auth && auth.root) { - const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, this.#pveRoot); + else if (auth && auth.root) { // upgraded request as root + const rootauth = await this.requestPVE("/access/ticket", "POST", null, this.#pveRoot); if (!(rootauth.status === 200)) { return rootauth.response; } - const rootcookie = rootauth.data.data.ticket; - const rootcsrf = rootauth.data.data.CSRFPreventionToken; + const rootcookie = rootauth.data.ticket; + const rootcsrf = rootauth.data.CSRFPreventionToken; content.headers.CSRFPreventionToken = rootcsrf; content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`; } try { - return await axios.request(url, content); + const result = await axios.request(url, content); + return { + ok: result.ok, + status: result.status, + data: result.data.data, // pve returns {data: {data: {...}}}, unwrap here to conform to standard {data: {...}} format + headers: result.headers + }; } catch (error) { - console.log(`backends: error ocuured in pve.requestPVE: ${error}`); - return error.response; - } - } - - /** - * Handle various proxmox API responses. Handles sync and async responses. - * In sync responses, responses are completed when the response arrives. Method returns the response directly. - * In async responses, proxmox sends responses with a UPID to track process completion. Method returns the status of the proxmox process once it completes. - * @param {string} node response originates from. - * @param {Object} result response from proxmox. - * @param {Object} res response object of ProxmoxAAS API call. - */ - async handleResponse (node, result, res) { - const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); - if (result.status !== 200) { - res.status(result.status).send({ error: result.statusText }); - res.end(); - } - else if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) { - const upid = result.data.data; - let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); - while (taskStatus.data.data.status !== "stopped") { - await waitFor(100); - taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); - } - if (taskStatus.data.data.exitstatus === "OK") { - const result = taskStatus.data.data; - const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true }); - result.log = taskLog.data.data; - res.status(200).send(result); - res.end(); - } - else { - const result = taskStatus.data.data; - const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true }); - result.log = taskLog.data.data; - res.status(500).send(result); - res.end(); - } - } - else { - res.status(result.status).send(result.data); - res.end(); + console.log(`pve: error ocuured in pve.requestPVE: ${error}`); + const result = error.response; + result.ok = result.status === 200; + return result; } } @@ -164,6 +130,48 @@ export default class PVE extends PVE_BACKEND { } } + /** + * Handle various proxmox API responses. Handles sync and async responses. + * In sync responses, responses are completed when the response arrives. Method returns the response directly. + * In async responses, proxmox sends responses with a UPID to track process completion. Method returns the status of the proxmox process once it completes. + * @param {string} node response originates from. + * @param {Object} result response from proxmox. + * @param {Object} res response object of ProxmoxAAS API call. + */ + async handleResponse (node, result, res) { + const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); + if (result.status !== 200) { + res.status(result.status).send({ error: result.statusText }); + res.end(); + } + else if (result.data && typeof (result.data) === "string" && result.data.startsWith("UPID:")) { + const upid = result.data; + let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); + while (taskStatus.data.status !== "stopped") { + await waitFor(100); + taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); + } + if (taskStatus.data.exitstatus === "OK") { + const result = taskStatus.data; + const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true }); + result.log = taskLog.data; + res.status(200).send(result); + res.end(); + } + else { + const result = taskStatus.data; + const taskLog = await this.requestPVE(`/nodes/${node}/tasks/${upid}/log`, "GET", { token: true }); + result.log = taskLog.data; + res.status(500).send(result); + res.end(); + } + } + else { + res.status(result.status).send(result.data); + res.end(); + } + } + async getNode (node) { const res = await this.requestFabric(`/nodes/${node}`, "GET"); if (res.status !== 200) { @@ -222,26 +230,36 @@ export default class PVE extends PVE_BACKEND { } } - async getUserResources (user, cookies) { - // get user resources with vm filter - const res = await this.requestPVE("/cluster/resources?type=vm", "GET", { cookies }); + async getPoolResources (cookies, pool) { + // get pool resources + const res = await this.requestPVE(`/pools/?poolid=${pool}`, "GET", { cookies }); if (res.status !== 200) { return null; } + const data = res.data; + if (data.length != 1) { + return null; + } + const poolPVE = data[0]; + if (poolPVE.poolid != pool) { + return null; + } - const userPVEResources = res.data.data; - + const poolPVEResources = poolPVE.members; const resources = {}; // for each resource, add to the object - for (const resource of userPVEResources) { - const instance = await this.getInstance(resource.node, resource.vmid); - if (instance) { - instance.node = resource.node; - resources[resource.vmid] = instance; + for (const resource of poolPVEResources) { + // only add type if it is vm or ct (ie has vmid) + if (resource.vmid) { + const instance = await this.getInstance(resource.node, resource.vmid); + if (instance) { + instance.node = resource.node; + resources[resource.vmid] = instance; + } } } - + return resources; } } diff --git a/src/main.js b/src/main.js index f3c3154..c0ac9c0 100644 --- a/src/main.js +++ b/src/main.js @@ -41,12 +41,3 @@ global.utils.recursiveImportRoutes(app, "/api", "routes"); app.get("/api/version", (req, res) => { res.status(200).send({ version: global.package.version }); }); - -/** - * GET - echo request - * responses: - * - 200: {body: request.body, cookies: request.cookies} - */ -app.get("/api/echo", (req, res) => { - res.status(200).send({ body: req.body, cookies: req.cookies }); -}); diff --git a/src/routes/access.js b/src/routes/access.js index d9e47c8..4b86248 100644 --- a/src/routes/access.js +++ b/src/routes/access.js @@ -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); }); diff --git a/src/routes/access/groups.js b/src/routes/access/groups.js index 7bd4de1..f0330b8 100644 --- a/src/routes/access/groups.js +++ b/src/routes/access/groups.js @@ -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 }); }); diff --git a/src/routes/access/pools.js b/src/routes/access/pools.js new file mode 100644 index 0000000..a0b8334 --- /dev/null +++ b/src/routes/access/pools.js @@ -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 }); +}); diff --git a/src/routes/access/users.js b/src/routes/access/users.js index 21f9ce9..c17f228 100644 --- a/src/routes/access/users.js +++ b/src/routes/access/users.js @@ -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 }); }); diff --git a/src/routes/cluster.js b/src/routes/cluster.js index 664e1c9..29567c3 100644 --- a/src/routes/cluster.js +++ b/src/routes/cluster.js @@ -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; diff --git a/src/routes/cluster/backup.js b/src/routes/cluster/backup.js index caf0b2a..fdcc07d 100644 --- a/src/routes/cluster/backup.js +++ b/src/routes/cluster/backup.js @@ -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(); } diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index 636170b..93abd33 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -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(); diff --git a/src/routes/cluster/net.js b/src/routes/cluster/net.js index 2abd81b..876a912 100644 --- a/src/routes/cluster/net.js +++ b/src/routes/cluster/net.js @@ -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(); diff --git a/src/routes/cluster/pci.js b/src/routes/cluster/pci.js index 138a4b5..2d9f20a 100644 --- a/src/routes/cluster/pci.js +++ b/src/routes/cluster/pci.js @@ -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(); diff --git a/src/routes/sync.js b/src/routes/sync.js index cd379c7..322b5ee 100644 --- a/src/routes/sync.js +++ b/src/routes/sync.js @@ -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 diff --git a/src/routes/user.js b/src/routes/user.js index 619413b..4eb80ca 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -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/`, ""); diff --git a/src/utils.js b/src/utils.js index cc1a9a0..b076247 100644 --- a/src/utils.js +++ b/src/utils.js @@ -36,7 +36,7 @@ export async function checkAuth (cookies, res, vmpath = null) { return false; } - if ((await global.userManager.getUser(userObj, cookies)) === null) { // check if user exists in database + if ((await global.access.getUser(userObj, cookies)) === null) { // check if user exists in database res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in database.` }); res.end(); return false; @@ -60,43 +60,44 @@ export async function checkAuth (cookies, res, vmpath = null) { } /** - * Get user resource data including used, available, and maximum resources. + * Get pool resource data including used, available, and maximum resources. * @param {Object} req ProxmoxAAS API request object. * @param {{id: string, realm: string}} user object 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, user) { - const dbResources = global.config.resources; - const userResources = (await global.userManager.getUser(user, req.cookies)).resources; +export async function getPoolResources (req, pool) { + const configResources = global.config.resources; + const poolConfig = await global.access.getPool(pool, req.cookies); + const poolResources = poolConfig.pool.resources; - // setup the user resource object with used and avail for each resource and each resource pool + // setup the pool 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) => { + for (const resourceName of Object.keys(poolResources)) { + if (configResources[resourceName].type === "list") { + poolResources[resourceName].total = []; + poolResources[resourceName].global.forEach((e) => { e.used = 0; e.avail = e.max; - const index = userResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); + const index = poolResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); if (index === -1) { - userResources[resourceName].total.push(structuredClone(e)); + poolResources[resourceName].total.push(structuredClone(e)); } else { - userResources[resourceName].total[index].max += e.max; - userResources[resourceName].total[index].avail += e.avail; + poolResources[resourceName].total[index].max += e.max; + poolResources[resourceName].total[index].avail += e.avail; } }); - for (const nodeName of Object.keys(userResources[resourceName].nodes)) { - userResources[resourceName].nodes[nodeName].forEach((e) => { + for (const nodeName of Object.keys(poolResources[resourceName].nodes)) { + poolResources[resourceName].nodes[nodeName].forEach((e) => { e.used = 0; e.avail = e.max; - const index = userResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); + const index = poolResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); if (index === -1) { - userResources[resourceName].total.push(structuredClone(e)); + poolResources[resourceName].total.push(structuredClone(e)); } else { - userResources[resourceName].total[index].max += e.max; - userResources[resourceName].total[index].avail += e.avail; + poolResources[resourceName].total[index].max += e.max; + poolResources[resourceName].total[index].avail += e.avail; } }); } @@ -107,21 +108,21 @@ export async function getUserResources (req, user) { 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; + poolResources[resourceName].global.used = 0; + poolResources[resourceName].global.avail = poolResources[resourceName].global.max; + total.max += poolResources[resourceName].global.max; + total.avail += poolResources[resourceName].global.avail; + for (const nodeName of Object.keys(poolResources[resourceName].nodes)) { + poolResources[resourceName].nodes[nodeName].used = 0; + poolResources[resourceName].nodes[nodeName].avail = poolResources[resourceName].nodes[nodeName].max; + total.max += poolResources[resourceName].nodes[nodeName].max; + total.avail += poolResources[resourceName].nodes[nodeName].avail; } - userResources[resourceName].total = total; + poolResources[resourceName].total = total; } } - const configs = await global.pve.getUserResources(user, req.cookies); + const configs = await global.pve.getPoolResources(req.cookies, pool); for (const vmid in configs) { const config = configs[vmid]; @@ -129,20 +130,20 @@ export async function getUserResources (req, user) { // count basic numeric resources for (const resourceName of Object.keys(config)) { // numeric resource type - if (resourceName in dbResources && dbResources[resourceName].type === "numeric") { + if (resourceName in configResources && configResources[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; + if (nodeName in poolResources[resourceName].nodes) { + poolResources[resourceName].nodes[nodeName].used += val; + poolResources[resourceName].nodes[nodeName].avail -= val; } // otherwise add the resource to the global pool else { - userResources[resourceName].global.used += val; - userResources[resourceName].global.avail -= val; + poolResources[resourceName].global.used += val; + poolResources[resourceName].global.avail -= val; } - userResources[resourceName].total.used += val; - userResources[resourceName].total.avail -= val; + poolResources[resourceName].total.used += val; + poolResources[resourceName].total.avail -= val; } } // count disk resources in volumes @@ -151,38 +152,38 @@ export async function getUserResources (req, user) { const storage = disk.storage; const size = disk.size; // only process disk if its storage is in the user resources to be counted - if (storage in userResources) { + if (storage in poolResources) { // 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 += size; - userResources[storage].nodes[nodeName].avail -= size; + if (nodeName in poolResources[storage].nodes) { + poolResources[storage].nodes[nodeName].used += size; + poolResources[storage].nodes[nodeName].avail -= size; } // otherwise add the resource to the global pool else { - userResources[storage].global.used += size; - userResources[storage].global.avail -= size; + poolResources[storage].global.used += size; + poolResources[storage].global.avail -= size; } - userResources[storage].total.used += size; - userResources[storage].total.avail -= size; + poolResources[storage].total.used += size; + poolResources[storage].total.avail -= size; } } // count net resources in nets for (const netid in config.nets) { const net = config.nets[netid]; const rate = net.rate; - if (userResources.network) { + if (poolResources.network) { // 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 += rate; - userResources.network.nodes[nodeName].avail -= rate; + if (nodeName in poolResources.network.nodes) { + poolResources.network.nodes[nodeName].used += rate; + poolResources.network.nodes[nodeName].avail -= rate; } // otherwise add the resource to the global pool else { - userResources.network.global.used += rate; - userResources.network.global.avail -= rate; + poolResources.network.global.used += rate; + poolResources.network.global.avail -= rate; } - userResources.network.total.used += rate; - userResources.network.total.avail -= rate; + poolResources.network.total.used += rate; + poolResources.network.total.avail -= rate; } } // count pci device resources in devices @@ -190,49 +191,51 @@ export async function getUserResources (req, user) { const device = config.devices[deviceid]; const name = device.device_name; // if the node has a node specific rule, add it there - if (nodeName in userResources.pci.nodes) { - const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match)); + if (nodeName in poolResources.pci.nodes) { + const index = poolResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match)); if (index >= 0) { - userResources.pci.nodes[nodeName][index].used++; - userResources.pci.nodes[nodeName][index].avail--; + poolResources.pci.nodes[nodeName][index].used++; + poolResources.pci.nodes[nodeName][index].avail--; } } // otherwise try to add the resource to the global pool else { - const index = userResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match)); + const index = poolResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match)); if (index >= 0) { // device resource is in the user's global list then increment it by 1 - userResources.pci.global[index].used++; - userResources.pci.global[index].avail--; + poolResources.pci.global[index].used++; + poolResources.pci.global[index].avail--; } } // finally, add the device to the total map - const index = userResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match)); + const index = poolResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match)); if (index >= 0) { - userResources.pci.total[index].used++; - userResources.pci.total[index].avail--; + poolResources.pci.total[index].used++; + poolResources.pci.total[index].avail--; } } } - return userResources; + return poolResources; } /** * 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 {{id: string, realm: string}} user object of user requesting additional resources. + * @param {string} node name of node hosting requested resource(s) + * @param {string} pool name of pool hosting requested resource(s) * @param {Object} request k-v pairs of resources and requested amounts * @returns {boolean, Object} true if the available resources can fullfill the requested resources, false otherwise. */ -export async function approveResources (req, user, request, node) { - const dbResources = global.config.resources; - const userResources = await getUserResources(req, user); +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 userResources)) { + if (!(key in poolResources)) { // approved = false; reason[key] = { approved: false, reason: `${key} not allowed` }; continue; @@ -240,17 +243,17 @@ export async function approveResources (req, user, request, node) { } // use node specific quota if there is one available, otherwise use the global resource quota - const inNode = node in userResources[key].nodes; - const resourceData = inNode ? userResources[key].nodes[node] : userResources[key].global; + const inNode = node in poolResources[key].nodes; + const resourceData = inNode ? poolResources[key].nodes[node] : poolResources[key].global; // if the resource type is list, check if the requested resource exists in the list - if (dbResources[key].type === "list") { + if (configResources[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) { + if (avail !== configResources[key].whitelist) { // approved = false; - reason[key] = { approved: false, reason: `${key} ${dbResources[key].whitelist ? "not in whitelist" : "in blacklist"}` }; + reason[key] = { approved: false, reason: `${key} ${configResources[key].whitelist ? "not in whitelist" : "in blacklist"}` }; // return; continue; } @@ -339,7 +342,7 @@ export function readJSONFile (path) { /** * * @param {*} username - * @returns {Object | null} user object containing username and realm or null if user does not exist + * @returns {Object | null} user object containing userid and realm or null if username format was invalid */ export function getUserObjFromUsername (username) { if (username) { @@ -352,3 +355,46 @@ export function getUserObjFromUsername (username) { return null; } } + +/** + * + * @param {*} groupname + * @returns {Object | null} user object containing groupid and realm or null if groupname format was invalid + */ +export function getGroupObjFromGroupname (groupname) { + if (groupname) { + if (groupname.includes("-")) { + const groupRealm = groupname.split("-").at(-1); + const groupID = groupname.replace(`-${groupRealm}`, ""); + const groupObj = { id: groupID, realm: groupRealm }; + return groupObj; + } + else { + const groupRealm = "pve"; + const groupID = groupname; + const groupObj = { id: groupID, realm: groupRealm }; + return groupObj; + } + } + else { + return null; + } +} + +/** + * + * @param {Object} poolObj pool data object + * @param {Object} userObj user object containing id and realm + * @returns {boolean} true if userObj in poolObj + */ +export function checkUserInPool(poolObj, userObj) { + for (const group of poolObj.groups) { + // assumption: pool listed groups are all relevant memberships (ie are paas client role) + for (const user of group.users) { + if (user.username.uid === userObj.id && user.username.realm === userObj.realm) { + return true; + } + } + } + return false; +} -- 2.53.0 From 4bc71e22127a773c151c8c027406ee5beaf6dbbd Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 26 May 2026 18:44:03 +0000 Subject: [PATCH 02/13] workaround for node pcie get devices --- src/routes/cluster.js | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/routes/cluster.js b/src/routes/cluster.js index 29567c3..d15b043 100644 --- a/src/routes/cluster.js +++ b/src/routes/cluster.js @@ -63,7 +63,10 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => { if (!auth) { return; } - const userNodes = (await global.access.getPool(userObj, req.cookies)).cluster.nodes; + + /* todo this should check pool membership + pool = await global.access.getPool(instance.pool, req.cookies) + const userNodes = pool["nodes-allowed"]; if (userNodes[params.node] !== true) { // user does not have access to the node res.status(401).send({ auth: false, path: params.node }); res.end(); @@ -76,24 +79,26 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => { res.status(200).send([]); res.end(); } - else { - // get node avail devices - const node = await global.pve.getNode(params.node); - let availableDevices = []; - // get each device and filter out only thise which are not reserved - for (const device of Object.values(node.devices)) { - if (device.reserved === false) { - availableDevices.push(device); - } - } - // further filter out only devices which the user has access to - availableDevices = availableDevices.filter(nodeAvail => userAvailPci.some((userAvail) => { - return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0; - })); + */ - res.status(200).send(availableDevices); - res.end(); + //else { + // get node avail devices + const node = await global.pve.getNode(params.node); + const availableDevices = []; + // get each device and filter out only thise which are not reserved + for (const device of Object.values(node.devices)) { + if (device.reserved === false) { + availableDevices.push(device); + } } + // further filter out only devices which the user has access to + //availableDevices = availableDevices.filter(nodeAvail => userAvailPci.some((userAvail) => { + // return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0; + //})); + + res.status(200).send(availableDevices); + res.end(); + //} }); /** -- 2.53.0 From 6c77443aee0f425a2b93785cece0c51e2c09bcf0 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 26 May 2026 19:04:10 +0000 Subject: [PATCH 03/13] move and fix get avaliable pci devices to GET /node/type/instance/pci --- src/routes/cluster.js | 60 ---------------------------------- src/routes/cluster/pci.js | 69 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 60 deletions(-) diff --git a/src/routes/cluster.js b/src/routes/cluster.js index d15b043..3999458 100644 --- a/src/routes/cluster.js +++ b/src/routes/cluster.js @@ -3,7 +3,6 @@ export const router = Router({ mergeParams: true }); const checkAuth = global.utils.checkAuth; const approveResources = global.utils.approveResources; -const getPoolResources = global.utils.getPoolResources; const checkUserInPool = global.utils.checkUserInPool; const nodeRegexP = "[\\w-]+"; @@ -42,65 +41,6 @@ router.get("/nodes", async (req, res) => { } }); -/** - * GET - get available pcie devices for the given node and user - * request: - * - node: string - vm host node id - * responses: - * - 200: PVE PCI Device Object - * - 401: {auth: false} - * - 401: {auth: false, path: string} - * - 500: {error: string} - */ -router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => { - const params = { - node: req.params.node - }; - const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - - // check auth - const auth = await checkAuth(req.cookies, res); - if (!auth) { - return; - } - - /* todo this should check pool membership - pool = await global.access.getPool(instance.pool, req.cookies) - const userNodes = pool["nodes-allowed"]; - if (userNodes[params.node] !== true) { // user does not have access to the node - res.status(401).send({ auth: false, path: params.node }); - res.end(); - return; - } - - // get remaining user resources - 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(); - } - */ - - //else { - // get node avail devices - const node = await global.pve.getNode(params.node); - const availableDevices = []; - // get each device and filter out only thise which are not reserved - for (const device of Object.values(node.devices)) { - if (device.reserved === false) { - availableDevices.push(device); - } - } - // further filter out only devices which the user has access to - //availableDevices = availableDevices.filter(nodeAvail => userAvailPci.some((userAvail) => { - // return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0; - //})); - - res.status(200).send(availableDevices); - res.end(); - //} -}); - /** * GET - get basic resources for vm using the fabric format * request: diff --git a/src/routes/cluster/pci.js b/src/routes/cluster/pci.js index 2d9f20a..5b84d79 100644 --- a/src/routes/cluster/pci.js +++ b/src/routes/cluster/pci.js @@ -2,8 +2,77 @@ 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: + * - node: string - vm host node id + * responses: + * - 200: PVE PCI Device Object + * - 401: {auth: false} + * - 401: {auth: false, path: string} + * - 500: {error: string} + */ +router.get("/", async (req, res) => { + const params = { + node: req.params.node, + type: req.params.type, + vmid: req.params.vmid, + }; + // check auth + const auth = await checkAuth(req.cookies, res); + if (!auth) { + return; + } + + // get instance config for pool membership + const instance = await global.pve.getInstance(params.node, params.vmid); + + // ensure that requested instance type is vmid + if (instance.type !== "VM") { + res.status(400).send({ auth: true, error: `actual instance type is ${instance.type} but must be VM` }); + } + else if (params.type !== "qemu") { + res.status(400).send({ auth: true, error: `requested instance type is ${params.type} but must be qemu` }); + } + + // get pool and pool allowed nodes + const pool = await global.access.getPool(instance.pool, req.cookies); + const poolNodes = pool.pool["nodes-allowed"]; + if (poolNodes[params.node] !== true) { // user does not have access to the node + res.status(401).send({ auth: false, path: params.node }); + res.end(); + return; + } + + // 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 + if (poolAvailPci === undefined) { // user has no available devices on this node, so send an empty list + res.status(200).send([]); + res.end(); + } + else { + // get node avail devices + const node = await global.pve.getNode(params.node); + let availableDevices = []; + // get each device and filter out only thise which are not reserved + for (const device of Object.values(node.devices)) { + if (device.reserved === false) { + availableDevices.push(device); + } + } + //further filter out only devices which the user has access to + availableDevices = availableDevices.filter(nodeAvail => poolAvailPci.some((userAvail) => { + return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0; + })); + + res.status(200).send(availableDevices); + res.end(); + } +}); + /** * GET - get instance pcie device data * request: -- 2.53.0 From 46295fabde9dfb0a40c8bc9cd89c29eb40a9b0cb Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 26 May 2026 19:13:52 +0000 Subject: [PATCH 04/13] improve various error logging messages --- src/backends/access_manager.js | 2 +- src/backends/pve.js | 3 ++- src/utils.js | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/backends/access_manager.js b/src/backends/access_manager.js index 2143652..e885443 100644 --- a/src/backends/access_manager.js +++ b/src/backends/access_manager.js @@ -44,7 +44,7 @@ export default class ACCESS_MANAGER_API extends ACCESS_BACKEND { }; } catch (error) { - console.log(`access: error ocuured in access.requestAPI: ${error}`); + console.log(`access: error ocuured in access.requestAPI: ${method} ${path} resulted in ${error}`); const result = error.response; result.ok = result.status === 200; return result; diff --git a/src/backends/pve.js b/src/backends/pve.js index 5ec3e99..fc539f6 100644 --- a/src/backends/pve.js +++ b/src/backends/pve.js @@ -95,7 +95,7 @@ export default class PVE extends PVE_BACKEND { }; } catch (error) { - console.log(`pve: error ocuured in pve.requestPVE: ${error}`); + console.log(`pve: error ocuured in pve.requestPVE: ${method} ${path} resulted in ${error}`); const result = error.response; result.ok = result.status === 200; return result; @@ -126,6 +126,7 @@ export default class PVE extends PVE_BACKEND { return await axios.request(url, content); } catch (error) { + console.log(`pve: error ocuured in pve.requestFabric: ${method} ${path} resulted in ${error}`); return error; } } diff --git a/src/utils.js b/src/utils.js index b076247..aa916e2 100644 --- a/src/utils.js +++ b/src/utils.js @@ -334,7 +334,7 @@ export function readJSONFile (path) { return JSON.parse(readFileSync(path)); } catch (e) { - console.log(`error opening ${path}: ${e}`); + console.log(`json: error opening ${path}: ${e}`); exit(1); } }; -- 2.53.0 From af2194a8b33fe9dff1cd4222970249fc714e6ed1 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 26 May 2026 22:35:56 +0000 Subject: [PATCH 05/13] various code cleanup and commenting --- api/test_access.http | 2 +- api/test_instance.http | 3 ++ src/routes/access.js | 16 ++++++++- src/routes/access/groups.js | 14 +++++--- src/routes/access/pools.js | 28 ++++++++------- src/routes/access/users.js | 14 +++++--- src/routes/cluster.js | 42 ++++++++++++---------- src/routes/cluster/backup.js | 42 ++++++++++++++-------- src/routes/cluster/disk.js | 70 ++++++++++++++++++++++++------------ src/routes/cluster/net.js | 36 ++++++++++++++----- src/routes/cluster/pci.js | 46 +++++++++++++++++------- src/routes/global.js | 4 +++ src/routes/sync.js | 14 +++----- src/utils.js | 21 ++++------- 14 files changed, 229 insertions(+), 123 deletions(-) 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 -- 2.53.0 From 104640facd65679718070262335375d6f4a9c824 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 27 May 2026 22:20:28 +0000 Subject: [PATCH 06/13] undo logical error in approveResources --- src/utils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils.js b/src/utils.js index 671a7bf..fd63305 100644 --- a/src/utils.js +++ b/src/utils.js @@ -255,13 +255,13 @@ export async function approveResources (req, user, node, pool, request) { } // if either the requested or avail resource is not strictly a number, block - if (typeof (resourceData.avail) !== "number" || typeof (request[key]) !== "number") { + else 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; } // if the avail resources is less than the requested resources, block - if (resourceData.avail - request[key] < 0) { + else if (resourceData.avail - request[key] < 0) { reason[key] = { approved: false, reason: `${key} requested ${request[key]} which is more than ${resourceData.avail} available` }; continue; } -- 2.53.0 From ca1041ca16ace860b13e76c1188f36a83c5634a2 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Fri, 29 May 2026 20:42:20 +0000 Subject: [PATCH 07/13] cleanup api test files --- api/test_access.http | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/api/test_access.http b/api/test_access.http index 509222d..606ba5e 100644 --- a/api/test_access.http +++ b/api/test_access.http @@ -15,7 +15,4 @@ GET {{baseUrl}}/access/groups/{{groupname}} GET {{baseUrl}}/access/pools/ ### Get pool -GET {{baseUrl}}/access/pools/{{poolname}} - -### Get all nodes -GET {{baseUrl}}/cluster/nodes \ No newline at end of file +GET {{baseUrl}}/access/pools/{{poolname}} \ No newline at end of file -- 2.53.0 From f64c8899fe56682263db6958364ca538555bb421 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Sun, 31 May 2026 02:37:23 +0000 Subject: [PATCH 08/13] fix bug in disk routes using getUserObjFromUsername --- src/routes/cluster/disk.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index c30ea4f..9e98b7e 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -140,9 +140,9 @@ router.post("/:disk/resize", async (req, res) => { }; // attempt to parse user from username - const userObj = global.utils.getUserObjFromUsername(params.username); + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); if (userObj == null) { - res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); + res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` }); } // check auth for specific instance @@ -211,9 +211,9 @@ router.post("/:disk/move", async (req, res) => { }; // attempt to parse user from username - const userObj = global.utils.getUserObjFromUsername(params.username); + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); if (userObj == null) { - res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); + res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` }); } // check auth for specific instance @@ -336,11 +336,14 @@ router.post("/:disk/create", async (req, res) => { size: req.body.size, iso: req.body.iso }; + + console.log(req.cookies) // attempt to parse user from username - const userObj = global.utils.getUserObjFromUsername(params.username); + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); if (userObj == null) { - res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); + res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` }); + return; } // check auth for specific instance -- 2.53.0 From 132e25b7f2adef058fd324f6d1983decb0de5ec6 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 10 Jun 2026 16:43:03 +0000 Subject: [PATCH 09/13] add failure path to approveResources if there was an error retrieving pool state from proxmox or fabric --- src/backends/pve.js | 5 ++++- src/routes/cluster/disk.js | 2 -- src/utils.js | 8 ++++++++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/backends/pve.js b/src/backends/pve.js index fc539f6..9fce1bb 100644 --- a/src/backends/pve.js +++ b/src/backends/pve.js @@ -254,7 +254,10 @@ export default class PVE extends PVE_BACKEND { // only add type if it is vm or ct (ie has vmid) if (resource.vmid) { const instance = await this.getInstance(resource.node, resource.vmid); - if (instance) { + if (instance === null) { + return null; + } + else { instance.node = resource.node; resources[resource.vmid] = instance; } diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index 9e98b7e..6ed9cc4 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -336,8 +336,6 @@ router.post("/:disk/create", async (req, res) => { size: req.body.size, iso: req.body.iso }; - - console.log(req.cookies) // attempt to parse user from username const userObj = global.utils.getUserObjFromUsername(req.cookies.username); diff --git a/src/utils.js b/src/utils.js index fd63305..f45d49c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -123,6 +123,9 @@ export async function getPoolResources (req, pool) { } const configs = await global.pve.getPoolResources(req.cookies, pool); + if (configs === null) { + return null; + } for (const vmid in configs) { const config = configs[vmid]; @@ -232,6 +235,11 @@ export async function approveResources (req, user, node, pool, request) { const poolResources = await getPoolResources(req, pool); const reason = {}; + if (poolResources === null) { + reason["server"] = "error in retrieving pool resource state"; + return {approved: false, reason}; + } + for (const key in request) { // if requested resource is not specified in user resources, assume it's not allowed if (!(key in poolResources)) { -- 2.53.0 From 13ef5fce094ad23c094c105309d5c082e494482a Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Wed, 10 Jun 2026 16:49:47 +0000 Subject: [PATCH 10/13] fix == and != to === and !==, add check to eslint --- dev_config/eslint.config.mjs | 9 +++++---- src/backends/pve.js | 10 +++++----- src/routes/access/groups.js | 2 +- src/routes/access/users.js | 2 +- src/routes/cluster/disk.js | 6 +++--- src/utils.js | 2 +- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/dev_config/eslint.config.mjs b/dev_config/eslint.config.mjs index ee766d2..8a47250 100644 --- a/dev_config/eslint.config.mjs +++ b/dev_config/eslint.config.mjs @@ -16,16 +16,17 @@ export default defineConfig([{ }, rules: { "no-tabs": ["error", { allowIndentationTabs: true }], - indent: ["error", "tab"], + "indent": ["error", "tab"], "linebreak-style": ["error", "unix"], - quotes: ["error", "double"], - semi: ["error", "always"], + "quotes": ["error", "double"], + "semi": ["error", "always"], "brace-style": ["error", "stroustrup", { allowSingleLine: false }], "no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }], - "prefer-const": ["error"] + "prefer-const": ["error"], + "eqeqeq": ["error"] } }]); diff --git a/src/backends/pve.js b/src/backends/pve.js index 9fce1bb..19feb3a 100644 --- a/src/backends/pve.js +++ b/src/backends/pve.js @@ -203,7 +203,7 @@ export default class PVE extends PVE_BACKEND { async getDisk (node, instance, disk) { const config = await this.getInstance(node, instance); - if (config != null && config.volumes[disk] != null) { + if (config !== null && config.volumes[disk] !== null) { return config.volumes[disk]; } else { @@ -213,7 +213,7 @@ export default class PVE extends PVE_BACKEND { async getNet (node, instance, netid) { const config = await this.getInstance(node, instance); - if (config != null && config.nets[netid] != null) { + if (config !== null && config.nets[netid] !== null) { return config.nets[netid]; } else { @@ -223,7 +223,7 @@ export default class PVE extends PVE_BACKEND { async getDevice (node, instance, deviceid) { const config = await this.getInstance(node, instance); - if (config != null && config.devices[deviceid] != null) { + if (config !== null && config.devices[deviceid] !== null) { return config.devices[deviceid]; } else { @@ -238,11 +238,11 @@ export default class PVE extends PVE_BACKEND { return null; } const data = res.data; - if (data.length != 1) { + if (data.length !== 1) { return null; } const poolPVE = data[0]; - if (poolPVE.poolid != pool) { + if (poolPVE.poolid !== pool) { return null; } diff --git a/src/routes/access/groups.js b/src/routes/access/groups.js index b134a8d..4c65c51 100644 --- a/src/routes/access/groups.js +++ b/src/routes/access/groups.js @@ -21,7 +21,7 @@ router.get("/:groupname", async (req, res) => { // attempt to parse group from groupname const groupObj = global.utils.getGroupObjFromGroupname(params.groupname); - if (groupObj == null) { + if (groupObj === null) { res.status(400).send({ auth: true, error:`Groupname ${params.groupname} does not match format gid-realm or gid.` }); } diff --git a/src/routes/access/users.js b/src/routes/access/users.js index 73414f5..8b98a50 100644 --- a/src/routes/access/users.js +++ b/src/routes/access/users.js @@ -21,7 +21,7 @@ router.get("/:username", async (req, res) => { // attempt to parse user from username const userObj = global.utils.getUserObjFromUsername(params.username); - if (userObj == null) { + if (userObj === null) { res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` }); } diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index 6ed9cc4..8ec8edc 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -141,7 +141,7 @@ router.post("/:disk/resize", async (req, res) => { // attempt to parse user from username const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - if (userObj == null) { + if (userObj === null) { res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` }); } @@ -212,7 +212,7 @@ router.post("/:disk/move", async (req, res) => { // attempt to parse user from username const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - if (userObj == null) { + if (userObj === null) { res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` }); } @@ -339,7 +339,7 @@ router.post("/:disk/create", async (req, res) => { // attempt to parse user from username const userObj = global.utils.getUserObjFromUsername(req.cookies.username); - if (userObj == null) { + if (userObj === null) { res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` }); return; } diff --git a/src/utils.js b/src/utils.js index f45d49c..893df02 100644 --- a/src/utils.js +++ b/src/utils.js @@ -254,7 +254,7 @@ export async function approveResources (req, user, node, pool, request) { // if the resource type is list, check if the requested resource exists in the list if (configResources[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 + // 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) { reason[key] = { approved: false, reason: `${key} ${configResources[key].whitelist ? "not in whitelist" : "in blacklist"}` }; -- 2.53.0 From 60ade723a4b22adee798ac9f18b33dbb745beee7 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 6 Jul 2026 18:03:05 +0000 Subject: [PATCH 11/13] minor refactor to getPoolResources --- src/utils.js | 132 ++++++++++++++++++++++++++------------------------- 1 file changed, 68 insertions(+), 64 deletions(-) diff --git a/src/utils.js b/src/utils.js index 893df02..2f4fb02 100644 --- a/src/utils.js +++ b/src/utils.js @@ -63,41 +63,44 @@ export async function checkAuth (cookies, res, vmpath = null) { * Get pool resource data including used, available, and maximum resources. * @param {Object} req ProxmoxAAS API request object. * @param {{id: string, realm: string}} user object 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. + * @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified pool or null if an error occured */ export async function getPoolResources (req, pool) { const configResources = global.config.resources; const poolConfig = await global.access.getPool(pool, req.cookies); - const poolResources = poolConfig.pool.resources; + if (poolConfig.ok !== true) { + return null; + } + const poolConfigResources = poolConfig.pool.resources; // setup the pool 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(poolResources)) { + for (const resourceName of Object.keys(poolConfigResources)) { if (configResources[resourceName].type === "list") { - poolResources[resourceName].total = []; - poolResources[resourceName].global.forEach((e) => { + poolConfigResources[resourceName].total = []; + poolConfigResources[resourceName].global.forEach((e) => { e.used = 0; e.avail = e.max; - const index = poolResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); + const index = poolConfigResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); if (index === -1) { - poolResources[resourceName].total.push(structuredClone(e)); + poolConfigResources[resourceName].total.push(structuredClone(e)); } else { - poolResources[resourceName].total[index].max += e.max; - poolResources[resourceName].total[index].avail += e.avail; + poolConfigResources[resourceName].total[index].max += e.max; + poolConfigResources[resourceName].total[index].avail += e.avail; } }); - for (const nodeName of Object.keys(poolResources[resourceName].nodes)) { - poolResources[resourceName].nodes[nodeName].forEach((e) => { + for (const nodeName of Object.keys(poolConfigResources[resourceName].nodes)) { + poolConfigResources[resourceName].nodes[nodeName].forEach((e) => { e.used = 0; e.avail = e.max; - const index = poolResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); + const index = poolConfigResources[resourceName].total.findIndex((availEelement) => e.match === availEelement.match); if (index === -1) { - poolResources[resourceName].total.push(structuredClone(e)); + poolConfigResources[resourceName].total.push(structuredClone(e)); } else { - poolResources[resourceName].total[index].max += e.max; - poolResources[resourceName].total[index].avail += e.avail; + poolConfigResources[resourceName].total[index].max += e.max; + poolConfigResources[resourceName].total[index].avail += e.avail; } }); } @@ -108,27 +111,27 @@ export async function getPoolResources (req, pool) { used: 0, avail: 0 }; - poolResources[resourceName].global.used = 0; - poolResources[resourceName].global.avail = poolResources[resourceName].global.max; - total.max += poolResources[resourceName].global.max; - total.avail += poolResources[resourceName].global.avail; - for (const nodeName of Object.keys(poolResources[resourceName].nodes)) { - poolResources[resourceName].nodes[nodeName].used = 0; - poolResources[resourceName].nodes[nodeName].avail = poolResources[resourceName].nodes[nodeName].max; - total.max += poolResources[resourceName].nodes[nodeName].max; - total.avail += poolResources[resourceName].nodes[nodeName].avail; + poolConfigResources[resourceName].global.used = 0; + poolConfigResources[resourceName].global.avail = poolConfigResources[resourceName].global.max; + total.max += poolConfigResources[resourceName].global.max; + total.avail += poolConfigResources[resourceName].global.avail; + for (const nodeName of Object.keys(poolConfigResources[resourceName].nodes)) { + poolConfigResources[resourceName].nodes[nodeName].used = 0; + poolConfigResources[resourceName].nodes[nodeName].avail = poolConfigResources[resourceName].nodes[nodeName].max; + total.max += poolConfigResources[resourceName].nodes[nodeName].max; + total.avail += poolConfigResources[resourceName].nodes[nodeName].avail; } - poolResources[resourceName].total = total; + poolConfigResources[resourceName].total = total; } } - const configs = await global.pve.getPoolResources(req.cookies, pool); - if (configs === null) { + const resources = await global.pve.getPoolResources(req.cookies, pool); + if (resources === null) { return null; } - for (const vmid in configs) { - const config = configs[vmid]; + for (const vmid in resources) { + const config = resources[vmid]; const nodeName = config.node; // count basic numeric resources for (const resourceName of Object.keys(config)) { @@ -136,17 +139,17 @@ export async function getPoolResources (req, pool) { if (resourceName in configResources && configResources[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 poolResources[resourceName].nodes) { - poolResources[resourceName].nodes[nodeName].used += val; - poolResources[resourceName].nodes[nodeName].avail -= val; + if (nodeName in poolConfigResources[resourceName].nodes) { + poolConfigResources[resourceName].nodes[nodeName].used += val; + poolConfigResources[resourceName].nodes[nodeName].avail -= val; } // otherwise add the resource to the global pool else { - poolResources[resourceName].global.used += val; - poolResources[resourceName].global.avail -= val; + poolConfigResources[resourceName].global.used += val; + poolConfigResources[resourceName].global.avail -= val; } - poolResources[resourceName].total.used += val; - poolResources[resourceName].total.avail -= val; + poolConfigResources[resourceName].total.used += val; + poolConfigResources[resourceName].total.avail -= val; } } // count disk resources in volumes @@ -155,38 +158,38 @@ export async function getPoolResources (req, pool) { const storage = disk.storage; const size = disk.size; // only process disk if its storage is in the user resources to be counted - if (storage in poolResources) { + if (storage in poolConfigResources) { // if the instance's node is restricted by this resource, add it to the instance's used value - if (nodeName in poolResources[storage].nodes) { - poolResources[storage].nodes[nodeName].used += size; - poolResources[storage].nodes[nodeName].avail -= size; + if (nodeName in poolConfigResources[storage].nodes) { + poolConfigResources[storage].nodes[nodeName].used += size; + poolConfigResources[storage].nodes[nodeName].avail -= size; } // otherwise add the resource to the global pool else { - poolResources[storage].global.used += size; - poolResources[storage].global.avail -= size; + poolConfigResources[storage].global.used += size; + poolConfigResources[storage].global.avail -= size; } - poolResources[storage].total.used += size; - poolResources[storage].total.avail -= size; + poolConfigResources[storage].total.used += size; + poolConfigResources[storage].total.avail -= size; } } // count net resources in nets for (const netid in config.nets) { const net = config.nets[netid]; const rate = net.rate; - if (poolResources.network) { + if (poolConfigResources.network) { // if the instance's node is restricted by this resource, add it to the instance's used value - if (nodeName in poolResources.network.nodes) { - poolResources.network.nodes[nodeName].used += rate; - poolResources.network.nodes[nodeName].avail -= rate; + if (nodeName in poolConfigResources.network.nodes) { + poolConfigResources.network.nodes[nodeName].used += rate; + poolConfigResources.network.nodes[nodeName].avail -= rate; } // otherwise add the resource to the global pool else { - poolResources.network.global.used += rate; - poolResources.network.global.avail -= rate; + poolConfigResources.network.global.used += rate; + poolConfigResources.network.global.avail -= rate; } - poolResources.network.total.used += rate; - poolResources.network.total.avail -= rate; + poolConfigResources.network.total.used += rate; + poolConfigResources.network.total.avail -= rate; } } // count pci device resources in devices @@ -194,31 +197,31 @@ export async function getPoolResources (req, pool) { const device = config.devices[deviceid]; const name = device.device_name; // if the node has a node specific rule, add it there - if (nodeName in poolResources.pci.nodes) { - const index = poolResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match)); + if (nodeName in poolConfigResources.pci.nodes) { + const index = poolConfigResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match)); if (index >= 0) { - poolResources.pci.nodes[nodeName][index].used++; - poolResources.pci.nodes[nodeName][index].avail--; + poolConfigResources.pci.nodes[nodeName][index].used++; + poolConfigResources.pci.nodes[nodeName][index].avail--; } } // otherwise try to add the resource to the global pool else { - const index = poolResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match)); + const index = poolConfigResources.pci.global.findIndex((availEelement) => name.includes(availEelement.match)); if (index >= 0) { // device resource is in the user's global list then increment it by 1 - poolResources.pci.global[index].used++; - poolResources.pci.global[index].avail--; + poolConfigResources.pci.global[index].used++; + poolConfigResources.pci.global[index].avail--; } } // finally, add the device to the total map - const index = poolResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match)); + const index = poolConfigResources.pci.total.findIndex((availEelement) => name.includes(availEelement.match)); if (index >= 0) { - poolResources.pci.total[index].used++; - poolResources.pci.total[index].avail--; + poolConfigResources.pci.total[index].used++; + poolConfigResources.pci.total[index].avail--; } } } - return poolResources; + return poolConfigResources; } /** @@ -228,7 +231,8 @@ export async function getPoolResources (req, pool) { * @param {string} node name of node hosting requested resource(s) * @param {string} pool name of pool hosting requested resource(s) * @param {Object} request k-v pairs of resources and requested amounts - * @returns {boolean, Object} true if the available resources can fullfill the requested resources, false otherwise. + * @returns {boolean} true if the available resources can fullfill the requested resources, false otherwise. + * @returns {Object} map of key values and reason for success or failure */ export async function approveResources (req, user, node, pool, request) { const configResources = global.config.resources; -- 2.53.0 From f3b279e636571651e78b6e0ba93ec1367227d353 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 6 Jul 2026 18:16:03 +0000 Subject: [PATCH 12/13] fix possible crash caused by malformed rate request --- src/routes/sync.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/routes/sync.js b/src/routes/sync.js index 9c6fdf1..dab37c3 100644 --- a/src/routes/sync.js +++ b/src/routes/sync.js @@ -118,9 +118,14 @@ if (schemes.interrupt.enabled) { const parsed = message.toString().split(" "); const cmd = parsed[0]; // command is rate and the value is valid - if (cmd === "rate" && parsed[1] >= schemes.interrupt["min-rate"] && parsed[1] <= schemes.interrupt["max-rate"]) { + if (cmd === "rate" && parsed.length === 2 && parsed[1] >= schemes.interrupt["min-rate"] && parsed[1] <= schemes.interrupt["max-rate"]) { // get requested rate in ms const rate = Number(parsed[1]) * 1000; + if (isNaN(rate)) { + socket.send("error: rate must be a number"); + socket.terminate(); + return; + } // if timer has not started, start it with requested rate if (!timer) { timer = setTimeout(handleInterruptSync, rate); @@ -142,12 +147,12 @@ if (schemes.interrupt.enabled) { } // command is rate but the requested value is out of bounds, terminate socket else if (cmd === "rate") { - socket.send(`error: rate must be in range [${schemes.interrupt["min-rate"]}, ${schemes.interrupt["max-rate"]}].`); + socket.send(`error: rate must be in range [${schemes.interrupt["min-rate"]}, ${schemes.interrupt["max-rate"]}].`); socket.terminate(); } // otherwise, command is invalid, terminate socket else { - socket.send(`error: ${cmd} command not found.`); + socket.send(`error: ${cmd} command not supported.`); socket.terminate(); } }); -- 2.53.0 From 734b0271faae99f808c461dc309e9b49078ae5cd Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Mon, 6 Jul 2026 23:40:10 +0000 Subject: [PATCH 13/13] update readme, update template config --- README.md | 100 +++++------------------------------- config/template.config.json | 28 ++++++---- 2 files changed, 32 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index 1417bd9..b452970 100644 --- a/README.md +++ b/README.md @@ -23,105 +23,31 @@ In the Proxmox web GUI, perform the following steps: - Sys.Audit 4. Add a new API Token Permission with path: `/`, select the API token created previously, and role: `proxmoxaas-api` 5. Add a new User Permission with path: `/`, select the `proxmoxaas-api` user, and role: `proxmoxaas-api` -6. To prevent users from bypassing the API provided methods, create a new role with only the following permssions: +6. To prevent users from bypassing the API provided methods, create a new role `PAAS-Client` with only the following permssions: - Datastore.Audit + - Pool.Audit - VM.Audit - VM.Console - - VM.Monitor - VM.PowerMgmt ### Installation - API 1. Clone this repo onto the `ProxmoxAAS-API` host 2. Run `npm install` to initiaze the package requirements 3. Copy `template.config.json` as `config.json` and modify the following values: - 1. In `backends/pve/config`: - - url: the URI to the Proxmox API, ie `https://pve.domain.net/api2/json` - - fabric: the URL to the ProxmoxAAS-Fabric, ie `https://fabric.local` - - token: the user(name), authentication realm (pam), token id, and token secrey key (uuid) - - root (**Optional**): In order to allow users to customize instance pcie devices, the API must use the root credentials for privilege elevation. Provide the root username, ie. `root@pam`, and root user password - 2. In `backends/paasldap/config` (**Optional**): - - url: url to a PAAS-LDAP server API ie. `http://ldap.local` - 3. In `handlers/auth`: - - Add any authentication handlers to be used by the API. Add the realm name (ie. `pve`) as the key and the handler name as provided in `backends`. For example, a PAAS-LDAP handler could be added as `"paas-ldap": "paasldap"` and users in the realm `user@paas-ldap` will use this handler to perform auth actions. Refer to [backends](#Backends) - 4. In `application`: + 1. In `application`: - hostname - the ProxmoxAAS-Dashboard URL, ie `paas.domain.net` - domain - the base domain for the dashboard and proxmox, ie `domain.net` - listenPort - the port you want the API to listen on, ie `8081` - 5. In `useriso`: + 1. In `backends/pve/config`: + - url: the URI to the Proxmox API, ie `https://pve.domain.net/api2/json` + - fabric: the URL to the ProxmoxAAS-Fabric, ie `https://fabric.local` + - token: the `proxmoxaas-api` user(name), authentication realm (pam), token id, and token secrey key (uuid) + - root (**Optional**): In order to allow users to customize instance pcie devices, the API must use the root credentials for privilege elevation. Provide the root username, ie. `root@pam`, and root user password + 2. In `backends/access_manager/config`: + - url: url to a access-mamanger-api server API ie. `http://access.local` + 4. In `useriso`: - node: host of storage with user accessible iso files - storage: name of storage with user accessible iso files - 6. In `backups`: + 5. In `backups`: - storage: name of storage for instance backups -4. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script - -# Backends - -Backend handlers are used to interface with any number and type of backend data source used to store ProxmoxAAS data. Most data involves users, groups, and membership relationships. The default backends are sufficient to run a small cluster, but additional backend handlers can be created. - -## Interface - -Each backend must implement the following methods: - - - - - - - - - - -
openSessionopens a session to the backend by creating a session token
closeSessioncloses a session to the backend
- -Additionally, backends dealing with user data may also need to implement: - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
addUsercreate a user
getUserretrieve user data including membership
setUsermodify a user
delUserdelete a user
addGroupcreate a group
getGroupretrieve group data including members
setGroupmodify group data except membership
delGroupdelete group
addUserToGroupadd user to group as member
delUserFromGroupremove user from group
- -Not all user backends will necessarily implement all the methods fully. For example, backends which do not store group data may not need to implement the group related methods. - -Specific documentation can be found in `src/backends/backends.js`. - -## Multiple Interfaces - -Multiple backends can be specified using the config. During a backend operation involving users, each backend method will be called in the order specified in the config. If the operation is to retrieve user data, the responses will be merged favoring the last backend called. \ No newline at end of file +4. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script \ No newline at end of file diff --git a/config/template.config.json b/config/template.config.json index af1ff14..b3aa0c6 100644 --- a/config/template.config.json +++ b/config/template.config.json @@ -31,7 +31,7 @@ }, "handlers": { "instance": "pve", - "users": ["access_manager"] + "users": "access_manager" }, "useriso": { "node": "examplenode1", @@ -44,7 +44,8 @@ "cpu": { "type": "list", "whitelist": true, - "display": false + "display": false, + "category": "" }, "cores": { "name": "vCPU", @@ -53,7 +54,8 @@ "base": 1024, "compact": false, "unit": "Cores", - "display": true + "display": true, + "category": "" }, "memory": { "name": "RAM", @@ -62,7 +64,8 @@ "base": 1024, "compact": true, "unit": "B", - "display": true + "display": true, + "category": "" }, "swap": { "name": "SWAP", @@ -71,7 +74,8 @@ "base": 1024, "compact": true, "unit": "B", - "display": true + "display": true, + "category": "" }, "local": { "name": "local", @@ -84,9 +88,11 @@ "rootfs", "mp", "sata", + "scsi", "unused" ], - "display": true + "display": true, + "category": "Storage" }, "cephpl": { "name": "cephpl", @@ -99,9 +105,11 @@ "rootfs", "mp", "sata", + "scsi", "unused" ], - "display": true + "display": true, + "category": "Storage" }, "network": { "name": "Network", @@ -110,12 +118,14 @@ "base": 1000, "compact": true, "unit": "B/s", - "display": true + "display": true, + "category": "" }, "pci": { "type": "list", "whitelist": true, - "display": true + "display": true, + "category": "Devices" } }, "clientsync": { -- 2.53.0