Support TLS CN authentication and per-image permissions in vitastor-cli serve
This commit is contained in:
@@ -16,6 +16,7 @@ const etcd_allow = new RegExp('^'+[
|
||||
'config/pools',
|
||||
'config/osd/[1-9]\\d*',
|
||||
'config/pgs', // old name
|
||||
'config/user/.*',
|
||||
'pg/config',
|
||||
'config/inode/[1-9]\\d*/[1-9]\\d*',
|
||||
'osd/state/[1-9]\\d*',
|
||||
@@ -234,6 +235,13 @@ const etcd_tree = {
|
||||
}
|
||||
}, */
|
||||
inode: {},
|
||||
/* user: {
|
||||
<username>: {
|
||||
type: 'osd'|'mon'|'admin'|'client',
|
||||
groups: string[],
|
||||
},
|
||||
}, */
|
||||
user: {},
|
||||
},
|
||||
osd: {
|
||||
state: {
|
||||
|
||||
@@ -262,6 +262,7 @@ void etcd_state_client_t::parse_config(const json11::Json & config)
|
||||
}
|
||||
this->etcd_ca = config["etcd_ca"].string_value();
|
||||
this->etcd_prefix = config["etcd_prefix"].string_value();
|
||||
this->use_auth = config["use_auth"].bool_value();
|
||||
if (this->etcd_prefix == "")
|
||||
{
|
||||
this->etcd_prefix = "/vitastor";
|
||||
@@ -1353,6 +1354,14 @@ void etcd_state_client_t::parse_state(const etcd_kv_t & kv)
|
||||
if (on_change_node_placement_hook)
|
||||
on_change_node_placement_hook();
|
||||
}
|
||||
else if (use_auth && key.substr(0, etcd_prefix.length()+13) == etcd_prefix+"/config/user/")
|
||||
{
|
||||
// <etcd_prefix>/config/user/<username>
|
||||
if (!value.is_object())
|
||||
user_info.erase(key.substr(etcd_prefix.length()+13));
|
||||
else
|
||||
user_info[key.substr(etcd_prefix.length()+13)] = value;
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t etcd_state_client_t::parse_immediate_commit(const std::string & immediate_commit_str, uint32_t default_value)
|
||||
|
||||
@@ -145,6 +145,7 @@ public:
|
||||
int etcd_slow_timeout = 5000;
|
||||
int etcd_min_reload_interval = 1000;
|
||||
bool infinite_start = true;
|
||||
bool use_auth = false;
|
||||
uint64_t global_block_size = DEFAULT_BLOCK_SIZE;
|
||||
uint32_t global_bitmap_granularity = DEFAULT_BITMAP_GRANULARITY;
|
||||
uint32_t global_immediate_commit = IMMEDIATE_NONE;
|
||||
@@ -170,6 +171,7 @@ public:
|
||||
std::set<osd_num_t> seen_peers;
|
||||
std::map<inode_t, inode_config_t> inode_config;
|
||||
std::map<std::string, inode_t> inode_by_name;
|
||||
std::map<std::string, json11::Json> user_info;
|
||||
json11::Json node_placement;
|
||||
|
||||
std::function<void(std::map<std::string, etcd_kv_t> &)> on_change_hook;
|
||||
|
||||
+19
-10
@@ -187,6 +187,19 @@ bool openssl_ctx_use_ca(SSL_CTX *ssl_ctx, const std::string & file_or_pem)
|
||||
: !!SSL_CTX_load_verify_locations(ssl_ctx, file_or_pem.c_str(), NULL);
|
||||
}
|
||||
|
||||
static std::string openssl_get_cn(X509 *x509)
|
||||
{
|
||||
X509_NAME* subj = X509_get_subject_name(x509);
|
||||
int pos = X509_NAME_get_index_by_NID(subj, NID_commonName, -1);
|
||||
if (pos != -1)
|
||||
{
|
||||
X509_NAME_ENTRY* cn = X509_NAME_get_entry(subj, pos);
|
||||
ASN1_STRING* str = X509_NAME_ENTRY_get_data(cn);
|
||||
return std::string((const char*)ASN1_STRING_get0_data(str), ASN1_STRING_length(str));
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
bool openssl_ctx_use_cert(SSL_CTX *ssl_ctx, const std::string & file_or_pem, std::string & common_name)
|
||||
{
|
||||
BIO *bio = NULL;
|
||||
@@ -208,16 +221,7 @@ bool openssl_ctx_use_cert(SSL_CTX *ssl_ctx, const std::string & file_or_pem, std
|
||||
{
|
||||
ok = SSL_CTX_use_certificate(ssl_ctx, x509);
|
||||
if (ok)
|
||||
{
|
||||
X509_NAME* subj = X509_get_subject_name(x509);
|
||||
int pos = X509_NAME_get_index_by_NID(subj, NID_commonName, -1);
|
||||
if (pos != -1)
|
||||
{
|
||||
X509_NAME_ENTRY* cn = X509_NAME_get_entry(subj, pos);
|
||||
ASN1_STRING* str = X509_NAME_ENTRY_get_data(cn);
|
||||
common_name = std::string((const char*)ASN1_STRING_get0_data(str), ASN1_STRING_length(str));
|
||||
}
|
||||
}
|
||||
common_name = openssl_get_cn(x509);
|
||||
X509_free(x509);
|
||||
}
|
||||
BIO_free(bio);
|
||||
@@ -1136,6 +1140,11 @@ bool http_co_t::handle_read()
|
||||
}
|
||||
state = HTTP_CO_REQ_HDR_RECEIVED;
|
||||
parse_http_headers(response, &parsed, true);
|
||||
if (ssl)
|
||||
{
|
||||
auto x509 = SSL_get0_peer_certificate(ssl_cli);
|
||||
parsed.headers["_tls_common_name"] = openssl_get_cn(x509);
|
||||
}
|
||||
auto conn_it = parsed.headers.find("connection");
|
||||
keepalive = (conn_it != parsed.headers.end() && conn_it->second == "keep-alive");
|
||||
auto enc_it = parsed.headers.find("transfer-encoding");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user