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; +}