diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index d3a3ea2b..f69d1df0 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -15,7 +15,6 @@ add_custom_command( add_library(vitastor_cli STATIC cli_common.cpp cli_alloc_osd.cpp - cli_status.cpp cli_describe.cpp cli_fix.cpp cli_ls.cpp @@ -23,6 +22,7 @@ add_library(vitastor_cli STATIC cli_dd.cpp cli_modify.cpp cli_modify_osd.cpp + cli_modify_user.cpp cli_osd_tree.cpp cli_pg_ls.cpp cli_flatten.cpp @@ -37,6 +37,8 @@ add_library(vitastor_cli STATIC cli_pool_modify.cpp cli_pool_rm.cpp cli_serve.cpp + cli_status.cpp + cli_user_ls.cpp ${OPENAPI_JSON_H} ) target_compile_options(vitastor_cli PUBLIC -fPIC) diff --git a/src/cmd/cli.cpp b/src/cmd/cli.cpp index 48b855b0..3d94e40d 100644 --- a/src/cmd/cli.cpp +++ b/src/cmd/cli.cpp @@ -244,6 +244,17 @@ static const char* help_text = " -r|--reverse Sort in descending order\n" " -n|--count N Only list first N items\n" "\n" + "vitastor-cli ls-users|user-ls|ls-user|list-users [ ...]\n" + " List users (only with specified names if passed).\n" + "\n" + "vitastor-cli modify-user --type --groups group1,group2,... \n" + " Create or update user permissions. User names match CN of their certificates.\n" + " --type TYPE Set user type: client, admin, mon or osd. Default is client.\n" + " --groups GROUPS Set user's groups.\n" + "\n" + "vitastor-cli rm-user|remove-user|delete-user \n" + " Remove a user.\n" + "\n" "vitastor-cli serve\n" " Start HTTP server able to handle CLI commands over a REST API. Options:\n" " --bind_address ADDR Specify server IP address or addresses, separated by space. Default is 127.0.0.1.\n" @@ -574,6 +585,35 @@ std::function cli_tool_t::start(json11::Json::object cfg, } action_cb = start_pool_ls(cfg); } + else if (cmd[0] == "user-ls" || cmd[0] == "ls-user" || cmd[0] == "ls-users" || cmd[0] == "list-users") + { + // List users + if (cmd.size() > 1) + { + cmd.erase(cmd.begin(), cmd.begin()+1); + cfg["names"] = cmd; + } + action_cb = start_user_ls(cfg); + } + else if (cmd[0] == "modify-user" || cmd[0] == "user-modify") + { + // Create/update user + if (cmd.size() > 1) + { + cfg["name"] = cmd[1]; + } + action_cb = start_modify_user(cfg); + } + else if (cmd[0] == "rm-user" || cmd[0] == "remove-user" || cmd[0] == "delete-user") + { + // Remove user + if (cmd.size() > 1) + { + cfg["name"] = cmd[1]; + } + cfg["remove"] = true; + action_cb = start_modify_user(cfg); + } else if (cmd[0] == "serve") { // Start HTTP server diff --git a/src/cmd/cli.h b/src/cmd/cli.h index e94e3f75..bc78e930 100644 --- a/src/cmd/cli.h +++ b/src/cmd/cli.h @@ -69,6 +69,7 @@ public: std::function start_merge(json11::Json); std::function start_modify(json11::Json); std::function start_modify_osd(json11::Json); + std::function start_modify_user(json11::Json); std::function start_osd_tree(json11::Json); std::function start_pg_list(json11::Json); std::function start_pool_create(json11::Json); @@ -81,6 +82,7 @@ public: std::function start_rm_wildcard(json11::Json); std::function start_serve(json11::Json); std::function start_status(json11::Json); + std::function start_user_ls(json11::Json); // Should be called like loop_and_wait(start_status(), ) void loop_and_wait(std::function loop_cb, std::function complete_cb); diff --git a/src/cmd/cli_modify_user.cpp b/src/cmd/cli_modify_user.cpp new file mode 100644 index 00000000..64e8e7a7 --- /dev/null +++ b/src/cmd/cli_modify_user.cpp @@ -0,0 +1,161 @@ +// Copyright (c) Vitaliy Filippov, 2019+ +// License: VNPL-1.1 (see README.md for details) + +#include "cli.h" +#include "cluster_client.h" +#include "str_util.h" + +// Create/update/delete a user +struct cli_modify_user_t +{ + cli_tool_t *parent; + + std::string user_name; + std::string user_type; + json11::Json groups; + bool del = false; + + int state = 0; + cli_result_t result; + etcd_kv_t kv; + json11::Json::object new_cfg; + + bool is_done() + { + return state == 100; + } + + void loop() + { + if (state == 1) + goto resume_1; + else if (state == 2) + goto resume_2; + if (user_type != "client" && user_type != "admin" && user_type != "mon" && user_type != "osd") + { + result = (cli_result_t){ .err = EINVAL, .text = "Unknown user type: "+user_type }; + state = 100; + return; + } + if (groups.is_string()) + { + groups = groups == "" ? std::vector() : explode(",", groups.string_value(), true); + } + else if (groups.is_array()) + { + for (auto & gr: groups.array_items()) + { + if (!gr.is_string()) + { + result = (cli_result_t){ .err = EINVAL, .text = "Group names must be strings" }; + state = 100; + return; + } + } + } + else if (!groups.is_null()) + { + result = (cli_result_t){ .err = EINVAL, .text = "Group names must be strings" }; + state = 100; + return; + } + if (user_name == "") + { + result = (cli_result_t){ .err = EINVAL, .text = "User name must not be empty" }; + state = 100; + return; + } + parent->etcd_txn(json11::Json::object { + { "success", json11::Json::array { json11::Json::object { + { "request_range", json11::Json::object { + { "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) }, + } }, + } } } + }); + state = 1; +resume_1: + if (parent->waiting > 0) + return; + if (parent->etcd_err.err) + { + result = parent->etcd_err; + state = 100; + return; + } + kv = parent->cli->st_cli.parse_etcd_kv(parent->etcd_result["responses"][0]["response_range"]["kvs"][0]); + while (true) + { + new_cfg = kv.value.object_items(); + if (!kv.mod_revision && del) + { + result = (cli_result_t){ .err = ENOENT, .text = "User "+user_name+" does not exist" }; + state = 100; + break; + } + if (!groups.is_null()) + new_cfg["groups"] = groups; + if (user_type != "") + new_cfg["type"] = user_type; + if (!new_cfg["type"].is_string()) + new_cfg["type"] = "client"; + parent->etcd_txn(json11::Json::object { + { "compare", json11::Json::array { json11::Json::object { + { "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) }, + { "target", kv.mod_revision ? "MOD" : "VERSION" }, + { kv.mod_revision ? "mod_revision" : "version", kv.mod_revision }, + } } }, + { "success", json11::Json::array { + del ? json11::Json::object { { "request_delete_range", json11::Json::object { + { "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) }, + } } } : json11::Json::object { { "request_put", json11::Json::object { + { "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) }, + { "value", base64_encode(json11::Json(new_cfg).dump()) } + } } }, + } }, + { "failure", json11::Json::array { json11::Json::object { + { "request_range", json11::Json::object { + { "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+user_name) }, + } }, + } } }, + }); + state = 2; +resume_2: + if (parent->waiting > 0) + return; + if (parent->etcd_err.err) + { + result = parent->etcd_err; + state = 100; + return; + } + if (parent->etcd_result["succeeded"].bool_value()) + break; + kv = parent->cli->st_cli.parse_etcd_kv(parent->etcd_result["responses"][0]["response_range"]["kvs"][0]); + } + state = 100; + result.text = del ? "User "+user_name+" removed" : "User "+user_name+" modified"; + new_cfg["name"] = user_name; + result.data = del ? json11::Json::object{ { "ok", true } } : new_cfg; + } +}; + +std::function cli_tool_t::start_modify_user(json11::Json cfg) +{ + auto creator = new cli_modify_user_t(); + creator->parent = this; + creator->user_name = cfg["name"].string_value(); + creator->user_type = cfg["type"].string_value(); + creator->groups = cfg["groups"]; + creator->del = cfg["remove"].bool_value(); + return [creator](cli_result_t & result) + { + creator->loop(); + if (creator->is_done()) + { + result = creator->result; + delete creator; + return true; + } + return false; + }; +} diff --git a/src/cmd/cli_serve.cpp b/src/cmd/cli_serve.cpp index 50a2aed3..419fd04a 100644 --- a/src/cmd/cli_serve.cpp +++ b/src/cmd/cli_serve.cpp @@ -59,6 +59,9 @@ struct cli_serve_t {"pool/delete", {"rm-pool", false}}, {"pool/list", {"pools", true}}, {"pool/modify", {"modify-pool", false}}, + {"user/delete", {"remove-user", false}}, + {"user/list", {"ls-user", false}}, + {"user/modify", {"modify-user", false}}, {"status", {"status", true}}, }; diff --git a/src/cmd/cli_user_ls.cpp b/src/cmd/cli_user_ls.cpp new file mode 100644 index 00000000..7c5a899c --- /dev/null +++ b/src/cmd/cli_user_ls.cpp @@ -0,0 +1,128 @@ +// Copyright (c) Vitaliy Filippov, 2019+ +// License: VNPL-1.1 (see README.md for details) + +#include +#include "cli.h" +#include "cluster_client.h" +#include "str_util.h" +#include "json_util.h" + +// List users +struct user_lister_t +{ + cli_tool_t *parent; + std::vector only_names; + + int state = 0; + cli_result_t result; + json11::Json::array users; + + bool is_done() + { + return state == 100; + } + + void loop() + { + if (state == 1) + goto resume_1; + if (state == 100) + return; + { + json11::Json::array select; + if (!only_names.size()) + { + select.push_back(json11::Json::object { + { "request_range", json11::Json::object { + { "key", base64_encode( + parent->cli->st_cli.etcd_prefix+"/config/user/" + ) }, + { "range_end", base64_encode( + parent->cli->st_cli.etcd_prefix+"/config/user0" + ) }, + } }, + }); + } + else + { + for (auto & name: only_names) + { + select.push_back(json11::Json::object { + { "request_range", json11::Json::object { + { "key", base64_encode(parent->cli->st_cli.etcd_prefix+"/config/user/"+name) }, + } } + }); + } + } + parent->etcd_txn(json11::Json::object { + { "success", select }, + }); + } + state = 1; +resume_1: + if (parent->waiting > 0) + return; + if (parent->etcd_err.err) + { + result = parent->etcd_err; + state = 100; + return; + } + for (auto & response: parent->etcd_result["responses"].array_items()) + { + for (auto & kv_item: response["response_range"]["kvs"].array_items()) + { + auto kv = parent->cli->st_cli.parse_etcd_kv(kv_item); + auto user = kv.value.object_items(); + user["name"] = kv.key.substr(parent->cli->st_cli.etcd_prefix.size()+13); + if (!parent->json_output) + user["groups_fmt"] = implode(",", user["groups"].array_items()); + users.push_back(std::move(user)); + } + } + if (parent->json_output) + { + // JSON output + result.data = users; + state = 100; + return; + } + // Table output: name, type, groups + json11::Json::array cols; + cols.push_back(json11::Json::object{ + { "key", "name" }, + { "title", "NAME" }, + }); + cols.push_back(json11::Json::object{ + { "key", "type" }, + { "title", "TYPE" }, + }); + cols.push_back(json11::Json::object{ + { "key", "groups_fmt" }, + { "title", "GROUPS" }, + }); + result.text = print_table(users, cols, parent->color); + state = 100; + } +}; + +std::function cli_tool_t::start_user_ls(json11::Json cfg) +{ + auto lister = new user_lister_t(); + lister->parent = this; + if (cfg["names"].is_string()) + lister->only_names.push_back(cfg["names"].string_value()); + for (auto & item: cfg["names"].array_items()) + lister->only_names.push_back(item.string_value()); + return [lister](cli_result_t & result) + { + lister->loop(); + if (lister->is_done()) + { + result = lister->result; + delete lister; + return true; + } + return false; + }; +} diff --git a/src/cmd/openapi.json b/src/cmd/openapi.json index a86de5c2..9bd1f40c 100644 --- a/src/cmd/openapi.json +++ b/src/cmd/openapi.json @@ -117,6 +117,7 @@ "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Image" } } } }, "400": { "$ref": "#/components/responses/Invalid" }, + "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/UpdateConflict" }, "412": { "$ref": "#/components/responses/PreconditionFailed" } } @@ -159,6 +160,7 @@ "responses": { "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/Image" } } } }, "400": { "$ref": "#/components/responses/Invalid" }, + "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/UpdateConflict" }, "412": { "$ref": "#/components/responses/PreconditionFailed" } } @@ -208,6 +210,7 @@ } } } } }, "400": { "$ref": "#/components/responses/Invalid" }, + "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/UpdateConflict" }, "412": { "$ref": "#/components/responses/PreconditionFailed" } } @@ -227,6 +230,7 @@ "responses": { "200": { "content": { "text/plain": { "schema": { "type": "string", "description": "Empty response" } } } }, "400": { "$ref": "#/components/responses/Invalid" }, + "403": { "$ref": "#/components/responses/Forbidden" }, "409": { "$ref": "#/components/responses/UpdateConflict" }, "412": { "$ref": "#/components/responses/PreconditionFailed" } } @@ -419,7 +423,7 @@ } } }, "/pool/delete": { "post": { - "summary": "Delete an pool", + "summary": "Delete a pool", "operationId": "poolDelete", "consumes": [ "application/json" ], "produces": [ "application/json" ], @@ -441,6 +445,66 @@ "412": { "$ref": "#/components/responses/PreconditionFailed" } } } }, + "/user/list": { "get": { + "summary": "List users", + "operationId": "userListGet", + "produces": [ "application/json" ], + "parameters": [ { + "name": "params", + "in": "query", + "schema": { "$ref": "#/components/schemas/UserListParams" }, + "style": "form", + "explode": true + } ], + "responses": { + "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserList" } } } } + } + }, "post": { + "summary": "List users", + "operationId": "userListPost", + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserListParams" } } } }, + "responses": { + "200": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/UserList" } } } } + } + } }, + "/user/modify": { "post": { + "summary": "Modify a user", + "operationId": "userModify", + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "requestBody": { "content": { "application/json": { "schema": { "$ref": "#/components/schemas/User" } } } }, + "responses": { + "200": { "content": { "application/json": { "schema": { + "$ref": "#/components/schemas/User" + } } } }, + "400": { "$ref": "#/components/responses/Invalid" }, + "409": { "$ref": "#/components/responses/UpdateConflict" }, + "412": { "$ref": "#/components/responses/PreconditionFailed" } + } + } }, + "/user/delete": { "post": { + "summary": "Delete a user", + "operationId": "userDelete", + "consumes": [ "application/json" ], + "produces": [ "application/json" ], + "requestBody": { "content": { "application/json": { "schema": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "User name to delete" } + } + } } } }, + "responses": { + "200": { "content": { "application/json": { "schema": { + "type": "object", + "properties": { "ok": { "type": "boolean" } } + } } } }, + "400": { "$ref": "#/components/responses/Invalid" }, + "409": { "$ref": "#/components/responses/UpdateConflict" }, + "412": { "$ref": "#/components/responses/PreconditionFailed" } + } + } }, "/pg/list": { "get": { "summary": "List PGs", "operationId": "pgListGet", @@ -611,6 +675,14 @@ } } }, + "Forbidden": { + "description": "Access Denied (EACCES)", + "content": { + "text/plain": { + "schema": { "type": "string", "description": "Error text" } + } + } + }, "PreconditionFailed": { "description": "Precondition Failed (ENOTEMPTY, EEXIST or ENOENT)", "content": { @@ -860,6 +932,24 @@ } } ] } }, + "UserListParams": { + "type": "object", + "properties": { + "names": { "type": "array", "items": { "type": "string" }, "description": "User names to list" } + } + }, + "UserList": { + "type": "array", + "items": { "$ref": "#/components/schemas/User" } + }, + "User": { + "type": "object", + "properties": { + "name": { "type": "string", "description": "User name" }, + "type": { "type": "string", "enum": [ "osd", "mon", "admin", "client" ], "description": "User type" }, + "groups": { "type": "array", "items": { "type": "string" }, "description": "User group names" } + } + }, "PgListParams": { "type": "object", "properties": {