WIP vitastor-cli create-user

This commit is contained in:
Vitaliy Filippov
2026-03-20 21:00:32 +03:00
parent ccbde3ff91
commit 8124ee9eec
5 changed files with 411 additions and 2 deletions
+1
View File
@@ -20,6 +20,7 @@ add_library(vitastor_cli STATIC
cli_fix.cpp
cli_ls.cpp
cli_create.cpp
cli_create_user.cpp
cli_dd.cpp
cli_modify.cpp
cli_modify_osd.cpp
+8
View File
@@ -237,6 +237,9 @@ static const char* help_text =
" -r|--reverse Sort in descending order\n"
" -n|--count N Only list first N items\n"
"\n"
"vitastor-cli create-user --type <type> --name <name> --grants <grants>\n"
" FIXME\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"
@@ -567,6 +570,11 @@ std::function<bool(cli_result_t &)> cli_tool_t::start(json11::Json::object cfg,
}
action_cb = start_pool_ls(cfg);
}
else if (cmd[0] == "create-user")
{
// Create/update user
action_cb = start_create_user(cfg);
}
else if (cmd[0] == "serve")
{
// Start HTTP server
+2
View File
@@ -61,6 +61,7 @@ public:
std::function<bool(cli_result_t &)> start(json11::Json::object cfg, cli_result_t & result);
std::function<bool(cli_result_t &)> start_alloc_osd(json11::Json);
std::function<bool(cli_result_t &)> start_create(json11::Json);
std::function<bool(cli_result_t &)> start_create_user(json11::Json);
std::function<bool(cli_result_t &)> start_dd(json11::Json);
std::function<bool(cli_result_t &)> start_describe(json11::Json);
std::function<bool(cli_result_t &)> start_fix(json11::Json);
@@ -85,6 +86,7 @@ public:
// 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);
void etcd_call(const std::string & api, json11::Json body);
void etcd_txn(json11::Json txn);
void iterate_kvs_1(json11::Json kvs, const std::string & prefix, std::function<void(uint64_t num, json11::Json)> cb);
+7 -2
View File
@@ -109,10 +109,10 @@ void cli_tool_t::change_parent(inode_t cur, inode_t new_parent, cli_result_t *re
});
}
void cli_tool_t::etcd_txn(json11::Json txn)
void cli_tool_t::etcd_call(const std::string & api, json11::Json body)
{
waiting++;
cli->st_cli.etcd_txn_slow(txn, [this](std::string err, json11::Json res)
cli->st_cli.etcd_call(api, body, cli->st_cli.etcd_slow_timeout, cli->st_cli.max_etcd_attempts, 0, [this](std::string err, json11::Json res)
{
waiting--;
if (err != "")
@@ -124,6 +124,11 @@ void cli_tool_t::etcd_txn(json11::Json txn)
});
}
void cli_tool_t::etcd_txn(json11::Json txn)
{
etcd_call("/kv/txn", txn);
}
inode_config_t* cli_tool_t::get_inode_cfg(const std::string & name)
{
for (auto & ic: cli->st_cli.inode_config)
+393
View File
@@ -0,0 +1,393 @@
// 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"
#include "json_util.h"
struct etcd_perm_t
{
std::string key;
std::string range_end;
std::string perm_type;
};
class etcd_user_sync_t
{
protected:
int state = 0;
std::vector<etcd_perm_t> new_perms;
int i = 0;
std::map<std::pair<std::string, std::string>, std::string> old_perms;
std::map<std::pair<std::string, std::string>, std::string>::iterator old_perm_it;
public:
cli_tool_t *parent = NULL;
std::string role_name;
std::vector<etcd_perm_t> perms;
cli_result_t result;
// returns boolean true when done
bool run()
{
if (state == 1)
goto resume_1;
else if (state == 2)
goto resume_2;
else if (state == 3)
goto resume_3;
else if (state == 4)
goto resume_4;
else if (state == 5)
goto resume_5;
else if (state == 6)
goto resume_6;
else if (state == 100)
return true;
parent->etcd_call("/auth/user/get", json11::Json::object{
{ "name", role_name },
});
resume_1:
state = 1;
if (parent->waiting > 0)
return false;
// etcd returns HTTP/1.1 400 Bad Request + {"error":"etcdserver: user name not found","code":9}
// FIXME Check if it works
if (parent->etcd_err.text.find("user name not found") != std::string::npos)
{
parent->etcd_call("/auth/user/add", json11::Json::object{
{ "name", role_name },
{ "options", json11::Json::object{
{ "no_password", true }
} },
});
resume_2:
state = 2;
if (parent->waiting > 0)
return false;
// etcd returns HTTP/1.1 400 Bad Request + {"error":"etcdserver: role name already exists","code":9}
if (parent->etcd_err.err && parent->etcd_err.text.find("role name already exists") == std::string::npos)
{
result = parent->etcd_err;
return true;
}
}
parent->etcd_call("/auth/role/get", json11::Json::object{
{ "role", role_name },
});
resume_3:
state = 3;
if (parent->waiting > 0)
return false;
// etcd returns HTTP/1.1 400 Bad Request + {"error":"etcdserver: role name not found","code":9}
if (parent->etcd_err.text.find("role name not found") != std::string::npos)
{
parent->etcd_call("/auth/role/add", json11::Json::object{
{ "name", role_name },
});
resume_4:
state = 4;
if (parent->waiting > 0)
return false;
// etcd returns HTTP/1.1 400 Bad Request + {"error":"etcdserver: role name already exists","code":9}
if (parent->etcd_err.err && parent->etcd_err.text.find("role name already exists") == std::string::npos)
{
state = 100;
result = parent->etcd_err;
return true;
}
}
else if (parent->etcd_err.err)
{
state = 100;
result = parent->etcd_err;
return true;
}
for (auto & old_perm: parent->etcd_result["perm"].array_items())
{
old_perms.emplace(std::make_pair(old_perm["key"].string_value(), old_perm["range_end"].string_value()), old_perm["perm_type"].string_value());
}
for (auto & perm: perms)
{
auto perm_key = std::make_pair(
base64_encode(parent->cli->st_cli.etcd_prefix+perm.key),
perm.range_end.empty() ? "" : base64_encode(parent->cli->st_cli.etcd_prefix+perm.range_end)
);
auto perm_it = old_perms.find(perm_key);
if (perm_it == old_perms.end() || perm_it->second != perm.perm_type)
new_perms.push_back(perm);
else
old_perms.erase(perm_it);
}
for (old_perm_it = old_perms.begin(); old_perm_it != old_perms.end(); old_perm_it++)
{
parent->etcd_call("/auth/role/revoke", json11::Json::object{
{ "role", role_name },
{ "key", old_perm_it->first.first },
{ "range_end", old_perm_it->first.second },
});
resume_5:
state = 5;
if (parent->waiting > 0)
return false;
if (parent->etcd_err.err)
{
state = 100;
result = parent->etcd_err;
return true;
}
}
for (i = 0; i < new_perms.size(); i++)
{
{
auto & perm = new_perms[i];
json11::Json::object obj;
obj["key"] = base64_encode(parent->cli->st_cli.etcd_prefix+perm.key);
if (!perm.range_end.empty())
obj["range_end"] = base64_encode(parent->cli->st_cli.etcd_prefix+perm.range_end);
obj["perm_type"] = perm.perm_type;
parent->etcd_call("/auth/role/grant", json11::Json::object{
{ "name", role_name },
{ "perm", obj },
});
}
resume_6:
state = 6;
if (parent->waiting > 0)
return false;
if (parent->etcd_err.err)
{
state = 100;
result = parent->etcd_err;
return true;
}
}
state = 100;
result = {};
return true;
}
};
// Create/update a user
struct cli_create_user_t
{
cli_tool_t *parent;
std::string user_name;
std::string user_type;
json11::Json grants;
etcd_user_sync_t *user_sync = NULL;
int state = 0;
cli_result_t result;
bool is_done()
{
return state == 100;
}
void loop()
{
if (state == 1)
goto resume_1;
else if (state == 2)
goto resume_2;
user_sync = new etcd_user_sync_t();
user_sync->parent = parent;
user_sync->role_name = user_name;
if (user_type == "osd")
{
user_sync->perms = {
{ "/config/", "/config0", "READ" },
{ "/osd/", "/osd0", "READWRITE" },
{ "/pg/config", "", "READ" },
{ "/pg/state/", "/pg/state0", "READWRITE" },
{ "/pg/history/", "/pg/history0", "READWRITE" },
{ "/pgstats/", "/pgstats0", "READWRITE" },
};
}
else if (user_type == "mon")
{
user_sync->perms = {
{ "/config/", "/config0", "READ" },
{ "/osd/", "/osd0", "READ" },
{ "/mon/", "/mon0", "READWRITE" },
{ "/pg/config", "", "READWRITE" },
{ "/pg/history/", "/pg/history0", "READWRITE" },
{ "/pgstats/", "/pgstats0", "READ" },
{ "/inode/stats/", "/inode/stats0", "READWRITE" },
{ "/pool/stats/", "/pool/stats0", "READWRITE" },
{ "/stats/", "/stats0", "READWRITE" },
{ "/history/last_clean_pgs", "", "READWRITE" },
{ "/index/", "/index0", "READ" },
};
}
else if (user_type == "admin")
{
// CLI admin + global I/O client
user_sync->perms = {
{ "/config/", "/config0", "READWRITE" },
{ "/osd", "/osd0", "READWRITE" },
{ "/mon/", "/mon0", "READ" },
{ "/pg/", "/pg0", "READ" },
{ "/pg/history/", "/pg/history0", "READWRITE" },
{ "/pgstats/", "/pgstats0", "READ" },
{ "/inode/stats/", "/inode/stats0", "READ" },
{ "/pool/stats/", "/pool/stats0", "READ" },
{ "/stats/", "/stats0", "READ" },
{ "/index/", "/index0", "READWRITE" },
};
}
else if (user_type == "client")
{
// I/O client without etcd write permissions
user_sync->perms = {
{ "/config/global", "", "READ" },
{ "/config/node_placement", "", "READ" },
{ "/config/pools", "", "READ" },
{ "/osd/state", "/osd/state0", "READ" },
{ "/pg/config", "", "READ" },
{ "/pg/state/", "/pg/state0", "READ" },
//{ "/index/image/<name>", "", "READ" },
};
{
std::string error;
grants = parse_grants(grants, error);
if (error != "")
{
result = (cli_result_t){ .err = EINVAL, .text = error };
state = 100;
return;
}
}
for (auto & grant: grants.array_items())
{
user_sync->perms.push_back({
"/index/image/"+grant["name"].string_value()+"/",
grant["prefix"].bool_value() ? "/index/image/"+grant["name"].string_value()+"0" : "",
"READ"
});
}
// Also write client's image permissions to etcd, for future vitastor-cli API support
parent->etcd_txn(json11::Json::object {
{ "success", json11::Json::array { 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(json11::Json::object{
{ "type", user_type },
{ "grants", grants },
}).dump()
) },
} },
} } }
});
state = 2;
resume_2:
if (parent->waiting > 0)
return;
if (parent->etcd_err.err)
{
result = parent->etcd_err;
state = 100;
return;
}
}
else
{
result = (cli_result_t){ .err = EINVAL, .text = "Invalid user type: "+user_type };
state = 100;
return;
}
resume_1:
state = 1;
while (!user_sync->run())
{
return;
}
if (user_sync->result.err)
{
result = user_sync->result;
state = 100;
return;
}
delete user_sync;
user_sync = NULL;
state = 100;
}
json11::Json::array parse_grants(json11::Json grants, std::string & error)
{
json11::Json::array parsed;
if (grants.is_string())
{
// [rwx]:[^ ]+*( +[rwx]:name*)
grants = explode(" ", grants.string_value(), true);
}
for (auto & grant: grants.array_items())
{
if (grant.is_string())
{
const auto & str = grant.string_value();
if (str.size() < 3 || str[0] != 'r' && str[0] != 'w' && str[0] != 'x' || str[1] != ':')
{
error = "Invalid image permission: "+str;
return json11::Json::array();
}
else
{
parsed.push_back(json11::Json::object{
{ "type", str[0] == 'r' ? "read" : (str[0] == 'w' ? "write" : "admin") },
{ "name", str.substr(2, str.size() - (str[str.size()-1] == '*' ? 3 : 2)) },
{ "prefix", str[str.size()-1] == '*' },
});
}
}
else if (!grant.is_object())
{
error = "Image permissions should be strings or objects";
return json11::Json::array();
}
else
{
const auto & obj = grant.object_items();
for (auto & kv: obj)
{
if (!(kv.first == "type" && (kv.second == "read" || kv.second == "write" || kv.second == "admin") ||
kv.first == "name" && kv.second.is_string() ||
kv.first == "prefix" && kv.second.is_bool()))
{
error = "Invalid image permission: "+grant.dump();
return json11::Json::array();
}
}
parsed.push_back(grant);
}
}
return parsed;
}
};
std::function<bool(cli_result_t &)> cli_tool_t::start_create_user(json11::Json cfg)
{
auto creator = new cli_create_user_t();
creator->parent = this;
creator->user_type = cfg["type"].string_value();
creator->user_name = cfg["name"].string_value();
creator->grants = cfg["grants"];
return [creator](cli_result_t & result)
{
creator->loop();
if (creator->is_done())
{
result = creator->result;
delete creator;
return true;
}
return false;
};
}