Implement vitastor-cli ls-user, modify-user, remove-user commands
This commit is contained in:
@@ -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
|
||||
@@ -38,6 +38,8 @@ add_library(vitastor_cli STATIC
|
||||
cli_pool_rm.cpp
|
||||
cli_raw_ls.cpp
|
||||
cli_serve.cpp
|
||||
cli_status.cpp
|
||||
cli_user_ls.cpp
|
||||
${OPENAPI_JSON_H}
|
||||
)
|
||||
target_compile_options(vitastor_cli PUBLIC -fPIC)
|
||||
|
||||
@@ -249,6 +249,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 [<name> ...]\n"
|
||||
" List users (only with specified names if passed).\n"
|
||||
"\n"
|
||||
"vitastor-cli modify-user --type <type> --groups group1,group2,... <username>\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 <username>\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"
|
||||
@@ -584,6 +595,35 @@ std::function<bool(cli_result_t &)> 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
|
||||
|
||||
@@ -70,6 +70,7 @@ public:
|
||||
std::function<bool(cli_result_t &)> start_merge(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_modify(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_modify_osd(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_modify_user(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_osd_tree(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_pg_list(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_pool_create(json11::Json);
|
||||
@@ -82,6 +83,7 @@ public:
|
||||
std::function<bool(cli_result_t &)> start_rm_wildcard(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_serve(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_status(json11::Json);
|
||||
std::function<bool(cli_result_t &)> start_user_ls(json11::Json);
|
||||
|
||||
// Should be called like loop_and_wait(start_status(), <completion callback>)
|
||||
void loop_and_wait(std::function<bool(cli_result_t &)> loop_cb, std::function<void(const cli_result_t &)> complete_cb);
|
||||
|
||||
@@ -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<std::string>() : 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<bool(cli_result_t &)> 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;
|
||||
};
|
||||
}
|
||||
@@ -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}},
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
// Copyright (c) Vitaliy Filippov, 2019+
|
||||
// License: VNPL-1.1 (see README.md for details)
|
||||
|
||||
#include <algorithm>
|
||||
#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<std::string> 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<bool(cli_result_t &)> 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;
|
||||
};
|
||||
}
|
||||
+91
-1
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user