8 Commits

Author SHA1 Message Date
alu f64c8899fe fix bug in disk routes using getUserObjFromUsername 2026-05-31 02:37:23 +00:00
alu ca1041ca16 cleanup api test files 2026-05-29 20:42:20 +00:00
alu 104640facd undo logical error in approveResources 2026-05-27 22:20:28 +00:00
alu af2194a8b3 various code cleanup and commenting 2026-05-26 22:35:56 +00:00
alu 46295fabde improve various error logging messages 2026-05-26 19:13:52 +00:00
alu 6c77443aee move and fix get avaliable pci devices to GET /node/type/instance/pci 2026-05-26 19:04:10 +00:00
alu 4bc71e2212 workaround for node pcie get devices 2026-05-26 18:44:03 +00:00
alu 24ed6907c7 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
2026-05-24 19:08:39 +00:00
27 changed files with 964 additions and 1190 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
+18
View File
@@ -0,0 +1,18 @@
### 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}}
+36
View File
@@ -0,0 +1,36 @@
### Get ticket
POST {{baseUrl}}/access/ticket
Content-Type: application/x-www-form-urlencoded
username={{username}}
&password={{password}}
### Get instance resources
GET {{baseUrl}}/cluster/{{testvmpath}}
### Get instance backups
GET {{baseUrl}}/cluster/{{testvmpath}}/backup
### 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: ${method} ${path} resulted in ${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) {}
}
+83 -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: ${method} ${path} resulted in ${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();
} }
} }
@@ -160,10 +126,53 @@ export default class PVE extends PVE_BACKEND {
return await axios.request(url, content); return await axios.request(url, content);
} }
catch (error) { catch (error) {
console.log(`pve: error ocuured in pve.requestFabric: ${method} ${path} resulted in ${error}`);
return error; return error;
} }
} }
/**
* 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 +231,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 });
});
+18 -9
View File
@@ -62,14 +62,11 @@ router.post("/ticket", async (req, res) => {
password: req.body.password password: req.body.password
}; };
const domain = global.config.application.domain; // get user and user backends from config
const userObj = global.utils.getUserObjFromUsername(params.username); const 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` }); // fetch cookies using cookie fetcher
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) {
@@ -77,6 +74,11 @@ router.post("/ticket", async (req, res) => {
return; return;
} }
const cookies = cm.exportCookies(); const cookies = cm.exportCookies();
// get global config domain name
const domain = global.config.application.domain;
// for each cookie, add the cookie to response and also compute the minimum across all cookies
let minimumExpires = Infinity; let minimumExpires = Infinity;
for (const cookie of cookies) { for (const cookie of cookies) {
const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow); const expiresDate = new Date(Date.now() + cookie.expiresMSFromNow);
@@ -85,6 +87,8 @@ router.post("/ticket", async (req, res) => {
minimumExpires = cookie.expiresMSFromNow; minimumExpires = cookie.expiresMSFromNow;
} }
} }
// set username and auth cookie with the minimum cookie length
const expiresDate = new Date(Date.now() + minimumExpires); const expiresDate = new Date(Date.now() + minimumExpires);
res.cookie("username", params.username, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" }); res.cookie("username", params.username, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
res.cookie("auth", 1, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" }); res.cookie("auth", 1, { domain, path: "/", secure: true, expires: expiresDate, sameSite: "none" });
@@ -97,17 +101,22 @@ router.post("/ticket", async (req, res) => {
* - 200: {auth: false} * - 200: {auth: false}
*/ */
router.delete("/ticket", async (req, res) => { router.delete("/ticket", async (req, res) => {
// must have cookies to delete, otherwise just return ok
if (Object.keys(req.cookies).length === 0) { if (Object.keys(req.cookies).length === 0) {
res.status(200).send({ auth: false }); res.status(200).send({ auth: false });
return; return;
} }
// for each cookie, set the expire date to 0
const domain = global.config.application.domain; const domain = global.config.application.domain;
const expire = new Date(0); const expire = new Date(0);
for (const cookie in req.cookies) { for (const cookie in req.cookies) {
res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" }); res.cookie(cookie, "", { domain, path: "/", expires: expire, secure: true, sameSite: "none" });
} }
// call close session on each backend, even if was not used
await global.pve.closeSession(req.cookies); await global.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 +143,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);
}); });
+17 -21
View File
@@ -1,24 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all groups
* responses:
* - 200: {auth: true, groups: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const groups = await global.userManager.getAllGroups(req.cookies);
res.status(200).send({ groups });
});
/** /**
* GET - get specific group * GET - get specific group
* request: * request:
@@ -32,10 +14,24 @@ router.get("/:groupname", async (req, res) => {
groupname: req.params.groupname groupname: req.params.groupname
}; };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
const group = await global.userManager.getGroup(params.groupname, req.cookies);
res.status(200).send({ group }); // attempt to parse group from groupname
const groupObj = global.utils.getGroupObjFromGroupname(params.groupname);
if (groupObj == null) {
res.status(400).send({ auth: true, error:`Groupname ${params.groupname} does not match format gid-realm or gid.` });
}
// get group
const g = await global.access.getGroup(groupObj, req.cookies);
if (g.ok !== true) {
res.status(g.status).send({ auth:true, error:g });
return;
}
const group = g.group;
res.status(200).send({ auth:true, group });
}); });
+78
View File
@@ -0,0 +1,78 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
/**
* 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 global.utils.checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get user object
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// get all pool names using api token
const poolnames = await global.pve.requestPVE("/pools", "GET", { token: true });
// setup pools (return value)
const pools = {};
// for each poolname
for (const poolpartial of poolnames.data) {
const poolname = poolpartial.poolid;
// get the pool
const p = await global.access.getPool(poolname, req.cookies);
if (p.ok !== true) {
continue;
}
const pool = p.pool;
// if user is in the pool, add it to pools (return value)
if (global.utils.checkUserInPool(pool, userObj)) {
const resources = await global.utils.getPoolResources(req, poolname);
pool.resources = resources;
pools[poolname] = pool;
}
}
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 global.utils.checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get pool
const p = await global.access.getPool(params.poolname, req.cookies);
if (p.ok !== true) {
res.status(p.status).send({ auth:true, error: p });
return;
}
const pool = p.pool;
// get resources
const resources = await global.utils.getPoolResources(req, params.poolname);
// append resources to pool
pool.resources = resources;
res.status(200).send({ auth: true, pool });
});
+15 -20
View File
@@ -1,24 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all users
* responses:
* - 200: {auth:true, users: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const users = await global.userManager.getAllUsers(req.cookies);
res.status(200).send({ users });
});
/** /**
* GET - get specific user * GET - get specific user
* request: * request:
@@ -32,11 +14,24 @@ router.get("/:username", async (req, res) => {
username: req.params.username username: req.params.username
}; };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(params.username); const userObj = global.utils.getUserObjFromUsername(params.username);
const user = await global.userManager.getUser(userObj, req.cookies); if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${params.username} does not match format uid@realm.` });
}
// get user
const u = await global.access.getUser(userObj, req.cookies);
if (u.ok !== true) {
res.status(u.status).send({ auth: true, error: u });
return;
}
const user = u.user;
res.status(200).send({ user }); res.status(200).send({ user });
}); });
+38 -113
View File
@@ -1,10 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
const getUserResources = global.utils.getUserResources;
const nodeRegexP = "[\\w-]+"; const nodeRegexP = "[\\w-]+";
const typeRegexP = "qemu|lxc"; const typeRegexP = "qemu|lxc";
const vmidRegexP = "\\d+"; const vmidRegexP = "\\d+";
@@ -13,33 +9,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
@@ -50,15 +19,15 @@ router.get("/pools", async (req, res) => {
*/ */
router.get("/nodes", async (req, res) => { router.get("/nodes", async (req, res) => {
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// get all nodes
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();
} }
@@ -68,60 +37,6 @@ router.get("/nodes", async (req, res) => {
} }
}); });
/**
* GET - get available pcie devices for the given node and user
* request:
* - node: string - vm host node id
* responses:
* - 200: PVE PCI Device Object
* - 401: {auth: false}
* - 401: {auth: false, path: string}
* - 500: {error: string}
*/
router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
const params = {
node: req.params.node
};
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes;
if (userNodes[params.node] !== true) { // user does not have access to the node
res.status(401).send({ auth: false, path: params.node });
res.end();
return;
}
// 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
if (userAvailPci === undefined) { // user has no available devices on this node, so send an empty list
res.status(200).send([]);
res.end();
}
else {
// get node avail devices
const node = await global.pve.getNode(params.node);
let availableDevices = [];
// get each device and filter out only thise which are not reserved
for (const device of Object.values(node.devices)) {
if (device.reserved === false) {
availableDevices.push(device);
}
}
// further filter out only devices which the user has access to
availableDevices = availableDevices.filter(nodeAvail => userAvailPci.some((userAvail) => {
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
}));
res.status(200).send(availableDevices);
res.end();
}
});
/** /**
* GET - get basic resources for vm using the fabric format * GET - get basic resources for vm using the fabric format
* request: * request:
@@ -141,14 +56,13 @@ router.get(`${basePath}`, async (req, res) => {
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get current config // get current config
const instance = await global.pve.getInstance(params.node, params.vmid); const instance = await global.pve.getInstance(params.node, params.vmid);
res.status(200).send(instance); res.status(200).send(instance);
}); });
@@ -180,11 +94,9 @@ router.post(`${basePath}/resources`, async (req, res) => {
boot: req.body.boot boot: req.body.boot
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -200,13 +112,16 @@ router.post(`${basePath}/resources`, async (req, res) => {
else if (params.type === "qemu") { else if (params.type === "qemu") {
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 userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const { approved, reason } = await global.utils.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();
return; return;
} }
// setup action // setup action
const action = { cores: params.cores, memory: params.memory }; const action = { cores: params.cores, memory: params.memory };
if (params.type === "lxc") { if (params.type === "lxc") {
@@ -217,6 +132,7 @@ router.post(`${basePath}/resources`, async (req, res) => {
action.boot = `order=${params.boot.toString().replaceAll(",", ";")};`; action.boot = `order=${params.boot.toString().replaceAll(",", ";")};`;
} }
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -262,36 +178,40 @@ router.post(`${basePath}/create`, async (req, res) => {
rootfssize: req.body.rootfssize rootfssize: req.body.rootfssize
}; };
const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// get user db config
const user = await global.userManager.getUser(userObj, req.cookies); // get pool config
const pool = (await global.access.getPool(params.pool, req.cookies)).pool;
const vmid = Number.parseInt(params.vmid); const 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}].` });
res.end(); res.end();
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
if (user.cluster.pools[params.pool] !== true) { // check if user is in pool
res.status(500).send({ error: `Requested pool ${params.pool} not in allowed pools [${user.pools}]` }); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if(global.utils.checkUserInPool(pool, userObj) !== true) {
res.status(500).send({ error: `Requested pool ${params.pool} does not contain user ${req.cookies.username}]` });
res.end(); res.end();
return; return;
} }
// setup request // setup request
const request = { const request = {
cores: Number(params.cores), cores: Number(params.cores),
@@ -301,9 +221,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;
} }
@@ -312,13 +232,15 @@ 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 global.utils.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();
return; return;
} }
// setup action by adding non resource values // setup action by adding non resource values
const action = { const action = {
vmid: params.vmid, vmid: params.vmid,
@@ -326,8 +248,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;
@@ -341,6 +263,7 @@ router.post(`${basePath}/create`, async (req, res) => {
else { else {
action.name = params.name; action.name = params.name;
} }
// commit action // commit action
const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, action); const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -364,12 +287,14 @@ router.delete(`${basePath}/delete`, async (req, res) => {
type: req.params.type, type: req.params.type,
vmid: req.params.vmid vmid: req.params.vmid
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// commit action // commit action
const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true }); const result = await global.pve.requestPVE(vmpath, "DELETE", { token: true });
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
+34 -21
View File
@@ -1,8 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth;
/** /**
* GET - get backups for an instance * GET - get backups for an instance
* request: * request:
@@ -24,7 +22,7 @@ router.get("/", async (req, res) => {
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -32,11 +30,12 @@ router.get("/", async (req, res) => {
// get vm backups // get vm backups
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({ auth: true, error: backups.statusText });
} }
}); });
@@ -64,22 +63,37 @@ router.post("/", async (req, res) => {
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// check if number of backups is less than the allowed number // get number of currently backups used
const storage = global.config.backups.storage; const 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 userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const maxAllowed = (await global.userManager.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;
} }
else if (numBackups >= maxAllowed) { const numBackups = backups.data.length;
// get instance
const instance = await global.pve.getInstance(params.node, params.vmid);
if (instance === null) {
res.status(400).send({ error: `failed to get instance ${params.node}/${params.vmid}` });
return;
}
// get pool and pool allowed nodes
const pool = await global.access.getPool(instance.pool, req.cookies);
if (!pool.ok) {
res.status(pool.status).send({ error: `failed to get pool ${pool}` });
return;
}
const maxAllowed = pool.pool["backups-allowed"].max;
// check if used backups is more than maximum allowed, if so exit
if (numBackups >= maxAllowed) {
res.status(backups.status).send({ error: `${params.vmid} already has ${numBackups} >= ${maxAllowed} max backups allowed` }); res.status(backups.status).send({ error: `${params.vmid} already has ${numBackups} >= ${maxAllowed} max backups allowed` });
return; return;
} }
@@ -94,7 +108,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);
}); });
/** /**
@@ -122,7 +136,7 @@ router.post("/notes", async (req, res) => {
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -136,7 +150,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;
} }
@@ -146,7 +160,7 @@ router.post("/notes", async (req, res) => {
return; return;
} }
// create backup using vzdump path // modify backup notes
const body = { const body = {
notes: params.notes notes: params.notes
}; };
@@ -182,7 +196,7 @@ router.delete("/", async (req, res) => {
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -196,7 +210,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 +222,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);
}); });
/** /**
@@ -234,7 +248,7 @@ router.post("/restore", async (req, res) => {
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -248,7 +262,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 +295,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();
} }
+54 -19
View File
@@ -1,9 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
/** /**
* POST - detach mounted disk from instance * POST - detach mounted disk from instance
* request: * request:
@@ -25,12 +22,14 @@ router.post("/:disk/detach", async (req, res) => {
vmid: req.params.vmid, vmid: req.params.vmid,
disk: req.params.disk disk: req.params.disk
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// disk must exist // disk must 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) {
@@ -38,14 +37,19 @@ router.post("/:disk/detach", async (req, res) => {
res.end(); res.end();
return; return;
} }
// disk cannot be unused // disk cannot be unused
if (params.disk.includes("unused")) { if (params.disk.includes("unused")) {
res.status(500).send({ error: `Requested disk ${params.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.` }); res.status(500).send({ error: `Requested disk ${params.disk} cannot be unused. Use /disk/delete to permanently delete unused disks.` });
res.end(); res.end();
return; return;
} }
// setup detach action
const action = { delete: params.disk }; const action = { delete: params.disk };
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid); await global.pve.syncInstance(params.node, params.vmid);
@@ -75,9 +79,10 @@ router.post("/:disk/attach", async (req, res) => {
source: req.body.source, source: req.body.source,
mp: req.body.mp mp: req.body.mp
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -89,6 +94,7 @@ router.post("/:disk/attach", async (req, res) => {
res.end(); res.end();
return; return;
} }
// target disk must be allowed according to source disk's storage options // target disk must be allowed according to source disk's storage options
const resourceConfig = global.config.resources; const resourceConfig = global.config.resources;
if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) { if (!resourceConfig[disk.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
@@ -96,14 +102,10 @@ router.post("/:disk/attach", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup action using source disk info from vm config // setup action using source disk info from vm config
const action = {}; const action = {};
if (params.type === "qemu") { action[params.disk] = params.type === "qemu" ? `${disk.file}` : `${disk.file},mp=${params.mp},backup=1`;
action[params.disk] = `${disk.file}`;
}
else if (params.type === "lxc") {
action[params.disk] = `${disk.file},mp=${params.mp},backup=1`;
}
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
@@ -137,14 +139,22 @@ router.post("/:disk/resize", async (req, res) => {
size: req.body.size size: req.body.size
}; };
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` });
}
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { 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
@@ -152,17 +162,20 @@ router.post("/:disk/resize", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request // setup request
const storage = disk.storage; // get the storage const storage = disk.storage; // get the storage
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 global.utils.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();
return; return;
} }
// action approved, commit to action // action approved, commit to action
const action = { disk: params.disk, size: `+${params.size}G` }; const action = { disk: params.disk, size: `+${params.size}G` };
const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/resize`, "PUT", { token: true }, action);
@@ -197,14 +210,20 @@ router.post("/:disk/move", async (req, res) => {
delete: req.body.delete delete: req.body.delete
}; };
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` });
}
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { 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 +239,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 global.utils.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();
@@ -264,7 +283,7 @@ router.delete("/:disk/delete", async (req, res) => {
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
@@ -317,13 +336,26 @@ router.post("/:disk/create", async (req, res) => {
size: req.body.size, size: req.body.size,
iso: req.body.iso iso: req.body.iso
}; };
console.log(req.cookies)
// attempt to parse user from username
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (userObj == null) {
res.status(400).send({ auth:true, error:`username ${req.cookies.username} does not match format uid@realm.` });
return;
}
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { 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) {
@@ -331,13 +363,14 @@ router.post("/:disk/create", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request // setup request
const request = {}; const request = {};
if (!params.disk.includes("ide")) { if (!params.disk.includes("ide")) { // ignore resource request if the type is ide (iso file)
// setup request // 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 global.utils.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();
@@ -351,6 +384,7 @@ router.post("/:disk/create", async (req, res) => {
return; return;
} }
} }
// setup action // setup action
const action = {}; const action = {};
if (params.disk.includes("ide") && params.iso) { if (params.disk.includes("ide") && params.iso) {
@@ -363,6 +397,7 @@ router.post("/:disk/create", async (req, res) => {
action[params.disk] = `${params.storage}:${params.size},mp=/${params.disk}/,backup=1`; action[params.disk] = `${params.storage}:${params.size},mp=/${params.disk}/,backup=1`;
} }
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
+32 -10
View File
@@ -1,9 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources;
/** /**
* POST - create new virtual network interface * POST - create new virtual network interface
* request: * request:
@@ -30,12 +27,17 @@ router.post("/:netid/create", async (req, res) => {
rate: req.body.rate, rate: req.body.rate,
name: req.body.name name: req.body.name
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { 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) {
@@ -48,19 +50,23 @@ router.post("/:netid/create", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request
const request = { const request = {
network: Number(params.rate) network: Number(params.rate)
}; };
// 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 global.utils.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}`;
@@ -69,6 +75,7 @@ router.post("/:netid/create", async (req, res) => {
action[`${params.netid}`] = `${nc.type},bridge=${nc.bridge},tag=${nc.vlan},rate=${params.rate}`; action[`${params.netid}`] = `${nc.type},bridge=${nc.bridge},tag=${nc.vlan},rate=${params.rate}`;
} }
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -99,12 +106,17 @@ router.post("/:netid/modify", async (req, res) => {
netid: req.params.netid, netid: req.params.netid,
rate: req.body.rate rate: req.body.rate
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { 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) {
@@ -112,21 +124,26 @@ router.post("/:netid/modify", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request
const request = { const request = {
network: Number(params.rate) - Number(net.rate) network: Number(params.rate) - Number(net.rate)
}; };
// 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 global.utils.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 action = {}; const action = {};
action[`${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`); action[`${params.netid}`] = net.value.replace(`rate=${net.rate}`, `rate=${params.rate}`);
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -154,12 +171,14 @@ router.delete("/:netid/delete", async (req, res) => {
vmid: req.params.vmid, vmid: req.params.vmid,
netid: req.params.netid netid: req.params.netid
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// 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) {
@@ -167,10 +186,13 @@ router.delete("/:netid/delete", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = { delete: `${params.netid}` };
const method = params.type === "qemu" ? "POST" : "PUT"; const method = params.type === "qemu" ? "POST" : "PUT";
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, { delete: `${params.netid}` }); const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
await global.pve.syncInstance(params.node, params.vmid); await global.pve.syncInstance(params.node, params.vmid);
}); });
+103 -8
View File
@@ -1,8 +1,74 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth; /**
const approveResources = global.utils.approveResources; * GET - get available pcie devices for the given node and user
* request:
* - node: string - vm host node id
* responses:
* - 200: PVE PCI Device Object
* - 401: {auth: false}
* - 401: {auth: false, path: string}
* - 500: {error: string}
*/
router.get("/", async (req, res) => {
const params = {
node: req.params.node,
type: req.params.type,
vmid: req.params.vmid,
};
// check auth
const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get instance config for pool membership
const instance = await global.pve.getInstance(params.node, params.vmid);
// ensure that requested instance type is vmid
if (instance.type !== "VM") {
res.status(400).send({ auth: true, error: `actual instance type is ${instance.type} but must be VM` });
}
else if (params.type !== "qemu") {
res.status(400).send({ auth: true, error: `requested instance type is ${params.type} but must be qemu` });
}
// get pool and pool allowed nodes
const pool = await global.access.getPool(instance.pool, req.cookies);
const poolNodes = pool.pool["nodes-allowed"];
if (poolNodes[params.node] !== true) { // user does not have access to the node
res.status(401).send({ auth: false, path: params.node });
res.end();
return;
}
// get remaining user resources
const poolAvailPci = (await global.utils.getPoolResources(req, instance.pool)).pci.nodes[params.node]; // we assume that the node list is used. TODO support global lists
if (poolAvailPci === undefined) { // user has no available devices on this node, so send an empty list
res.status(200).send([]);
res.end();
}
else {
// get node avail devices
const node = await global.pve.getNode(params.node);
let availableDevices = [];
// get each device and filter out only thise which are not reserved
for (const device of Object.values(node.devices)) {
if (device.reserved === false) {
availableDevices.push(device);
}
}
//further filter out only devices which the user has access to
availableDevices = availableDevices.filter(nodeAvail => poolAvailPci.some((userAvail) => {
return nodeAvail.device_name && nodeAvail.device_name.includes(userAvail.match) && userAvail.avail > 0;
}));
res.status(200).send(availableDevices);
res.end();
}
});
/** /**
* GET - get instance pcie device data * GET - get instance pcie device data
@@ -24,12 +90,14 @@ router.get("/:hostpci", async (req, res) => {
vmid: req.params.vmid, vmid: req.params.vmid,
hostpci: req.params.hostpci hostpci: req.params.hostpci
}; };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// get device // get device
const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const device = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!device) { if (!device) {
@@ -66,20 +134,27 @@ router.post("/:hostpci/modify", async (req, res) => {
device: req.body.device, device: req.body.device,
pcie: req.body.pcie pcie: req.body.pcie
}; };
// check if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." }); res.status(500).send({ error: "Type must be qemu (vm)." });
res.end(); res.end();
return; return;
} }
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { 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
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!existingDevice) { if (!existingDevice) {
@@ -87,6 +162,7 @@ router.post("/:hostpci/modify", async (req, res) => {
res.end(); res.end();
return; return;
} }
// only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci // only check user and node availability if base id is different, we do the split in case of existing partial-function hostpci
const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
if (existingDevice.device_bus.split(".")[0] !== params.device) { if (existingDevice.device_bus.split(".")[0] !== params.device) {
@@ -100,7 +176,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 global.utils.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();
@@ -113,9 +189,11 @@ router.post("/:hostpci/modify", async (req, res) => {
return; return;
} }
} }
// setup action // setup action
const action = {}; const action = {};
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -146,20 +224,27 @@ router.post("/:hostpci/create", async (req, res) => {
device: req.body.device, device: req.body.device,
pcie: req.body.pcie pcie: req.body.pcie
}; };
// check if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." }); res.status(500).send({ error: "Type must be qemu (vm)." });
res.end(); res.end();
return; return;
} }
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { 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
const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci); const existingDevice = await global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (existingDevice) { if (existingDevice) {
@@ -167,27 +252,32 @@ router.post("/:hostpci/create", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup request // setup request
const node = await global.pve.getNode(params.node); const node = await global.pve.getNode(params.node);
const requestedDevice = node.devices[`${params.device}`]; const requestedDevice = node.devices[`${params.device}`];
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 global.utils.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();
return; return;
} }
// check node availability // check node availability
if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) { if (!Object.values(node.devices).some(element => element.device_bus.split(".")[0] === params.device && element.reserved === false)) {
res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` }); res.status(500).send({ error: `Device ${params.device} is already in use on ${params.node}.` });
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = {}; const action = {};
action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; action[`${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
// commit action // commit action
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
@@ -215,18 +305,21 @@ router.delete("/:hostpci/delete", async (req, res) => {
vmid: req.params.vmid, vmid: req.params.vmid,
hostpci: req.params.hostpci hostpci: req.params.hostpci
}; };
// check if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {
res.status(500).send({ error: "Type must be qemu (vm)." }); res.status(500).send({ error: "Type must be qemu (vm)." });
res.end(); res.end();
return; return;
} }
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
const auth = await checkAuth(req.cookies, res, vmpath); const auth = await global.utils.checkAuth(req.cookies, res, vmpath);
if (!auth) { if (!auth) {
return; return;
} }
// check device is in instance config // check device is in instance config
const device = global.pve.getDevice(params.node, params.vmid, params.hostpci); const device = global.pve.getDevice(params.node, params.vmid, params.hostpci);
if (!device) { if (!device) {
@@ -234,8 +327,10 @@ router.delete("/:hostpci/delete", async (req, res) => {
res.end(); res.end();
return; return;
} }
// setup action // setup action
const action = { delete: `${params.hostpci}` }; const action = { delete: `${params.hostpci}` };
// commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason // commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason
const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action); const result = await global.pve.requestPVE(`${vmpath}/config`, "POST", { root: true }, action);
await global.pve.handleResponse(params.node, result, res); await global.pve.handleResponse(params.node, result, res);
+4
View File
@@ -12,11 +12,15 @@ router.get("/config/:key", async (req, res) => {
const params = { const params = {
key: req.params.key key: req.params.key
}; };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
// check if users are allowed to get the config value
// return the value if so, otherwise send unauthorized
const allowKeys = ["resources"]; const allowKeys = ["resources"];
if (allowKeys.includes(params.key)) { if (allowKeys.includes(params.key)) {
const config = global.config; const config = global.config;
+10 -15
View File
@@ -2,11 +2,7 @@ import { WebSocketServer } from "ws";
import * as cookie from "cookie"; import * as cookie from "cookie";
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
const getObjectHash = global.utils.getObjectHash;
const getTimeLeft = global.utils.getTimeLeft;
// maps usernames to socket object(s) // maps usernames to socket object(s)
const userSocketMap = {}; const userSocketMap = {};
@@ -47,15 +43,15 @@ if (schemes.hash.enabled) {
*/ */
router.get("/hash", async (req, res) => { router.get("/hash", async (req, res) => {
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await global.utils.checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
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(global.utils.getObjectHash(state));
}); });
console.log("clientsync: enabled hash sync"); console.log("clientsync: enabled hash sync");
} }
@@ -135,7 +131,7 @@ if (schemes.interrupt.enabled) {
// AND if the next event trigger is more than the new rate in the future, // AND if the next event trigger is more than the new rate in the future,
// restart the timer with the new rate // restart the timer with the new rate
// avoids a large requested rate preventing a faster rate from being fulfilled // avoids a large requested rate preventing a faster rate from being fulfilled
else if (rate < Math.min.apply(null, Object.values(requestedRates)) && getTimeLeft(timer) > rate) { else if (rate < Math.min.apply(null, Object.values(requestedRates)) && global.utils.getTimeLeft(timer) > rate) {
clearTimeout(timer); clearTimeout(timer);
timer = setTimeout(handleInterruptSync, rate); timer = setTimeout(handleInterruptSync, rate);
const time = global.process.uptime(); const time = global.process.uptime();
@@ -166,11 +162,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 +185,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
@@ -260,7 +255,7 @@ function extractClusterState (status, resourceTypes, hashIndividual = false) {
pool: resource.pool || null pool: resource.pool || null
}; };
if (hashIndividual) { if (hashIndividual) {
const hash = getObjectHash(state[resource.id]); const hash = global.utils.getObjectHash(state[resource.id]);
state[resource.id].hash = hash; state[resource.id].hash = hash;
} }
} }
+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/`, "");
+126 -87
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,84 +191,79 @@ 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;
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;
reason[key] = { approved: false, reason: `${key} not allowed` }; reason[key] = { approved: false, reason: `${key} not allowed` };
continue; continue;
// return;
} }
// 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; reason[key] = { approved: false, reason: `${key} ${configResources[key].whitelist ? "not in whitelist" : "in blacklist"}` };
reason[key] = { approved: false, reason: `${key} ${dbResources[key].whitelist ? "not in whitelist" : "in blacklist"}` };
// return;
continue; continue;
} }
} }
// if either the requested or avail resource is not strictly a number, block // if either the requested or avail resource is not strictly a number, block
else if (typeof (resourceData.avail) !== "number" || typeof (request[key]) !== "number") { else if (typeof (resourceData.avail) !== "number" || typeof (request[key]) !== "number") {
// approved = false;
reason[key] = { approved: false, reason: `expected ${key} to be a number but got ${request[key]}` }; reason[key] = { approved: false, reason: `expected ${key} to be a number but got ${request[key]}` };
continue; continue;
// return;
} }
// if the avail resources is less than the requested resources, block // if the avail resources is less than the requested resources, block
else if (resourceData.avail - request[key] < 0) { else if (resourceData.avail - request[key] < 0) {
// approved = false;
reason[key] = { approved: false, reason: `${key} requested ${request[key]} which is more than ${resourceData.avail} available` }; reason[key] = { approved: false, reason: `${key} requested ${request[key]} which is more than ${resourceData.avail} available` };
continue; continue;
// return;
} }
reason[key] = { approved: true, reason: "ok" }; reason[key] = { approved: true, reason: "ok" };
@@ -331,15 +327,15 @@ export function readJSONFile (path) {
return JSON.parse(readFileSync(path)); return JSON.parse(readFileSync(path));
} }
catch (e) { catch (e) {
console.log(`error opening ${path}: ${e}`); console.log(`json: error opening ${path}: ${e}`);
exit(1); exit(1);
} }
}; };
/** /**
* * Parse username into user object using the uid@realm format.
* @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 +348,46 @@ export function getUserObjFromUsername (username) {
return null; return null;
} }
} }
/**
* Parse groupname into group object using the gid-realm format.
* @param {*} groupname
* @returns {Object | null} user object containing groupid and realm or null if groupname format was invalid
*/
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;
}
}
/**
* Inspect pool object and return true if pool contains any groups which contain the user object.
* @param {Object} poolObj pool data object
* @param {Object} userObj user object containing id and realm
* @returns {boolean} true if userObj in poolObj
*/
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;
}