From 8124ee9eec56b7d42333ff5fd1a3dd7cf059d732 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sun, 15 Mar 2026 12:24:24 +0300 Subject: [PATCH] WIP vitastor-cli create-user --- src/cmd/CMakeLists.txt | 1 + src/cmd/cli.cpp | 8 + src/cmd/cli.h | 2 + src/cmd/cli_common.cpp | 9 +- src/cmd/cli_create_user.cpp | 393 ++++++++++++++++++++++++++++++++++++ 5 files changed, 411 insertions(+), 2 deletions(-) create mode 100644 src/cmd/cli_create_user.cpp diff --git a/src/cmd/CMakeLists.txt b/src/cmd/CMakeLists.txt index 56bcaa61..f70e7870 100644 --- a/src/cmd/CMakeLists.txt +++ b/src/cmd/CMakeLists.txt @@ -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 diff --git a/src/cmd/cli.cpp b/src/cmd/cli.cpp index 9f578d21..d3a76cdd 100644 --- a/src/cmd/cli.cpp +++ b/src/cmd/cli.cpp @@ -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 --name --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 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 diff --git a/src/cmd/cli.h b/src/cmd/cli.h index ec3bd9fa..dc30c454 100644 --- a/src/cmd/cli.h +++ b/src/cmd/cli.h @@ -61,6 +61,7 @@ public: std::function start(json11::Json::object cfg, cli_result_t & result); std::function start_alloc_osd(json11::Json); std::function start_create(json11::Json); + std::function start_create_user(json11::Json); std::function start_dd(json11::Json); std::function start_describe(json11::Json); std::function start_fix(json11::Json); @@ -85,6 +86,7 @@ public: // Should be called like loop_and_wait(start_status(), ) void loop_and_wait(std::function loop_cb, std::function 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 cb); diff --git a/src/cmd/cli_common.cpp b/src/cmd/cli_common.cpp index 8a1f6067..775d622e 100644 --- a/src/cmd/cli_common.cpp +++ b/src/cmd/cli_common.cpp @@ -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) diff --git a/src/cmd/cli_create_user.cpp b/src/cmd/cli_create_user.cpp new file mode 100644 index 00000000..1aee6901 --- /dev/null +++ b/src/cmd/cli_create_user.cpp @@ -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 new_perms; + int i = 0; + std::map, std::string> old_perms; + std::map, std::string>::iterator old_perm_it; + +public: + cli_tool_t *parent = NULL; + std::string role_name; + std::vector 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/", "", "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 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; + }; +}