UNPKG

hfs

Version:
207 lines (206 loc) 8.87 kB
"use strict"; // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.accounts = void 0; exports.expandUsername = expandUsername; exports.ctxBelongsTo = ctxBelongsTo; exports.getUsernames = getUsernames; exports.getAccount = getAccount; exports.saveSrpInfo = saveSrpInfo; exports.createAdmin = createAdmin; exports.updateAccount = updateAccount; exports.normalizeUsername = normalizeUsername; exports.renameAccount = renameAccount; exports.addAccount = addAccount; exports.delAccount = delAccount; exports.getFromAccount = getFromAccount; exports.accountHasPassword = accountHasPassword; exports.accountCanLogin = accountCanLogin; exports.accountCanLoginAdmin = accountCanLoginAdmin; exports.changeSrpHelper = changeSrpHelper; const lodash_1 = __importDefault(require("lodash")); const misc_1 = require("./misc"); const config_1 = require("./config"); const tssrp6a_1 = require("tssrp6a"); const events_1 = __importDefault(require("./events")); const apiMiddleware_1 = require("./apiMiddleware"); const auth_1 = require("./auth"); // provides the username and all other usernames it inherits based on the 'belongs' attribute. Useful to check permissions function expandUsername(who) { const ret = []; const q = [who]; for (const u of q) { const a = getAccount(u); if (!a || a.disabled) continue; ret.push(u); if (a.belongs) q.push(...a.belongs); } return ret; } // check if current username or any ancestor match the provided usernames function ctxBelongsTo(ctx, usernames) { var _a; return ((_a = ctx.state).usernames || (_a.usernames = expandUsername((0, auth_1.getCurrentUsername)(ctx)))) // cache ancestors' usernames inside context state .some((u) => usernames.includes(u)); } function getUsernames() { return Object.keys(exports.accounts.get()); } function getAccount(username, normalize = true) { if (normalize) username = normalizeUsername(username); return username ? exports.accounts.get()[username] : undefined; } function saveSrpInfo(account, salt, verifier) { account.srp = String(salt) + '|' + String(verifier); } const createAdminConfig = (0, config_1.defineConfig)('create-admin', ''); createAdminConfig.sub(v => { if (!v) return; createAdminConfig.set(''); // we can't createAdmin right away, as its changes will be lost after return, when our caller (setConfig) applies undefined properties. setTimeout is good enough, as the process is sync. setTimeout(() => createAdmin(v)); }); async function createAdmin(password, username = 'admin') { const acc = await addAccount(username, { admin: true, password }, true); console.log(acc ? "account admin set" : "something went wrong"); } const srp6aNimbusRoutines = new tssrp6a_1.SRPRoutines(new tssrp6a_1.SRPParameters()); async function updateAccount(account, change) { const jsonWas = JSON.stringify(account); const { username: usernameWas } = account; if (typeof change === 'function') await (change === null || change === void 0 ? void 0 : change(account)); else Object.assign(account, (0, misc_1.objSameKeys)(change, x => x || undefined)); for (const [k, v] of (0, misc_1.typedEntries)(account)) if (!v) delete account[k]; // we consider all account fields, when falsy, as equivalent to be missing (so, default value applies) const { username, password } = account; if (password) { console.debug('hashing password for', username); delete account.password; const res = await (0, tssrp6a_1.createVerifierAndSalt)(srp6aNimbusRoutines, username, password); saveSrpInfo(account, res.s, res.v); } if (account.belongs) { account.belongs = (0, misc_1.wantArray)(account.belongs); lodash_1.default.remove(account.belongs, b => { if (exports.accounts.get().hasOwnProperty(b)) return; console.error(`account ${username} belongs to non-existing ${b}`); return true; }); if (!account.belongs.length) delete account.belongs; } account.expire && (account.expire = new Date(account.expire)); if (username !== usernameWas) renameAccount(usernameWas, username); if (jsonWas !== JSON.stringify(account)) // this test will miss the 'username' field, because hidden, but renameAccount is already calling saveAccountsASAP saveAccountsAsap(); } const saveAccountsAsap = config_1.saveConfigAsap; exports.accounts = (0, config_1.defineConfig)('accounts', {}); exports.accounts.sub(lodash_1.default.debounce(obj => { // consider some validation here, in case of manual edit of the config lodash_1.default.each(obj, (rec, k) => { const norm = normalizeUsername(k); if ((rec === null || rec === void 0 ? void 0 : rec.username) !== norm) { if (!rec) // an empty object in yaml is parsed as null rec = obj[norm] = { username: norm }; else if ((0, misc_1.objRenameKey)(obj, k, norm)) saveAccountsAsap(); (0, misc_1.setHidden)(rec, { username: norm }); } void updateAccount(rec, {}); // work fields }); })); // don't trigger in the middle of a series of deletion, as we may have an inconsistent state function normalizeUsername(username) { return username.toLocaleLowerCase(); } function renameAccount(from, to) { var _a; from = normalizeUsername(from); const as = exports.accounts.get(); to = normalizeUsername(to); if (!to || !as[from] || as[to]) return false; if (to === from) return true; (0, misc_1.objRenameKey)(as, from, to); (0, misc_1.setHidden)(as[to], { username: to }); // update references for (const a of Object.values(as)) { const idx = (_a = a.belongs) === null || _a === void 0 ? void 0 : _a.indexOf(from); if (idx !== undefined && idx >= 0) a.belongs[idx] = to; } exports.accounts.set(as); events_1.default.emit('accountRenamed', { from, to }); // everybody, take care of your stuff saveAccountsAsap(); return true; } function addAccount(username, props, updateExisting = false) { username = normalizeUsername(username); if (!username) return; let account = getAccount(username, false); if (account && !updateExisting) return; account = (0, misc_1.setHidden)(account || {}, { username }); // hidden so that stringification won't include it Object.assign(account, lodash_1.default.pickBy(props, Boolean)); exports.accounts.set(accounts => Object.assign(accounts, { [username]: account })); return updateAccount(account, account).then(() => account); } function delAccount(username) { if (!getAccount(username)) return false; exports.accounts.set(x => lodash_1.default.omit(x, normalizeUsername(username))); saveAccountsAsap(); return true; } // get some property from account, searching in its groups if necessary. Search is breadth-first, and this determines priority of inheritance. function getFromAccount(account, getter) { const search = [account]; for (const accountOrUsername of search) { const a = typeof accountOrUsername === 'string' ? getAccount(accountOrUsername) : accountOrUsername; if (!a) continue; const res = getter(a); if (res !== undefined) return res; if (a.belongs) search.push(...a.belongs); } } function accountHasPassword(account) { return Boolean(account.password || account.srp); } function accountCanLogin(account) { var _a; return (accountHasPassword(account) || ((_a = account.plugin) === null || _a === void 0 ? void 0 : _a.auth)) && !allDisabled(account); } function allDisabled(account) { var _a; return Boolean(account.disabled || account.expire < Date.now() || ((_a = account.belongs) === null || _a === void 0 ? void 0 : _a.length // don't every() on empty array, as it returns true ) && account.belongs.map(u => getAccount(u, false)).every(a => a && allDisabled(a))); } function accountCanLoginAdmin(account) { return accountCanLogin(account) && getFromAccount(account, a => a.admin) || false; } async function changeSrpHelper(account, salt, verifier) { if (!salt || !verifier) return new apiMiddleware_1.ApiError(misc_1.HTTP_BAD_REQUEST, 'missing parameters'); await updateAccount(account, account => saveSrpInfo(account, salt, verifier)); return {}; }