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:
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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")));
|
||||
|
@ -19,3 +19,6 @@ do
|
||||
$maxscaledir/bin/maxadmin help >& /dev/null && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# Give MaxScale some time to settle
|
||||
sleep 1
|
||||
|
116
server/core/test/rest-api/test/auth.js
Normal file
116
server/core/test/rest-api/test/auth.js
Normal 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)
|
||||
});
|
39
server/core/test/rest-api/test/core.js
Normal file
39
server/core/test/rest-api/test/core.js
Normal 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)
|
||||
});
|
@ -4,7 +4,7 @@ require("../utils.js")()
|
||||
describe("Users", function() {
|
||||
before(startMaxScale)
|
||||
|
||||
user = {
|
||||
var user = {
|
||||
data: {
|
||||
id: "user1",
|
||||
type: "inet",
|
||||
|
@ -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) {
|
||||
|
Reference in New Issue
Block a user