MXS-1220: Add PUT support for /maxscale/ resource

The /maxscale/ resource now supports PUT requests which modify core
parameters. As not all parameters can be changed at runtime, only
modifications to parameters that support runtime configuration are
allowed.
This commit is contained in:
Markus Mäkelä
2017-05-22 11:14:41 +03:00
parent 3fd82ebae6
commit 6b8b19b439
9 changed files with 282 additions and 9 deletions

View File

@ -151,18 +151,19 @@ void close_client(void *cls,
delete client; 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; 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)) 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; rval = false;
static char error_resp[] = "{\"errors\": [ { \"detail\": \"Access denied\" } ] }"; static char error_resp[] = "{\"errors\": [ { \"detail\": \"Access denied\" } ] }";
MHD_Response *resp = MHD_Response *resp =
@ -172,6 +173,11 @@ bool do_auth(MHD_Connection *connection)
MHD_queue_basic_auth_fail_response(connection, "maxscale", resp); MHD_queue_basic_auth_fail_response(connection, "maxscale", resp);
MHD_destroy_response(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; return rval;
@ -187,7 +193,7 @@ int handle_client(void *cls,
void **con_cls) void **con_cls)
{ {
if (!do_auth(connection)) if (!do_auth(connection, url))
{ {
return MHD_YES; return MHD_YES;
} }

View File

@ -1739,3 +1739,89 @@ bool runtime_remove_user(const char* id, enum user_type type)
return rval; 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;
}

View File

@ -294,4 +294,13 @@ bool runtime_create_user_from_json(json_t* json);
*/ */
bool runtime_remove_user(const char* id, enum user_type type); 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 MXS_END_DECLS

View File

@ -440,6 +440,18 @@ HttpResponse cb_maxscale(const HttpRequest& request)
return HttpResponse(MHD_HTTP_OK, config_maxscale_to_json(request.host())); 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) HttpResponse cb_logs(const HttpRequest& request)
{ {
return HttpResponse(MHD_HTTP_OK, mxs_logs_to_json(request.host())); 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_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_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_logs, 2, "maxscale", "logs")));
m_put.push_back(SResource(new Resource(cb_alter_maxscale, 1, "maxscale")));
/** Change resource states */ /** Change resource states */
m_put.push_back(SResource(new Resource(cb_stop_monitor, 3, "monitors", ":monitor", "stop"))); m_put.push_back(SResource(new Resource(cb_stop_monitor, 3, "monitors", ":monitor", "stop")));

View File

@ -19,3 +19,6 @@ do
$maxscaledir/bin/maxadmin help >& /dev/null && break $maxscaledir/bin/maxadmin help >& /dev/null && break
sleep 0.1 sleep 0.1
done done
# Give MaxScale some time to settle
sleep 1

View File

@ -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)
});

View File

@ -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)
});

View File

@ -4,7 +4,7 @@ require("../utils.js")()
describe("Users", function() { describe("Users", function() {
before(startMaxScale) before(startMaxScale)
user = { var user = {
data: { data: {
id: "user1", id: "user1",
type: "inet", type: "inet",

View File

@ -420,7 +420,8 @@ module.exports = function() {
this.ajv = new Ajv({$data: true, allErrors: true, extendRefs: true, verbose: true}) this.ajv = new Ajv({$data: true, allErrors: true, extendRefs: true, verbose: true})
this.validate_func = ajv.compile(json_api_schema) this.validate_func = ajv.compile(json_api_schema)
this.validate = validate_json 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) { this.startMaxScale = function(done) {
child_process.execFile("./before.sh", function(err, stdout, stderr) { child_process.execFile("./before.sh", function(err, stdout, stderr) {
if (process.env.MAXSCALE_DIR == null) { if (process.env.MAXSCALE_DIR == null) {