diff --git a/README.md b/README.md index e69cfee..0a90bf2 100644 --- a/README.md +++ b/README.md @@ -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 `:8006/api2/json` or `/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` diff --git a/config/localdb.json.template b/config/localdb.json.template deleted file mode 100644 index 5eef203..0000000 --- a/config/localdb.json.template +++ /dev/null @@ -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 - } - - } - } - } - } -} \ No newline at end of file diff --git a/config/template.localdb.json b/config/template.localdb.json new file mode 100644 index 0000000..f083e4d --- /dev/null +++ b/config/template.localdb.json @@ -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 + } + } + } + } + } +} \ No newline at end of file diff --git a/src/db.js b/src/db.js index 02f06b9..0326d6e 100644 --- a/src/db.js +++ b/src/db.js @@ -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; - } + return this.#data.users[username]; } } 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; diff --git a/src/main.js b/src/main.js index 308d271..bb0f521 100644 --- a/src/main.js +++ b/src/main.js @@ -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 config = db.getUserConfig(req.cookies.username); - res.status(200).send(config.resources); + let allowKeys = ["resources", "cluster", "nodes"]; + if (allowKeys.includes(params.key)){ + let config = db.getUserConfig(req.cookies.username); + 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(); diff --git a/src/utils.js b/src/utils.js index 85ea061..dd92b40 100644 --- a/src/utils.js +++ b/src/utils.js @@ -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 = {};