]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/commitdiff
rgw: Inject keystone roles into IAM policy
authorSupriti Singh <supriti.singh@clyso.com>
Thu, 26 Feb 2026 08:59:52 +0000 (09:59 +0100)
committerSupriti Singh <supriti.singh@clyso.com>
Tue, 21 Apr 2026 11:40:19 +0000 (13:40 +0200)
Keystone roles are injected into the IAM policy evaluation
environment as "keystone:role" condition keys. This enables
bucket policies to grant or deny access based on Keystone roles.
It bridges the gap between Openstack RBAC and S3 IAM policy.

Document keystone:role in bukcet policy and Keystone S3
integration

Signed-off-by: Supriti Singh <supriti.singh@clyso.com>
Signed-off-by: Marcel Lauhoff <marcel.lauhoff@clyso.com>
On-behalf-of: SAP <supriti.singh@clyso.com>
On-behalf-of: SAP <marcel.lauhoff@sap.com>

doc/radosgw/bucketpolicy.rst
doc/radosgw/keystone.rst
src/rgw/rgw_auth.cc
src/rgw/rgw_auth.h
src/rgw/rgw_auth_keystone.cc
src/test/rgw/test_rgw_iam_policy.cc

index 29e9aa062b11fef1c2227f53ed3caea9a5ee2fd6..a92f836680f13c4516545b6b0a38d38a2521d2ad 100644 (file)
@@ -123,6 +123,10 @@ For all requests, condition keys we support are:
 - aws:UserAgent
 - aws:username
 
+Request that authenticate with Keystone also include:
+
+- keystone:role
+
 We support certain S3 condition keys for bucket and object requests.
 
 *Support for the following bucket-related operations was added in the Mimic
index 741b2d23bd805e2b085629de8e1ba27bf82f764d..577ef5fd2c4cd91371524b3b775f73f0410b92ef 100644 (file)
@@ -181,6 +181,12 @@ S3 API (with AWS-like access and secret keys), if the ``rgw s3 auth
 use keystone`` option is set. For details, see
 :doc:`s3/authentication`.
 
+Requests authenticated via Keystone expose the Keystone role names in the
+IAM policy environment as the condition key ``keystone:role``. It can be
+used in bucket policies and idenitity policies to allow or deny access by
+role (e.g. ``StringEquals`` on ``keystone:role``).
+See :doc:`bucketpolicy` for list of supported condition keys.
+
 Service Token Support
 ---------------------
 
index 75d88bab7e3e4bcb3f3799ea5f171e86614be1b5..4de695839dd9b27ae5015949edf8e94e18721969 100644 (file)
@@ -1043,6 +1043,15 @@ void rgw::auth::RemoteApplier::modify_request_state(const DoutPrefixProvider* dp
   // copy our identity policies into req_state
   s->iam_identity_policies.insert(s->iam_identity_policies.end(),
                                   policies.begin(), policies.end());
+
+  for (auto role : this->info.keystone_roles) {
+    // Keystone roles are case-insensitive. Normalize the roles to
+    // lowercase before placing them into the environment.
+    std::transform(role.begin(), role.end(), role.begin(),
+      [](unsigned char c){ return std::tolower(c); });
+    s->env.emplace("keystone:role", std::move(role));
+  }
+
 }
 
 std::optional<rgw::ARN> rgw::auth::RemoteApplier::get_caller_identity() const 
index 6dcf0c6db35306e56699fd9e0cf08a8317afa226..3cb5f0c265a6fe0126cd96bae4324ca35081a737 100644 (file)
@@ -600,6 +600,7 @@ public:
     const std::string subuser;
     const std::string keystone_user;
     const std::optional<rgw::keystone::ScopeInfo> keystone_scope;
+    const std::vector<std::string> keystone_roles;
 
   public:
     enum class acct_privilege_t {
@@ -619,7 +620,8 @@ public:
              const std::string subuser,
              const std::string keystone_user,
              const uint32_t acct_type=TYPE_NONE,
-             std::optional<rgw::keystone::ScopeInfo> keystone_scope=std::nullopt)
+             std::optional<rgw::keystone::ScopeInfo> keystone_scope=std::nullopt,
+             std::vector<std::string> keystone_roles = {})
     : acct_user(acct_user),
       acct_name(acct_name),
       perm_mask(perm_mask),
@@ -628,7 +630,8 @@ public:
       access_key_id(access_key_id),
       subuser(subuser),
       keystone_user(keystone_user),
-      keystone_scope(std::move(keystone_scope)) {
+      keystone_scope(std::move(keystone_scope)),
+      keystone_roles(std::move(keystone_roles)) {
     }
   };
 
index 0045248720d58f0292f695fde98a0f94532a2a68..31c65b991c11129eff6f58a74e83870240005c4e 100644 (file)
@@ -144,13 +144,14 @@ TokenEngine::get_creds_info(const TokenEngine::token_envelope_t& token
                            ) const noexcept
 {
   using acct_privilege_t = rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t;
+  std::vector<std::string> role_names;
 
   /* Check whether the user has an admin status. */
   acct_privilege_t level = acct_privilege_t::IS_PLAIN_ACCT;
   for (const auto& role : token.roles) {
+    role_names.push_back(role.name);
     if (role.is_admin && !role.is_reader) {
       level = acct_privilege_t::IS_ADMIN_ACCT;
-      break;
     }
   }
 
@@ -170,8 +171,9 @@ TokenEngine::get_creds_info(const TokenEngine::token_envelope_t& token
     rgw::auth::RemoteApplier::AuthInfo::NO_SUBUSER,
     token.get_user_name(),
     TYPE_KEYSTONE,
-    std::move(keystone_scope)
-};
+    std::move(keystone_scope),
+    std::move(role_names)
+  };
 }
 
 static inline const std::string
