From 1afda35bd3e1c0ddbe4511f4d18bdb3a2b898479 Mon Sep 17 00:00:00 2001 From: Vitaliy Filippov Date: Sun, 22 Mar 2026 19:35:16 +0300 Subject: [PATCH] Add image owner/owner_group/reader_group support (for antietcd VitastorAuthFilter) --- mon/etcd_schema.js | 6 ++++ src/client/etcd_state_client.cpp | 17 +++++++++ src/client/etcd_state_client.h | 4 +++ src/client/etcd_state_client_http.cpp | 5 +++ src/client/etcd_state_client_http.h | 1 + src/client/etcd_state_client_mock.cpp | 5 +++ src/client/etcd_state_client_mock.h | 2 ++ src/client/http_client.cpp | 52 +++++++++++++++++++-------- src/client/http_client.h | 1 + src/client/msgr_send.cpp | 2 +- src/cmd/cli.cpp | 17 ++++++--- src/cmd/cli_create.cpp | 18 ++++++++++ src/cmd/cli_modify.cpp | 16 +++++++++ src/cmd/cli_pool_cfg.cpp | 2 +- src/cmd/openapi.json | 11 ++++-- 15 files changed, 136 insertions(+), 23 deletions(-) diff --git a/mon/etcd_schema.js b/mon/etcd_schema.js index f3ed7caf..ee8b7516 100644 --- a/mon/etcd_schema.js +++ b/mon/etcd_schema.js @@ -208,6 +208,8 @@ const etcd_tree = { primary_affinity_tags?: 'nvme' | [ 'nvme', ... ], // scrub interval scrub_interval?: '30d', + // users allowed to create images in this pool + creator_group?: '', }, ... }, */ @@ -224,6 +226,10 @@ const etcd_tree = { parent_id?: , readonly?: boolean, deleted?: boolean, + enc_key?: string, + owner?: string, + owner_group?: string, + reader_group?: string, } } }, */ diff --git a/src/client/etcd_state_client.cpp b/src/client/etcd_state_client.cpp index 2fe840ac..c7b6b374 100644 --- a/src/client/etcd_state_client.cpp +++ b/src/client/etcd_state_client.cpp @@ -513,6 +513,8 @@ void etcd_state_client_t::parse_state(const etcd_kv_t & kv) pc.used_for_app = "fs:"+pc.used_for_app; else pc.used_for_app = pool_item.second["used_for_app"].as_string(); + // Create group permission + pc.creator_group = pool_item.second["creator_group"].string_value(); // Local Read Configuration std::string local_reads = pool_item.second["local_reads"].string_value(); if (local_reads == "nearest") @@ -941,6 +943,18 @@ json11::Json::object etcd_state_client_t::serialize_inode_cfg(inode_config_t *cf { new_cfg["deleted"] = true; } + if (!cfg->owner.empty()) + { + new_cfg["owner"] = cfg->owner; + } + if (!cfg->owner_group.empty()) + { + new_cfg["owner_group"] = cfg->owner_group; + } + if (!cfg->reader_group.empty()) + { + new_cfg["reader_group"] = cfg->reader_group; + } if (cfg->meta.is_object()) { new_cfg["meta"] = cfg->meta; @@ -987,6 +1001,9 @@ inode_config_t etcd_state_client_t::deserialize_inode_cfg(uint64_t inode_num, js .readonly = value["readonly"].bool_value(), .deleted = value["deleted"].bool_value(), .enc_key = std::move(enc_key), + .owner = value["owner"].string_value(), + .owner_group = value["owner_group"].string_value(), + .reader_group = value["reader_group"].string_value(), .meta = value["meta"], .mod_revision = mod_revision, }; diff --git a/src/client/etcd_state_client.h b/src/client/etcd_state_client.h index 8ff8abd5..7c3581bf 100644 --- a/src/client/etcd_state_client.h +++ b/src/client/etcd_state_client.h @@ -69,6 +69,7 @@ struct pool_config_t std::map pg_config; uint64_t scrub_interval = 0; std::string used_for_app; + std::string creator_group; int backfillfull = 0; int local_reads = 0; @@ -87,6 +88,8 @@ struct inode_config_t bool readonly = false; bool deleted = false; std::string enc_key; + // Permissions + std::string owner, owner_group, reader_group; // Arbitrary metadata json11::Json meta; // Change revision of the metadata in etcd @@ -178,6 +181,7 @@ public: void etcd_txn(json11::Json txn, int timeout, int retries, int interval, std::function callback); void etcd_txn_slow(json11::Json txn, std::function callback); virtual void etcd_add_watch(json11::Json watch) = 0; + virtual std::string get_username() = 0; void load_global_config(std::function cb); virtual void load_global_config() = 0; void load_pgs(std::function cb); diff --git a/src/client/etcd_state_client_http.cpp b/src/client/etcd_state_client_http.cpp index da86c49d..a264e459 100644 --- a/src/client/etcd_state_client_http.cpp +++ b/src/client/etcd_state_client_http.cpp @@ -46,6 +46,11 @@ void etcd_state_client_http_t::etcd_add_watch(json11::Json watch) } } +std::string etcd_state_client_http_t::get_username() +{ + return http_context_get_ssl_cn(get_http_ctx()); +} + http_context_t *etcd_state_client_http_t::get_http_ctx() { if (!http_ctx) diff --git a/src/client/etcd_state_client_http.h b/src/client/etcd_state_client_http.h index 06c74b52..56e89d42 100644 --- a/src/client/etcd_state_client_http.h +++ b/src/client/etcd_state_client_http.h @@ -42,6 +42,7 @@ public: void etcd_call_selected(const std::string & api, json11::Json payload, int timeout, int retries, int interval, std::function callback); void etcd_add_watch(json11::Json watch) override; + std::string get_username() override; void load_global_config() override; void load_pgs() override; void parse_config(const json11::Json & config) override; diff --git a/src/client/etcd_state_client_mock.cpp b/src/client/etcd_state_client_mock.cpp index 0a34566c..bad43b1f 100644 --- a/src/client/etcd_state_client_mock.cpp +++ b/src/client/etcd_state_client_mock.cpp @@ -16,6 +16,11 @@ void etcd_state_client_mock_t::etcd_add_watch(json11::Json watch) { } +std::string etcd_state_client_mock_t::get_username() +{ + return username; +} + void etcd_state_client_mock_t::etcd_call_oneshot(const std::string & etcd_address, const std::string & api, json11::Json payload, int timeout, std::function callback) { diff --git a/src/client/etcd_state_client_mock.h b/src/client/etcd_state_client_mock.h index ee743653..9f801c72 100644 --- a/src/client/etcd_state_client_mock.h +++ b/src/client/etcd_state_client_mock.h @@ -30,6 +30,7 @@ struct etcd_state_client_mock_t: public etcd_state_client_t public: std::map leases; std::map data; + std::string username; etcd_state_client_mock_t(); void set(const std::string& key, json11::Json data, uint64_t mod_revision = 0, uint64_t lease_id = 0); void pause(); @@ -37,6 +38,7 @@ public: void etcd_call_oneshot(const std::string & etcd_address, const std::string & api, json11::Json payload, int timeout, std::function callback) override; void etcd_call(const std::string & api, json11::Json payload, int timeout, int retries, int interval, std::function callback) override; void etcd_add_watch(json11::Json watch) override; + std::string get_username() override; void load_global_config() override; void load_pgs() override; }; diff --git a/src/client/http_client.cpp b/src/client/http_client.cpp index cf0127f5..9e5ea723 100644 --- a/src/client/http_client.cpp +++ b/src/client/http_client.cpp @@ -48,6 +48,7 @@ struct http_context_t #ifdef WITH_OPENSSL SSL_CTX *ssl_ctx = NULL; + std::string ssl_cn; #endif ~http_context_t() @@ -186,24 +187,41 @@ 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); } -bool openssl_ctx_use_cert(SSL_CTX *ssl_ctx, const std::string & file_or_pem) +bool openssl_ctx_use_cert(SSL_CTX *ssl_ctx, const std::string & file_or_pem, std::string & common_name) { + BIO *bio = NULL; + std::string contents; if (file_or_pem.substr(0, 5) == "-----") + bio = BIO_new_mem_buf(file_or_pem.data(), file_or_pem.size()); + else { - BIO *bio = BIO_new_mem_buf(file_or_pem.data(), file_or_pem.size()); - if (!bio) + contents = read_file(file_or_pem); + if (!contents.size()) return false; - X509 *x509 = PEM_read_bio_X509(bio, NULL, 0, NULL); - bool ok = !!x509; - if (x509) - { - ok = SSL_CTX_use_certificate(ssl_ctx, x509); - X509_free(x509); - } - BIO_free(bio); - return ok; + bio = BIO_new_mem_buf(contents.data(), contents.size()); } - return !!SSL_CTX_use_certificate_file(ssl_ctx, file_or_pem.c_str(), SSL_FILETYPE_PEM); + if (!bio) + return false; + X509 *x509 = PEM_read_bio_X509(bio, NULL, 0, NULL); + bool ok = !!x509; + if (x509) + { + 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)); + } + } + X509_free(x509); + } + BIO_free(bio); + return ok; } bool openssl_ctx_use_key(SSL_CTX *ssl_ctx, const std::string & file_or_pem) @@ -243,6 +261,7 @@ http_context_t* http_context_init(timerfd_manager_t *tfd, const std::string & ss ctx->ssl_key = ssl_key; ctx->ssl_ca = ssl_ca; ctx->ssl_ctx = ssl_ctx; + ctx->ssl_cn = ""; if (!ssl_ctx) goto init_err; SSL_CTX_set_verify(ssl_ctx, verify_peer ? SSL_VERIFY_PEER : SSL_VERIFY_NONE, NULL); @@ -251,7 +270,7 @@ http_context_t* http_context_init(timerfd_manager_t *tfd, const std::string & ss if (!openssl_ctx_use_ca(ssl_ctx, ssl_ca)) goto init_err; if (ssl_cert != "" && ssl_key != "" && - (!openssl_ctx_use_cert(ssl_ctx, ssl_cert) || + (!openssl_ctx_use_cert(ssl_ctx, ssl_cert, ctx->ssl_cn) || !openssl_ctx_use_key(ssl_ctx, ssl_key))) goto init_err; #endif @@ -262,6 +281,11 @@ init_err: return NULL; } +std::string http_context_get_ssl_cn(http_context_t *ctx) +{ + return ctx->ssl_cn; +} + struct http_ctx_resolve_t { std::function & addrs)> cb; diff --git a/src/client/http_client.h b/src/client/http_client.h index 72761d0c..fc663698 100644 --- a/src/client/http_client.h +++ b/src/client/http_client.h @@ -48,6 +48,7 @@ struct http_co_t; http_context_t* http_context_init(timerfd_manager_t *tfd, const std::string & ssl_cert, const std::string & ssl_key, const std::string & ssl_ca, bool verify_peer, std::string & error); +std::string http_context_get_ssl_cn(http_context_t *ctx); void http_resolve(http_context_t *ctx, bool ssl, std::string host, std::function & addrs)> cb); void http_context_destroy(http_context_t *ctx); diff --git a/src/client/msgr_send.cpp b/src/client/msgr_send.cpp index 1edb67e3..c36d1cf4 100644 --- a/src/client/msgr_send.cpp +++ b/src/client/msgr_send.cpp @@ -206,7 +206,7 @@ void osd_messenger_t::handle_send(int result, bool prev, bool more, osd_client_t { fprintf(stderr, "Client %ju socket write error: expected to send " "%zu bytes with MSG_WAITALL but sent %u. Disconnecting client\n", cl->client_id, cl->send_list_size, result); - stop_client(cl->peer_fd); + stop_client(cl->client_id); return; } for (auto op: cl->send_free_ops) diff --git a/src/cmd/cli.cpp b/src/cmd/cli.cpp index f1907bd1..93afcc71 100644 --- a/src/cmd/cli.cpp +++ b/src/cmd/cli.cpp @@ -49,6 +49,9 @@ static const char* help_text = " --enc-key random Generate a new random AES-256-XTS encryption key for the new image.\n" " --enc-key HEX Set a specified AES-256-XTS key (64 bytes in hex) for the new image.\n" " --enc-key vault:ID Use an encryption key from an external Vault secret with specified ID.\n" + " --owner username Set owner (default is the current user from TLS certificate).\n" + " --owner_group name Set owner group name.\n" + " --reader_group rdr Set reader group name.\n" "\n" "vitastor-cli create --snapshot [OPTIONS] \n" "vitastor-cli snap-create [OPTIONS] @\n" @@ -63,10 +66,13 @@ static const char* help_text = " Rename, resize image or change its readonly status. Images with children can't be made read-write.\n" " If the new size is smaller than the old size, extra data will be purged.\n" " You should resize file system in the image, if present, before shrinking it.\n" - " --deleted 1|0 Set/clear 'deleted image' flag (set automatically during unfinished deletes).\n" - " -f|--force Proceed with shrinking or setting readwrite flag even if the image has children.\n" - " --down-ok Proceed with shrinking even if some data will be left on unavailable OSDs.\n" - " --enc-key HEX Change image encryption key (allowed only with --force).\n" + " --deleted 1|0 Set/clear 'deleted image' flag (set automatically during unfinished deletes).\n" + " -f|--force Proceed with shrinking or setting readwrite flag even if the image has children.\n" + " --down-ok Proceed with shrinking even if some data will be left on unavailable OSDs.\n" + " --enc-key HEX Change image encryption key (allowed only with --force).\n" + " --owner username Change image owner.\n" + " --owner_group name Change image owner group name.\n" + " --reader_group rdr Change image reader group name.\n" "\n" "vitastor-cli dd [iimg= | if=] [oimg= | of=] [bs=1M]\n" " [count=N] [seek/oseek=N] [skip/iseek=M] [iodepth=N] [status=progress]\n" @@ -210,6 +216,7 @@ static const char* help_text = " --used_for_app s3: Mark pool as used for S3 location with name \n" " --pg_stripe_size Increase object grouping stripe\n" " --max_osd_combinations 10000 Maximum number of random combinations for LP solver input\n" + " --creator_group User group allowed to create images in this pool.\n" " --wait Wait for the new pool to come online\n" " -f|--force Do not check that cluster has enough OSDs to create the pool\n" " Examples:\n" @@ -221,7 +228,7 @@ static const char* help_text = " [-s|--pg_size ] [--pg_minsize ] [-n|--pg_count ]\n" " [--failure_domain ] [--root_node ] [--osd_tags ] [--used_for_app :]\n" " [--max_osd_combinations ] [--primary_affinity_tags ] [--scrub_interval