consolidate user config paths,

move global config values to global key in localdb
This commit is contained in:
Arthur Lu 2023-07-05 23:14:45 +00:00
parent 8e476ea352
commit 4fbb64745b
6 changed files with 233 additions and 242 deletions

View File

@ -22,7 +22,7 @@ In Proxmox VE, follow the following steps:
## Installation - API
1. Clone this repo onto `Client Host`
2. Run `npm install` to initiaze the package requirements
3. Copy `localdb.json.template` as `localdb.json` and modify the following values under `pveAPIToken`:
3. Copy `template.localdb.json` as `localdb.json` and modify the following values under `pveAPIToken`:
- pveAPI - the URI to the Proxmox API, ie `<proxmoxhost>:8006/api2/json` or `<proxmox URL>/api2/json` if Proxmox VE is behind a reverse proxy.
- hostname - the ProxmoxAAS-Client URL, ie `host.domain.tld`
- domain - the base domain for the client and proxmox, ie `domain.tld`

View File

@ -1,158 +0,0 @@
{
"application": {
"pveAPI": "https://pve.mydomain.example/api2/json",
"pveAPIToken": {
"user": "proxmoxaas-api",
"realm": "pve",
"id": "token",
"uuid": "token-secret-value"
},
"pveroot": {
"username": "root@pam",
"password": "rootpassword"
},
"listenPort": 80,
"hostname": "client.mydomain.example",
"domain": "mydomain.example"
},
"resources": {
"cpu": {
"type": "list",
"whitelist": true,
"display": false
},
"cores": {
"type": "numeric",
"multiplier": 1,
"base": 1024,
"compact": false,
"unit": "Cores",
"display": true
},
"memory": {
"type": "numeric",
"multiplier": 1048576,
"base": 1024,
"compact": true,
"unit": "B",
"display": true
},
"swap": {
"type": "numeric",
"multiplier": 1048576,
"base": 1024,
"compact": true,
"unit": "B",
"display": true
},
"local": {
"type": "storage",
"multiplier": 1,
"base": 1024,
"compact": true,
"unit": "B",
"disks": [
"rootfs",
"mp",
"sata",
"unused"
],
"display": true
},
"cephpl": {
"type": "storage",
"multiplier": 1,
"base": 1024,
"compact": true,
"unit": "B",
"disks": [
"rootfs",
"mp",
"sata",
"unused"
],
"display": true
},
"network": {
"type": "network",
"multiplier": 1000000,
"base": 1000,
"compact": true,
"unit": "B/s",
"display": true
},
"pci": {
"type": "list",
"whitelist": true,
"display": true
}
},
"users": {
"exampleuser@examplepool": {
"resources": {
"max": {
"cpu": ["kvm64", "host"],
"cores": 128,
"memory": 131072,
"swap": 131072,
"local": 1099511627776,
"cephpl": 1099511627776,
"network": 100000,
"pci": ["Device Name Matcher 1", "Device Name Matcher 2"]
}
},
"nodes": [
"examplenode1",
"examplenode2"
],
"cluster": {
"vmid": {
"min": 200,
"max": 299
},
"pool": "examplepool"
},
"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
},
"net0": {
"value": "virtio,bridge=vmbr0,tag=10,rate=1000",
"resource": {
"name": "network",
"amount": 1000
}
}
}
},
"network": {
"lxc": {
"type": "veth",
"bridge": "vmbr0",
"vlan": 10,
"ip": "dhcp",
"ip6": "dhcp"
},
"qemu": {
"type": "virtio",
"bridge": "vmbr0",
"vlan": 10
}
}
}
}
}
}

View File

