initial updates to api v2.0.0:

-  switch access backend to access-manager-api
- change resource quota to pool based
-  simplify backend system
- various cleanup
This commit is contained in:
2026-05-24 19:08:39 +00:00
parent cf47cf6c71
commit 24ed6907c7
26 changed files with 708 additions and 1055 deletions
+2 -1
View File
@@ -2,4 +2,5 @@
**/node_modules **/node_modules
**/localdb.json **/localdb.json
**/docs **/docs
**/config.json **/config.json
.vscode/settings.json
+21
View File
@@ -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
+33
View File
@@ -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
+2
View File
@@ -0,0 +1,2 @@
### Get version
GET {{baseUrl}}/version
+10 -109
View File
@@ -1,4 +1,9 @@
{ {
"application": {
"hostname": "paas.mydomain.example",
"domain": "mydomain.example",
"listenPort": 8081
},
"backends": { "backends": {
"pve": { "pve": {
"import": "pve.js", "import": "pve.js",
@@ -17,44 +22,16 @@
} }
} }
}, },
"localdb": { "access_manager": {
"import": "localdb.js", "import": "access_manager.js",
"config": { "config": {
"dbfile": "localdb.json" "url": "http://localhost:8083"
}
},
"paasldap": {
"import": "paasldap.js",
"config": {
"url": "http://paasldap.mydomain.example",
"realm": "ldap"
} }
} }
}, },
"handlers": { "handlers": {
"instance": { "instance": "pve",
"pve": "pve" "users": ["access_manager"]
},
"users": {
"realm": {
"pve": [
"localdb"
],
"ldap": [
"localdb",
"paasldap"
]
},
"any": [
"localdb",
"paasldap"
]
}
},
"application": {
"hostname": "paas.mydomain.example",
"domain": "mydomain.example",
"listenPort": 8081
}, },
"useriso": { "useriso": {
"node": "examplenode1", "node": "examplenode1",
@@ -160,81 +137,5 @@
"enabled": true "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
}
}
} }
} }
-143
View File
@@ -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
}
}
}
}
}
}
+2 -1
View File
@@ -25,6 +25,7 @@ export default defineConfig([{
"argsIgnorePattern": "^_", "argsIgnorePattern": "^_",
"varsIgnorePattern": "^_", "varsIgnorePattern": "^_",
"caughtErrorsIgnorePattern": "^_" "caughtErrorsIgnorePattern": "^_"
}] }],
"prefer-const": ["error"]
} }
}]); }]);
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "proxmoxaas-api", "name": "proxmoxaas-api",
"version": "1.0.0", "version": "2.0.0",
"description": "REST API for ProxmoxAAS", "description": "REST API for ProxmoxAAS",
"main": "src/main.js", "main": "src/main.js",
"type": "module", "type": "module",
+163
View File
@@ -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) {}
}
+116 -184
View File
@@ -17,43 +17,10 @@ export default async () => {
global.backends[name] = new Backend(config); global.backends[name] = new Backend(config);
console.log(`backends: initialized backend ${name} from ${importPath}`); console.log(`backends: initialized backend ${name} from ${importPath}`);
} }
global.pve = global.backends[global.config.handlers.instance.pve]; global.pve = global.backends[global.config.handlers.instance];
global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users); 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 { export class AtomicChange {
constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) { constructor (valid, delta, callback, status = { ok: true, status: 200, message: "" }) {
this.valid = valid; 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. * 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. * 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. * 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 * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
addUser (user, attributes, params) {} async addUser (user, attributes, params) {}
/** /**
* Get user from backend * Get user from backend
@@ -96,14 +96,7 @@ class USER_BACKEND extends BACKEND {
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {Object} containing user data from this backend, null if user does not exist * @returns {Object} containing user data from this backend, null if user does not exist
*/ */
getUser (user, params) {} async 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) {}
/** /**
* Validate a set user operation with the following parameters. * 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 * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
setUser (user, attributes, params) {} async setUser (user, attributes, params) {}
/** /**
* Validate a delete user operation with the following parameters. * 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 * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
delUser (user, params) {} async delUser (user, params) {}
/** /**
* Validate an add group operation with the following parameters. * 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 * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
addGroup (group, attributes, params) {} async addGroup (group, attributes, params) {}
/** /**
* Get group from backend * Get group from backend
* @param {{id: string}} group * @param {{id: string, realm: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {Object} containing group data from this backend, null if user does not exist * @returns {Object} containing group data from this backend, null if user does not exist
*/ */
getGroup (group, params) {} async 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) {}
/** /**
* Validate a set group operation with the following parameters. * 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 * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @returns {AtomicChange} atomic change object
*/ */
setGroup (group, attributes, params) {} async setGroup (group, attributes, params) {}
/** /**
* Validate a del group operation with the following parameters. * 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. * Returns whether the change is valid and a delta object to be used in the operation.
* @param {{id: string, realm: string}} group * @param {{id: string, realm: string}} group
* @param {Object} params authentication params, usually req.cookies * @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. * 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. * 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, realm: string}} user
* @param {{id: string}} group * @param {{id: string, realm: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @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. * 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. * 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, realm: string}} user
* @param {{id: string}} group * @param {{id: string, realm: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {AtomicChange} atomic change object * @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 * @param {string} node node id
* @returns {} * @returns {}
*/ */
getNode (node) {} async getNode (node) {}
/** /**
* Send a signal to synchronize a node after some change has been made. * Send a signal to synchronize a node after some change has been made.
* * @param {string} node node id * * @param {string} node node id
*/ */
syncNode (node) {} async syncNode (node) {}
/** /**
* Get and return instance data. * Get and return instance data.
@@ -213,14 +257,14 @@ export class PVE_BACKEND extends BACKEND {
* @param {string} type instance type * @param {string} type instance type
* @param {string} vmid instance id * @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. * Send a signal to synchronize an instance after some change has been made.
* @param {string} node node id * @param {string} node node id
* @param {string} instance instance 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. * 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) {} async getDevice (node, instance, deviceid) {}
/** /**
* Get user resource data including used, available, and maximum resources. * Get pool resource data including used, available, and maximum resources.
* @param {{id: string, realm: string}} user object of user to get resource data. * @param {string} pool
* @param {Object} cookies object containing k-v store of cookies * @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. * @returns {{used: Object, avail: Object, max: Object, resources: Object}} used, available, maximum, and resource metadata for the specified user.
*/ */
getUserResources (user, cookies) {} async getPoolResources (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) {}
} }
-115
View File
@@ -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) {}
}
-184
View File
@@ -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) {}
}
+82 -64
View File
@@ -26,8 +26,8 @@ export default class PVE extends PVE_BACKEND {
cookies: [] cookies: []
}; };
} }
const ticket = response.data.data.ticket; const ticket = response.data.ticket;
const csrftoken = response.data.data.CSRFPreventionToken; const csrftoken = response.data.CSRFPreventionToken;
return { return {
ok: true, ok: true,
status: response.status, status: response.status,
@@ -66,73 +66,39 @@ export default class PVE extends PVE_BACKEND {
data: body data: body
}; };
if (auth && auth.cookies) { if (auth && auth.cookies) { // user cookie credentials
content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken; content.headers.CSRFPreventionToken = auth.cookies.CSRFPreventionToken;
content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; 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; const token = this.#pveAPIToken;
content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`; content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`;
} }
else if (auth && auth.root) { else if (auth && auth.root) { // upgraded request as root
const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, this.#pveRoot); const rootauth = await this.requestPVE("/access/ticket", "POST", null, this.#pveRoot);
if (!(rootauth.status === 200)) { if (!(rootauth.status === 200)) {
return rootauth.response; return rootauth.response;
} }
const rootcookie = rootauth.data.data.ticket; const rootcookie = rootauth.data.ticket;
const rootcsrf = rootauth.data.data.CSRFPreventionToken; const rootcsrf = rootauth.data.CSRFPreventionToken;
content.headers.CSRFPreventionToken = rootcsrf; content.headers.CSRFPreventionToken = rootcsrf;
content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`; content.headers.Cookie = `PVEAuthCookie=${rootcookie}; CSRFPreventionToken=${rootcsrf}`;
} }
try { 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) { catch (error) {
console.log(`backends: error ocuured in pve.requestPVE: ${error}`); console.log(`pve: error ocuured in pve.requestPVE: ${error}`);
return error.response; const result = error.response;
} result.ok = result.status === 200;
} return result;
/**
* 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();
} }
} }
@@ -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) { async getNode (node) {
const res = await this.requestFabric(`/nodes/${node}`, "GET"); const res = await this.requestFabric(`/nodes/${node}`, "GET");
if (res.status !== 200) { if (res.status !== 200) {
@@ -222,26 +230,36 @@ export default class PVE extends PVE_BACKEND {
} }
} }
async getUserResources (user, cookies) { async getPoolResources (cookies, pool) {
// get user resources with vm filter // get pool resources
const res = await this.requestPVE("/cluster/resources?type=vm", "GET", { cookies }); const res = await this.requestPVE(`/pools/?poolid=${pool}`, "GET", { cookies });
if (res.status !== 200) { if (res.status !== 200) {
return null; 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 = {}; const resources = {};
// for each resource, add to the object // for each resource, add to the object
for (const resource of userPVEResources) { for (const resource of poolPVEResources) {
const instance = await this.getInstance(resource.node, resource.vmid); // only add type if it is vm or ct (ie has vmid)
if (instance) { if (resource.vmid) {
instance.node = resource.node; const instance = await this.getInstance(resource.node, resource.vmid);
resources[resource.vmid] = instance; if (instance) {
instance.node = resource.node;
resources[resource.vmid] = instance;
}
} }
} }
return resources; return resources;
} }
} }
-9
View File
@@ -41,12 +41,3 @@ global.utils.recursiveImportRoutes(app, "/api", "routes");
app.get("/api/version", (req, res) => { app.get("/api/version", (req, res) => {
res.status(200).send({ version: global.package.version }); 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 });
});
+3 -8
View File
@@ -64,12 +64,7 @@ router.post("/ticket", async (req, res) => {
const domain = global.config.application.domain; const domain = global.config.application.domain;
const userObj = global.utils.getUserObjFromUsername(params.username); const userObj = global.utils.getUserObjFromUsername(params.username);
let backends = global.userManager.getBackendsByUser(userObj); const backends = [global.config.handlers.users, global.config.handlers.instance];
if (backends == null) {
res.status(401).send({ auth: false, error: `${params.username} not found in any ProxmoxAAS backends` });
return;
}
backends = backends.concat(["pve"]);
const cm = new CookieFetcher(); const cm = new CookieFetcher();
const error = await cm.fetchBackends(backends, userObj, params.password); const error = await cm.fetchBackends(backends, userObj, params.password);
if (error) { if (error) {
@@ -107,7 +102,7 @@ router.delete("/ticket", async (req, res) => {
res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" }); res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" });
} }
await global.pve.closeSession(req.cookies); await global.pve.closeSession(req.cookies);
await global.userManager.closeSession(req.cookies); await global.access.closeSession(req.cookies);
res.status(200).send({ auth: false }); res.status(200).send({ auth: false });
}); });
@@ -134,6 +129,6 @@ router.post("/password", async (req, res) => {
const newAttributes = { const newAttributes = {
userpassword: params.password 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); res.status(response.status).send(response);
}); });
+9 -17
View File
@@ -3,22 +3,6 @@ export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth; 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 * GET - get specific group
* request: * request:
@@ -36,6 +20,14 @@ router.get("/:groupname", async (req, res) => {
if (!auth) { if (!auth) {
return; 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 }); res.status(200).send({ group });
}); });
+76
View File
@@ -0,0 +1,76 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const checkUserInPool = global.utils.checkUserInPool;
/**
* GET - get all available cluster pools
* returns only pool IDs
* responses:
* - 200: List of pools
* - PVE error
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const pools = {};
const poolnames = await global.pve.requestPVE("/pools", "GET", { token: true });
for (const poolpartial of poolnames.data) {
const poolname = poolpartial.poolid;
const p = await global.access.getPool(poolname, req.cookies);
if (p.ok !== true) {
continue;
}
const pool = p.pool;
if (checkUserInPool(pool, userObj)) {
const resources = await global.utils.getPoolResources(req, poolname);
pool.resources = resources;
pools[poolname] = pool;
}
}
res.status(200).send({ pools });
res.end();
});
/**
* GET - get specific pool
* request:
* - poolname: name of pool to get
* responses:
* - 200: {auth: true, pool: Object}
* - 401: {auth: false}
*/
router.get("/:poolname", async (req, res) => {
const params = {
poolname: req.params.poolname
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const p = await global.access.getPool(params.poolname, req.cookies);
if (p.ok !== true) {
res.status(p.status).send(p);
return;
}
const pool = p.pool;
const resources = await global.utils.getPoolResources(req, params.poolname);
pool.resources = resources;
res.status(200).send({ pool });
});
+8 -17
View File
@@ -3,22 +3,6 @@ export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth; 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 * GET - get specific user
* request: * request:
@@ -36,7 +20,14 @@ router.get("/:username", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
const userObj = global.utils.getUserObjFromUsername(params.username); 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 }); res.status(200).send({ user });
}); });
+21 -47
View File
@@ -3,7 +3,8 @@ export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources; const approveResources = global.utils.approveResources;
const getUserResources = global.utils.getUserResources; const getPoolResources = global.utils.getPoolResources;
const checkUserInPool = global.utils.checkUserInPool;
const nodeRegexP = "[\\w-]+"; const nodeRegexP = "[\\w-]+";
const typeRegexP = "qemu|lxc"; 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); 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 * GET - get all available cluster nodes
* uses existing user permissions without elevation * 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 }); const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies });
if (allNodes.status === 200) { 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.status(allNodes.status).send({ nodes: allNodesIDs });
res.end(); res.end();
} }
@@ -89,7 +63,7 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
if (!auth) { if (!auth) {
return; 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 if (userNodes[params.node] !== true) { // user does not have access to the node
res.status(401).send({ auth: false, path: params.node }); res.status(401).send({ auth: false, path: params.node });
res.end(); res.end();
@@ -97,7 +71,7 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
} }
// get remaining user resources // 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 if (userAvailPci === undefined) { // user has no available devices on this node, so send an empty list
res.status(200).send([]); res.status(200).send([]);
res.end(); res.end();
@@ -201,7 +175,7 @@ router.post(`${basePath}/resources`, async (req, res) => {
request.cpu = params.proctype; request.cpu = params.proctype;
} }
// check resource approval // 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) { if (!approved) {
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason }); res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
res.end(); res.end();
@@ -269,11 +243,11 @@ router.post(`${basePath}/create`, async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get user db config // get pool config
const user = await global.userManager.getUser(userObj, req.cookies); const pool = (await global.access.getPool(params.pool, req.cookies)).pool;
const vmid = Number.parseInt(params.vmid); const vmid = Number.parseInt(params.vmid);
const vmidMin = user.cluster.vmid.min; const vmidMin = pool["vmid-allowed"].min;
const vmidMax = user.cluster.vmid.max; const vmidMax = pool["vmid-allowed"].max;
// check vmid is within allowed range // check vmid is within allowed range
if (vmid < vmidMin || vmid > vmidMax) { if (vmid < vmidMin || vmid > vmidMax) {
res.status(500).send({ error: `Requested vmid ${vmid} is out of allowed range [${vmidMin},${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; return;
} }
// check node is within allowed list // check node is within allowed list
if (user.cluster.nodes[params.node] !== true) { if (pool["nodes-allowed"][params.node] !== true) {
res.status(500).send({ error: `Requested node ${params.node} is not in allowed nodes [${user.cluster.nodes}].` }); res.status(500).send({ error: `Requested node ${params.node} is not in allowed nodes [${pool["nodes-allowed"]}].` });
res.end(); res.end();
return; return;
} }
// check if pool is in user allowed pools // check if user is in pool
if (user.cluster.pools[params.pool] !== true) { if(checkUserInPool(pool, userObj) !== true) {
res.status(500).send({ error: `Requested pool ${params.pool} not in allowed pools [${user.pools}]` }); res.status(500).send({ error: `Requested pool ${params.pool} does not contain user ${req.cookies.username}]` });
res.end(); res.end();
return; return;
} }
@@ -301,9 +275,9 @@ router.post(`${basePath}/create`, async (req, res) => {
request.swap = Number(params.swap) * 1024 ** 2; request.swap = Number(params.swap) * 1024 ** 2;
request[params.rootfslocation] = params.rootfssize * 1024 ** 3; request[params.rootfslocation] = params.rootfssize * 1024 ** 3;
} }
for (const key of Object.keys(user.templates.instances[params.type])) { for (const key of Object.keys(pool.templates.instances[params.type])) {
const item = user.templates.instances[params.type][key]; const item = pool.templates.instances[params.type][key];
if (item.resource) { if (item.resource.enabled) {
if (request[item.resource.name]) { if (request[item.resource.name]) {
request[item.resource.name] += item.resource.amount; request[item.resource.name] += item.resource.amount;
} }
@@ -313,7 +287,7 @@ router.post(`${basePath}/create`, async (req, res) => {
} }
} }
// check resource approval // 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) { if (!approved) {
res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason }); res.status(400).send({ request, error: "Not enough resources to satisfy request.", reason });
res.end(); res.end();
@@ -326,8 +300,8 @@ router.post(`${basePath}/create`, async (req, res) => {
memory: Number(params.memory), memory: Number(params.memory),
pool: params.pool pool: params.pool
}; };
for (const key of Object.keys(user.templates.instances[params.type])) { for (const key of Object.keys(pool.templates.instances[params.type])) {
action[key] = user.templates.instances[params.type][key].value; action[key] = pool.templates.instances[params.type][key].value;
} }
if (params.type === "lxc") { if (params.type === "lxc") {
action.swap = params.swap; action.swap = params.swap;
+8 -9
View File
@@ -33,7 +33,7 @@ router.get("/", async (req, res) => {
const storage = global.config.backups.storage; 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 backups = await global.pve.requestPVE(`/nodes/${params.node}/storage/${storage}/content?content=backup&vmid=${params.vmid}`, "GET", { token: true });
if (backups.status === 200) { if (backups.status === 200) {
res.status(backups.status).send(backups.data.data); res.status(backups.status).send(backups.data);
} }
else { else {
res.status(backups.status).send({ error: backups.statusText }); 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 // check if number of backups is less than the allowed number
const storage = global.config.backups.storage; 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 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 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) { if (backups.status !== 200) {
res.status(backups.status).send({ error: backups.statusText }); res.status(backups.status).send({ error: backups.statusText });
return; return;
@@ -94,7 +94,7 @@ router.post("/", async (req, res) => {
"notes-template": params.notes "notes-template": params.notes
}; };
const result = await global.pve.requestPVE(`/nodes/${params.node}/vzdump`, "POST", { token: true }, body); 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; return;
} }
let found = false; 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) { if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true; found = true;
} }
@@ -196,7 +196,7 @@ router.delete("/", async (req, res) => {
return; return;
} }
let found = false; 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) { if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true; found = true;
} }
@@ -208,7 +208,7 @@ router.delete("/", async (req, res) => {
// found a valid backup with matching vmid and volid // 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 }); 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; return;
} }
let found = false; 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) { if (volume.subtype === params.type && String(volume.vmid) === params.vmid && volume.content === "backup" && volume.volid === params.volid) {
found = true; 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); const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}/`, "POST", { token: true }, body);
console.log(result);
if (result.status === 200) { if (result.status === 200) {
res.status(result.status).send(); res.status(result.status).send();
} }
+9 -3
View File
@@ -145,6 +145,8 @@ router.post("/:disk/resize", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// check disk existence // check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist if (!disk) { // exit if disk does not exist
@@ -157,7 +159,7 @@ router.post("/:disk/resize", async (req, res) => {
const request = {}; const request = {};
request[storage] = Number(params.size * 1024 ** 3); // setup request object request[storage] = Number(params.size * 1024 ** 3); // setup request object
// check request approval // 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) { if (!approved) {
res.status(500).send({ request, error: `Storage ${storage} could not fulfill request of size ${params.size}G.` }); res.status(500).send({ request, error: `Storage ${storage} could not fulfill request of size ${params.size}G.` });
res.end(); res.end();
@@ -205,6 +207,8 @@ router.post("/:disk/move", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// check disk existence // check disk existence
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); // get target disk
if (!disk) { // exit if disk does not exist 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 request[dstStorage] = Number(size); // always decrease destination storage by size
} }
// check request approval // 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) { if (!approved) {
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` }); res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
res.end(); res.end();
@@ -324,6 +328,8 @@ router.post("/:disk/create", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// disk must not exist // disk must not exist
const disk = await global.pve.getDisk(params.node, params.vmid, params.disk); const disk = await global.pve.getDisk(params.node, params.vmid, params.disk);
if (disk) { if (disk) {
@@ -337,7 +343,7 @@ router.post("/:disk/create", async (req, res) => {
// setup request // setup request
request[params.storage] = Number(params.size * 1024 ** 3); request[params.storage] = Number(params.size * 1024 ** 3);
// check request approval // 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) { if (!approved) {
res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` }); res.status(500).send({ request, error: `Storage ${params.storage} could not fulfill request of size ${params.size}G.` });
res.end(); res.end();
+7 -3
View File
@@ -36,6 +36,8 @@ router.post("/:netid/create", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// net interface must not exist // net interface must not exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (net) { if (net) {
@@ -53,14 +55,14 @@ router.post("/:netid/create", async (req, res) => {
}; };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); 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) { if (!approved) {
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` }); res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end(); res.end();
return; return;
} }
// setup action // 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 = {}; const action = {};
if (params.type === "lxc") { 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}`; 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) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// net interface must already exist // net interface must already exist
const net = await global.pve.getNet(params.node, params.vmid, params.netid); const net = await global.pve.getNet(params.node, params.vmid, params.netid);
if (!net) { if (!net) {
@@ -117,7 +121,7 @@ router.post("/:netid/modify", async (req, res) => {
}; };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); 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) { if (!approved) {
res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` }); res.status(500).send({ request, error: `Could not fulfil network request of ${params.rate}MB/s.` });
res.end(); res.end();
+6 -2
View File
@@ -78,6 +78,8 @@ router.post("/:hostpci/modify", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// force all functions // force all functions
params.device = params.device.split(".")[0]; params.device = params.device.split(".")[0];
// device must exist to be modified // device must exist to be modified
@@ -100,7 +102,7 @@ router.post("/:hostpci/modify", async (req, res) => {
return; return;
} }
// check resource approval // 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) { if (!approved) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` }); res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
res.end(); res.end();
@@ -158,6 +160,8 @@ router.post("/:hostpci/create", async (req, res) => {
if (!auth) { if (!auth) {
return; return;
} }
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// force all functions // force all functions
params.device = params.device.split(".")[0]; params.device = params.device.split(".")[0];
// device must not exist to be added // device must not exist to be added
@@ -173,7 +177,7 @@ router.post("/:hostpci/create", async (req, res) => {
const request = { pci: requestedDevice.device_name }; const request = { pci: requestedDevice.device_name };
// check resource approval // check resource approval
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); 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) { if (!approved) {
res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` }); res.status(500).send({ request, error: `Could not fulfil request for ${requestedDevice.device_name}.` });
res.end(); res.end();
+5 -6
View File
@@ -52,7 +52,7 @@ if (schemes.hash.enabled) {
return; return;
} }
// get current cluster resources - do not use fabric here because fabric is not always updated to changes like up/down state changes // 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 // filter out just state information of resources that are needed
const state = extractClusterState(status, resourceTypes); const state = extractClusterState(status, resourceTypes);
res.status(200).send(getObjectHash(state)); res.status(200).send(getObjectHash(state));
@@ -166,11 +166,10 @@ if (schemes.interrupt.enabled) {
} }
else { else {
wsServer.handleUpgrade(req, socket, head, async (socket) => { wsServer.handleUpgrade(req, socket, head, async (socket) => {
// get the user 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 userObj = global.utils.getUserObjFromUsername(cookies.username); const pools = await global.pve.requestPVE("/pools", "GET", { cookies });
const pools = Object.keys((await global.userManager.getUser(userObj, cookies)).cluster.pools);
// emit the connection to initialize socket // 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; return;
} }
// get current cluster resources // 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 // filter out just state information of resources that are needed, and hash each one
const currState = extractClusterState(status, resourceTypes, true); const currState = extractClusterState(status, resourceTypes, true);
// get a map of users to send sync notifications // get a map of users to send sync notifications
+2 -56
View File
@@ -4,60 +4,6 @@ export const router = Router({ mergeParams: true }); ;
const config = global.config; const config = global.config;
const checkAuth = global.utils.checkAuth; 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 * GET - get user accessible iso files
* response: * response:
@@ -78,7 +24,7 @@ router.get("/vm-isos", async (req, res) => {
res.status(content.status).send({ error: content.statusText }); res.status(content.status).send({ error: content.statusText });
return; return;
} }
const isos = content.data.data; const isos = content.data;
const userIsos = []; const userIsos = [];
isos.forEach((iso) => { isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}: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 }); res.status(content.status).send({ error: content.statusText });
return; return;
} }
const isos = content.data.data; const isos = content.data;
const userIsos = []; const userIsos = [];
isos.forEach((iso) => { isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, ""); iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, "");
+122 -76
View File
@@ -36,7 +36,7 @@ export async function checkAuth (cookies, res, vmpath = null) {
return false; 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.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in database.` });
res.end(); res.end();
return false; 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 {Object} req ProxmoxAAS API request object.
* @param {{id: string, realm: string}} user object of user to get resource data. * @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 user.
*/ */
export async function getUserResources (req, user) { export async function getPoolResources (req, pool) {
const dbResources = global.config.resources; const configResources = global.config.resources;
const userResources = (await global.userManager.getUser(user, req.cookies)).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) // also add a total counter for each resource (only used for display, not used to check requests)
for (const resourceName of Object.keys(userResources)) { for (const resourceName of Object.keys(poolResources)) {
if (dbResources[resourceName].type === "list") { if (configResources[resourceName].type === "list") {
userResources[resourceName].total = []; poolResources[resourceName].total = [];
userResources[resourceName].global.forEach((e) => { poolResources[resourceName].global.forEach((e) => {
e.used = 0; e.used = 0;
e.avail = e.max; 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) { if (index === -1) {
userResources[resourceName].total.push(structuredClone(e)); poolResources[resourceName].total.push(structuredClone(e));
} }
else { else {
userResources[resourceName].total[index].max += e.max; poolResources[resourceName].total[index].max += e.max;
userResources[resourceName].total[index].avail += e.avail; poolResources[resourceName].total[index].avail += e.avail;
} }
}); });
for (const nodeName of Object.keys(userResources[resourceName].nodes)) { for (const nodeName of Object.keys(poolResources[resourceName].nodes)) {
userResources[resourceName].nodes[nodeName].forEach((e) => { poolResources[resourceName].nodes[nodeName].forEach((e) => {
e.used = 0; e.used = 0;
e.avail = e.max; 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) { if (index === -1) {
userResources[resourceName].total.push(structuredClone(e)); poolResources[resourceName].total.push(structuredClone(e));
} }
else { else {
userResources[resourceName].total[index].max += e.max; poolResources[resourceName].total[index].max += e.max;
userResources[resourceName].total[index].avail += e.avail; poolResources[resourceName].total[index].avail += e.avail;
} }
}); });
} }
@@ -107,21 +108,21 @@ export async function getUserResources (req, user) {
used: 0, used: 0,
avail: 0 avail: 0
}; };
userResources[resourceName].global.used = 0; poolResources[resourceName].global.used = 0;
userResources[resourceName].global.avail = userResources[resourceName].global.max; poolResources[resourceName].global.avail = poolResources[resourceName].global.max;
total.max += userResources[resourceName].global.max; total.max += poolResources[resourceName].global.max;
total.avail += userResources[resourceName].global.avail; total.avail += poolResources[resourceName].global.avail;
for (const nodeName of Object.keys(userResources[resourceName].nodes)) { for (const nodeName of Object.keys(poolResources[resourceName].nodes)) {
userResources[resourceName].nodes[nodeName].used = 0; poolResources[resourceName].nodes[nodeName].used = 0;
userResources[resourceName].nodes[nodeName].avail = userResources[resourceName].nodes[nodeName].max; poolResources[resourceName].nodes[nodeName].avail = poolResources[resourceName].nodes[nodeName].max;
total.max += userResources[resourceName].nodes[nodeName].max; total.max += poolResources[resourceName].nodes[nodeName].max;
total.avail += userResources[resourceName].nodes[nodeName].avail; 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) { for (const vmid in configs) {
const config = configs[vmid]; const config = configs[vmid];
@@ -129,20 +130,20 @@ export async function getUserResources (req, user) {
// count basic numeric resources // count basic numeric resources
for (const resourceName of Object.keys(config)) { for (const resourceName of Object.keys(config)) {
// numeric resource type // numeric resource type
if (resourceName in dbResources && dbResources[resourceName].type === "numeric") { if (resourceName in configResources && configResources[resourceName].type === "numeric") {
const val = Number(config[resourceName]); const val = Number(config[resourceName]);
// if the instance's node is restricted by this resource, add it to the instance's used value // if the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources[resourceName].nodes) { if (nodeName in poolResources[resourceName].nodes) {
userResources[resourceName].nodes[nodeName].used += val; poolResources[resourceName].nodes[nodeName].used += val;
userResources[resourceName].nodes[nodeName].avail -= val; poolResources[resourceName].nodes[nodeName].avail -= val;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources[resourceName].global.used += val; poolResources[resourceName].global.used += val;
userResources[resourceName].global.avail -= val; poolResources[resourceName].global.avail -= val;
} }
userResources[resourceName].total.used += val; poolResources[resourceName].total.used += val;
userResources[resourceName].total.avail -= val; poolResources[resourceName].total.avail -= val;
} }
} }
// count disk resources in volumes // count disk resources in volumes
@@ -151,38 +152,38 @@ export async function getUserResources (req, user) {
const storage = disk.storage; const storage = disk.storage;
const size = disk.size; const size = disk.size;
// only process disk if its storage is in the user resources to be counted // 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 the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources[storage].nodes) { if (nodeName in poolResources[storage].nodes) {
userResources[storage].nodes[nodeName].used += size; poolResources[storage].nodes[nodeName].used += size;
userResources[storage].nodes[nodeName].avail -= size; poolResources[storage].nodes[nodeName].avail -= size;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources[storage].global.used += size; poolResources[storage].global.used += size;
userResources[storage].global.avail -= size; poolResources[storage].global.avail -= size;
} }
userResources[storage].total.used += size; poolResources[storage].total.used += size;
userResources[storage].total.avail -= size; poolResources[storage].total.avail -= size;
} }
} }
// count net resources in nets // count net resources in nets
for (const netid in config.nets) { for (const netid in config.nets) {
const net = config.nets[netid]; const net = config.nets[netid];
const rate = net.rate; 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 the instance's node is restricted by this resource, add it to the instance's used value
if (nodeName in userResources.network.nodes) { if (nodeName in poolResources.network.nodes) {
userResources.network.nodes[nodeName].used += rate; poolResources.network.nodes[nodeName].used += rate;
userResources.network.nodes[nodeName].avail -= rate; poolResources.network.nodes[nodeName].avail -= rate;
} }
// otherwise add the resource to the global pool // otherwise add the resource to the global pool
else { else {
userResources.network.global.used += rate; poolResources.network.global.used += rate;
userResources.network.global.avail -= rate; poolResources.network.global.avail -= rate;
} }
userResources.network.total.used += rate; poolResources.network.total.used += rate;
userResources.network.total.avail -= rate; poolResources.network.total.avail -= rate;
} }
} }
// count pci device resources in devices // count pci device resources in devices
@@ -190,49 +191,51 @@ export async function getUserResources (req, user) {
const device = config.devices[deviceid]; const device = config.devices[deviceid];
const name = device.device_name; const name = device.device_name;
// if the node has a node specific rule, add it there // if the node has a node specific rule, add it there
if (nodeName in userResources.pci.nodes) { if (nodeName in poolResources.pci.nodes) {
const index = userResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match)); const index = poolResources.pci.nodes[nodeName].findIndex((availEelement) => name.includes(availEelement.match));
if (index >= 0) { if (index >= 0) {
userResources.pci.nodes[nodeName][index].used++; poolResources.pci.nodes[nodeName][index].used++;
userResources.pci.nodes[nodeName][index].avail--; poolResources.pci.nodes[nodeName][index].avail--;
} }
} }
// otherwise try to add the resource to the global pool // otherwise try to add the resource to the global pool
else { 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 if (index >= 0) { // device resource is in the user's global list then increment it by 1
userResources.pci.global[index].used++; poolResources.pci.global[index].used++;
userResources.pci.global[index].avail--; poolResources.pci.global[index].avail--;
} }
} }
// finally, add the device to the total map // 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) { if (index >= 0) {
userResources.pci.total[index].used++; poolResources.pci.total[index].used++;
userResources.pci.total[index].avail--; 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. * 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 {Object} req ProxmoxAAS API request object.
* @param {{id: string, realm: string}} user object of user requesting additional resources. * @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 * @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, Object} true if the available resources can fullfill the requested resources, false otherwise.
*/ */
export async function approveResources (req, user, request, node) { export async function approveResources (req, user, node, pool, request) {
const dbResources = global.config.resources; const configResources = global.config.resources;
const userResources = await getUserResources(req, user); const poolResources = await getPoolResources(req, pool);
// let approved = true; // let approved = true;
const reason = {}; const reason = {};
for (const key in request) { for (const key in request) {
// if requested resource is not specified in user resources, assume it's not allowed // if requested resource is not specified in user resources, assume it's not allowed
if (!(key in userResources)) { if (!(key in poolResources)) {
// approved = false; // approved = false;
reason[key] = { approved: false, reason: `${key} not allowed` }; reason[key] = { approved: false, reason: `${key} not allowed` };
continue; 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 // use node specific quota if there is one available, otherwise use the global resource quota
const inNode = node in userResources[key].nodes; const inNode = node in poolResources[key].nodes;
const resourceData = inNode ? userResources[key].nodes[node] : userResources[key].global; 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 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)); 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; const avail = index === -1 ? false : resourceData[index].avail > 0;
if (avail !== dbResources[key].whitelist) { if (avail !== configResources[key].whitelist) {
// approved = false; // 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; // return;
continue; continue;
} }
@@ -339,7 +342,7 @@ export function readJSONFile (path) {
/** /**
* *
* @param {*} username * @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) { export function getUserObjFromUsername (username) {
if (username) { if (username) {
@@ -352,3 +355,46 @@ export function getUserObjFromUsername (username) {
return null; 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;
}