Compare commits

...

18 Commits

Author SHA1 Message Date
ee3e768ada add nodes and pools routes 2024-09-12 22:00:22 +00:00
c059b528fa add get user/group,
invert  return value for CookieFetcher
2024-08-02 04:35:04 +00:00
783bc37c94 require params in all backend calls 2024-07-23 18:08:36 +00:00
9f6b03db32 fix utils.js,
implement getAllUsers/getAllGroups in backends,
add paasldap realm config option
2024-07-15 19:14:03 +00:00
3b81bd20ea fix delUserFromGroup in README 2024-07-10 22:38:39 +00:00
79ec20ad74 fix delUser naming,
update config template,
implement getAllUsers/getAllGroups in USER_BACKEND_MANAGER
2024-07-10 22:38:14 +00:00
8f7ea51787 add missing valid pve token check to checkAuth 2024-07-08 19:25:23 +00:00
800033c6f8 add get all users/groups routes 2024-07-03 23:46:00 +00:00
7f48f49445 rename auth to access 2024-07-02 19:38:21 +00:00
c8404c366f update readme with new permissions required for proxmox 8,
improve handling of proxmox responses
2024-07-01 19:03:36 +00:00
c63690c181 fix linting 2024-06-28 07:18:27 +00:00
34f2669ab9 add valid user cookie check to checkAuth,
add admin flag in user data
2024-06-28 07:14:41 +00:00
afecfcafd0 fix readme tables 2024-06-26 06:46:31 +00:00
ab0188a8bc add backend info to readme,
fix docstring in backends.js
2024-06-26 06:39:43 +00:00
85b8ae8560 update paasldap backend 2024-06-20 03:18:52 +00:00
01f55aa0cb add return values to backend docstring,
fix return values of all backends
2024-06-04 23:09:55 +00:00
b12f38e608 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
2024-06-03 18:09:28 +00:00
alu
bb7404a82d Merge pull request 'Update DB Interface' (#2) from update-localdb into main
Reviewed-on: #2
2024-04-24 20:29:31 +00:00
17 changed files with 622 additions and 173 deletions

View File

@ -19,6 +19,7 @@ In Proxmox VE, follow the following steps:
- Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit - Datastore.Allocate, Datastore.AllocateSpace, Datastore.Audit
- User.Modify - User.Modify
- Pool.Audit - Pool.Audit
- SDN.Use (if instances use SDN networks)
4. Add a new API Token Permission with path: `/`, select the API token created previously, and role: `proxmoxaas-api` 4. Add a new API Token Permission with path: `/`, select the API token created previously, and role: `proxmoxaas-api`
5. Add a new User Permission with path: `/`, select the `proxmoxaas-api` user, and role: `proxmoxaas-api` 5. Add a new User Permission with path: `/`, select the `proxmoxaas-api` user, and role: `proxmoxaas-api`
@ -67,4 +68,74 @@ server {
### Result ### Result
After these steps, the ProxmoxAAS Dashboard should be available and fully functional at `paas.<FQDN>` or `paas.<FQDN>/dashboard/`. After these steps, the ProxmoxAAS Dashboard should be available and fully functional at `paas.<FQDN>` or `paas.<FQDN>/dashboard/`.
# Backends # Backends
Backend handlers are used to interface with any number and type of backend data source used to store ProxmoxAAS data. Most data involves users, groups, and membership relationships. The default backends are sufficient to run a small cluster, but additional backend handlers can be created.
## Interface
Each backend must implement the following methods:
<table>
<tr>
<td>openSession</td>
<td>opens a session to the backend by creating a session token</td>
</tr>
<tr>
<td>closeSession</td>
<td>closes a session to the backend</td>
</tr>
</table>
Additionally, backends dealing with user data may also need to implement:
<table>
<tr>
<td>addUser</td>
<td>create a user</td>
</tr>
<tr>
<td>getUser</td>
<td>retrieve user data including membership</td>
</tr>
<tr>
<td>setUser</td>
<td>modify a user</td>
</tr>
<tr>
<td>delUser</td>
<td>delete a user</td>
</tr>
<tr>
<td>addGroup</td>
<td>create a group</td>
</tr>
<tr>
<td>getGroup</td>
<td>retrieve group data including members</td>
</tr>
<tr>
<td>setGroup</td>
<td>modify group data except membership</td>
</tr>
<tr>
<td>delGroup</td>
<td>delete group</td>
</tr>
<tr>
<td>addUserToGroup</td>
<td>add user to group as member</td>
</tr>
<tr>
<td>delUserFromGroup</td>
<td>remove user from group</td>
</tr>
</table>
Not all user backends will necessarily implement all the methods fully. For example, backends which do not store group data may not need to implement the group related methods.
Specific documentation can be found in `src/backends/backends.js`.
## Multiple Interfaces
Multiple backends can be specified using the config. During a backend operation involving users, each backend method will be called in the order specified in the config. If the operation is to retrieve user data, the responses will be merged favoring the last backend called.

View File

@ -25,15 +25,29 @@
"paasldap": { "paasldap": {
"import": "paasldap.js", "import": "paasldap.js",
"config": { "config": {
"url": "http://paasldap.mydomain.example" "url": "http://paasldap.mydomain.example",
"realm": "ldap"
} }
} }
}, },
"handlers": { "handlers": {
"pve": "pve", "instance": {
"db": "localdb",
"auth": {
"pve": "pve" "pve": "pve"
},
"users": {
"realm": {
"pve": [
"localdb"
],
"ldap": [
"localdb",
"paasldap"
]
},
"any": [
"localdb",
"paasldap"
]
} }
}, },
"application": { "application": {

View File

@ -2,7 +2,7 @@ import path from "path";
import url from "url"; import url from "url";
export default async () => { export default async () => {
const backends = {}; global.backends = {};
for (const name in global.config.backends) { for (const name in global.config.backends) {
// get files and config // get files and config
const target = global.config.backends[name].import; const target = global.config.backends[name].import;
@ -14,17 +14,11 @@ export default async () => {
const importPath = `./${path.relative(thisPath, targetPath)}`; const importPath = `./${path.relative(thisPath, targetPath)}`;
// import and add to list of imported handlers // import and add to list of imported handlers
const Backend = (await import(importPath)).default; 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}`); console.log(`backends: initialized backend ${name} from ${importPath}`);
} }
// assign backends to handlers by type global.pve = global.backends[global.config.handlers.instance.pve];
const handlers = global.config.handlers; global.userManager = new USER_BACKEND_MANAGER(global.config.handlers.users);
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]];
});
}; };
/** /**
@ -34,13 +28,15 @@ export default async () => {
class BACKEND { class BACKEND {
/** /**
* Opens a session with the backend and creates session tokens if needed * 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 id and realm
* @returns {{ok: boolean, status: number, cookies: {name: string, value: string}[]}} response like object with list of session token objects with token name and value * @param {string} password
* @returns {{ok: boolean, status: number, message: string, 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 { return {
ok: true, ok: true,
status: 200, status: 200,
message: "",
cookies: [] cookies: []
}; };
} }
@ -68,69 +64,100 @@ class USER_BACKEND extends BACKEND {
* @param {{id: string, realm: string}} user * @param {{id: string, realm: string}} user
* @param {Object} attributes user attributes * @param {Object} attributes user attributes
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
addUser (user, attributes, params = null) {} addUser (user, attributes, params) {}
/** /**
* Get user from backend * Get user from backend
* @param {{id: string, realm: string}} user * @param {{id: string, realm: string}} user
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {Object} containing user data from this backend, null if user does not exist
*/ */
getUser (user, params = null) {} getUser (user, params) {}
/**
* Get all users from backend
* @param {Object} params authentication params, usually req.cookies
* @returns {Array} containing each user data from this backend
*/
getAllUsers (params) {}
/** /**
* Modify user in backend * Modify user in backend
* @param {{id: string, realm: string}} user * @param {{id: string, realm: string}} user
* @param {Object} attributes new user attributes to modify * @param {Object} attributes new user attributes to modify
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
setUser (user, attributes, params = null) {} setUser (user, attributes, params) {}
/** /**
* Delete user from backend * Delete user from backend
* @param {{id: string, realm: string}} user * @param {{id: string, realm: string}} user
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
deluser (user, params = null) {} delUser (user, params) {}
/** /**
* Add group to backend * Add group to backend
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} attributes group attributes * @param {Object} attributes group attributes
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
addGroup (group, attributes, params = null) {} addGroup (group, attributes, params) {}
/** /**
* Get group from backend * Get group from backend
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {Object} containing group data from this backend, null if user does not exist
*/ */
getGroup (group, params = null) {} getGroup (group, params) {}
/**
* Get all users from backend
* @param {Object} params authentication params, usually req.cookies
* @returns {Array} containing each group data from this backend
*/
getAllGroups (params) {}
/** /**
* Modify group in backend * Modify group in backend
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} attributes new group attributes to modify * @param {Object} attributes new group attributes to modify
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
setGroup (group, attributes, params = null) {} setGroup (group, attributes, params) {}
/** /**
* Delete group from backend * Delete group from backend
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
delGroup (group, params = null) {} delGroup (group, params) {}
/** /**
* Add user to group * Add user to group
* @param {{id: string, realm: string}} user * @param {{id: string, realm: string}} user
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
addUserToGroup (user, group, params = null) {} addUserToGroup (user, group, params) {}
/** /**
* Remove user from group * Remove user from group
* @param {{id: string, realm: string}} user * @param {{id: string, realm: string}} user
* @param {{id: string}} group * @param {{id: string}} group
* @param {Object} params authentication params, usually req.cookies * @param {Object} params authentication params, usually req.cookies
* @returns {{ok: boolean, status: number, message: string}} error object or null
*/ */
delUserFromGroup (user, group, params = null) {} delUserFromGroup (user, group, params) {}
} }
/** /**
@ -147,3 +174,90 @@ export class DB_BACKEND extends USER_BACKEND {}
* Interface for user auth backends. * Interface for user auth backends.
*/ */
export class AUTH_BACKEND extends USER_BACKEND {} export class AUTH_BACKEND extends USER_BACKEND {}
/**
* Interface combining all user backends into a single interface
* Calling methods will also call sub handler methods
*/
class USER_BACKEND_MANAGER extends USER_BACKEND {
#config = null;
constructor (config) {
super();
this.#config = config;
}
getBackendsByUser (user) {
return this.#config.realm[user.realm];
}
addUser (user, attributes, params) {}
async getUser (user, params) {
let userData = {};
for (const backend of this.#config.realm[user.realm]) {
const backendData = await global.backends[backend].getUser(user, params);
if (backendData) {
userData = { ...backendData, ...userData };
}
}
return userData;
}
async getAllUsers (params) {
const userData = {};
for (const backend of this.#config.any) {
const backendData = await global.backends[backend].getAllUsers(params);
if (backendData) {
for (const user of Object.keys(backendData)) {
userData[user] = { ...backendData[user], ...userData[user] };
}
}
}
return userData;
}
async setUser (user, attributes, params) {
const results = {
ok: true,
status: 200,
message: ""
};
for (const backend of this.#config.realm[user.realm]) {
const result = await global.backends[backend].setUser(user, attributes, params);
if (!result) {
results.ok = false;
results.status = 500;
return results;
}
}
return results;
}
delUser (user, params) {}
addGroup (group, attributes, params) {}
getGroup (group, params) {}
async getAllGroups (params) {
const groupData = {};
for (const backend of this.#config.any) {
const backendData = await global.backends[backend].getAllGroups(params);
if (backendData) {
for (const group of Object.keys(backendData)) {
groupData[group] = { ...backendData[group], ...groupData[group] };
}
}
}
return groupData;
}
setGroup (group, attributes, params) {}
delGroup (group, params) {}
addUserToGroup (user, group, params) {}
delUserFromGroup (user, group, params) {}
}

View File

@ -35,36 +35,64 @@ export default class LocalDB extends DB_BACKEND {
writeFileSync(this.#path, JSON.stringify(this.#data)); writeFileSync(this.#path, JSON.stringify(this.#data));
} }
addUser (user, attributes, params = null) { addUser (user, attributes, params) {
const username = `${user.id}@${user.realm}`; const username = `${user.id}@${user.realm}`;
attributes = attributes || this.#defaultuser; if (this.#data.users[username]) { // user already exists
this.#data.users[username] = attributes; return {
this.#save(); ok: false,
status: 1,
message: "User already exists"
};
}
else {
attributes = attributes || this.#defaultuser;
this.#data.users[username] = attributes;
this.#save();
return null;
}
} }
getUser (user, params = null) { getUser (user, params) {
const username = `${user.id}@${user.realm}`; const requestedUser = `${user.id}@${user.realm}`;
if (this.#data.users[username]) { const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
return this.#data.users[username]; // user can access a user's db data if they are an admin OR are requesting own data
const authorized = this.#data.users[requestingUser].cluster.admin || requestingUser === requestedUser;
if (authorized && this.#data.users[requestedUser]) {
return this.#data.users[requestedUser];
} }
else { else {
return null; return null;
} }
} }
setUser (user, attributes, params = null) { async getAllUsers (params) {
const username = `${user.id}@${user.realm}`; const requestingUser = params.username; // assume checkAuth has been run, which already checks that username matches PVE token
if (this.#data.users[username]) { if (this.#data.users[requestingUser].cluster.admin === true) {
this.#data.users[username] = attributes; return this.#data.users;
this.#save();
return true;
} }
else { else {
return false; return null;
} }
} }
delUser (user, params = null) { setUser (user, attributes, params) {
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 { // if request is not setting these attributes, then assume its fine but do nothing
return true;
}
}
delUser (user, params) {
const username = `${user.id}@${user.realm}`; const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) { if (this.#data.users[username]) {
delete this.#data.users[username]; delete this.#data.users[username];
@ -77,13 +105,17 @@ export default class LocalDB extends DB_BACKEND {
} }
// group methods not implemented because db backend does not store groups // group methods not implemented because db backend does not store groups
addGroup (group, atrributes, params = null) {} addGroup (group, atrributes, params) {}
getGroup (group, params = null) {} getGroup (group, params) {}
setGroup (group, attributes, params = null) {} getAllGroups (params) {
delGroup (group, params = null) {} return null;
}
setGroup (group, attributes, params) {}
delGroup (group, params) {}
// assume that adding to group also adds to group's pool // assume that adding to group also adds to group's pool
addUserToGroup (user, group, params = null) { addUserToGroup (user, group, params) {
const username = `${user.id}@${user.realm}`; const username = `${user.id}@${user.realm}`;
if (this.#data.users[username]) { if (this.#data.users[username]) {
this.#data.users[username].cluster.pools[group.id] = true; this.#data.users[username].cluster.pools[group.id] = true;
@ -95,7 +127,7 @@ export default class LocalDB extends DB_BACKEND {
} }
// assume that adding to group also adds to group's pool // assume that adding to group also adds to group's pool
delUserFromGroup (user, group, params = null) { delUserFromGroup (user, group, params) {
const username = `${user.id}@${user.realm}`; const username = `${user.id}@${user.realm}`;
if (this.#data.users[username] && this.#data.users[username].cluster.pools[group.id]) { if (this.#data.users[username] && this.#data.users[username].cluster.pools[group.id]) {
delete this.#data.users[username].cluster.pools[group.id]; delete this.#data.users[username].cluster.pools[group.id];

View File

@ -4,10 +4,12 @@ import * as setCookie from "set-cookie-parser";
export default class PAASLDAP extends AUTH_BACKEND { export default class PAASLDAP extends AUTH_BACKEND {
#url = null; #url = null;
#realm = null;
constructor (config) { constructor (config) {
super(); super();
this.#url = config.url; this.#url = config.url;
this.#realm = config.realm;
} }
/** /**
@ -15,7 +17,7 @@ export default class PAASLDAP extends AUTH_BACKEND {
* @param {*} path HTTP path, prepended with the paas-LDAP API base url * @param {*} path HTTP path, prepended with the paas-LDAP API base url
* @param {*} method HTTP method * @param {*} method HTTP method
* @param {*} body body parameters and data to be sent. Optional. * @param {*} body body parameters and data to be sent. Optional.
* @returns {Object} HTTP response object or HTTP error object. * @returns {Object} HTTP response object
*/ */
async #request (path, method, auth = null, body = null) { async #request (path, method, auth = null, body = null) {
const url = `${this.#url}${path}`; const url = `${this.#url}${path}`;
@ -39,19 +41,28 @@ export default class PAASLDAP extends AUTH_BACKEND {
return result; return result;
} }
catch (error) { catch (error) {
error.ok = false; const result = error.response;
error.status = 500; result.ok = result.status === 200;
error.data = { return result;
error: error.code
};
return error;
} }
} }
async openSession (credentials) { #handleGenericReturn (res) {
const userRealm = credentials.username.split("@").at(-1); if (res.ok) { // if ok, return null
const uid = credentials.username.replace(`@${userRealm}`, ""); return null;
const content = { uid, password: credentials.password }; }
else { // if not ok, return error obj
return {
ok: res.ok,
status: res.status,
message: res.ok ? "" : res.data.error
};
}
}
async openSession (user, password) {
const username = user.id;
const content = { username, password };
const result = await this.#request("/ticket", "POST", null, content); const result = await this.#request("/ticket", "POST", null, content);
if (result.ok) { if (result.ok) {
const cookies = setCookie.parse(result.headers["set-cookie"]); const cookies = setCookie.parse(result.headers["set-cookie"]);
@ -61,51 +72,112 @@ export default class PAASLDAP extends AUTH_BACKEND {
return { return {
ok: true, ok: true,
status: result.status, status: result.status,
message: "",
cookies cookies
}; };
} }
else { else {
return result; return {
ok: false,
status: result.status,
message: result.data.error,
cookies: []
};
} }
} }
async addUser (user, attributes, params = null) { async addUser (user, attributes, params) {
return await this.#request(`/users/${user.id}`, "POST", params, attributes); const res = await this.#request(`/users/${user.id}`, "POST", params, attributes);
return this.#handleGenericReturn(res);
} }
async getUser (user, params = null) { async getUser (user, params) {
return await this.#request(`/users/${user.id}`, "GET", params); if (!params) { // params required, do nothing if params are missing
return null;
}
const res = await this.#request(`/users/${user.id}`, "GET", params);
if (res.ok) { // if ok, return user data
return res.data.user;
}
else { // else return null
return null;
}
} }
async setUser (user, attributes, params = null) { async getAllUsers (params) {
return await this.#request(`/users/${user.id}`, "POST", params, attributes); if (!params) {
return null;
}
const res = await this.#request("/users", "GET", params);
if (res.ok) { // if ok, return user data
const users = res.data.users;
const usersFormatted = {};
// label each user object by user@realm
for (const user of users) {
usersFormatted[`${user.attributes.uid}@${this.#realm}`] = user;
}
return usersFormatted;
}
else { // else return null
return null;
}
} }
async delUser (user, params = null) { async setUser (user, attributes, params) {
return await this.#request(`/users/${user.id}`, "DELETE", params); const res = await this.#request(`/users/${user.id}`, "POST", params, attributes);
return this.#handleGenericReturn(res);
} }
async addGroup (group, attributes, params = null) { async delUser (user, params) {
return await this.#request(`/groups/${group.id}`, "POST", params); const res = await this.#request(`/users/${user.id}`, "DELETE", params);
return this.#handleGenericReturn(res);
} }
async getGroup (group, params = null) { async addGroup (group, attributes, params) {
const res = await this.#request(`/groups/${group.id}`, "POST", params);
return this.#handleGenericReturn(res);
}
async getGroup (group, params) {
return await this.#request(`/groups/${group.id}`, "GET", params); return await this.#request(`/groups/${group.id}`, "GET", params);
} }
async setGroup (group, attributes, params = null) { async getAllGroups (params) {
if (!params) {
return null;
}
const res = await this.#request("/groups", "GET", params);
if (res.ok) { // if ok, return user data
const groups = res.data.groups;
const groupsFormatted = {};
// label each user object by user@realm
for (const group of groups) {
groupsFormatted[`${group.attributes.cn}@${this.#realm}`] = group;
}
return groupsFormatted;
}
else { // else return null
return null;
}
}
async setGroup (group, attributes, params) {
// not implemented, LDAP groups do not have any attributes to change // not implemented, LDAP groups do not have any attributes to change
return null;
} }
async delGroup (group, params = null) { async delGroup (group, params) {
return await this.#request(`/groups/${group.id}`, "DELETE", params); const res = await this.#request(`/groups/${group.id}`, "DELETE", params);
return this.#handleGenericReturn(res);
} }
async addUserToGroup (user, group, params = null) { async addUserToGroup (user, group, params) {
return await this.#request(`/groups/${group.id}/members/${user.id}`, "POST", params); const res = await this.#request(`/groups/${group.id}/members/${user.id}`, "POST", params);
return this.#handleGenericReturn(res);
} }
async delUserFromGroup (user, group, params = null) { async delUserFromGroup (user, group, params) {
return await this.#request(`/groups/${group.id}/members/${user.id}`, "DELETE", params); const res = await this.#request(`/groups/${group.id}/members/${user.id}`, "DELETE", params);
return this.#handleGenericReturn(res);
} }
} }

