- '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
rgw_formats.cc
rgw_http_client.cc
rgw_keystone.cc
+ rgw_keystone_scope.cc
rgw_ldap.cc
rgw_lc.cc
rgw_lc_s3.cc
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. */
#include "rgw_common.h"
#include "rgw_web_idp.h"
+#include "rgw_keystone_scope.h"
#define RGW_USER_ANON_ID "anonymous"
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 {
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),
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)) {
}
};
#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"
}
}
+ /* 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()),
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)
};
}
}
}
+ /* 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()),
access_key_id,
rgw::auth::RemoteApplier::AuthInfo::NO_SUBUSER,
token.get_user_name(),
- TYPE_KEYSTONE
+ TYPE_KEYSTONE,
+ std::move(keystone_scope)
};
}
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;
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);
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);
--- /dev/null
+// -*- 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
--- /dev/null
+// -*- 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
}
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);
if (!role_id.empty()) {
f->dump_string("role_id", role_id);
}
+ if (keystone_scope.has_value()) {
+ keystone_scope->dump(f);
+ }
}
#include <vector>
#include <fstream>
#include "rgw_sal_fwd.h"
+#include "rgw_keystone_scope.h"
class RGWOp;
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);
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);
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;