From: Supriti Singh Date: Thu, 26 Feb 2026 08:59:52 +0000 (+0100) Subject: rgw: Inject keystone roles into IAM policy X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=1e6e39346667f82591644760311093729db8a915;p=ceph.git rgw: Inject keystone roles into IAM policy 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 Signed-off-by: Marcel Lauhoff On-behalf-of: SAP On-behalf-of: SAP --- diff --git a/doc/radosgw/bucketpolicy.rst b/doc/radosgw/bucketpolicy.rst index 29e9aa062b11..a92f836680f1 100644 --- a/doc/radosgw/bucketpolicy.rst +++ b/doc/radosgw/bucketpolicy.rst @@ -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 diff --git a/doc/radosgw/keystone.rst b/doc/radosgw/keystone.rst index 741b2d23bd80..577ef5fd2c4c 100644 --- a/doc/radosgw/keystone.rst +++ b/doc/radosgw/keystone.rst @@ -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 --------------------- diff --git a/src/rgw/rgw_auth.cc b/src/rgw/rgw_auth.cc index 75d88bab7e3e..4de695839dd9 100644 --- a/src/rgw/rgw_auth.cc +++ b/src/rgw/rgw_auth.cc @@ -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::auth::RemoteApplier::get_caller_identity() const diff --git a/src/rgw/rgw_auth.h b/src/rgw/rgw_auth.h index 6dcf0c6db353..3cb5f0c265a6 100644 --- a/src/rgw/rgw_auth.h +++ b/src/rgw/rgw_auth.h @@ -600,6 +600,7 @@ public: const std::string subuser; const std::string keystone_user; const std::optional keystone_scope; + const std::vector 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 keystone_scope=std::nullopt) + std::optional keystone_scope=std::nullopt, + std::vector 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)) { } }; diff --git a/src/rgw/rgw_auth_keystone.cc b/src/rgw/rgw_auth_keystone.cc index 0045248720d5..31c65b991c11 100644 --- a/src/rgw/rgw_auth_keystone.cc +++ b/src/rgw/rgw_auth_keystone.cc @@ -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 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 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) }; } diff --git a/src/test/rgw/test_rgw_iam_policy.cc b/src/test/rgw/test_rgw_iam_policy.cc index 956db92c1443..266b78bb2c82 100644 --- a/src/test/rgw/test_rgw_iam_policy.cc +++ b/src/test/rgw/test_rgw_iam_policy.cc @@ -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 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