hfs
Version:
HTTP File Server
207 lines (206 loc) • 8.87 kB
JavaScript
// 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 {};
}
;