@ -0,0 +1,165 @@
{
"global": {
"application": {
"pveAPI": "https://pve.mydomain.example/api2/json",
"pveAPIToken": {
"user": "proxmoxaas-api",
"realm": "pve",
"id": "token",
"uuid": "token-secret-value"
},
"pveroot": {
"username": "root@pam",
"password": "rootpassword"
},
"listenPort": 80,
"hostname": "client.mydomain.example",
"domain": "mydomain.example"
},
"resources": {
"cpu": {
"type": "list",
"whitelist": true,
"display": false
},
"cores": {
"type": "numeric",
"multiplier": 1,
"base": 1024,
"compact": false,
"unit": "Cores",
"display": true
},
"memory": {
"type": "numeric",
"multiplier": 1048576,
"base": 1024,
"compact": true,
"unit": "B",
"display": true
},
"swap": {
"type": "numeric",
"multiplier": 1048576,
"base": 1024,
"compact": true,
"unit": "B",
"display": true
},
"local": {
"type": "storage",
"multiplier": 1,
"base": 1024,
"compact": true,
"unit": "B",
"disks": [
"rootfs",
"mp",
"sata",
"unused"
],
"display": true
},
"cephpl": {
"type": "storage",
"multiplier": 1,
"base": 1024,
"compact": true,
"unit": "B",
"disks": [
"rootfs",
"mp",
"sata",
"unused"
],
"display": true
},
"network": {
"type": "network",
"multiplier": 1000000,
"base": 1000,
"compact": true,
"unit": "B/s",
"display": true
},
"pci": {
"type": "list",
"whitelist": true,
"display": true
}
}
},
"users": {
"exampleuser@examplepool": {
"resources": {
"max": {
"cpu": [
"kvm64",
"host"
],
"cores": 128,
"memory": 131072,
"swap": 131072,
"local": 1099511627776,
"cephpl": 1099511627776,
"network": 100000,
"pci": [
"Device Name Matcher 1",
"Device Name Matcher 2"
]
}
},
"nodes": [
"examplenode1",
"examplenode2"
],
"cluster": {
"vmid": {
"min": 100,
"max": 199
},
"pool": "examplepool"
},
"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
},
"net0": {
"value": "virtio,bridge=vmbr0,tag=10,rate=1000",
"resource": {
"name": "network",
"amount": 1000
}
}
}
},
"network": {
"lxc": {
"type": "veth",
"bridge": "vmbr0",
"vlan": 10,
"ip": "dhcp",
"ip6": "dhcp"
},
"qemu": {
"type": "virtio",
"bridge": "vmbr0",
"vlan": 10
}
}
}
}
}
}

View File

