From: Filipp Akinfiev Date: Tue, 2 Dec 2025 12:02:04 +0000 (+0100) Subject: rgw: Add Keystone scope information to ops logging X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=3180812e54197784ba5b7fc7027e62553144e175;p=ceph.git rgw: Add Keystone scope information to ops logging Fixes: https://tracker.ceph.com/issues/73702 On-behalf-of: SAP Signed-off-by: Filipp Akinfiev --- diff --git a/src/common/options/rgw.yaml.in b/src/common/options/rgw.yaml.in index fe47a4da398..ddf66151848 100644 --- a/src/common/options/rgw.yaml.in +++ b/src/common/options/rgw.yaml.in @@ -935,6 +935,42 @@ options: - '1' - none with_legacy: true +- name: rgw_keystone_scope_enabled + type: bool + level: advanced + desc: Enable logging of Keystone scope information in ops log + long_desc: When enabled, operations authenticated via Keystone will include scope + information (project, domain, roles) in the ops log. This is disabled by default + as it adds additional data to each log entry and most deployments do not require + this level of Keystone audit detail. User identity logging is controlled separately + by rgw_keystone_scope_include_user for privacy considerations. Requires + rgw_enable_ops_log to be enabled. + default: false + services: + - rgw + with_legacy: true +- name: rgw_keystone_scope_include_user + type: bool + level: advanced + desc: Include human-readable identity names in Keystone scope logs + long_desc: When false (default), only opaque IDs are logged for all identity fields + (project_id, domain_id, user_id, app_cred_id), which is GDPR-compliant and + privacy-friendly. When true, human-readable names (project_name, domain names, + user_name, app_cred_name) are included. Opaque IDs are always logged regardless + of this setting, allowing operators to correlate with Keystone without exposing + names in logs. + default: false + services: + - rgw + with_legacy: true +- name: rgw_keystone_scope_include_roles + type: bool + level: advanced + desc: Include role names in Keystone scope logs + default: true + services: + - rgw + with_legacy: true - name: rgw_cross_domain_policy type: str level: advanced diff --git a/src/rgw/CMakeLists.txt b/src/rgw/CMakeLists.txt index d175d18065a..d42e2b099be 100644 --- a/src/rgw/CMakeLists.txt +++ b/src/rgw/CMakeLists.txt @@ -63,6 +63,7 @@ set(librgw_common_srcs rgw_formats.cc rgw_http_client.cc rgw_keystone.cc + rgw_keystone_scope.cc rgw_ldap.cc rgw_lc.cc rgw_lc_s3.cc diff --git a/src/rgw/rgw_auth.cc b/src/rgw/rgw_auth.cc index 7d6e850aeee..75d88bab7e3 100644 --- a/src/rgw/rgw_auth.cc +++ b/src/rgw/rgw_auth.cc @@ -961,6 +961,10 @@ void rgw::auth::RemoteApplier::write_ops_log_entry(rgw_log_entry& entry) const entry.account_id = account->id; } entry.user = info.keystone_user; + + if (info.keystone_scope.has_value()) { + entry.keystone_scope = info.keystone_scope; + } } /* TODO(rzarzynski): we need to handle display_name changes. */ diff --git a/src/rgw/rgw_auth.h b/src/rgw/rgw_auth.h index f63927d9321..6dcf0c6db35 100644 --- a/src/rgw/rgw_auth.h +++ b/src/rgw/rgw_auth.h @@ -15,6 +15,7 @@ #include "rgw_common.h" #include "rgw_web_idp.h" +#include "rgw_keystone_scope.h" #define RGW_USER_ANON_ID "anonymous" @@ -598,6 +599,7 @@ public: const std::string access_key_id; const std::string subuser; const std::string keystone_user; + const std::optional keystone_scope; public: enum class acct_privilege_t { @@ -616,7 +618,8 @@ public: const std::string access_key_id, const std::string subuser, const std::string keystone_user, - const uint32_t acct_type=TYPE_NONE) + const uint32_t acct_type=TYPE_NONE, + std::optional keystone_scope=std::nullopt) : acct_user(acct_user), acct_name(acct_name), perm_mask(perm_mask), @@ -624,7 +627,8 @@ public: acct_type(acct_type), access_key_id(access_key_id), subuser(subuser), - keystone_user(keystone_user) { + keystone_user(keystone_user), + keystone_scope(std::move(keystone_scope)) { } }; diff --git a/src/rgw/rgw_auth_keystone.cc b/src/rgw/rgw_auth_keystone.cc index ee36639ff4b..0045248720d 100644 --- a/src/rgw/rgw_auth_keystone.cc +++ b/src/rgw/rgw_auth_keystone.cc @@ -16,6 +16,7 @@ #include "rgw_common.h" #include "rgw_keystone.h" +#include "rgw_keystone_scope.h" #include "rgw_auth_keystone.h" #include "rgw_rest_s3.h" #include "rgw_auth_s3.h" @@ -153,6 +154,9 @@ TokenEngine::get_creds_info(const TokenEngine::token_envelope_t& token } } + /* Build keystone scope info if ops logging is enabled */ + auto keystone_scope = rgw::keystone::build_scope_info(cct, token); + return auth_info_t { /* Suggested account name for the authenticated user. */ rgw_user(token.get_project_id()), @@ -165,7 +169,8 @@ TokenEngine::get_creds_info(const TokenEngine::token_envelope_t& token rgw::auth::RemoteApplier::AuthInfo::NO_ACCESS_KEY, rgw::auth::RemoteApplier::AuthInfo::NO_SUBUSER, token.get_user_name(), - TYPE_KEYSTONE + TYPE_KEYSTONE, + std::move(keystone_scope) }; } @@ -660,6 +665,9 @@ EC2Engine::get_creds_info(const EC2Engine::token_envelope_t& token, } } + /* Build keystone scope info if ops logging is enabled */ + auto keystone_scope = rgw::keystone::build_scope_info(cct, token); + return auth_info_t { /* Suggested account name for the authenticated user. */ rgw_user(token.get_project_id()), @@ -672,7 +680,8 @@ EC2Engine::get_creds_info(const EC2Engine::token_envelope_t& token, access_key_id, rgw::auth::RemoteApplier::AuthInfo::NO_SUBUSER, token.get_user_name(), - TYPE_KEYSTONE + TYPE_KEYSTONE, + std::move(keystone_scope) }; } diff --git a/src/rgw/rgw_keystone.cc b/src/rgw/rgw_keystone.cc index 81992684df5..7f2ec8d5efa 100644 --- a/src/rgw/rgw_keystone.cc +++ b/src/rgw/rgw_keystone.cc @@ -486,6 +486,13 @@ void rgw::keystone::TokenEnvelope::User::decode_json(JSONObj *obj) JSONDecoder::decode_json("domain", domain, obj); } +void rgw::keystone::TokenEnvelope::ApplicationCredential::decode_json(JSONObj *obj) +{ + JSONDecoder::decode_json("id", id, obj, true); + JSONDecoder::decode_json("name", name, obj, true); + JSONDecoder::decode_json("restricted", restricted, obj, true); +} + void rgw::keystone::TokenEnvelope::decode(JSONObj* const root_obj) { std::string expires_iso8601; @@ -495,6 +502,12 @@ void rgw::keystone::TokenEnvelope::decode(JSONObj* const root_obj) JSONDecoder::decode_json("roles", roles, root_obj, true); JSONDecoder::decode_json("project", project, root_obj, true); + // Optional application_credential field (only present for app cred auth) + ApplicationCredential tmp_app_cred; + if (JSONDecoder::decode_json("application_credential", tmp_app_cred, root_obj, false)) { + app_cred = std::move(tmp_app_cred); + } + struct tm t; if (parse_iso8601(expires_iso8601.c_str(), &t)) { token.expires = internal_timegm(&t); diff --git a/src/rgw/rgw_keystone.h b/src/rgw/rgw_keystone.h index 62b1ae81716..2dc17dfeabe 100644 --- a/src/rgw/rgw_keystone.h +++ b/src/rgw/rgw_keystone.h @@ -173,10 +173,20 @@ public: void decode_json(JSONObj *obj); }; + class ApplicationCredential { + public: + ApplicationCredential() : restricted(false) { } + std::string id; + std::string name; + bool restricted; + void decode_json(JSONObj *obj); + }; + Token token; Project project; User user; std::list roles; + std::optional app_cred; void decode(JSONObj* obj); diff --git a/src/rgw/rgw_keystone_scope.cc b/src/rgw/rgw_keystone_scope.cc new file mode 100644 index 00000000000..53c81ddb4cc --- /dev/null +++ b/src/rgw/rgw_keystone_scope.cc @@ -0,0 +1,111 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab ft=cpp + +#include "rgw_keystone_scope.h" +#include "rgw_keystone.h" +#include "common/ceph_context.h" +#include "common/Formatter.h" + +#define dout_subsys ceph_subsys_rgw + +namespace rgw::keystone { + +void ScopeInfo::dump(ceph::Formatter *f) const +{ + f->open_object_section("keystone_scope"); + + // Project + f->open_object_section("project"); + f->dump_string("id", project.id); + f->dump_string("name", project.name); + f->open_object_section("domain"); + f->dump_string("id", project.domain.id); + f->dump_string("name", project.domain.name); + f->close_section(); // domain + f->close_section(); // project + + // User (optional based on config) + if (user.has_value()) { + f->open_object_section("user"); + f->dump_string("id", user->id); + f->dump_string("name", user->name); + f->open_object_section("domain"); + f->dump_string("id", user->domain.id); + f->dump_string("name", user->domain.name); + f->close_section(); // domain + f->close_section(); // user + } + + // Roles (may be empty based on config) + if (!roles.empty()) { + f->open_array_section("roles"); + for (const auto& role : roles) { + f->dump_string("role", role); + } + f->close_section(); // roles + } + + // Application credential (optional, present only for app cred auth) + if (app_cred.has_value()) { + f->open_object_section("application_credential"); + f->dump_string("id", app_cred->id); + f->dump_string("name", app_cred->name); + f->dump_bool("restricted", app_cred->restricted); + f->close_section(); // application_credential + } + + f->close_section(); // keystone_scope +} + +std::optional build_scope_info( + CephContext* cct, + const TokenEnvelope& token) +{ + // Check if scope logging is enabled + if (!cct->_conf->rgw_keystone_scope_enabled) { + return std::nullopt; + } + + ScopeInfo scope; + bool include_names = cct->_conf->rgw_keystone_scope_include_user; + + // Project/tenant scope - IDs always included, names only if include_user=true + scope.project.id = token.get_project_id(); + scope.project.domain.id = token.project.domain.id; + if (include_names) { + scope.project.name = token.get_project_name(); + scope.project.domain.name = token.project.domain.name; + } + + // User identity (controlled by include_user flag - both field and names) + if (include_names) { + ScopeInfo::user_t user; + user.id = token.get_user_id(); + user.name = token.get_user_name(); + user.domain.id = token.user.domain.id; + user.domain.name = token.user.domain.name; + scope.user = std::move(user); + } + + // Roles (controlled by include_roles flag) + if (cct->_conf->rgw_keystone_scope_include_roles) { + for (const auto& role : token.roles) { + scope.roles.push_back(role.name); + } + } + + // Application credential (if present in token) - ID always, name only if include_user=true + if (token.app_cred.has_value()) { + ScopeInfo::app_cred_t app_cred; + app_cred.id = token.app_cred->id; + if (include_names) { + app_cred.name = token.app_cred->name; + } + app_cred.restricted = token.app_cred->restricted; + scope.app_cred = std::move(app_cred); + } + + return scope; +} + +} // namespace rgw::keystone diff --git a/src/rgw/rgw_keystone_scope.h b/src/rgw/rgw_keystone_scope.h new file mode 100644 index 00000000000..db4c1bc4cf1 --- /dev/null +++ b/src/rgw/rgw_keystone_scope.h @@ -0,0 +1,201 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab ft=cpp + +#pragma once + +#include +#include +#include + +#include "include/buffer.h" +#include "include/encoding.h" +#include "include/common_fwd.h" + +namespace ceph { + class Formatter; +} + +namespace rgw::keystone { + +// Import ceph encode/decode templates for visibility +using ceph::encode; +using ceph::decode; + +// Forward declaration +class TokenEnvelope; + +/** + * Keystone authentication scope information. + * + * This structure captures the OpenStack Keystone authentication context + * including project, user, and roles. It's used throughout the + * authentication and logging pipeline. + * + * The structure supports: + * - Binary encoding/decoding for RADOS backend (ops log storage) + * - JSON formatting for file/socket backend (ops log output) + * - Granular control via configuration flags (include_user, include_roles) + */ +struct ScopeInfo { + /** + * Keystone domain information. + * Domains provide namespace isolation in Keystone. + */ + struct domain_t { + static constexpr uint8_t kEncV = 1; + std::string id; + std::string name; + + void encode(bufferlist &bl) const { + ENCODE_START(kEncV, kEncV, bl); + encode(id, bl); + encode(name, bl); + ENCODE_FINISH(bl); + } + void decode(bufferlist::const_iterator &p) { + DECODE_START(kEncV, p); + decode(id, p); + decode(name, p); + DECODE_FINISH(p); + } + }; + + /** + * Keystone project (tenant) information. + * Projects are the primary authorization scope in Keystone. + */ + struct project_t { + static constexpr uint8_t kEncV = 1; + std::string id; + std::string name; + domain_t domain; + + void encode(bufferlist &bl) const { + ENCODE_START(kEncV, kEncV, bl); + encode(id, bl); + encode(name, bl); + domain.encode(bl); + ENCODE_FINISH(bl); + } + void decode(bufferlist::const_iterator &p) { + DECODE_START(kEncV, p); + decode(id, p); + decode(name, p); + domain.decode(p); + DECODE_FINISH(p); + } + }; + + /** + * Keystone user information. + * Optional based on rgw_keystone_scope_include_user configuration. + */ + struct user_t { + static constexpr uint8_t kEncV = 1; + std::string id; + std::string name; + domain_t domain; + + void encode(bufferlist &bl) const { + ENCODE_START(kEncV, kEncV, bl); + encode(id, bl); + encode(name, bl); + domain.encode(bl); + ENCODE_FINISH(bl); + } + void decode(bufferlist::const_iterator &p) { + DECODE_START(kEncV, p); + decode(id, p); + decode(name, p); + domain.decode(p); + DECODE_FINISH(p); + } + }; + + /** + * Keystone application credential information. + * Only present when authentication used application credentials. + */ + struct app_cred_t { + static constexpr uint8_t kEncV = 1; + std::string id; + std::string name; + bool restricted; + + void encode(bufferlist &bl) const { + ENCODE_START(kEncV, kEncV, bl); + encode(id, bl); + encode(name, bl); + encode(restricted, bl); + ENCODE_FINISH(bl); + } + void decode(bufferlist::const_iterator &p) { + DECODE_START(kEncV, p); + decode(id, p); + decode(name, p); + decode(restricted, p); + DECODE_FINISH(p); + } + }; + + // Fields + project_t project; // Always present + std::optional user; // Optional (controlled by include_user config) + std::vector roles; // May be empty (controlled by include_roles config) + std::optional app_cred; // Optional (present only for app cred auth) + + // Serialization for RADOS backend + static constexpr uint8_t kEncV = 1; + void encode(bufferlist &bl) const; + void decode(bufferlist::const_iterator &p); + + // JSON formatting for file/socket backend + void dump(ceph::Formatter *f) const; +}; + +// Free function wrappers at namespace level for std::optional encoding support +inline void encode(const ScopeInfo::domain_t& v, bufferlist& bl) { v.encode(bl); } +inline void decode(ScopeInfo::domain_t& v, bufferlist::const_iterator& p) { v.decode(p); } +inline void encode(const ScopeInfo::project_t& v, bufferlist& bl) { v.encode(bl); } +inline void decode(ScopeInfo::project_t& v, bufferlist::const_iterator& p) { v.decode(p); } +inline void encode(const ScopeInfo::user_t& v, bufferlist& bl) { v.encode(bl); } +inline void decode(ScopeInfo::user_t& v, bufferlist::const_iterator& p) { v.decode(p); } +inline void encode(const ScopeInfo::app_cred_t& v, bufferlist& bl) { v.encode(bl); } +inline void decode(ScopeInfo::app_cred_t& v, bufferlist::const_iterator& p) { v.decode(p); } +inline void encode(const ScopeInfo& v, bufferlist& bl) { v.encode(bl); } +inline void decode(ScopeInfo& v, bufferlist::const_iterator& p) { v.decode(p); } + +// ScopeInfo encode/decode implementations (defined after free functions for visibility) +inline void ScopeInfo::encode(bufferlist &bl) const { + ENCODE_START(kEncV, kEncV, bl); + encode(project, bl); + encode(user, bl); + encode(roles, bl); + encode(app_cred, bl); + ENCODE_FINISH(bl); +} + +inline void ScopeInfo::decode(bufferlist::const_iterator &p) { + DECODE_START(kEncV, p); + decode(project, p); + decode(user, p); + decode(roles, p); + decode(app_cred, p); + DECODE_FINISH(p); +} + +/** + * Build ScopeInfo from Keystone token and configuration. + * + * This helper function eliminates code duplication between TokenEngine + * and EC2Engine by centralizing the scope building logic. + * + * @param cct CephContext for configuration access + * @param token TokenEnvelope containing Keystone authentication data + * @return ScopeInfo if scope logging enabled, nullopt otherwise + */ +std::optional build_scope_info( + CephContext* cct, + const TokenEnvelope& token); + +} // namespace rgw::keystone diff --git a/src/rgw/rgw_log.cc b/src/rgw/rgw_log.cc index 36872497982..27580cce267 100644 --- a/src/rgw/rgw_log.cc +++ b/src/rgw/rgw_log.cc @@ -331,6 +331,11 @@ void rgw_format_ops_log_entry(struct rgw_log_entry& entry, Formatter *formatter) } formatter->dump_bool("temp_url", entry.temp_url); + // Keystone scope information (if present) + if (entry.keystone_scope.has_value()) { + entry.keystone_scope->dump(formatter); + } + if (entry.op == "multi_object_delete") { formatter->open_object_section("op_data"); formatter->dump_int("num_ok", entry.delete_multi_obj_meta.num_ok); @@ -735,4 +740,7 @@ void rgw_log_entry::dump(Formatter *f) const if (!role_id.empty()) { f->dump_string("role_id", role_id); } + if (keystone_scope.has_value()) { + keystone_scope->dump(f); + } } diff --git a/src/rgw/rgw_log.h b/src/rgw/rgw_log.h index 2d1ba72ca41..d818560f640 100644 --- a/src/rgw/rgw_log.h +++ b/src/rgw/rgw_log.h @@ -10,6 +10,7 @@ #include #include #include "rgw_sal_fwd.h" +#include "rgw_keystone_scope.h" class RGWOp; @@ -105,8 +106,11 @@ struct rgw_log_entry { rgw_account_id account_id; std::string role_id; + // Keystone scope (optional) - uses unified structure from rgw_keystone_scope.h + std::optional keystone_scope; + void encode(bufferlist &bl) const { - ENCODE_START(15, 5, bl); + ENCODE_START(16, 5, bl); // old object/bucket owner ids, encoded in full in v8 std::string empty_owner_id; encode(empty_owner_id, bl); @@ -142,10 +146,11 @@ struct rgw_log_entry { encode(delete_multi_obj_meta, bl); encode(account_id, bl); encode(role_id, bl); + encode(keystone_scope, bl); ENCODE_FINISH(bl); } void decode(bufferlist::const_iterator &p) { - DECODE_START_LEGACY_COMPAT_LEN(15, 5, 5, p); + DECODE_START_LEGACY_COMPAT_LEN(16, 5, 5, p); std::string object_owner_id; std::string bucket_owner_id; decode(object_owner_id, p); @@ -218,6 +223,9 @@ struct rgw_log_entry { decode(account_id, p); decode(role_id, p); } + if (struct_v >= 16) { + decode(keystone_scope, p); + } DECODE_FINISH(p); } void dump(ceph::Formatter *f) const;