Support TLS CN authentication and per-image permissions in vitastor-cli serve

This commit is contained in:
Vitaliy Filippov
2026-04-03 21:22:42 +03:00
parent 3fa75dfa6a
commit 3d6e804437
12 changed files with 214 additions and 36 deletions
+12
View File
@@ -9,6 +9,7 @@
#include "object_id.h"
#include "ringloop.h"
#include <functional>
#include <set>
struct rm_inode_t;
struct snap_merger_t;
@@ -26,6 +27,13 @@ struct cli_result_t
json11::Json data;
};
struct cli_user_t
{
std::string name;
std::string type;
std::set<std::string> groups;
};
class cli_tool_t
{
public:
@@ -37,6 +45,8 @@ public:
bool is_command_line = false;
bool color = false;
std::unique_ptr<cli_user_t> user; // for http mode
ring_loop_t *ringloop = NULL;
epoll_manager_t *epmgr = NULL;
cluster_client_t *cli = NULL;
@@ -53,6 +63,8 @@ public:
void change_parent(inode_t cur, inode_t new_parent, cli_result_t *result);
inode_config_t* get_inode_cfg(const std::string & name);
bool check_image_perm(const inode_config_t & cfg, bool write);
friend struct rm_inode_t;
friend struct snap_merger_t;
friend struct snap_flattener_t;
+21
View File
@@ -6,6 +6,15 @@
#include "cluster_client.h"
#include "cli.h"
bool cli_tool_t::check_image_perm(const inode_config_t & cfg, bool write)
{
return !user ||
user->type == "admin" ||
user->name == cfg.owner ||
cfg.owner_group != "" && user->groups.find(cfg.owner_group) != user->groups.end() ||
!write && cfg.reader_group != "" && user->groups.find(cfg.reader_group) != user->groups.end();
}
json11::Json::object cli_tool_t::format_image(const inode_config_t & cfg)
{
auto pool_it = cli->st_cli.pool_config.find(INODE_POOL(cfg.num));
@@ -20,6 +29,18 @@ json11::Json::object cli_tool_t::format_image(const inode_config_t & cfg)
{ "readonly", cfg.readonly },
{ "deleted", cfg.deleted },
};
if (cfg.owner != "")
{
img["owner"] = cfg.owner;
}
if (cfg.owner_group != "")
{
img["owner_group"] = cfg.owner_group;
}
if (cfg.reader_group != "")
{
img["reader_group"] = cfg.reader_group;
}
if (!cfg.enc_key.empty())
{
img["encrypted"] = true;
+32
View File
@@ -121,6 +121,23 @@ struct image_creator_t
create_snapshot();
}
bool check_pool_permission()
{
if (!parent->user || parent->user->type == "admin")
{
return true;
}
auto pool_it = parent->cli->st_cli.pool_config.find(new_pool_id);
if (pool_it == parent->cli->st_cli.pool_config.end() ||
(pool_it->second.creator_group == "" || parent->user->groups.find(pool_it->second.creator_group) == parent->user->groups.end()))
{
result = (cli_result_t){ .err = EACCES, .text = "Pool image create permission denied" };
state = 100;
return false;
}
return true;
}
void create_image()
{
if (state == 2)
@@ -160,6 +177,10 @@ struct image_creator_t
state = 100;
return;
}
if (!check_pool_permission())
{
return;
}
if (!size && !force_size)
{
result = (cli_result_t){ .err = EINVAL, .text = "Image size is missing" };
@@ -251,11 +272,22 @@ resume_3:
state = 100;
return;
}
if (!parent->check_image_perm(cur_cfg, true))
{
result = (cli_result_t){ .err = EACCES, .text = "Image permission denied" };
state = 100;
return;
}
if (!new_pool_id)
{
// Create snapshot in the same pool by default
new_pool_id = old_pool_id;
}
// Verify pool permissions if the pool is different from the original
if (new_pool_id != old_pool_id && !check_pool_permission())
{
return;
}
attempt_create();
state = 4;
resume_4:
+6
View File
@@ -35,6 +35,12 @@ struct snap_flattener_t
state = 100;
return;
}
if (!parent->check_image_perm(*target_cfg, true))
{
result = (cli_result_t){ .err = EACCES, .text = "Image permission denied" };
state = 100;
return;
}
target_id = target_cfg->num;
std::vector<inode_t> chain_list;
inode_config_t *cur = target_cfg;
+15 -4
View File
@@ -53,13 +53,19 @@ struct image_lister_t
return;
}
}
for (auto & ic: parent->cli->st_cli.inode_config)
auto begin_it = list_pool_id
? parent->cli->st_cli.inode_config.lower_bound(INODE_WITH_POOL(list_pool_id, 0))
: parent->cli->st_cli.inode_config.begin();
auto end_it = list_pool_id
? parent->cli->st_cli.inode_config.lower_bound(INODE_WITH_POOL(list_pool_id+1, 0))
: parent->cli->st_cli.inode_config.end();
for (auto it = begin_it; it != end_it; it++)
{
if (list_pool_id && INODE_POOL(ic.second.num) != list_pool_id)
if (!parent->check_image_perm(it->second, false))
{
continue;
}
stats[ic.second.num] = parent->format_image(ic.second);
stats[it->second.num] = parent->format_image(it->second);
}
}
@@ -106,6 +112,7 @@ resume_1:
state = 100;
return;
}
// FIXME: Do not always read everything
space_info = parent->etcd_result;
std::map<pool_id_t, uint64_t> pool_pg_real_size;
for (auto & kv_item: space_info["responses"][0]["response_range"]["kvs"].array_items())
@@ -139,6 +146,11 @@ resume_1:
}
inode_t inode_num = INODE_WITH_POOL(pool_id, only_inode_num);
uint64_t used_size = kv.value["raw_used"].uint64_value();
auto stat_it = stats.find(inode_num);
if (parent->user && parent->user->type != "admin" && stat_it == stats.end())
{
continue;
}
// save stats
auto pool_it = parent->cli->st_cli.pool_config.find(pool_id);
if (pool_it != parent->cli->st_cli.pool_config.end())
@@ -147,7 +159,6 @@ resume_1:
used_size = used_size / (pool_pg_real_size[pool_id] ? pool_pg_real_size[pool_id] : 1)
* (pool_cfg.scheme == POOL_SCHEME_REPLICATED ? 1 : pool_cfg.pg_size-pool_cfg.parity_chunks);
}
auto stat_it = stats.find(inode_num);
if (stat_it == stats.end())
{
stats[inode_num] = json11::Json::object {
+6
View File
@@ -77,6 +77,12 @@ struct image_changer_t
state = 100;
return;
}
if (!parent->check_image_perm(cfg, true))
{
result = (cli_result_t){ .err = EACCES, .text = "Image permission denied" };
state = 100;
return;
}
for (auto & ic: parent->cli->st_cli.inode_config)
{
if (ic.second.parent_id == inode_num)
+13
View File
@@ -269,10 +269,17 @@ resume_100:
{
char buf[1024];
snprintf(buf, 1024, "Parent inode of layer %s (id 0x%jx) not found", cur->name.c_str(), cur->parent_id);
result = (cli_result_t){ .err = ENOENT, .text = buf };
state = 100;
return;
}
cur = &it->second;
if (!parent->check_image_perm(*cur, true))
{
result = (cli_result_t){ .err = EACCES, .text = "Image permission denied" };
state = 100;
return;
}
chain_list.push_back(cur->num);
}
if (cur->num != from_cfg->num)
@@ -297,6 +304,12 @@ resume_100:
auto it = sources.find(ic.second.parent_id);
if (it != sources.end() && sources.find(ic.second.num) == sources.end())
{
if (!parent->check_image_perm(ic.second, true))
{
result = (cli_result_t){ .err = EACCES, .text = "Image permission denied" };
state = 100;
return;
}
merge_children.push_back(ic.second.num);
if (ic.second.readonly || writers_stopped)
{
+71 -22
View File
@@ -35,34 +35,35 @@ struct cli_serve_path_t
{
std::string cmd;
bool allow_get;
bool allow_client;
};
// Serve vitastor-cli commands over HTTP in JSON format
struct cli_serve_t
{
std::map<std::string, cli_serve_path_t> cmd_paths = {
{"data/delete", {"rm-data", false}},
{"data/describe", {"describe", true}},
{"data/fix", {"fix", false}},
{"data/merge", {"merge-data", false}},
{"image/create", {"create", false}},
{"image/delete", {"rm", false}},
{"image/flatten", {"flatten", false}},
{"image/list", {"ls", true}},
{"image/modify", {"modify", false}},
{"osd/alloc", {"alloc-osd", false}},
{"osd/delete", {"rm-osd", false}},
{"osd/list", {"ls-osd", true}},
{"osd/modify", {"modify-osd", false}},
{"pg/list", {"ls-pgs", true}},
{"pool/create", {"create-pool", false}},
{"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}},
{"data/delete", {"rm-data", false, false}},
{"data/describe", {"describe", true, false}},
{"data/fix", {"fix", false, false}},
{"data/merge", {"merge-data", false, false}},
{"image/create", {"create", false, true}},
{"image/delete", {"rm", false, true}},
{"image/flatten", {"flatten", false, true}},
{"image/list", {"ls", true, true}},
{"image/modify", {"modify", false, true}},
{"osd/alloc", {"alloc-osd", false, false}},
{"osd/delete", {"rm-osd", false, false}},
{"osd/list", {"ls-osd", true, false}},
{"osd/modify", {"modify-osd", false, false}},
{"pg/list", {"ls-pgs", true, false}},
{"pool/create", {"create-pool", false, false}},
{"pool/delete", {"rm-pool", false, false}},
{"pool/list", {"pools", true, false}},
{"pool/modify", {"modify-pool", false, false}},
{"user/delete", {"remove-user", false, false}},
{"user/list", {"ls-user", false, false}},
{"user/modify", {"modify-user", false, false}},
{"status", {"status", true, false}},
};
cli_tool_t *parent = NULL;
@@ -203,6 +204,12 @@ struct cli_serve_t
if (text)
*text = "Bad Request";
}
else if (err == EACCES)
{
code = 403;
if (text)
*text = "Forbidden";
}
else if (err == EOPNOTSUPP)
{
code = 404;
@@ -337,6 +344,18 @@ struct cli_serve_t
conn->request_path = std::move(req_line[1]);
conn->request_body = std::move(msg->body);
conn->response_type = "";
if (parent->cli->st_cli.use_auth)
{
auto user = std::make_unique<cli_user_t>();
user->name = msg->headers["_tls_common_name"];
auto user_it = parent->cli->st_cli.user_info.find(user->name);
auto userinfo = user_it == parent->cli->st_cli.user_info.end() ? user_it->second : json11::Json();
user->type = user->name == "root" ? "admin" : userinfo["type"].string_value();
for (auto & gr: userinfo["groups"].array_items())
{
user->groups.insert(gr.string_value());
}
}
auto ctype = msg->headers["content-type"];
if (conn->request_method != "GET" && conn->request_method != "POST")
{
@@ -364,6 +383,32 @@ struct cli_serve_t
{
conn->response_type = "application/json";
conn->result = { .text = openapi_description };
if (parent->cli->st_cli.use_auth)
{
// Filter available paths by privileges
if (conn->p->user->type == "client")
{
std::string error;
auto openapi = json11::Json::parse(openapi_description, error).object_items();
json11::Json::object paths;
for (auto & kv: openapi["paths"].object_items())
{
auto cmd_it = cmd_paths.find(kv.first.substr(1));
if (cmd_it != cmd_paths.end() && cmd_it->second.allow_client)
{
paths[kv.first] = kv.second;
}
}
openapi["paths"] = paths;
conn->response_type = "application/json";
conn->result = { .text = json11::Json(openapi).dump() };
}
else if (conn->p->user->type != "admin")
{
conn->response_type = "";
conn->result = { .err = EACCES, .text = "Access denied" };
}
}
}
else if (cmd_it == cmd_paths.end())
{
@@ -373,6 +418,10 @@ struct cli_serve_t
{
conn->result = { .err = ENOSYS, .text = "method /"+uri[0]+" only allows POST requests" };
}
else if (parent->cli->st_cli.use_auth && conn->p->user->type == "client" && !cmd_it->second.allow_client)
{
conn->result = { .err = EACCES, .text = "Access denied" };
}
else
{
std::string error;