10 Commits
Author SHA1 Message Date
alu 2a2bc0b3fa update README 2026-10-27 21:11:24 +00:00
alu e1c1de3fee move mv-isos and ct-templates paths to /access/users/:username/...
tronnet/workflows: Code Style -- Web Components / style-web (push) Successful in 16s
2026-09-14 19:00:46 +00:00
alu 4731b699c2 fix example network unit in template.config,json 2026-08-26 21:00:12 +00:00
alu 4557586e88 fix linting issue
tronnet/workflows: Code Style -- Web Components / style-web (push) Successful in 15s
2026-08-26 20:56:52 +00:00
alu da92ce9ea0 swap access-amanger-api backend to json request encoding
tronnet/workflows: Code Style -- Web Components / style-web (push) Successful in 17s
2026-08-20 20:20:52 +00:00
alu be82519d80 move api tests 2026-08-18 22:59:10 +00:00
alu 54c1fe846e fix possible bug when access-manager-api resource name is mismatched with api config
tronnet/workflows: Code Style -- Web Components / style-web (push) Successful in 15s
2026-08-11 23:49:59 +00:00
alu 44b5c2b121 Merge pull request 'API v2.0.0' (#3) from v2.0.0 into main
Reviewed-on: #3
2026-08-10 21:10:31 +00:00
alu 734b0271fa update readme, update template config 2026-07-06 23:40:10 +00:00
alu f3b279e636 fix possible crash caused by malformed rate request 2026-07-06 18:28:53 +00:00
11 changed files with 131 additions and 224 deletions

No files matched your search

+1 -1
View File
@@ -3,4 +3,4 @@
**/localdb.json
**/docs
**/config.json
.vscode/settings.json
.vscode/*
+12 -86
View File
@@ -23,105 +23,31 @@ In the Proxmox web GUI, perform the following steps:
- Sys.Audit
4. Add a new API Token Permission with path: `/`, select the API token created previously, and role: `proxmoxaas-api`
5. Add a new User Permission with path: `/`, select the `proxmoxaas-api` user, and role: `proxmoxaas-api`
6. To prevent users from bypassing the API provided methods, create a new role with only the following permssions:
6. To prevent users from bypassing the API provided methods, create a new role `PAAS-Client` with only the following permssions:
- Datastore.Audit
- Pool.Audit
- VM.Audit
- VM.Console
- VM.Monitor
- VM.PowerMgmt
### Installation - API
1. Clone this repo onto the `ProxmoxAAS-API` host
2. Run `npm install` to initiaze the package requirements
3. Copy `template.config.json` as `config.json` and modify the following values:
1. In `backends/pve/config`:
- url: the URI to the Proxmox API, ie `https://pve.domain.net/api2/json`
- fabric: the URL to the ProxmoxAAS-Fabric, ie `https://fabric.local`
- token: the user(name), authentication realm (pam), token id, and token secrey key (uuid)
- root (**Optional**): In order to allow users to customize instance pcie devices, the API must use the root credentials for privilege elevation. Provide the root username, ie. `root@pam`, and root user password
2. In `backends/paasldap/config` (**Optional**):
- url: url to a PAAS-LDAP server API ie. `http://ldap.local`
3. In `handlers/auth`:
- Add any authentication handlers to be used by the API. Add the realm name (ie. `pve`) as the key and the handler name as provided in `backends`. For example, a PAAS-LDAP handler could be added as `"paas-ldap": "paasldap"` and users in the realm `user@paas-ldap` will use this handler to perform auth actions. Refer to [backends](#Backends)
4. In `application`:
1. In `application`:
- hostname - the ProxmoxAAS-Dashboard URL, ie `paas.domain.net`
- domain - the base domain for the dashboard and proxmox, ie `domain.net`
- listenPort - the port you want the API to listen on, ie `8081`
5. In `useriso`:
1. In `backends/pve/config`:
- url: the URI to the **Proxmox API**, ie `https://pve.domain.net/api2/json`
- fabric: the URL to **ProxmoxAAS-Fabric**, ie `http://fabric.local:8082`
- token: the `proxmoxaas-api` user(name), authentication realm (pam), token id, and token secrey key (uuid)
- root (**Optional**): In order to allow users to customize instance pcie devices, the API must use the root credentials for privilege elevation. Provide the root username, ie. `root@pam`, and root user password
2. In `backends/access_manager/config`:
- url: url to a access-mamanger-api server API ie. `http://access.local:8083`
4. In `useriso`:
- node: host of storage with user accessible iso files
- storage: name of storage with user accessible iso files
6. In `backups`:
5. In `backups`:
- storage: name of storage for instance backups
4. Start the service using `node .`, or call the provided shell script, or use the provided systemctl service script
# Backends
Backend handlers are used to interface with any number and type of backend data source used to store ProxmoxAAS data. Most data involves users, groups, and membership relationships. The default backends are sufficient to run a small cluster, but additional backend handlers can be created.
## Interface
Each backend must implement the following methods:
<table>
<tr>
<td>openSession</td>
<td>opens a session to the backend by creating a session token</td>
</tr>
<tr>
<td>closeSession</td>
<td>closes a session to the backend</td>
</tr>
</table>
Additionally, backends dealing with user data may also need to implement:
<table>
<tr>
<td>addUser</td>
<td>create a user</td>
</tr>
<tr>
<td>getUser</td>
<td>retrieve user data including membership</td>
</tr>
<tr>
<td>setUser</td>
<td>modify a user</td>
</tr>
<tr>
<td>delUser</td>
<td>delete a user</td>
</tr>
<tr>
<td>addGroup</td>
<td>create a group</td>
</tr>
<tr>
<td>getGroup</td>
<td>retrieve group data including members</td>
</tr>
<tr>
<td>setGroup</td>
<td>modify group data except membership</td>
</tr>
<tr>
<td>delGroup</td>
<td>delete group</td>
</tr>
<tr>
<td>addUserToGroup</td>
<td>add user to group as member</td>
</tr>
<tr>
<td>delUserFromGroup</td>
<td>remove user from group</td>
</tr>
</table>
Not all user backends will necessarily implement all the methods fully. For example, backends which do not store group data may not need to implement the group related methods.
Specific documentation can be found in `src/backends/backends.js`.
## Multiple Interfaces
Multiple backends can be specified using the config. During a backend operation involving users, each backend method will be called in the order specified in the config. If the operation is to retrieve user data, the responses will be merged favoring the last backend called.
-18
View File
@@ -1,18 +0,0 @@
### 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
@@ -1,36 +0,0 @@
### 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
@@ -1,2 +0,0 @@
### Get version
GET {{baseUrl}}/version
+20 -10
View File
@@ -31,7 +31,7 @@
},
"handlers": {
"instance": "pve",
"users": ["access_manager"]
"users": "access_manager"
},
"useriso": {
"node": "examplenode1",
@@ -44,7 +44,8 @@
"cpu": {
"type": "list",
"whitelist": true,
"display": false
"display": false,
"category": ""
},
"cores": {
"name": "vCPU",
@@ -53,7 +54,8 @@
"base": 1024,
"compact": false,
"unit": "Cores",
"display": true
"display": true,
"category": ""
},
"memory": {
"name": "RAM",
@@ -62,7 +64,8 @@
"base": 1024,
"compact": true,
"unit": "B",
"display": true
"display": true,
"category": ""
},
"swap": {
"name": "SWAP",
@@ -71,7 +74,8 @@
"base": 1024,
"compact": true,
"unit": "B",
"display": true
"display": true,
"category": ""
},
"local": {
"name": "local",
@@ -84,9 +88,11 @@
"rootfs",
"mp",
"sata",
"scsi",
"unused"
],
"display": true
"display": true,
"category": "Storage"
},
"cephpl": {
"name": "cephpl",
@@ -99,9 +105,11 @@
"rootfs",
"mp",
"sata",
"scsi",
"unused"
],
"display": true
"display": true,
"category": "Storage"
},
"network": {
"name": "Network",
@@ -109,13 +117,15 @@
"multiplier": 1000000,
"base": 1000,
"compact": true,
"unit": "B/s",
"display": true
"unit": "b/s",
"display": true,
"category": ""
},
"pci": {
"type": "list",
"whitelist": true,
"display": true
"display": true,
"category": "Devices"
}
},
"clientsync": {
+1 -1
View File
@@ -25,7 +25,7 @@ export default class ACCESS_MANAGER_API extends ACCESS_BACKEND {
mode: "cors",
credentials: "include",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
"Content-Type": "application/json"
},
data: body
};
+79
View File
@@ -1,6 +1,9 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const config = global.config;
const checkAuth = global.utils.checkAuth;
/**
* GET - get specific user
* request:
@@ -35,3 +38,79 @@ router.get("/:username", async (req, res) => {
res.status(200).send({ user });
});
/**
* GET - get user accessible iso files
* response:
* - 200: Array.<Object>
* - 401: {auth: false}
*/
router.get("/:username/vm-isos", async (req, res) => {
const params = {
username: req.params.username
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
// check requested username is current user
if (req.cookies.username !== params.username) {
res.status(401).send({ auth: false });
return;
}
// get user iso config
const userIsoConfig = config.useriso;
// get all isos
const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true });
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data;
const userIsos = [];
isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, "");
userIsos.push(iso);
});
userIsos.sort();
res.status(200).send(userIsos);
});
/**
* GET - get user accessible container template files
* response:
* - 200: Array.<Object>
* - 401: {auth: false}
*/
router.get("/:username/ct-templates", async (req, res) => {
const params = {
username: req.params.username
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
// check requested username is current user
if (req.cookies.username !== params.username) {
res.status(401).send({ auth: false });
return;
}
// get user iso config
const userIsoConfig = config.useriso;
// get all isos
const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=vztmpl`, "GET", { token: true });
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data;
const userIsos = [];
isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, "");
userIsos.push(iso);
});
userIsos.sort();
res.status(200).send(userIsos);
});
+8 -3
View File
@@ -118,9 +118,14 @@ if (schemes.interrupt.enabled) {
const parsed = message.toString().split(" ");
const cmd = parsed[0];
// command is rate and the value is valid
if (cmd === "rate" && parsed[1] >= schemes.interrupt["min-rate"] && parsed[1] <= schemes.interrupt["max-rate"]) {
if (cmd === "rate" && parsed.length === 2 && parsed[1] >= schemes.interrupt["min-rate"] && parsed[1] <= schemes.interrupt["max-rate"]) {
// get requested rate in ms
const rate = Number(parsed[1]) * 1000;
if (isNaN(rate)) {
socket.send("error: rate <rate> must be a number");
socket.terminate();
return;
}
// if timer has not started, start it with requested rate
if (!timer) {
timer = setTimeout(handleInterruptSync, rate);
@@ -142,12 +147,12 @@ if (schemes.interrupt.enabled) {
}
// command is rate but the requested value is out of bounds, terminate socket
else if (cmd === "rate") {
socket.send(`error: rate must be in range [${schemes.interrupt["min-rate"]}, ${schemes.interrupt["max-rate"]}].`);
socket.send(`error: rate <rate> must be in range [${schemes.interrupt["min-rate"]}, ${schemes.interrupt["max-rate"]}].`);
socket.terminate();
}
// otherwise, command is invalid, terminate socket
else {
socket.send(`error: ${cmd} command not found.`);
socket.send(`error: ${cmd} command not supported.`);
socket.terminate();
}
});
-65
View File
@@ -1,65 +0,0 @@
import { Router } from "express";
export const router = Router({ mergeParams: true }); ;
const config = global.config;
const checkAuth = global.utils.checkAuth;
/**
* GET - get user accessible iso files
* response:
* - 200: Array.<Object>
* - 401: {auth: false}
*/
router.get("/vm-isos", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get user iso config
const userIsoConfig = config.useriso;
// get all isos
const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=iso`, "GET", { token: true });
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data;
const userIsos = [];
isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:iso/`, "");
userIsos.push(iso);
});
userIsos.sort();
res.status(200).send(userIsos);
});
/**
* GET - get user accessible container template files
* response:
* - 200: Array.<Object>
* - 401: {auth: false}
*/
router.get("/ct-templates", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
// get user iso config
const userIsoConfig = config.useriso;
// get all isos
const content = await global.pve.requestPVE(`/nodes/${userIsoConfig.node}/storage/${userIsoConfig.storage}/content?content=vztmpl`, "GET", { token: true });
if (content.status !== 200) {
res.status(content.status).send({ error: content.statusText });
return;
}
const isos = content.data;
const userIsos = [];
isos.forEach((iso) => {
iso.name = iso.volid.replace(`${userIsoConfig.storage}:vztmpl/`, "");
userIsos.push(iso);
});
userIsos.sort();
res.status(200).send(userIsos);
});
+10 -2
View File
@@ -4,6 +4,7 @@ import url from "url";
import * as fs from "fs";
import { readFileSync } from "fs";
import { exit } from "process";
import { log } from "console";
/**
* Check if a user is authorized to access a specified vm, or the cluster in general.
@@ -76,7 +77,7 @@ export async function getPoolResources (req, pool) {
// setup the pool resource object with used and avail for each resource and each resource pool
// also add a total counter for each resource (only used for display, not used to check requests)
for (const resourceName of Object.keys(poolConfigResources)) {
if (configResources[resourceName].type === "list") {
if (resourceName in configResources && configResources[resourceName].type === "list") {
poolConfigResources[resourceName].total = [];
poolConfigResources[resourceName].global.forEach((e) => {
e.used = 0;
@@ -105,7 +106,7 @@ export async function getPoolResources (req, pool) {
});
}
}
else {
else if (resourceName in configResources){ // numeric or storage
const total = {
max: 0,
used: 0,
@@ -123,6 +124,13 @@ export async function getPoolResources (req, pool) {
}
poolConfigResources[resourceName].total = total;
}
else {
// pool's resource config included a resource which was not in the metadata config
// delete it from the config
// remeber that a resource requested with no config limit is default deny
delete poolConfigResources[resourceName];
console.log(`utils: pool had resource key ${resourceName} which is not in the config.json`);
}
}
const resources = await global.pve.getPoolResources(req.cookies, pool);