From b27172dd9e427c21c747e85470f2c2ee71bb89a7 Mon Sep 17 00:00:00 2001 From: Arthur Lu Date: Tue, 9 Jan 2024 00:47:33 +0000 Subject: [PATCH] fix various formatting, add interface for generic backends, add interfaces for DB and AUTH type backends, implement basic user password change method --- src/backends/backends.js | 36 ++++++++++++++++++++++++++ src/backends/localdb.js | 11 ++++++-- src/backends/paasldap.js | 52 +++++++++++++++++++++++++++++++++++++- src/backends/pve.js | 13 ++++------ src/routes/auth.js | 47 +++++++++++++++++++++++++--------- src/routes/cluster.js | 6 ++--- src/routes/cluster/disk.js | 15 +++++------ src/routes/cluster/net.js | 9 +++---- src/routes/cluster/pci.js | 14 +++++----- src/routes/proxmox.js | 3 ++- src/utils.js | 2 +- 11 files changed, 156 insertions(+), 52 deletions(-) diff --git a/src/backends/backends.js b/src/backends/backends.js index fcada40..b517e8e 100644 --- a/src/backends/backends.js +++ b/src/backends/backends.js @@ -26,3 +26,39 @@ export default async () => { global.auth[e] = backends[global.auth[e]]; }); }; + +/** + * 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 {Object} credentials object containing username and password fields + * @returns {Object[]} list of session token objects with token name and value + */ + openSession (credentials) {} + /** + * Closes an opened session with the backend if needed + * @param {*} token list of session token objects with token name and value + * @returns {Boolean} true if session was closed successfully, false otherwise + */ + closeSesssion (tokens) {} +} + +/** + * Interface for user database backends. + */ +export class DB_BACKEND extends BACKEND { + addUser (username, config = null) {} + getUser (username) {} + setUser (username, config) {} + deluser (username) {} +} + +/** + * Interface for user auth backends. + */ +export class AUTH_BACKEND extends BACKEND{ + modUser (username, attributes, params = null) {} +} diff --git a/src/backends/localdb.js b/src/backends/localdb.js index c26aa10..4dffcb7 100644 --- a/src/backends/localdb.js +++ b/src/backends/localdb.js @@ -1,11 +1,14 @@ import { readFileSync, writeFileSync } from "fs"; import { exit } from "process"; +import { DB_BACKEND } from "./backends.js"; -export default class LocalDB { +export default class LocalDB extends DB_BACKEND { #path = null; #data = null; #defaultuser = null; + constructor (config) { + super(); const path = config.dbfile; try { this.#path = path; @@ -13,7 +16,7 @@ export default class LocalDB { this.#defaultuser = global.config.defaultuser; } catch { - console.log(`Error: ${path} was not found. Please follow the directions in the README to initialize localdb.json.`); + console.log(`error: ${path} was not found. Please follow the directions in the README to initialize localdb.json.`); exit(1); } } @@ -32,6 +35,10 @@ export default class LocalDB { writeFileSync(this.#path, JSON.stringify(this.#data)); } + openSession (credentials) { return [] } + + closeSesssion (tokens) {return true } + addUser (username, config = null) { config = config || this.#defaultuser; this.#data.users[username] = config; diff --git a/src/backends/paasldap.js b/src/backends/paasldap.js index 38239a0..c25b41c 100644 --- a/src/backends/paasldap.js +++ b/src/backends/paasldap.js @@ -1,3 +1,53 @@ -export default class PAASLDAP { +import axios from "axios"; +import { AUTH_BACKEND } from "./backends.js"; +export default class PAASLDAP extends AUTH_BACKEND { + #url = null; + + constructor (config) { + super(); + this.#url = 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 {*} body body parameters and data to be sent. Optional. + * @returns {Object} HTTP response object or HTTP error 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.data.binduser = auth.binduser; + content.data.bindpass = auth.bindpass; + } + + try { + return await axios.request(url, content); + } + catch (error) { + error.ok = false; + error.status = 500; + error.data = { + error: error.code + }; + return error; + } + } + + async modUser (userid, attributes, params = null) { + const bind = { binduser: params.binduser, bindpass: params.bindpass }; + return await this.#request(`/users/${userid}`, "POST", bind, attributes); + } } diff --git a/src/backends/pve.js b/src/backends/pve.js index f13f5b8..f07ae51 100644 --- a/src/backends/pve.js +++ b/src/backends/pve.js @@ -13,7 +13,7 @@ export default class PVE { /** * Send HTTP request to proxmox API. Allows requests to be made with user cookie credentials or an API token for controlled priviledge elevation. - * @param {string} path HTTP path, prepended with the proxmox API base path. + * @param {string} path HTTP path, prepended with the proxmox API base url. * @param {string} method HTTP method. * @param {Object} auth authentication method. Set auth.cookies with user cookies or auth.token with PVE API Token. Optional. * @param {string} body body parameters and data to be sent. Optional. @@ -27,7 +27,8 @@ export default class PVE { credentials: "include", headers: { "Content-Type": "application/x-www-form-urlencoded" - } + }, + data: body }; if (auth && auth.cookies) { @@ -35,12 +36,8 @@ export default class PVE { content.headers.Cookie = `PVEAuthCookie=${auth.cookies.PVEAuthCookie}; CSRFPreventionToken=${auth.cookies.CSRFPreventionToken}`; } else if (auth && auth.token) { - auth.token = this.#pveAPIToken; - content.headers.Authorization = `PVEAPIToken=${auth.token.user}@${auth.token.realm}!${auth.token.id}=${auth.token.uuid}`; - } - - if (body) { - content.data = JSON.parse(body); + const token = this.#pveAPIToken; + content.headers.Authorization = `PVEAPIToken=${token.user}@${token.realm}!${token.id}=${token.uuid}`; } try { diff --git a/src/routes/auth.js b/src/routes/auth.js index 67c23f7..aea4ea3 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -27,7 +27,8 @@ router.get("/", async (req, res) => { * - 401: {auth: false} */ router.post("/ticket", async (req, res) => { - const response = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(req.body)); + const body = JSON.parse(JSON.stringify(req.body)); + const response = await global.pve.requestPVE("/access/ticket", "POST", null, body); if (!(response.status === 200)) { res.status(response.status).send({ auth: false }); res.end(); @@ -60,26 +61,48 @@ router.delete("/ticket", async (req, res) => { res.status(200).send({ auth: false }); }); +/** + * POST - change user password + * request: + * - binduser: string + * - bindpass: string + * - username: string + * - password: string + * responses: + * - PAAS-LDAP API response + */ router.post("/password", async (req, res) => { const params = { - password: req.body.password, - userid: req.cookies.username + binduser: req.body.binduser, + bindpass: req.body.bindpass, + username: req.body.username, + password: req.body.password }; - const userRealm = params.userid.split("@").at(-1); + const userRealm = params.username.split("@").at(-1); const domains = (await global.pve.requestPVE("/access/domains", "GET", { token: true })).data.data; const realm = domains.find((e) => e.realm === userRealm); const authHandlers = global.config.handlers.auth; - const handlerType = authHandlers[realm.type]; - if (handlerType === "pve") { - const response = await global.pve.requestPVE("/access/password", "PUT", { cookies: req.cookies }, JSON.stringify(params)); - res.status(response.status).send(response.data); - } - else if (handlerType === "paasldap") { - res.status(501).send({ error: `Auth type ${handlerType} not implemented yet.` }); + if (realm.type in authHandlers) { + const handler = authHandlers[realm.type]; + const userID = params.username.replace(`@${realm.realm}`, ""); + const newAttributes = { + userpassword: params.password + }; + const bindParams = { + binduser: params.binduser, + bindpass: params.bindpass + }; + const response = await handler.modUser(userID, newAttributes, bindParams); + if (response.ok) { + res.status(response.status).send(); + } + else { + res.status(response.status).send({error: response.data.error}); + } } else { - res.status(501).send({ error: `Auth type ${handlerType} not implemented yet.` }); + res.status(501).send({ error: `Auth type ${realm.type} not implemented yet.` }); } }); diff --git a/src/routes/cluster.js b/src/routes/cluster.js index 4e7df76..0570b7b 100644 --- a/src/routes/cluster.js +++ b/src/routes/cluster.js @@ -102,7 +102,7 @@ router.post(`${basePath}/resources`, async (req, res) => { return; } // setup action - let action = { cores: params.cores, memory: params.memory }; + const action = { cores: params.cores, memory: params.memory }; if (params.type === "lxc") { action.swap = Number(params.swap); } @@ -110,7 +110,6 @@ router.post(`${basePath}/resources`, async (req, res) => { action.cpu = params.proctype; action.boot = `order=${params.boot.toString().replaceAll(",", ";")};`; } - action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); @@ -203,7 +202,7 @@ router.post(`${basePath}/create`, async (req, res) => { return; } // setup action by adding non resource values - let action = { + const action = { vmid: params.vmid, cores: Number(params.cores), memory: Number(params.memory), @@ -223,7 +222,6 @@ router.post(`${basePath}/create`, async (req, res) => { else { action.name = params.name; } - action = JSON.stringify(action); // commit action const result = await global.pve.requestPVE(`/nodes/${params.node}/${params.type}`, "POST", { token: true }, action); await global.pve.handleResponse(params.node, result, res); diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index c708f18..107d5a9 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -45,7 +45,7 @@ router.post("/:disk/detach", async (req, res) => { res.end(); return; } - const action = JSON.stringify({ delete: params.disk }); + const action = { delete: params.disk }; const method = params.type === "qemu" ? "POST" : "PUT"; const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); await global.pve.handleResponse(params.node, result, res); @@ -97,9 +97,8 @@ router.post("/:disk/attach", async (req, res) => { return; } // setup action using source disk info from vm config - let action = {}; + const action = {}; action[params.disk] = config[`unused${params.source}`]; - action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); @@ -156,7 +155,7 @@ router.post("/:disk/resize", async (req, res) => { return; } // action approved, commit to action - const action = JSON.stringify({ 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); await global.pve.handleResponse(params.node, result, res); }); @@ -216,14 +215,13 @@ router.post("/:disk/move", async (req, res) => { return; } // create action - let action = { storage: params.storage, delete: params.delete }; + const action = { storage: params.storage, delete: params.delete }; if (params.type === "qemu") { action.disk = params.disk; } else { action.volume = params.disk; } - action = JSON.stringify(action); const route = params.type === "qemu" ? "move_disk" : "move_volume"; // commit action const result = await global.pve.requestPVE(`${vmpath}/${route}`, "POST", { token: true }, action); @@ -272,7 +270,7 @@ router.delete("/:disk/delete", async (req, res) => { return; } // create action - const action = JSON.stringify({ delete: params.disk }); + const action = { delete: params.disk }; const method = params.type === "qemu" ? "POST" : "PUT"; // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); @@ -340,7 +338,7 @@ router.post("/:disk/create", async (req, res) => { } } // setup action - let action = {}; + const action = {}; if (params.disk.includes("ide") && params.iso) { action[params.disk] = `${params.iso},media=cdrom`; } @@ -350,7 +348,6 @@ router.post("/:disk/create", async (req, res) => { else { // type is lxc, use mp and add mp and backup values action[params.disk] = `${params.storage}:${params.size},mp=/${params.disk}/,backup=1`; } - action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); diff --git a/src/routes/cluster/net.js b/src/routes/cluster/net.js index 2153357..e198b8c 100644 --- a/src/routes/cluster/net.js +++ b/src/routes/cluster/net.js @@ -61,14 +61,13 @@ router.post("/:netid/create", async (req, res) => { } // setup action const nc = db.getUser(req.cookies.username).templates.network[params.type]; - let action = {}; + const action = {}; if (params.type === "lxc") { action[`net${params.netid}`] = `name=${params.name},bridge=${nc.bridge},ip=${nc.ip},ip6=${nc.ip6},tag=${nc.vlan},type=${nc.type},rate=${params.rate}`; } else { action[`net${params.netid}`] = `${nc.type},bridge=${nc.bridge},tag=${nc.vlan},rate=${params.rate}`; } - action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); @@ -125,9 +124,8 @@ router.post("/:netid/modify", async (req, res) => { return; } // setup action - let action = {}; + const action = {}; action[`net${params.netid}`] = currentNetworkConfig.replace(`rate=${currentNetworkRate}`, `rate=${params.rate}`); - action = JSON.stringify(action); const method = params.type === "qemu" ? "POST" : "PUT"; // commit action const result = await global.pve.requestPVE(`${vmpath}/config`, method, { token: true }, action); @@ -170,9 +168,8 @@ router.delete("/:netid/delete", async (req, res) => { return; } // setup action - const action = JSON.stringify({ delete: `net${params.netid}` }); 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 }, { delete: `net${params.netid}` }); await global.pve.handleResponse(params.node, result, res); }); diff --git a/src/routes/cluster/pci.js b/src/routes/cluster/pci.js index 135b083..aa4268a 100644 --- a/src/routes/cluster/pci.js +++ b/src/routes/cluster/pci.js @@ -116,11 +116,10 @@ router.post("/:hostpci/modify", async (req, res) => { } } // setup action - let action = {}; + const action = {}; action[`hostpci${params.hostpci}`] = `${params.device},pcie=${params.pcie}`; - action = JSON.stringify(action); // commit action - const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(global.config.backends.pve.config.root)); + const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root); if (!(rootauth.status === 200)) { res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." }); res.end(); @@ -196,11 +195,10 @@ router.post("/create", async (req, res) => { return; } // setup action - let action = {}; + const action = {}; action[`hostpci${hostpci}`] = `${params.device},pcie=${params.pcie}`; - action = JSON.stringify(action); // commit action - const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(global.config.backends.pve.config.root)); + const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root); if (!(rootauth.status === 200)) { res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." }); res.end(); @@ -255,9 +253,9 @@ router.delete("/:hostpci/delete", async (req, res) => { return; } // setup action - const action = JSON.stringify({ delete: `hostpci${params.hostpci}` }); + const action = { delete: `hostpci${params.hostpci}` }; // commit action, need to use root user here because proxmox api only allows root to modify hostpci for whatever reason - const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, JSON.stringify(global.config.backends.pve.config.root)); + const rootauth = await global.pve.requestPVE("/access/ticket", "POST", null, global.config.backends.pve.config.root); if (!(rootauth.status === 200)) { res.status(rootauth.status).send({ auth: false, error: "API could not authenticate as root user." }); res.end(); diff --git a/src/routes/proxmox.js b/src/routes/proxmox.js index 54d1fdf..52099da 100644 --- a/src/routes/proxmox.js +++ b/src/routes/proxmox.js @@ -18,6 +18,7 @@ router.get("/*", async (req, res) => { // proxy endpoint for GET proxmox api wit */ router.post("/*", async (req, res) => { // proxy endpoint for POST proxmox api with no token const path = req.url.replace("/api/proxmox", ""); - const result = await global.pve.requestPVE(path, "POST", { cookies: req.cookies }, JSON.stringify(req.body)); // need to stringify body because of other issues + const body = JSON.parse(JSON.stringify(req.body)); + const result = await global.pve.requestPVE(path, "POST", { cookies: req.cookies }, body); // need to stringify body because of other issues res.status(result.status).send(result.data); }); diff --git a/src/utils.js b/src/utils.js index 92750ba..21de03e 100644 --- a/src/utils.js +++ b/src/utils.js @@ -354,7 +354,7 @@ export function readJSONFile (path) { return JSON.parse(readFileSync(path)); } catch (e) { - console.log(`Error: ${path} was not found.`); + console.log(`error: ${path} was not found.`); exit(1); } };