@ -22,27 +22,18 @@ class LocalDB {
writeFileSync(path, JSON.stringify(this.#data));
}
getApplicationConfig () {
return this.#data.application;
}
getResourceConfig () {
return this.#data.resources;
getGlobalConfig () {
return this.#data.global;
}
getUserConfig (username) {
if (this.#data.users[username]) {
return this.#data.users[username];
}
else {
return null;
}
}
}
export const db = new LocalDB();
export const pveAPI = db.getApplicationConfig().pveAPI;
export const pveAPIToken = db.getApplicationConfig().pveAPIToken;
export const listenPort = db.getApplicationConfig().listenPort;
export const hostname = db.getApplicationConfig().hostname;
export const domain = db.getApplicationConfig().domain;
export const pveAPI = db.getGlobalConfig().application.pveAPI;
export const pveAPIToken = db.getGlobalConfig().application.pveAPIToken;
export const listenPort = db.getGlobalConfig().application.listenPort;
export const hostname = db.getGlobalConfig().application.hostname;
export const domain = db.getGlobalConfig().application.domain;

View File

@ -37,18 +37,6 @@ app.get("/api/echo", (req, res) => {
res.status(200).send({ body: req.body, cookies: req.cookies });
});
/**
* GET - check authentication
* responses:
* - 200: {auth: true, path: String}
* - 401: {auth: false, path: String}
*/
app.get("/api/auth", async (req, res) => {
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
res.status(200).send({ auth: true });
});
/**
* GET - proxy proxmox api without privilege elevation
* request and responses passed through to/from proxmox
@ -69,6 +57,18 @@ app.post("/api/proxmox/*", async (req, res) => { // proxy endpoint for POST prox
res.status(result.status).send(result.data);
});
/**
* GET - check authentication
* responses:
* - 200: {auth: true, path: String}
* - 401: {auth: false, path: String}
*/
app.get("/api/auth", async (req, res) => {
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
res.status(200).send({ auth: true });
});
/**
* POST - safer ticket generation using proxmox authentication but adding HttpOnly
* request:
@ -78,7 +78,7 @@ app.post("/api/proxmox/*", async (req, res) => { // proxy endpoint for POST prox
* - 200: {auth: true, path: String}
* - 401: {auth: false, path: String}
*/
app.post("/api/ticket", async (req, res) => {
app.post("/api/auth/ticket", async (req, res) => {
let response = await requestPVE("/access/ticket", "POST", null, JSON.stringify(req.body));
if (!(response.status === 200)) {
res.status(response.status).send({ auth: false });
@ -101,7 +101,7 @@ app.post("/api/ticket", async (req, res) => {
* responses:
* - 200: {auth: false, path: String}
*/
app.delete("/api/ticket", async (req, res) => {
app.delete("/api/auth/ticket", async (req, res) => {
let expire = new Date(0);
res.cookie("PVEAuthCookie", "", { domain: domain, path: "/", httpOnly: true, secure: true, expires: expire });
res.cookie("CSRFPreventionToken", "", { domain: domain, path: "/", httpOnly: true, secure: true, expires: expire });
@ -110,13 +110,35 @@ app.delete("/api/ticket", async (req, res) => {
res.status(200).send({ auth: false });
});
/**
* GET - get db global resource configuration
* responses:
* - 200: Object
*/
app.get("/api/global/config/:key", async (req, res) => {
let params = {
key: req.params.key
}
// check auth
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
let allowKeys = ["resources"];
if (allowKeys.includes(params.key)){
let config = db.getGlobalConfig();
res.status(200).send(config[params.key]);
}
else {
res.status(401).send({auth: false, error: `User is not authorized to access /global/config/${params.key}.`});
}
});
/**
* 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, path: String}
*/
app.get("/api/user/resources", async (req, res) => {
app.get("/api/user/dynamic/resources", async (req, res) => {
// check auth
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
@ -125,60 +147,31 @@ app.get("/api/user/resources", async (req, res) => {
});
/**
* GET - get db global resource configuration
* responses:
* - 200: Object
*/
app.get("/api/global/config/resources", async (req, res) => {
// check auth
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
let config = db.getResourceConfig();
res.status(200).send(config);
});
/**
* GET - get db user resource configuration
* GET - get db user configuration by key
* request:
* - key: User config key
* responses:
* - 200: Object
* - 401: {auth: false, path: String}
* - 401: {auth: false, error: String}
*/
app.get("/api/user/config/resources", async (req, res) => {
app.get(`/api/user/config/:key`, async (req, res) => {
let params = {
key: req.params.key
}
// check auth
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
let allowKeys = ["resources", "cluster", "nodes"];
if (allowKeys.includes(params.key)){
let config = db.getUserConfig(req.cookies.username);
res.status(200).send(config.resources);
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 db user cluster configuration
* responses:
* - 200: {pool: String, templates: {lxc: Object, qemu: Object}, vmid: {min: Number, max: Number}}
* - 401: {auth: false, path: String}
*/
app.get("/api/user/config/cluster", async (req, res) => {
// check auth
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
let config = db.getUserConfig(req.cookies.username);
res.status(200).send(config.cluster)
});
/**
* GET - get db user node configuration
* responses:
* - 200: {nodes: String[]}
* - 401: {auth: false, path: String}
*/
app.get("/api/user/config/nodes", async (req, res) => {
// check auth
let auth = await checkAuth(req.cookies, res);
if (!auth) { return; }
let config = db.getUserConfig(req.cookies.username);
res.status(200).send(config.nodes)
})
/**
* POST - detach mounted disk from instance
* request:
@ -259,7 +252,7 @@ app.post(`/api/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP})/di
}
// target disk must be allowed according to source disk's storage options
let diskConfig = await getDiskInfo(params.node, params.type, params.vmid, `unused${params.source}`); // get target disk
let resourceConfig = db.getResourceConfig();
let resourceConfig = db.getGlobalConfig().resources;
if (!resourceConfig[diskConfig.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[diskConfig.storage].disks}].` });
res.end();
@ -486,7 +479,7 @@ app.post(`/api/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP})/di
return;
}
// target disk must be allowed according to storage options
let resourceConfig = db.getResourceConfig();
let resourceConfig = db.getGlobalConfig().resources;
if (!resourceConfig[params.storage].disks.some(diskPrefix => params.disk.startsWith(diskPrefix))) {
res.status(500).send({ error: `Requested target ${params.disk} is not in allowed list [${resourceConfig[params.storage].disks}].` });
res.end();
@ -811,7 +804,7 @@ app.post(`/api/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP})/pc
action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`;
action = JSON.stringify(action);
// commit action
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getApplicationConfig().pveroot), null);
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getGlobalConfig().application.pveroot), null);
if (!(rootauth.status === 200)) {
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();
@ -888,7 +881,7 @@ app.post(`/api/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP})/pc
action[`hostpci${hostpci}`] = `${params.device},pcie=${params.pcie}`;
action = JSON.stringify(action);
// commit action
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getApplicationConfig().pveroot), null);
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getGlobalConfig().application.pveroot), null);
if (!(rootauth.status === 200)) {
res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();
@ -942,7 +935,7 @@ app.delete(`/api/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP})/
// setup action
let action = JSON.stringify({ delete: `hostpci${params.hostpci}` });
// commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getApplicationConfig().pveroot), null);
let rootauth = await requestPVE("/access/ticket", "POST", null, JSON.stringify(db.getGlobalConfig().application.pveroot), null);
if (!(rootauth.status === 200)) {
res.status(response.status).send({ auth: false, error: "API could not authenticate as root user." });
res.end();

View File

@ -28,7 +28,7 @@ export async function checkAuth (cookies, res, vmpath = null) {
}
export async function getUserResources (req, username) {
const dbResources = db.getResourceConfig();
const dbResources = db.getGlobalConfig().resources;
const used = await getUsedResources(req, dbResources);
const max = db.getUserConfig(username).resources.max;
const avail = {};