]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: Add Keystone scope information to ops logging 66111/head
authorFilipp Akinfiev <filipp.akinfiev@clyso.com>
Tue, 2 Dec 2025 12:02:04 +0000 (13:02 +0100)
committerFilipp Akinfiev <filipp.akinfiev@clyso.com>
Thu, 26 Feb 2026 12:05:02 +0000 (13:05 +0100)
Fixes: https://tracker.ceph.com/issues/73702
On-behalf-of: SAP <filipp.akinfiev@clyso.com>
Signed-off-by: Filipp Akinfiev <filipp.akinfiev@clyso.com>
src/common/options/rgw.yaml.in
src/rgw/CMakeLists.txt
src/rgw/rgw_auth.cc
src/rgw/rgw_auth.h
src/rgw/rgw_auth_keystone.cc
src/rgw/rgw_keystone.cc
src/rgw/rgw_keystone.h
src/rgw/rgw_keystone_scope.cc [new file with mode: 0644]
src/rgw/rgw_keystone_scope.h [new file with mode: 0644]
src/rgw/rgw_log.cc
src/rgw/rgw_log.h

index fe47a4da3986aed7312f89a9651f47e8d448d60d..ddf6615184807ae6af4207f7c64622ec48c47a9c 100644 (file)
@@ -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
index d175d18065a2d85d91a3c6af0104f28eed1db870..d42e2b099be5a1723d2888a9d606cfa9ce17bc38 100644 (file)
@@ -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
index 7d6e850aeeee544428a98d611894c5cb23455915..75d88bab7e3e4bcb3f3799ea5f171e86614be1b5 100644 (file)
@@ -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. */
index f63927d9321a4a602ef33d5de847247d0b9ff798..6dcf0c6db35306e56699fd9e0cf08a8317afa226 100644 (file)
@@ -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<rgw::keystone::ScopeInfo> 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<rgw::keystone::ScopeInfo> 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)) {
     }
   };
 
index ee36639ff4b841339441e37559917b6de13f550b..0045248720d58f0292f695fde98a0f94532a2a68 100644 (file)
@@ -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)
   };
 }
 
index 81992684df591181b35533ce6d5d67e56f70019f..7f2ec8d5efa25e7ed7e9a04454980f12cb50a2da 100644 (file)
@@ -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);
index 62b1ae8171652e7b00ccc2026f29cfcdc41217f8..2dc17dfeabe1ae4877864113a532b70cef5dd10a 100644 (file)
@@ -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<Role> roles;
+  std::optional<ApplicationCredential> 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 (file)
index 0000000..53c81dd
--- /dev/null
@@ -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<ScopeInfo> 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 (file)
index 0000000..db4c1bc
--- /dev/null
@@ -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 <optional>
+#include <string>
+#include <vector>
+
+#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_t> user;               // Optional (controlled by include_user config)
+  std::vector<std::string> roles;           // May be empty (controlled by include_roles config)
+  std::optional<app_cred_t> 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<ScopeInfo> build_scope_info(
+    CephContext* cct,
+    const TokenEnvelope& token);
+
+} // namespace rgw::keystone
index 36872497982b6d747ab0068337d2ab7fddff9402..27580cce2678f3d1ca2561b44d3a1e0f9bbd9439 100644 (file)
@@ -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);
+  }
 }
index 2d1ba72ca41a8bb5d3d571f469fbe0907c175b71..d818560f640ee242914ccb4846407f282d80ea26 100644 (file)
@@ -10,6 +10,7 @@
 #include <vector>
 #include <fstream>
 #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<rgw::keystone::ScopeInfo> 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;