@@ -668,6 +670,11 @@ 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);
 
+  std::vector<std::string> role_names;
+  for (const auto& role : token.roles) {
+    role_names.push_back(role.name);
+  }
+
   return auth_info_t {
     /* Suggested account name for the authenticated user. */
     rgw_user(token.get_project_id()),
@@ -681,7 +688,8 @@ EC2Engine::get_creds_info(const EC2Engine::token_envelope_t& token,
     rgw::auth::RemoteApplier::AuthInfo::NO_SUBUSER,
     token.get_user_name(),
     TYPE_KEYSTONE,
-    std::move(keystone_scope)
+    std::move(keystone_scope),
+    std::move(role_names)
   };
 }
 
index 956db92c1443afc06025d45037c85fe3b7bf2a4d..266b78bb2c82597782574a716768f50ba849e354 100644 (file)
@@ -1786,3 +1786,119 @@ TEST_F(ConditionTest, Null)
     EXPECT_TRUE(notNull.eval({{key, "admin/config.txt"}}));
   }
 }
+
+TEST_F(ConditionTest, KeyStoneRoleStringEquals)
+{
+  std::string key = "keystone:role";
+
+  Condition cond{TokenID::StringEquals, key.data(), key.size(), false};
+  cond.vals.push_back("testrole");
+
+  // No roles
+  EXPECT_FALSE(cond.eval({}));
+
+  // Single matching role
+  EXPECT_TRUE(cond.eval({{key, "testrole"}}));
+
+  // Single non-matching role
+  EXPECT_FALSE(cond.eval({{key, "member"}}));
+
+  //Multiple roles in env,one matches
+  Environment multi_env;
+  multi_env.emplace(key, "member");
+  multi_env.emplace(key, "testrole");
+  multi_env.emplace(key, "reader");
+  EXPECT_TRUE(cond.eval(multi_env));
+
+  //Multiple roles in env, no match
+  Environment no_match_env;
+  no_match_env.emplace(key, "member");
+  no_match_env.emplace(key, "reader");
+  EXPECT_FALSE(cond.eval(no_match_env));
+
+  // Multiple identical roles (redundancy check)
+  Environment duplicate_env;
+  duplicate_env.emplace(key, "testrole");
+  duplicate_env.emplace(key, "testrole");
+  EXPECT_TRUE(cond.eval(duplicate_env));
+}
+
+TEST_F(ConditionTest, KeyStoneRoleNotStringEquals)
+{
+  std::string key = "keystone:role";
+
+  Condition cond{TokenID::StringNotEquals, key.data(), key.size(), false};
+  cond.vals.push_back("admin");
+
+  // No roles
+  EXPECT_FALSE(cond.eval({}));
+
+  // Role matches
+  EXPECT_FALSE(cond.eval({{key, "admin"}}));
+
+  // Role doesn't match
+  EXPECT_TRUE(cond.eval({{key, "member"}}));
+
+  // Multiple roles in env, one matches -> false
+  Environment multi_env;
+  multi_env.emplace(key, "member");
+  multi_env.emplace(key, "admin");
+  EXPECT_FALSE(multi_env.count(key) == 0);
+  EXPECT_FALSE(cond.eval(multi_env));
+
+  // Multiple roles, none match -> true
+  Environment no_match_env;
+  no_match_env.emplace(key, "member");
+  no_match_env.emplace(key, "reader");
+  EXPECT_TRUE(cond.eval(no_match_env));
+}
+
+TEST_F(ConditionTest, KeyStoneRolePolicyParsing)
+{
+  string keystone_role_policy = R"(
+  {
+    "Version": "2012-10-17",
+    "Statement": [{
+      "Effect" : "Allow",
+      "Principal": "*",
+      "Action": "s3:GetObject",
+      "Resource": [
+        "arn:aws:s3:::example_bucket/*"
+      ],
+      "Condition": {
+        "StringEquals": {
+          "keystone:role": "testrole"
+        }
+      }
+    }]
+  }
+  )";
+
+  string tenant = "arbitrary_tenant";
+  boost::optional<Policy> p;
+
+  ASSERT_NO_THROW(
+    p = Policy(cct.get(), &tenant, keystone_role_policy, true));
+  ASSERT_TRUE(p);
+  EXPECT_EQ(p->statements.size(), 1U);
+  EXPECT_EQ(p->statements[0].conditions.size(), 1U);
+  EXPECT_EQ(p->statements[0].conditions[0].key, "keystone:role");
+  EXPECT_EQ(p->statements[0].conditions[0].vals.size(), 1U);
+  EXPECT_EQ(p->statements[0].conditions[0].vals[0], "testrole");
+
+  // Eval with matching role in environment
+  Environment match_env;
+  match_env.emplace("keystone:role", "testrole");
+  EXPECT_TRUE(p->statements[0].conditions[0].eval(match_env));
+
+  // Eval with non-matching role
+  Environment nomatch_env;
+  nomatch_env.emplace("keystone:role", "member");
+  EXPECT_FALSE(p->statements[0].conditions[0].eval(nomatch_env));
+
+  // Eval with multiple roles, one matching
+  Environment multi_env;
+  multi_env.emplace("keystone:role", "member");
+  multi_env.emplace("keystone:role", "testrole");
+  EXPECT_TRUE(p->statements[0].conditions[0].eval(multi_env));
+}
\ No newline at end of file