Add a --config / -c option to the start CLI command to arbitrarily override any configuration key

The biggest caveat is that JS code (such as functions) will not be interpreted as such, on purpose, for security precautions. If such thing is needed, then a configuration file must be used.
This commit is contained in:
Jérémie Astori 2017-12-09 15:06:41 -05:00
parent b90c224a99
commit df2787d3e9
No known key found for this signature in database
GPG key ID: B9A4F245CD67BDE8
3 changed files with 163 additions and 0 deletions

View file

@ -2,6 +2,7 @@
global.log = require("../log.js");
const _ = require("lodash");
const fs = require("fs");
const path = require("path");
const program = require("commander");
@ -16,6 +17,11 @@ if (require("semver").lt(process.version, "6.0.0")) {
program.version(Helper.getVersion(), "-v, --version")
.option("--home <path>", `${colors.bold("[DEPRECATED]")} Use the ${colors.green("THELOUNGE_HOME")} environment variable instead.`)
.option(
"-c, --config <key=value>",
"override entries of the configuration file, must be specified for each entry that needs to be overriden",
Utils.parseConfigOptions
)
.on("--help", Utils.extraHelp)
.parseOptions(process.argv);
@ -49,6 +55,9 @@ if (!home) {
Helper.setHome(home);
// Merge config key-values passed as CLI options into the main config
_.merge(Helper.config, program.config);
require("./start");
require("./config");
if (!Helper.config.public && !Helper.config.ldap.enable) {

View file

@ -1,5 +1,6 @@
"use strict";
const _ = require("lodash");
const colors = require("colors/safe");
const fs = require("fs");
const Helper = require("../helper");
@ -51,6 +52,55 @@ class Utils {
return home;
}
// Parses CLI options such as `-c public=true`, `-c debug.raw=true`, etc.
static parseConfigOptions(val, memo) {
// Invalid option that is not of format `key=value`, do nothing
if (!val.includes("=")) {
return memo;
}
const parseValue = (value) => {
if (value === "true") {
return true;
} else if (value === "false") {
return false;
} else if (value === "undefined") {
return undefined;
} else if (value === "null") {
return null;
} else if (/^\[.*\]$/.test(value)) { // Arrays
// Supporting arrays `[a,b]` and `[a, b]`
const array = value.slice(1, -1).split(/,\s*/);
// If [] is given, it will be parsed as `[ "" ]`, so treat this as empty
if (array.length === 1 && array[0] === "") {
return [];
}
return array.map(parseValue); // Re-parses all values of the array
}
return value;
};
// First time the option is parsed, memo is not set
if (memo === undefined) {
memo = {};
}
// Note: If passed `-c foo="bar=42"` (with single or double quotes), `val`
// will always be passed as `foo=bar=42`, never with quotes.
const position = val.indexOf("="); // Only split on the first = found
const key = val.slice(0, position);
const value = val.slice(position + 1);
const parsedValue = parseValue(value);
if (_.has(memo, key)) {
log.warn(`Configuration key ${colors.bold(key)} was already specified, ignoring...`);
} else {
memo = _.set(memo, key, parsedValue);
}
return memo;
}
}
module.exports = Utils;

View file

@ -43,4 +43,108 @@ describe("Utils", function() {
expect(stdout).to.include("THELOUNGE_HOME");
});
});
describe(".parseConfigOptions", function() {
describe("when it's the first option given", function() {
it("should return nothing when passed an invalid config", function() {
expect(Utils.parseConfigOptions("foo")).to.be.undefined;
});
it("should correctly parse boolean values", function() {
expect(Utils.parseConfigOptions("foo=true")).to.deep.equal({foo: true});
expect(Utils.parseConfigOptions("foo=false")).to.deep.equal({foo: false});
});
it("should correctly parse empty strings", function() {
expect(Utils.parseConfigOptions("foo=")).to.deep.equal({foo: ""});
});
it("should correctly parse null values", function() {
expect(Utils.parseConfigOptions("foo=null")).to.deep.equal({foo: null});
});
it("should correctly parse undefined values", function() {
expect(Utils.parseConfigOptions("foo=undefined"))
.to.deep.equal({foo: undefined});
});
it("should correctly parse array values", function() {
expect(Utils.parseConfigOptions("foo=[bar,true]"))
.to.deep.equal({foo: ["bar", true]});
expect(Utils.parseConfigOptions("foo=[bar, true]"))
.to.deep.equal({foo: ["bar", true]});
});
it("should correctly parse empty array values", function() {
expect(Utils.parseConfigOptions("foo=[]"))
.to.deep.equal({foo: []});
});
it("should correctly parse values that contain `=` sign", function() {
expect(Utils.parseConfigOptions("foo=bar=42"))
.to.deep.equal({foo: "bar=42"});
});
it("should correctly parse keys using dot-notation", function() {
expect(Utils.parseConfigOptions("foo.bar=value"))
.to.deep.equal({foo: {bar: "value"}});
});
it("should correctly parse keys using array-notation", function() {
expect(Utils.parseConfigOptions("foo[0]=value"))
.to.deep.equal({foo: ["value"]});
});
});
describe("when some options have already been parsed", function() {
it("should not modify existing options when passed an invalid config", function() {
const memo = {foo: "bar"};
expect(Utils.parseConfigOptions("foo", memo)).to.equal(memo);
});
it("should combine a new option with previously parsed ones", function() {
expect(Utils.parseConfigOptions("bar=false", {foo: true}))
.to.deep.equal({foo: true, bar: false});
});
it("should maintain existing properties of a nested object", function() {
expect(Utils.parseConfigOptions("foo.bar=true", {foo: {baz: false}}))
.to.deep.equal({foo: {bar: true, baz: false}});
});
it("should maintain existing entries of an array", function() {
expect(Utils.parseConfigOptions("foo[1]=baz", {foo: ["bar"]}))
.to.deep.equal({foo: ["bar", "baz"]});
});
describe("when given the same key multiple times", function() {
let originalWarn;
beforeEach(function() {
originalWarn = log.warn;
});
afterEach(function() {
log.warn = originalWarn;
});
it("should not override options", function() {
log.warn = () => {};
expect(Utils.parseConfigOptions("foo=baz", {foo: "bar"}))
.to.deep.equal({foo: "bar"});
});
it("should display a warning", function() {
let warning = "";
log.warn = TestUtil.mockLogger((str) => warning += str);
Utils.parseConfigOptions("foo=bar", {foo: "baz"});
expect(warning).to.include("foo was already specified");
});
});
});
});
});