diff --git a/server/core/admin.cc b/server/core/admin.cc index 7b48913d8..4f925d5e5 100644 --- a/server/core/admin.cc +++ b/server/core/admin.cc @@ -151,18 +151,19 @@ void close_client(void *cls, delete client; } -bool do_auth(MHD_Connection *connection) +bool do_auth(MHD_Connection *connection, const char* url) { - bool admin_auth = config_get_global_options()->admin_auth; - - char* pw = NULL; - char* user = MHD_basic_auth_get_username_password(connection, &pw); bool rval = true; - if (admin_auth) + if (config_get_global_options()->admin_auth) { + char* pw = NULL; + char* user = MHD_basic_auth_get_username_password(connection, &pw); + if (!user || !pw || !admin_verify_inet_user(user, pw)) { + MXS_WARNING("Authentication failed for '%s', %s. Request: %s", user ? user : "", + pw ? "using password" : "no password", url); rval = false; static char error_resp[] = "{\"errors\": [ { \"detail\": \"Access denied\" } ] }"; MHD_Response *resp = @@ -172,6 +173,11 @@ bool do_auth(MHD_Connection *connection) MHD_queue_basic_auth_fail_response(connection, "maxscale", resp); MHD_destroy_response(resp); } + else + { + MXS_INFO("Accept authentication from '%s', %s. Request: %s", user ? user : "", + pw ? "using password" : "no password", url); + } } return rval; @@ -187,7 +193,7 @@ int handle_client(void *cls, void **con_cls) { - if (!do_auth(connection)) + if (!do_auth(connection, url)) { return MHD_YES; } diff --git a/server/core/config_runtime.cc b/server/core/config_runtime.cc index fc8824fe1..408bf2877 100644 --- a/server/core/config_runtime.cc +++ b/server/core/config_runtime.cc @@ -1739,3 +1739,89 @@ bool runtime_remove_user(const char* id, enum user_type type) return rval; } + +bool validate_maxscale_json(json_t* json) +{ + bool rval = false; + json_t* param = mxs_json_pointer(json, MXS_JSON_PTR_PARAMETERS); + + if (param) + { + rval = is_null_or_count(param, CN_AUTH_CONNECT_TIMEOUT) && + is_null_or_count(param, CN_AUTH_READ_TIMEOUT) && + is_null_or_count(param, CN_AUTH_WRITE_TIMEOUT) && + is_null_or_bool(param, CN_ADMIN_AUTH); + } + + return rval; +} + +bool ignored_core_parameters(const char* key) +{ + static const char* params[] = + { + "libdir", + "datadir", + "process_datadir", + "cachedir", + "configdir", + "config_persistdir", + "module_configdir", + "piddir", + "logdir", + "langdir", + "execdir", + "connector_plugindir", + NULL + }; + + for (int i = 0; params[i]; i++) + { + if (strcmp(key, params[i]) == 0) + { + return true; + } + } + + return false; +} + +bool runtime_alter_maxscale_from_json(json_t* new_json) +{ + bool rval = false; + + if (validate_maxscale_json(new_json)) + { + rval = true; + json_t* old_json = config_maxscale_to_json(""); + ss_dassert(old_json); + + json_t* new_param = mxs_json_pointer(new_json, MXS_JSON_PTR_PARAMETERS); + json_t* old_param = mxs_json_pointer(old_json, MXS_JSON_PTR_PARAMETERS); + + const char* key; + json_t* value; + + json_object_foreach(new_param, key, value) + { + json_t* new_val = json_object_get(new_param, key); + json_t* old_val = json_object_get(old_param, key); + + if (old_val && new_val && mxs::json_to_string(new_val) == mxs::json_to_string(old_val)) + { + /** No change in values */ + } + else if (ignored_core_parameters(key)) + { + /** We can't change these at runtime */ + MXS_DEBUG("Ignoring runtime change to '%s'", key); + } + else if (!runtime_alter_maxscale(key, mxs::json_to_string(value).c_str())) + { + rval = false; + } + } + } + + return rval; +} diff --git a/server/core/maxscale/config_runtime.h b/server/core/maxscale/config_runtime.h index cd8135533..d52f2c0ff 100644 --- a/server/core/maxscale/config_runtime.h +++ b/server/core/maxscale/config_runtime.h @@ -294,4 +294,13 @@ bool runtime_create_user_from_json(json_t* json); */ bool runtime_remove_user(const char* id, enum user_type type); +/** + * @brief Alter core MaxScale parameters from JSON + * + * @param new_json JSON defining the new core parameters + * + * @return True if the core parameters are valid and were successfully applied + */ +bool runtime_alter_maxscale_from_json(json_t* new_json); + MXS_END_DECLS diff --git a/server/core/resource.cc b/server/core/resource.cc index a3a353a6c..52ef6f0b0 100644 --- a/server/core/resource.cc +++ b/server/core/resource.cc @@ -440,6 +440,18 @@ HttpResponse cb_maxscale(const HttpRequest& request) return HttpResponse(MHD_HTTP_OK, config_maxscale_to_json(request.host())); } +HttpResponse cb_alter_maxscale(const HttpRequest& request) +{ + json_t* json = request.get_json(); + + if (json && runtime_alter_maxscale_from_json(json)) + { + return HttpResponse(MHD_HTTP_NO_CONTENT); + } + + return HttpResponse(MHD_HTTP_FORBIDDEN, runtime_get_json_error()); +} + HttpResponse cb_logs(const HttpRequest& request) { return HttpResponse(MHD_HTTP_OK, mxs_logs_to_json(request.host())); @@ -602,6 +614,7 @@ public: m_put.push_back(SResource(new Resource(cb_alter_monitor, 2, "monitors", ":monitor"))); m_put.push_back(SResource(new Resource(cb_alter_service, 2, "services", ":service"))); m_put.push_back(SResource(new Resource(cb_alter_logs, 2, "maxscale", "logs"))); + m_put.push_back(SResource(new Resource(cb_alter_maxscale, 1, "maxscale"))); /** Change resource states */ m_put.push_back(SResource(new Resource(cb_stop_monitor, 3, "monitors", ":monitor", "stop"))); diff --git a/server/core/test/rest-api/before.sh b/server/core/test/rest-api/before.sh index 1a05da0f9..eb07e99c8 100755 --- a/server/core/test/rest-api/before.sh +++ b/server/core/test/rest-api/before.sh @@ -19,3 +19,6 @@ do $maxscaledir/bin/maxadmin help >& /dev/null && break sleep 0.1 done + +# Give MaxScale some time to settle +sleep 1 diff --git a/server/core/test/rest-api/test/auth.js b/server/core/test/rest-api/test/auth.js new file mode 100644 index 000000000..b8489b905 --- /dev/null +++ b/server/core/test/rest-api/test/auth.js @@ -0,0 +1,116 @@ +require("../utils.js")() + + +function set_auth(auth, value) { + return request.get(auth + host + "/maxscale") + .then(function(resp) { + var d = JSON.parse(resp) + d.data.attributes.parameters.admin_auth = value; + return request.put(auth + host + "/maxscale", { json: d }) + }) + .then(function() { + return request.get(auth + host + "/maxscale") + }) + .then(function(resp) { + var d = JSON.parse(resp) + d.data.attributes.parameters.admin_auth.should.equal(value) + }) +} + +describe("Authentication", function() { + before(startMaxScale) + + var user1 = { + data: { + id: "user1", + type: "inet", + attributes: { + password: "pw1" + } + } + } + + var user2 = { + data: { + id: "user2", + type: "inet", + attributes: { + password: "pw2" + } + } + } + + var auth1 = "http://" + user1.data.id + ":" + user1.data.attributes.password + "@" + var auth2 = "http://" + user2.data.id + ":" + user2.data.attributes.password + "@" + + it("unauthorized request without authentication", function() { + return request.get(base_url + "/maxscale") + .should.be.fulfilled + }) + + it("authorized request without authentication", function() { + return request.get(auth1 + host + "/maxscale") + .should.be.fulfilled + }) + + it("add user", function() { + return request.post(base_url + "/users/inet", { json: user1 }) + .should.be.fulfilled + }) + + it("request created user", function() { + return request.get(base_url + "/users/inet/" + user1.data.id) + .should.be.fulfilled + }) + + it("enable authentication", function() { + return set_auth(auth1, true).should.be.fulfilled + }) + + it("unauthorized request with authentication", function() { + return request.get(base_url + "/maxscale").auth() + .should.be.rejected + }) + + it("authorized request with authentication", function() { + return request.get(auth1 + host + "/maxscale") + .should.be.fulfilled + }) + + it("replace user", function() { + return request.post(auth1 + host + "/users/inet", { json: user2 }) + .then(function() { + return request.get(auth1 + host + "/users/inet/" + user2.data.id) + }) + .then(function() { + return request.delete(auth1 + host + "/users/inet/" + user1.data.id) + }) + .should.be.fulfilled + }) + + it("request with wrong user", function() { + return request.get(auth1 + host + "/maxscale") + .should.be.rejected + }) + + it("request with correct user", function() { + return request.get(auth2 + host + "/maxscale") + .should.be.fulfilled + }) + + it("disable authentication", function() { + return set_auth(auth2, false).should.be.fulfilled + }) + + it("unauthorized request without authentication ", function() { + return request.get(base_url + "/maxscale/logs") + .should.be.fulfilled + }) + + it("authorized request without authentication", function() { + return request.get(auth2 + host + "/maxscale") + .should.be.fulfilled + }) + + after(stopMaxScale) +}); diff --git a/server/core/test/rest-api/test/core.js b/server/core/test/rest-api/test/core.js new file mode 100644 index 000000000..46fa3f279 --- /dev/null +++ b/server/core/test/rest-api/test/core.js @@ -0,0 +1,39 @@ +require("../utils.js")() + + +function set_value(key, value) { + return request.get(base_url + "/maxscale") + .then(function(resp) { + var d = JSON.parse(resp) + d.data.attributes.parameters[key] = value; + return request.put(base_url + "/maxscale", { json: d }) + }) + .then(function() { + return request.get(base_url + "/maxscale") + }) + .then(function(resp) { + var d = JSON.parse(resp) + d.data.attributes.parameters[key].should.equal(value) + }) +} + +describe("Core Parameters", function() { + before(startMaxScale) + + it("auth_connect_timeout", function() { + return set_value("auth_connect_timeout", 10) + .should.be.fulfilled + }) + + it("auth_read_timeout", function() { + return set_value("auth_read_timeout", 10) + .should.be.fulfilled + }) + + it("auth_write_timeout", function() { + return set_value("auth_write_timeout", 10) + .should.be.fulfilled + }) + + after(stopMaxScale) +}); diff --git a/server/core/test/rest-api/test/users.js b/server/core/test/rest-api/test/users.js index aee68967e..de8af6fe7 100644 --- a/server/core/test/rest-api/test/users.js +++ b/server/core/test/rest-api/test/users.js @@ -4,7 +4,7 @@ require("../utils.js")() describe("Users", function() { before(startMaxScale) - user = { + var user = { data: { id: "user1", type: "inet", diff --git a/server/core/test/rest-api/utils.js b/server/core/test/rest-api/utils.js index 88664616e..ac1ea3f03 100644 --- a/server/core/test/rest-api/utils.js +++ b/server/core/test/rest-api/utils.js @@ -420,7 +420,8 @@ module.exports = function() { this.ajv = new Ajv({$data: true, allErrors: true, extendRefs: true, verbose: true}) this.validate_func = ajv.compile(json_api_schema) this.validate = validate_json - this.base_url = "http://localhost:8989/v1" + this.host = "localhost:8989/v1" + this.base_url = "http://" + this.host this.startMaxScale = function(done) { child_process.execFile("./before.sh", function(err, stdout, stderr) { if (process.env.MAXSCALE_DIR == null) {