diff --git a/config/template.config.json b/config/template.config.json index 94cc7a5..a396259 100644 --- a/config/template.config.json +++ b/config/template.config.json @@ -30,10 +30,14 @@ } }, "handlers": { - "pve": "pve", - "db": "localdb", - "auth": { + "instance": { "pve": "pve" + }, + "users": { + "pve": [ + "localdb", + "pve" + ] } }, "application": { diff --git a/src/backends/backends.js b/src/backends/backends.js index 7d7f0b2..3dd8a4b 100644 --- a/src/backends/backends.js +++ b/src/backends/backends.js @@ -2,7 +2,7 @@ import path from "path"; import url from "url"; export default async () => { - const backends = {}; + global.backends = {}; for (const name in global.config.backends) { // get files and config const target = global.config.backends[name].import; @@ -14,17 +14,11 @@ export default async () => { const importPath = `./${path.relative(thisPath, targetPath)}`; // import and add to list of imported handlers const Backend = (await import(importPath)).default; - backends[name] = new Backend(config); + global.backends[name] = new Backend(config); console.log(`backends: initialized backend ${name} from ${importPath}`); } - // assign backends to handlers by type - const handlers = global.config.handlers; - global.pve = backends[handlers.pve]; - global.db = backends[handlers.db]; - global.auth = handlers.auth; - Object.keys(global.auth).forEach((e) => { - global.auth[e] = backends[global.auth[e]]; - }); + global.pve = global.backends[global.config.handlers.instance.pve]; + global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users); }; /** @@ -34,10 +28,11 @@ export default async () => { class BACKEND { /** * Opens a session with the backend and creates session tokens if needed - * @param {{username: string, password: string}} credentials object containing username and password fields + * @param {{id: string, realm: string}} user object containing username and password fields + * @param {string} password * @returns {{ok: boolean, status: number, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value */ - openSession (credentials) { + openSession (user, password) { return { ok: true, status: 200, @@ -70,12 +65,14 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies */ addUser (user, attributes, params = null) {} + /** * Get user from backend * @param {{id: string, realm: string}} user * @param {Object} params authentication params, usually req.cookies */ getUser (user, params = null) {} + /** * Modify user in backend * @param {{id: string, realm: string}} user @@ -83,6 +80,7 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies */ setUser (user, attributes, params = null) {} + /** * Delete user from backend * @param {{id: string, realm: string}} user @@ -97,12 +95,14 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies */ addGroup (group, attributes, params = null) {} + /** * Get group from backend * @param {{id: string}} group * @param {Object} params authentication params, usually req.cookies */ getGroup (group, params = null) {} + /** * Modify group in backend * @param {{id: string}} group @@ -110,6 +110,7 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies */ setGroup (group, attributes, params = null) {} + /** * Delete group from backend * @param {{id: string}} group @@ -124,6 +125,7 @@ class USER_BACKEND extends BACKEND { * @param {Object} params authentication params, usually req.cookies */ addUserToGroup (user, group, params = null) {} + /** * Remove user from group * @param {{id: string, realm: string}} user @@ -147,3 +149,122 @@ 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 + * Also handles refreshing proxmox handler + */ +class USER_BACKEND_MANAGER extends USER_BACKEND { + #config = null; + + constructor (config) { + super(); + this.#config = config; + } + + getBackendsByUser (user) { + return this.#config[user.realm]; + } + + /** + * Add user to backend + * @param {{id: string, realm: string}} user + * @param {Object} attributes user attributes + * @param {Object} params authentication params, usually req.cookies + */ + addUser (user, attributes, params = null) {} + + /** + * Get user from backend + * @param {{id: string, realm: string}} user + * @param {Object} params authentication params, usually req.cookies + */ + async getUser (user, params = null) { + let userData = {}; + for (const backend of this.#config[user.realm]) { + let backendData = await global.backends[backend].getUser(user, params) + if (backendData) { + userData = { ...backendData, ...userData }; + } + } + return userData; + } + + /** + * Modify user in backend + * @param {{id: string, realm: string}} user + * @param {Object} attributes new user attributes to modify + * @param {Object} params authentication params, usually req.cookies + */ + async setUser (user, attributes, params = null) { + const results = { + ok: true, + status: 200, + log: [] + }; + for (const backend of this.#config[user.realm]) { + const r = await global.backends[backend].setUser(user, attributes, params); + results.log.push(backend) + if (!r) { + results.ok = false; + results.status = 500; + return results; + } + } + return results; + } + + /** + * Delete user from backend + * @param {{id: string, realm: string}} user + * @param {Object} params authentication params, usually req.cookies + */ + deluser (user, params = null) {} + + /** + * Add group to backend + * @param {{id: string}} group + * @param {Object} attributes group attributes + * @param {Object} params authentication params, usually req.cookies + */ + addGroup (group, attributes, params = null) {} + + /** + * Get group from backend + * @param {{id: string}} group + * @param {Object} params authentication params, usually req.cookies + */ + getGroup (group, params = null) {} + + /** + * Modify group in backend + * @param {{id: string}} group + * @param {Object} attributes new group attributes to modify + * @param {Object} params authentication params, usually req.cookies + */ + setGroup (group, attributes, params = null) {} + + /** + * Delete group from backend + * @param {{id: string}} group + * @param {Object} params authentication params, usually req.cookies + */ + delGroup (group, params = null) {} + + /** + * Add user to group + * @param {{id: string, realm: string}} user + * @param {{id: string}} group + * @param {Object} params authentication params, usually req.cookies + */ + addUserToGroup (user, group, params = null) {} + + /** + * Remove user from group + * @param {{id: string, realm: string}} user + * @param {{id: string}} group + * @param {Object} params authentication params, usually req.cookies + */ + delUserFromGroup (user, group, params = null) {} +} diff --git a/src/backends/localdb.js b/src/backends/localdb.js index c385156..007c561 100644 --- a/src/backends/localdb.js +++ b/src/backends/localdb.js @@ -53,14 +53,19 @@ export default class LocalDB extends DB_BACKEND { } setUser (user, attributes, params = null) { - const username = `${user.id}@${user.realm}`; - if (this.#data.users[username]) { - this.#data.users[username] = attributes; - this.#save(); - return true; + if (attributes.resources && attributes.cluster && attributes.templates) { // localdb should only deal with these attributes + const username = `${user.id}@${user.realm}`; + if (this.#data.users[username]) { + this.#data.users[username] = attributes; + this.#save(); + return true; + } + else { + return false; + } } - else { - return false; + else { // if request is not setting these attributes, then assume its fine but do nothing + return true; } } diff --git a/src/backends/paasldap.js b/src/backends/paasldap.js index 976b81a..0124203 100644 --- a/src/backends/paasldap.js +++ b/src/backends/paasldap.js @@ -48,10 +48,9 @@ export default class PAASLDAP extends AUTH_BACKEND { } } - async openSession (credentials) { - const userRealm = credentials.username.split("@").at(-1); - const uid = credentials.username.replace(`@${userRealm}`, ""); - const content = { uid, password: credentials.password }; + async openSession (user, password) { + const uid = user.id; + const content = { uid, password }; const result = await this.#request("/ticket", "POST", null, content); if (result.ok) { const cookies = setCookie.parse(result.headers["set-cookie"]); @@ -74,7 +73,13 @@ export default class PAASLDAP extends AUTH_BACKEND { } async getUser (user, params = null) { - return await this.#request(`/users/${user.id}`, "GET", params); + const res = await this.#request(`/users/${user.id}`, "GET", params); + if (res.ok) { + return res.data; + } + else { + return false; + } } async setUser (user, attributes, params = null) { diff --git a/src/backends/pve.js b/src/backends/pve.js index be34b02..b9cd92f 100644 --- a/src/backends/pve.js +++ b/src/backends/pve.js @@ -13,7 +13,8 @@ export default class PVE extends PVE_BACKEND { this.#pveRoot = config.root; } - async openSession (credentials) { + async openSession (user, password) { + const credentials = { username: `${user.id}@${user.realm}`, password }; const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials); if (!(response.status === 200)) { return response; diff --git a/src/routes/auth.js b/src/routes/auth.js index 54e20c1..3c9a781 100644 --- a/src/routes/auth.js +++ b/src/routes/auth.js @@ -23,10 +23,10 @@ router.get("/", async (req, res) => { class CookieFetcher { #fetchedBackends = []; #cookies = []; - async fetchBackends (backends, credentials) { + async fetchBackends (backends, user, password) { for (const backend of backends) { if (this.#fetchedBackends.indexOf(backend) === -1) { - const response = await backend.openSession(credentials); + const response = await global.backends[backend].openSession(user, password); if (!response.ok) { return false; } @@ -60,13 +60,16 @@ router.post("/ticket", async (req, res) => { password: req.body.password }; const domain = global.config.application.domain; - const userRealm = params.username.split("@").at(-1); - const backends = [global.pve, global.db]; - if (userRealm in global.auth) { - backends.push(global.auth[userRealm]); - } + // const userRealm = params.username.split("@").at(-1); + const userObj = global.utils.getUserObjFromUsername(params.username); + let backends = global.userManager.getBackendsByUser(userObj); + backends = backends.concat(["pve"]); + // const backends = [global.pve, global.db]; + // if (userRealm in global.auth) { + // backends.push(global.auth[userRealm]); + // } const cm = new CookieFetcher(); - const success = await cm.fetchBackends(backends, params); + const success = await cm.fetchBackends(backends, userObj, params.password); if (!success) { res.status(401).send({ auth: false }); return; @@ -97,7 +100,7 @@ router.delete("/ticket", async (req, res) => { res.cookie(cookie, "", { domain, path: "/", expires: expire }); } await global.pve.closeSession(req.cookies); - await global.db.closeSession(req.cookies); + await global.userManager.closeSession(req.cookies); res.status(200).send({ auth: false }); }); @@ -114,24 +117,10 @@ router.post("/password", async (req, res) => { password: req.body.password }; - const userRealm = params.username.split("@").at(-1); - const authHandlers = global.config.handlers.auth; - const userID = params.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; - if (userRealm in authHandlers) { - const handler = authHandlers[userRealm]; - const newAttributes = { - userpassword: params.password - }; - const response = await handler.setUser(userObj, newAttributes, req.cookies); - if (response.ok) { - res.status(response.status).send(response.data); - } - else { - res.status(response.status).send({ error: response.data.error }); - } - } - else { - res.status(501).send({ error: `Auth type ${userRealm} not implemented yet.` }); - } + const userObj = global.utils.getUserObjFromUsername(params.username); + const newAttributes = { + userpassword: params.password + }; + const response = await global.userManager.setUser(userObj, newAttributes, req.cookies); + res.status(response.status).send(response); }); diff --git a/src/routes/cluster.js b/src/routes/cluster.js index 9eb48c1..c9beb53 100644 --- a/src/routes/cluster.js +++ b/src/routes/cluster.js @@ -1,7 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); -const db = global.db; const checkAuth = global.utils.checkAuth; const approveResources = global.utils.approveResources; const getUserResources = global.utils.getUserResources; @@ -29,16 +28,14 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => { node: req.params.node }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth const auth = await checkAuth(req.cookies, res); if (!auth) { return; } - const userNodes = db.getUser(userObj).cluster.nodes; + const userNodes = (await global.userManager.getUser(userObj)).cluster.nodes; if (userNodes[params.node] !== true) { res.status(401).send({ auth: false, path: params.node }); res.end(); @@ -83,9 +80,7 @@ router.post(`${basePath}/resources`, async (req, res) => { boot: req.body.boot }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; @@ -165,9 +160,7 @@ router.post(`${basePath}/create`, async (req, res) => { rootfssize: req.body.rootfssize }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth const auth = await checkAuth(req.cookies, res); @@ -175,7 +168,7 @@ router.post(`${basePath}/create`, async (req, res) => { return; } // get user db config - const user = await db.getUser(userObj); + const user = await global.userManager.getUser(userObj); const vmid = Number.parseInt(params.vmid); const vmidMin = user.cluster.vmid.min; const vmidMax = user.cluster.vmid.max; diff --git a/src/routes/cluster/disk.js b/src/routes/cluster/disk.js index bdb0597..ec25ce6 100644 --- a/src/routes/cluster/disk.js +++ b/src/routes/cluster/disk.js @@ -130,9 +130,7 @@ router.post("/:disk/resize", async (req, res) => { size: req.body.size }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; @@ -192,9 +190,7 @@ router.post("/:disk/move", async (req, res) => { delete: req.body.delete }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; @@ -315,9 +311,7 @@ router.post("/:disk/create", async (req, res) => { iso: req.body.iso }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; diff --git a/src/routes/cluster/net.js b/src/routes/cluster/net.js index cdd4530..a412ebe 100644 --- a/src/routes/cluster/net.js +++ b/src/routes/cluster/net.js @@ -1,7 +1,6 @@ import { Router } from "express"; export const router = Router({ mergeParams: true }); ; -const db = global.db; const checkAuth = global.utils.checkAuth; const approveResources = global.utils.approveResources; @@ -32,9 +31,7 @@ router.post("/:netid/create", async (req, res) => { name: req.body.name }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; @@ -65,7 +62,7 @@ router.post("/:netid/create", async (req, res) => { return; } // setup action - const nc = db.getUser(userObj).templates.network[params.type]; + const nc = (await global.userManager.getUser(userObj)).templates.network[params.type]; 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}`; @@ -104,9 +101,7 @@ router.post("/:netid/modify", async (req, res) => { rate: req.body.rate }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth for specific instance const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; diff --git a/src/routes/cluster/pci.js b/src/routes/cluster/pci.js index 6790dbb..8367d87 100644 --- a/src/routes/cluster/pci.js +++ b/src/routes/cluster/pci.js @@ -75,9 +75,7 @@ router.post("/:hostpci/modify", async (req, res) => { pcie: req.body.pcie }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check if type is qemu if (params.type !== "qemu") { @@ -162,9 +160,7 @@ router.post("/create", async (req, res) => { pcie: req.body.pcie }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check if type is qemu if (params.type !== "qemu") { diff --git a/src/routes/sync.js b/src/routes/sync.js index aa46620..befa2b6 100644 --- a/src/routes/sync.js +++ b/src/routes/sync.js @@ -165,12 +165,10 @@ if (schemes.interrupt.enabled) { socket.destroy(); } else { - wsServer.handleUpgrade(req, socket, head, (socket) => { + wsServer.handleUpgrade(req, socket, head, async (socket) => { // get the user pools - const userRealm = cookies.username.split("@").at(-1); - const userID = cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; - const pools = Object.keys(global.db.getUser(userObj).cluster.pools); + const userObj = global.utils.getUserObjFromUsername(cookies.username); + const pools = Object.keys((await global.userManager.getUser(userObj)).cluster.pools); // emit the connection to initialize socket wsServer.emit("connection", socket, cookies.username, pools); }); diff --git a/src/routes/user.js b/src/routes/user.js index 53f58cd..0eb704b 100644 --- a/src/routes/user.js +++ b/src/routes/user.js @@ -18,9 +18,7 @@ router.get("/dynamic/resources", async (req, res) => { return; } - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); const resources = await getUserResources(req, userObj); res.status(200).send(resources); @@ -40,9 +38,7 @@ router.get("/config/:key", async (req, res) => { key: req.params.key }; - const userRealm = req.cookies.username.split("@").at(-1); - const userID = req.cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = global.utils.getUserObjFromUsername(req.cookies.username); // check auth const auth = await checkAuth(req.cookies, res); @@ -51,7 +47,7 @@ router.get("/config/:key", async (req, res) => { } const allowKeys = ["resources", "cluster"]; if (allowKeys.includes(params.key)) { - const config = global.db.getUser(userObj); + const config = await global.userManager.getUser(userObj); res.status(200).send(config[params.key]); } else { diff --git a/src/utils.js b/src/utils.js index c213a15..7628ec6 100644 --- a/src/utils.js +++ b/src/utils.js @@ -15,11 +15,9 @@ import { exit } from "process"; export async function checkAuth (cookies, res, vmpath = null) { let auth = false; - const userRealm = cookies.username.split("@").at(-1); - const userID = cookies.username.replace(`@${userRealm}`, ""); - const userObj = { id: userID, realm: userRealm }; + const userObj = getUserObjFromUsername(cookies.username); - if (global.db.getUser(userObj) === null) { + if ((await global.userManager.getUser(userObj)) === null) { auth = false; res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in localdb.` }); res.end(); @@ -113,7 +111,7 @@ async function getAllInstanceConfigs (req, diskprefixes) { */ export async function getUserResources (req, user) { const dbResources = global.config.resources; - const userResources = global.db.getUser(user).resources; + const userResources = (await global.userManager.getUser(user)).resources; // setup disk prefixes object const diskprefixes = []; @@ -362,3 +360,10 @@ export function readJSONFile (path) { exit(1); } }; + +export function getUserObjFromUsername (username) { + const userRealm = username.split("@").at(-1); + const userID = username.replace(`@${userRealm}`, ""); + const userObj = { id: userID, realm: userRealm }; + return userObj; +}