View File

@ -13,10 +13,16 @@ export default class PVE extends PVE_BACKEND {
this.#pveRoot = config.root; 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); const response = await global.pve.requestPVE("/access/ticket", "POST", null, credentials);
if (!(response.status === 200)) { if (!(response.status === 200)) {
return response; return {
ok: false,
status: response.status,
message: "Authorization failed",
cookies: []
};
} }
const ticket = response.data.data.ticket; const ticket = response.data.data.ticket;
const csrftoken = response.data.data.CSRFPreventionToken; const csrftoken = response.data.data.CSRFPreventionToken;
@ -85,7 +91,11 @@ export default class PVE extends PVE_BACKEND {
*/ */
async handleResponse (node, result, res) { async handleResponse (node, result, res) {
const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay)); const waitFor = delay => new Promise(resolve => setTimeout(resolve, delay));
if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) { if (result.status !== 200) {
res.status(result.status).send({ error: result.statusText });
res.end();
}
else if (result.data.data && typeof (result.data.data) === "string" && result.data.data.startsWith("UPID:")) {
const upid = result.data.data; const upid = result.data.data;
let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true }); let taskStatus = await this.requestPVE(`/nodes/${node}/tasks/${upid}/status`, "GET", { token: true });
while (taskStatus.data.data.status !== "stopped") { while (taskStatus.data.data.status !== "stopped") {

View File

@ -3,6 +3,8 @@ export const router = Router({ mergeParams: true }); ;
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
global.utils.recursiveImportRoutes(router, "", "access", import.meta.url);
/** /**
* GET - check authentication * GET - check authentication
* responses: * responses:
@ -23,12 +25,12 @@ router.get("/", async (req, res) => {
class CookieFetcher { class CookieFetcher {
#fetchedBackends = []; #fetchedBackends = [];
#cookies = []; #cookies = [];
async fetchBackends (backends, credentials) { async fetchBackends (backends, user, password) {
for (const backend of backends) { for (const backend of backends) {
if (this.#fetchedBackends.indexOf(backend) === -1) { if (this.#fetchedBackends.indexOf(backend) === -1) {
const response = await backend.openSession(credentials); const response = await global.backends[backend].openSession(user, password);
if (!response.ok) { if (!response.ok) {
return false; return response.message;
} }
this.#cookies = this.#cookies.concat(response.cookies); this.#cookies = this.#cookies.concat(response.cookies);
this.#fetchedBackends.push(backend); this.#fetchedBackends.push(backend);
@ -37,7 +39,7 @@ class CookieFetcher {
continue; continue;
} }
} }
return true; return null;
} }
exportCookies () { exportCookies () {
@ -60,15 +62,14 @@ router.post("/ticket", async (req, res) => {
password: req.body.password password: req.body.password
}; };
const domain = global.config.application.domain; const domain = global.config.application.domain;
const userRealm = params.username.split("@").at(-1); // const userRealm = params.username.split("@").at(-1);
const backends = [global.pve, global.db]; const userObj = global.utils.getUserObjFromUsername(params.username);
if (userRealm in global.auth) { let backends = global.userManager.getBackendsByUser(userObj);
backends.push(global.auth[userRealm]); backends = backends.concat(["pve"]);
}
const cm = new CookieFetcher(); const cm = new CookieFetcher();
const success = await cm.fetchBackends(backends, params); const error = await cm.fetchBackends(backends, userObj, params.password);
if (!success) { if (error) {
res.status(401).send({ auth: false }); res.status(401).send({ auth: false, error });
return; return;
} }
const cookies = cm.exportCookies(); const cookies = cm.exportCookies();
@ -97,7 +98,7 @@ router.delete("/ticket", async (req, res) => {
res.cookie(cookie, "", { domain, path: "/", expires: expire }); res.cookie(cookie, "", { domain, path: "/", expires: expire });
} }
await global.pve.closeSession(req.cookies); await global.pve.closeSession(req.cookies);
await global.db.closeSession(req.cookies); await global.userManager.closeSession(req.cookies);
res.status(200).send({ auth: false }); res.status(200).send({ auth: false });
}); });
@ -114,24 +115,16 @@ router.post("/password", async (req, res) => {
password: req.body.password password: req.body.password
}; };
const userRealm = params.username.split("@").at(-1); // check auth
const authHandlers = global.config.handlers.auth; const auth = await checkAuth(req.cookies, res);
const userID = params.username.replace(`@${userRealm}`, ""); if (!auth) {
const userObj = { id: userID, realm: userRealm }; return;
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

@ -0,0 +1,41 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all groups
* responses:
* - 200: {auth: true, groups: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const groups = await global.userManager.getAllGroups(req.cookies);
res.status(200).send({ groups });
});
/**
* GET - get specific group
* request:
* - groupname: name of group to get
* responses:
* - 200: {auth: true, group: Object}
* - 401: {auth: false}
*/
router.get("/:groupname", async (req, res) => {
const params = {
groupname: req.params.groupname
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const group = await global.userManager.getGroup(params.groupname, req.cookies);
res.status(200).send({ group });
});

View File

@ -0,0 +1,42 @@
import { Router } from "express";
export const router = Router({ mergeParams: true });
const checkAuth = global.utils.checkAuth;
/**
* GET - get all users
* responses:
* - 200: {auth:true, users: Array}
* - 401: {auth: false}
*/
router.get("/", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const users = await global.userManager.getAllUsers(req.cookies);
res.status(200).send({ users });
});
/**
* GET - get specific user
* request:
* - username: username (id@realm) of user to get
* responses:
* - 200: {auth: true, user: Object}
* - 401: {auth: false}
*/
router.get("/:username", async (req, res) => {
const params = {
username: req.params.username
};
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const userObj = global.utils.getUserObjFromUsername(params.username);
const user = await global.userManager.getUser(userObj, req.cookies);
res.status(200).send({ user });
});

View File

@ -1,7 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); export const router = Router({ mergeParams: true });
const db = global.db;
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources; const approveResources = global.utils.approveResources;
const getUserResources = global.utils.getUserResources; const getUserResources = global.utils.getUserResources;
@ -14,6 +13,61 @@ const basePath = `/:node(${nodeRegexP})/:type(${typeRegexP})/:vmid(${vmidRegexP}
global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url); global.utils.recursiveImportRoutes(router, basePath, "cluster", import.meta.url);
/**
* GET - get all available cluster pools
* returns only pool IDs
* responses:
* - 200: List of pools
* - PVE error
*/
router.get("/pools", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const allPools = await global.pve.requestPVE("/pools", "GET", { token: true });
if (allPools.status === 200) {
const allPoolsIDs = Array.from(allPools.data.data, (x) => x.poolid);
res.status(allPools.status).send({ pools: allPoolsIDs });
res.end();
}
else {
res.status(allPools.status).send({ error: allPools.statusMessage });
res.end();
}
});
/**
* GET - get all available cluster nodes
* uses existing user permissions without elevation
* returns only node IDs
* responses:
* - 200: List of nodes
* - PVE error
*/
router.get("/nodes", async (req, res) => {
// check auth
const auth = await checkAuth(req.cookies, res);
if (!auth) {
return;
}
const allNodes = await global.pve.requestPVE("/nodes", "GET", { cookies: req.cookies });
if (allNodes.status === 200) {
const allNodesIDs = Array.from(allNodes.data.data, (x) => x.node);
res.status(allNodes.status).send({ nodes: allNodesIDs });
res.end();
}
else {
res.status(allNodes.status).send({ error: allNodes.statusMessage });
res.end();
}
});
/** /**
* GET - get available pcie devices given node and user * GET - get available pcie devices given node and user
* request: * request:
@ -29,16 +83,14 @@ router.get(`/:node(${nodeRegexP})/pci`, async (req, res) => {
node: req.params.node node: req.params.node
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
const userNodes = db.getUser(userObj).cluster.nodes; const userNodes = (await global.userManager.getUser(userObj, req.cookies)).cluster.nodes;
if (userNodes[params.node] !== true) { if (userNodes[params.node] !== true) {
res.status(401).send({ auth: false, path: params.node }); res.status(401).send({ auth: false, path: params.node });
res.end(); res.end();
@ -83,9 +135,7 @@ router.post(`${basePath}/resources`, async (req, res) => {
boot: req.body.boot boot: req.body.boot
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -165,9 +215,7 @@ router.post(`${basePath}/create`, async (req, res) => {
rootfssize: req.body.rootfssize rootfssize: req.body.rootfssize
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
@ -175,7 +223,7 @@ router.post(`${basePath}/create`, async (req, res) => {
return; return;
} }
// get user db config // get user db config
const user = await db.getUser(userObj); const user = await global.userManager.getUser(userObj, req.cookies);
const vmid = Number.parseInt(params.vmid); const vmid = Number.parseInt(params.vmid);
const vmidMin = user.cluster.vmid.min; const vmidMin = user.cluster.vmid.min;
const vmidMax = user.cluster.vmid.max; const vmidMax = user.cluster.vmid.max;

View File

@ -130,9 +130,7 @@ router.post("/:disk/resize", async (req, res) => {
size: req.body.size size: req.body.size
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -192,9 +190,7 @@ router.post("/:disk/move", async (req, res) => {
delete: req.body.delete delete: req.body.delete
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -315,9 +311,7 @@ router.post("/:disk/create", async (req, res) => {
iso: req.body.iso iso: req.body.iso
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;

View File

@ -1,7 +1,6 @@
import { Router } from "express"; import { Router } from "express";
export const router = Router({ mergeParams: true }); ; export const router = Router({ mergeParams: true }); ;
const db = global.db;
const checkAuth = global.utils.checkAuth; const checkAuth = global.utils.checkAuth;
const approveResources = global.utils.approveResources; const approveResources = global.utils.approveResources;
@ -32,9 +31,7 @@ router.post("/:netid/create", async (req, res) => {
name: req.body.name name: req.body.name
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`;
@ -65,7 +62,7 @@ router.post("/:netid/create", async (req, res) => {
return; return;
} }
// setup action // setup action
const nc = db.getUser(userObj).templates.network[params.type]; const nc = (await global.userManager.getUser(userObj, req.cookies)).templates.network[params.type];
const action = {}; const action = {};
if (params.type === "lxc") { 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}`; 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 rate: req.body.rate
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth for specific instance // check auth for specific instance
const vmpath = `/nodes/${params.node}/${params.type}/${params.vmid}`; 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 pcie: req.body.pcie
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {
@ -162,9 +160,7 @@ router.post("/create", async (req, res) => {
pcie: req.body.pcie pcie: req.body.pcie
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check if type is qemu // check if type is qemu
if (params.type !== "qemu") { if (params.type !== "qemu") {

View File

@ -165,12 +165,10 @@ if (schemes.interrupt.enabled) {
socket.destroy(); socket.destroy();
} }
else { else {
wsServer.handleUpgrade(req, socket, head, (socket) => { wsServer.handleUpgrade(req, socket, head, async (socket) => {
// get the user pools // get the user pools
const userRealm = cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(cookies.username);
const userID = cookies.username.replace(`@${userRealm}`, ""); const pools = Object.keys((await global.userManager.getUser(userObj, cookies)).cluster.pools);
const userObj = { id: userID, realm: userRealm };
const pools = Object.keys(global.db.getUser(userObj).cluster.pools);
// emit the connection to initialize socket // emit the connection to initialize socket
wsServer.emit("connection", socket, cookies.username, pools); wsServer.emit("connection", socket, cookies.username, pools);
}); });

View File

@ -12,15 +12,17 @@ const getUserResources = global.utils.getUserResources;
* - 401: {auth: false} * - 401: {auth: false}
*/ */
router.get("/dynamic/resources", async (req, res) => { router.get("/dynamic/resources", async (req, res) => {
const params = {
username: req.cookies.username
};
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
if (!auth) { if (!auth) {
return; return;
} }
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(params.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
const resources = await getUserResources(req, userObj); const resources = await getUserResources(req, userObj);
res.status(200).send(resources); res.status(200).send(resources);
@ -40,9 +42,7 @@ router.get("/config/:key", async (req, res) => {
key: req.params.key key: req.params.key
}; };
const userRealm = req.cookies.username.split("@").at(-1); const userObj = global.utils.getUserObjFromUsername(req.cookies.username);
const userID = req.cookies.username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
// check auth // check auth
const auth = await checkAuth(req.cookies, res); const auth = await checkAuth(req.cookies, res);
@ -51,7 +51,7 @@ router.get("/config/:key", async (req, res) => {
} }
const allowKeys = ["resources", "cluster"]; const allowKeys = ["resources", "cluster"];
if (allowKeys.includes(params.key)) { if (allowKeys.includes(params.key)) {
const config = global.db.getUser(userObj); const config = await global.userManager.getUser(userObj, req.cookies);
res.status(200).send(config[params.key]); res.status(200).send(config[params.key]);
} }
else { else {

View File

@ -15,18 +15,34 @@ import { exit } from "process";
export async function checkAuth (cookies, res, vmpath = null) { export async function checkAuth (cookies, res, vmpath = null) {
let auth = false; let auth = false;
const userRealm = cookies.username.split("@").at(-1); const userObj = getUserObjFromUsername(cookies.username); // check if username exists and is valid
const userID = cookies.username.replace(`@${userRealm}`, ""); if (!userObj) {
const userObj = { id: userID, realm: userRealm }; res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username was missing or invalid." });
if (global.db.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(); res.end();
return false; return false;
} }
if (vmpath) { if (!cookies.PVEAuthCookie) { // check if PVE token exists
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Token was missing or invalid." });
res.end();
return false;
}
const pveTicket = cookies.PVEAuthCookie;
const result = await global.pve.requestPVE("/access/ticket", "POST", null, { username: cookies.username, password: pveTicket });
if (result.status !== 200) { // check if PVE token is valid by using /access/ticket to validate ticket with Proxmox
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "Username did not match token." });
res.end();
return false;
}
if ((await global.userManager.getUser(userObj, cookies)) === null) { // check if user exists in database
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: `User ${cookies.username} not found in database.` });
res.end();
return false;
}
if (vmpath) { // if a path is specified, check the permissions on the path
const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies }); const result = await global.pve.requestPVE(`/${vmpath}/config`, "GET", { cookies });
auth = result.status === 200; auth = result.status === 200;
} }
@ -39,6 +55,7 @@ export async function checkAuth (cookies, res, vmpath = null) {
res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." }); res.status(401).send({ auth, path: vmpath ? `${vmpath}/config` : "/version", error: "User token did not pass authentication check." });
res.end(); res.end();
} }
return auth; return auth;
} }
@ -113,8 +130,7 @@ async function getAllInstanceConfigs (req, diskprefixes) {
*/ */
export async function getUserResources (req, user) { export async function getUserResources (req, user) {
const dbResources = global.config.resources; const dbResources = global.config.resources;
const userResources = global.db.getUser(user).resources; const userResources = (await global.userManager.getUser(user, req.cookies)).resources;
// setup disk prefixes object // setup disk prefixes object
const diskprefixes = []; const diskprefixes = [];
for (const resourceName of Object.keys(dbResources)) { for (const resourceName of Object.keys(dbResources)) {
@ -331,7 +347,7 @@ export function getTimeLeft (timeout) {
/** /**
* Recursively import routes from target folder. * Recursively import routes from target folder.
* @param {Object} router or app object. * @param {Object} router or app object.
* @param {string} baseroute API route for each imported module. * @param {string} baseroute base route of imported modules starting from the current path.
* @param {string} target folder to import modules. * @param {string} target folder to import modules.
* @param {string} from source folder of calling module, optional for imports from the same base directory. * @param {string} from source folder of calling module, optional for imports from the same base directory.
*/ */
@ -362,3 +378,15 @@ export function readJSONFile (path) {
exit(1); exit(1);
} }
}; };
export function getUserObjFromUsername (username) {
if (username) {
const userRealm = username.split("@").at(-1);
const userID = username.replace(`@${userRealm}`, "");
const userObj = { id: userID, realm: userRealm };
return userObj;
}
else {
return null;
}
}

View File

@ -72,6 +72,7 @@
} }
}, },
"cluster": { "cluster": {
"admin": false,
"nodes": { "nodes": {
"example-node-0": true, "example-node-0": true,
"example-node-1": true, "example-node-1": true,