From 1067fd352cc4eec0b0b2600ee51f76e8b846b183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20M=C3=A4kel=C3=A4?= Date: Sat, 20 May 2017 08:18:25 +0300 Subject: [PATCH] MXS-1220: Add creation and deletion of admin users Admin users can now be created via the REST API. This allows remote management of the administrative interface itself. --- include/maxscale/adminusers.h.in | 14 ++++ include/maxscale/config.h | 2 + server/core/adminusers.cc | 13 --- server/core/config_runtime.cc | 105 ++++++++++++++++++++++++ server/core/maxscale/config_runtime.h | 20 +++++ server/core/resource.cc | 30 +++++++ server/core/test/rest-api/after.sh | 4 + server/core/test/rest-api/test/users.js | 59 +++++++++++++ 8 files changed, 234 insertions(+), 13 deletions(-) create mode 100644 server/core/test/rest-api/test/users.js diff --git a/include/maxscale/adminusers.h.in b/include/maxscale/adminusers.h.in index 1563f77a4..9cc53f589 100644 --- a/include/maxscale/adminusers.h.in +++ b/include/maxscale/adminusers.h.in @@ -33,6 +33,20 @@ MXS_BEGIN_DECLS static const char INET_DEFAULT_USERNAME[] = "admin"; static const char INET_DEFAULT_PASSWORD[] = "mariadb"; +/** Return values for the functions */ +static const char *ADMIN_ERR_NOMEM = "Out of memory"; +static const char *ADMIN_ERR_FILEOPEN = "Unable to create password file"; +static const char *ADMIN_ERR_DUPLICATE = "Duplicate username specified"; +static const char *ADMIN_ERR_USERNOTFOUND = "User not found"; +static const char *ADMIN_ERR_AUTHENTICATION = "Authentication failed"; +static const char *ADMIN_ERR_FILEAPPEND = "Unable to append to password file"; +static const char *ADMIN_ERR_PWDFILEOPEN = "Failed to open password file"; +static const char *ADMIN_ERR_TMPFILEOPEN = "Failed to open temporary password file"; +static const char *ADMIN_ERR_PWDFILEACCESS = "Failed to access password file"; +static const char *ADMIN_ERR_DELLASTUSER = "Deleting the last user is forbidden"; +static const char *ADMIN_ERR_DELROOT = "Deleting the default admin user is forbidden"; +static const char *ADMIN_SUCCESS = NULL; + /** User types */ enum user_type { diff --git a/include/maxscale/config.h b/include/maxscale/config.h index 3ac75f219..d402b9ff1 100644 --- a/include/maxscale/config.h +++ b/include/maxscale/config.h @@ -60,7 +60,9 @@ MXS_BEGIN_DECLS #define MXS_JSON_PTR_PARAM_SSL_VERSION MXS_JSON_PTR_PARAMETERS "/ssl_version" #define MXS_JSON_PTR_PARAM_SSL_CERT_VERIFY_DEPTH MXS_JSON_PTR_PARAMETERS "/ssl_cert_verify_depth" +/** Non-parameter JSON pointers */ #define MXS_JSON_PTR_MODULE "/data/attributes/module" +#define MXS_JSON_PTR_PASSWORD "/data/attributes/password" /** * Common configuration parameters names diff --git a/server/core/adminusers.cc b/server/core/adminusers.cc index 639069828..155ea5c3d 100644 --- a/server/core/adminusers.cc +++ b/server/core/adminusers.cc @@ -45,19 +45,6 @@ static bool admin_search_user(USERS *users, const char *uname); static USERS *linux_users = NULL; static USERS *inet_users = NULL; -static const char *ADMIN_ERR_NOMEM = "Out of memory"; -static const char *ADMIN_ERR_FILEOPEN = "Unable to create password file"; -static const char *ADMIN_ERR_DUPLICATE = "Duplicate username specified"; -static const char *ADMIN_ERR_USERNOTFOUND = "User not found"; -static const char *ADMIN_ERR_AUTHENTICATION = "Authentication failed"; -static const char *ADMIN_ERR_FILEAPPEND = "Unable to append to password file"; -static const char *ADMIN_ERR_PWDFILEOPEN = "Failed to open password file"; -static const char *ADMIN_ERR_TMPFILEOPEN = "Failed to open temporary password file"; -static const char *ADMIN_ERR_PWDFILEACCESS = "Failed to access password file"; -static const char *ADMIN_ERR_DELLASTUSER = "Deleting the last user is forbidden"; -static const char *ADMIN_ERR_DELROOT = "Deleting the default admin user is forbidden"; -static const char *ADMIN_SUCCESS = NULL; - static const int LINELEN = 80; static const char LINUX_USERS_FILE_NAME[] = "maxadmin-users"; diff --git a/server/core/config_runtime.cc b/server/core/config_runtime.cc index 1dadd1914..2cd9ef4bf 100644 --- a/server/core/config_runtime.cc +++ b/server/core/config_runtime.cc @@ -1546,3 +1546,108 @@ json_t* runtime_get_json_error() return obj; } + +bool validate_user_json(json_t* json) +{ + bool rval = false; + json_t* id = mxs_json_pointer(json, MXS_JSON_PTR_ID); + json_t* type = mxs_json_pointer(json, MXS_JSON_PTR_TYPE); + json_t* password = mxs_json_pointer(json, MXS_JSON_PTR_PASSWORD); + + if (!id) + { + runtime_error("Request body does not define the '%s' field", MXS_JSON_PTR_ID); + } + else if (!json_is_string(id)) + { + runtime_error("The '%s' field is not a string", MXS_JSON_PTR_ID); + } + else if (!type) + { + runtime_error("Request body does not define the '%s' field", MXS_JSON_PTR_TYPE); + } + else if (!json_is_string(type)) + { + runtime_error("The '%s' field is not a string", MXS_JSON_PTR_TYPE); + } + else + { + if (strcmp(json_string_value(type), CN_INET) == 0) + { + if (!password) + { + runtime_error("Request body does not define the '%s' field", MXS_JSON_PTR_PASSWORD); + } + else if (!json_is_string(password)) + { + runtime_error("The '%s' field is not a string", MXS_JSON_PTR_PASSWORD); + } + else + { + rval = true; + } + } + else if (strcmp(json_string_value(type), CN_UNIX) == 0) + { + rval = true; + } + else + { + runtime_error("Invalid value for field '%s': %s", MXS_JSON_PTR_TYPE, + json_string_value(type)); + } + } + + return rval; +} + +bool runtime_create_user_from_json(json_t* json) +{ + bool rval = false; + + if (validate_user_json(json)) + { + const char* user = json_string_value(mxs_json_pointer(json, MXS_JSON_PTR_ID)); + const char* password = json_string_value(mxs_json_pointer(json, MXS_JSON_PTR_PASSWORD)); + string strtype = json_string_value(mxs_json_pointer(json, MXS_JSON_PTR_TYPE)); + const char* err = NULL; + + if (strtype == CN_INET && (err = admin_add_inet_user(user, password)) == ADMIN_SUCCESS) + { + MXS_NOTICE("Create network user '%s'", user); + rval = true; + } + else if (strtype == CN_UNIX && (err = admin_enable_linux_account(user)) == ADMIN_SUCCESS) + { + MXS_NOTICE("Enabled account '%s'", user); + rval = true; + } + else if (err) + { + runtime_error("Failed to add user '%s': %s", user, err); + } + } + + return rval; +} + +bool runtime_remove_user(const char* id, enum user_type type) +{ + bool rval = false; + const char* err = type == USER_TYPE_INET ? + admin_remove_inet_user(id, NULL) : + admin_disable_linux_account(id); + + if (err == ADMIN_SUCCESS) + { + MXS_NOTICE("%s '%s'", type == USER_TYPE_INET ? + "Deleted network user" : "Disabled account", id); + rval = true; + } + else + { + runtime_error("Failed to remove user '%s': %s", id, err); + } + + return rval; +} diff --git a/server/core/maxscale/config_runtime.h b/server/core/maxscale/config_runtime.h index 07b80fb89..04ec642ec 100644 --- a/server/core/maxscale/config_runtime.h +++ b/server/core/maxscale/config_runtime.h @@ -18,6 +18,7 @@ #include +#include #include #include #include @@ -264,4 +265,23 @@ bool runtime_alter_logs_from_json(json_t* json); */ json_t* runtime_get_json_error(); +/** + * @brief Create a new user account + * + * @param json JSON defining the user + * + * @return True if the user was successfully created + */ +bool runtime_create_user_from_json(json_t* json); + +/** + * @brief Remove admin user + * + * @param id Username of the network user + * @param type USER_TYPE_INET for network user and USER_TYPE_UNIX for enabled accounts + * + * @return True if user was successfully removed + */ +bool runtime_remove_user(const char* id, enum user_type type); + MXS_END_DECLS diff --git a/server/core/resource.cc b/server/core/resource.cc index 144156b9b..a3a353a6c 100644 --- a/server/core/resource.cc +++ b/server/core/resource.cc @@ -512,6 +512,32 @@ HttpResponse cb_unix_user(const HttpRequest& request) return HttpResponse(MHD_HTTP_OK, admin_user_to_json(request.host(), user.c_str(), USER_TYPE_UNIX)); } +HttpResponse cb_create_user(const HttpRequest& request) +{ + json_t* json = request.get_json(); + + if (runtime_create_user_from_json(json)) + { + return HttpResponse(MHD_HTTP_NO_CONTENT); + } + + return HttpResponse(MHD_HTTP_FORBIDDEN, runtime_get_json_error()); +} + +HttpResponse cb_delete_user(const HttpRequest& request) +{ + string user = request.last_uri_part(); + string type = request.uri_part(1); + + if ((type == CN_INET && runtime_remove_user(user.c_str(), USER_TYPE_INET)) || + (type == CN_UNIX && runtime_remove_user(user.c_str(), USER_TYPE_UNIX))) + { + return HttpResponse(MHD_HTTP_NO_CONTENT); + } + + return HttpResponse(MHD_HTTP_FORBIDDEN, runtime_get_json_error()); +} + HttpResponse cb_send_ok(const HttpRequest& request) { return HttpResponse(MHD_HTTP_OK); @@ -568,6 +594,8 @@ public: m_post.push_back(SResource(new Resource(cb_create_monitor, 1, "monitors"))); m_post.push_back(SResource(new Resource(cb_create_service_listener, 3, "services", ":service", "listeners"))); + m_post.push_back(SResource(new Resource(cb_create_user, 2, "users", "inet"))); + m_post.push_back(SResource(new Resource(cb_create_user, 2, "users", "unix"))); /** Update resources */ m_put.push_back(SResource(new Resource(cb_alter_server, 2, "servers", ":server"))); @@ -583,6 +611,8 @@ public: m_delete.push_back(SResource(new Resource(cb_delete_server, 2, "servers", ":server"))); m_delete.push_back(SResource(new Resource(cb_delete_monitor, 2, "monitors", ":monitor"))); + m_delete.push_back(SResource(new Resource(cb_delete_user, 3, "users", "inet", ":inetuser"))); + m_delete.push_back(SResource(new Resource(cb_delete_user, 3, "users", "unix", ":unixuser"))); } ~RootResource() diff --git a/server/core/test/rest-api/after.sh b/server/core/test/rest-api/after.sh index 982514de0..67af755ed 100755 --- a/server/core/test/rest-api/after.sh +++ b/server/core/test/rest-api/after.sh @@ -18,6 +18,10 @@ done # If it wasn't dead before, now it is pgrep maxscale && pkill -9 maxscale +# Remove created users +rm $maxscaledir/passwd +rm $maxscaledir/maxadmin-users + rm -r $maxscaledir/lib/maxscale rm -r $maxscaledir/cache/maxscale rm -r $maxscaledir/run/maxscale diff --git a/server/core/test/rest-api/test/users.js b/server/core/test/rest-api/test/users.js new file mode 100644 index 000000000..aee68967e --- /dev/null +++ b/server/core/test/rest-api/test/users.js @@ -0,0 +1,59 @@ +require("../utils.js")() + + +describe("Users", function() { + before(startMaxScale) + + user = { + data: { + id: "user1", + type: "inet", + attributes: { + } + } + } + + it("add new user without password", function() { + return request.post(base_url + "/users/inet", { json: user }) + .should.be.rejected + }) + + it("add user", function() { + user.data.attributes.password = "pw1" + return request.post(base_url + "/users/inet", { json: user }) + .should.be.fulfilled + }) + + it("add user again", function() { + return request.post(base_url + "/users/inet", { json: user }) + .should.be.rejected + }) + + it("add user again but without password", function() { + delete user.data.attributes.password + return request.post(base_url + "/users/inet", { json: user }) + .should.be.rejected + }) + + it("get created user", function() { + return request.get(base_url + "/users/inet/user1") + .should.be.fulfilled + }) + + it("get non-existent user", function() { + return request.get(base_url + "/users/inet/user2") + .should.be.rejected + }) + + it("delete created user", function() { + return request.delete(base_url + "/users/inet/user1") + .should.be.fulfilled + }) + + it("delete created user again", function() { + return request.delete(base_url + "/users/inet/user1") + .should.be.rejected + }) + + after(stopMaxScale) +});