add getUserObjFromUsername util function,

update all backends to use userObj,
add user backend manager wrapper which calls all linked backends dealing with user data,
list backend handlers for each realm
This commit is contained in:
Arthur Lu 2024-06-03 18:09:28 +00:00
parent bb7404a82d
commit b12f38e608
13 changed files with 211 additions and 109 deletions

View File

@ -30,10 +30,14 @@
}
},
"handlers": {
"pve": "pve",
"db": "localdb",
"auth": {
"instance": {
"pve": "pve"
},
"users": {
"pve": [
"localdb",
"pve"
]
}
},
"application": {

View File

@ -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) {}
}

View File

@ -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;
}
}

View File

@ -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) {

View File

@ -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;

View File

@ -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);
});

View File

@ -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;

View File

@ -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}`;

View File

@ -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}`;

View File

@ -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") {

View File

@ -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);
});

View File

@ -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 {

View File

@ -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;
}