implement persistence of config changes from admin panel

This commit is contained in:
ansuz 2020-10-02 12:54:17 +05:30
parent 54b32690b2
commit 7aa7d5978f
3 changed files with 204 additions and 2 deletions

View file

@ -4,6 +4,7 @@ const nThen = require("nthen");
const getFolderSize = require("get-folder-size");
const Util = require("../common-util");
const Ulimit = require("ulimit");
const Decrees = require("../decrees");
var Fs = require("fs");
@ -175,6 +176,8 @@ var restoreArchivedDocument = function (Env, Server, cb) {
};
// CryptPad_AsyncStore.rpc.send('ADMIN', ['SET_DEFAULT_STORAGE_LIMIT', 1024 * 1024 * 1024 /* 1GB */], console.log)
// XXX make this a decree
// XXX expose this via the admin panel (with UI)
var setDefaultStorageLimit = function (Env, Server, cb, data) {
var value = Array.isArray(data) && data[1];
if (typeof(value) !== 'number' || value <= 0) { return void cb('EINVAL'); }
@ -190,6 +193,56 @@ var setDefaultStorageLimit = function (Env, Server, cb, data) {
cb(void 0, change);
};
// CryptPad_AsyncStore.rpc.send('ADMIN', [ 'ADMIN_DECREE', ['RESTRICT_REGISTRATION', [true]]], console.log)
var adminDecree = function (Env, Server, cb, data, safeKey) {
var value = data[1];
if (!Array.isArray(value)) { return void cb('INVALID_DECREE'); }
var command = value[0];
var args = value[1];
var unsafeKey = Util.unescapeKeyCharacters(safeKey);
/*
The admin should have sent a command to be run:
the server adds two pieces of information to the supplied decree:
* the unsafeKey of the admin who uploaded it
* the current time
1. test the command to see if it's valid and will result in a change
2. if so, apply it and write it to the log for persistence
3. respond to the admin with an error or nothing
*/
var decree = [command, args, unsafeKey, +new Date()];
var changed;
try {
changed = Decrees.handleCommand(Env, decree) || false;
} catch (err) {
return void cb(err);
}
if (!changed) { return void cb(); }
Decrees.write(Env, decree, cb);
};
// CryptPad_AsyncStore.rpc.send('ADMIN', ['INSTANCE_STATUS], console.log)
var instanceStatus = function (Env, Server, cb) {
cb(void 0, {
restrictRegistration: Boolean(Env.restrictRegistration),
launchTime: Env.launchTime,
currentTime: +new Date(),
accountRetentionTime: Env.accountRetentionTime,
archiveRetentionTime: Env.archiveRetentionTime,
defaultStorageLimit: Env.defaultStorageLimit,
});
};
var commands = {
ACTIVE_SESSIONS: getActiveSessions,
ACTIVE_PADS: getActiveChannelCount,
@ -204,6 +257,9 @@ var commands = {
ARCHIVE_DOCUMENT: archiveDocument,
RESTORE_ARCHIVED_DOCUMENT: restoreArchivedDocument,
ADMIN_DECREE: adminDecree,
INSTANCE_STATUS: instanceStatus,
};
Admin.command = function (Env, safeKey, data, _cb, Server) {
@ -218,10 +274,9 @@ Admin.command = function (Env, safeKey, data, _cb, Server) {
var command = commands[data[0]];
if (typeof(command) === 'function') {
return void command(Env, Server, cb, data);
return void command(Env, Server, cb, data, safeKey);
}
return void cb('UNHANDLED_ADMIN_COMMAND');
};

132
lib/decrees.js Normal file
View file

@ -0,0 +1,132 @@
var Decrees = module.exports;
/* Admin decrees which modify global server state
IMPLEMENTED:
RESTRICT_REGISTRATION
NOT IMPLEMENTED:
ADD_QUOTA
RM_QUOTA
UPDATE_QUOTA
ADD_INVITE
REVOKE_INVITE
REDEEM_INVITE
*/
var commands = {};
/* commands have a simple API:
* they receive the global Env and the arguments to be applied
* if the arguments are invalid the operation will not be applied
* the command throws
* if the arguments are valid but do not result in a change, the operation is redundant.
* return false
* if the arguments are valid and will result in a change, the operation should be applied
* apply it
* return true to indicate that it was applied
*/
// Toggles a simple boolean
commands.RESTRICT_REGISTRATION = function (Env, args) {
if (!Array.isArray(args) || typeof(args[0]) !== 'boolean') {
throw new Error('INVALID_ARGS');
}
var bool = args[0];
if (bool === Env.restrictRegistration) { return false; }
Env.restrictRegistration = bool;
return true;
};
// [<command>, <args>, <author>, <time>]
var handleCommand = Decrees.handleCommand = function (Env, line) {
var command = line[0];
var args = line[1];
if (typeof(commands[command]) !== 'function') {
console.error(line);
throw new Error("DECREE_UNSUPPORTED_COMMAND");
}
return commands[command](Env, args);
};
Decrees.createLineHandler = function (Env) {
var Log = Env.Log;
var index = -1;
return function (err, line) {
index++;
if (err) {
// Log the error and bail out
return void Log.error("DECREE_LINE_ERR", {
error: err.message,
index: index,
line: line,
});
}
if (Array.isArray(line)) {
try {
return void handleCommand(Env, line);
} catch (err2) {
return void Log.error("DECREE_COMMAND_ERR", {
error: err2.message,
index: index,
line: line,
});
}
}
Log.error("DECREE_HANDLER_WEIRD_LINE", {
line: line,
index: index,
});
};
};
var Fs = require("fs");
var Path = require("path");
var readFileBin = require("./stream-file").readFileBin;
var Util = require("./common-util");
var Schedule = require("./schedule");
Decrees.load = function (Env, cb) {
Env.scheduleDecree = Env.scheduleDecree || Schedule();
var decreePath = Env.paths.decree;
var decreeName = Path.join(Env.paths.decree, 'decree.ndjson'); // XXX
var stream = Fs.createReadStream(decreeName, {start: 0});
var handler = Decrees.createLineHandler(Env);
Env.scheduleDecree.blocking('', function (unblock) {
var done = Util.once(Util.both(cb, unblock));
readFileBin(stream, function (msgObj, next) {
var text = msgObj.buff.toString('utf8');
try {
handler(void 0, JSON.parse(text));
} catch (err) {
handler(err);
}
next();
}, function (err) {
done(err);
});
});
};
Decrees.write = function (Env, decree, _cb) {
var path = Path.join(Env.paths.decree, 'decree.ndjson');
Env.scheduleDecree.ordered('', function (next) {
var cb = Util.both(_cb, next);
Fs.appendFile(path, JSON.stringify(decree) + '\n', cb);
});
};

View file

@ -32,6 +32,8 @@ module.exports.create = function (config, cb) {
// store
id: Crypto.randomBytes(8).toString('hex'),
launchTime: +new Date(),
inactiveTime: config.inactiveTime,
archiveRetentionTime: config.archiveRetentionTime,
accountRetentionTime: config.accountRetentionTime,
@ -60,6 +62,7 @@ module.exports.create = function (config, cb) {
netfluxUsers: {},
pinStore: undefined,
// XXX deprecated?
pinnedPads: {},
pinsLoaded: false,
pendingPinInquiries: {},
@ -100,6 +103,7 @@ module.exports.create = function (config, cb) {
paths.data = keyOrDefaultString('filePath', './datastore');
paths.staging = keyOrDefaultString('blobStagingPath', './blobstage');
paths.blob = keyOrDefaultString('blobPath', './blob');
paths.decree = keyOrDefaultString('decreePath', './data/');
Env.defaultStorageLimit = typeof(config.defaultStorageLimit) === 'number' && config.defaultStorageLimit >= 0?
config.defaultStorageLimit:
@ -306,6 +310,17 @@ module.exports.create = function (config, cb) {
});
}, 60 * 1000);
}).nThen(function () {
// XXX scan the decree log to update Env
var Decrees = require("./decrees");
Decrees.load(Env, function (err) {
console.log("DONE LOADING DECREES"); // XXX
if (err) {
console.error(err);
}
});
}).nThen(function () {
RPC.create(Env, function (err, _rpc) {
if (err) { throw err; }