From 2016d460de44fb4db13745526af98fad810ceed2 Mon Sep 17 00:00:00 2001 From: Radoslaw Zarzynski Date: Wed, 26 Oct 2016 18:10:15 +0200 Subject: [PATCH] rgw: implement the rgw::auth::KeystoneEngine. Signed-off-by: Radoslaw Zarzynski --- src/rgw/CMakeLists.txt | 1 + src/rgw/rgw_auth_keystone.cc | 278 +++++++++++++++++++++++++++++++++++ src/rgw/rgw_auth_keystone.h | 66 +++++++++ 3 files changed, 345 insertions(+) create mode 100644 src/rgw/rgw_auth_keystone.cc create mode 100644 src/rgw/rgw_auth_keystone.h diff --git a/src/rgw/CMakeLists.txt b/src/rgw/CMakeLists.txt index 4a06cf371d7..d48905b86ac 100644 --- a/src/rgw/CMakeLists.txt +++ b/src/rgw/CMakeLists.txt @@ -30,6 +30,7 @@ set(rgw_a_srcs rgw_acl_s3.cc rgw_acl_swift.cc rgw_auth.cc + rgw_auth_keystone.cc rgw_auth_s3.cc rgw_basic_types.cc rgw_bucket.cc diff --git a/src/rgw/rgw_auth_keystone.cc b/src/rgw/rgw_auth_keystone.cc new file mode 100644 index 00000000000..e80b4d93a97 --- /dev/null +++ b/src/rgw/rgw_auth_keystone.cc @@ -0,0 +1,278 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + +#include +#include + +#include +#include + + +#include "common/errno.h" +#include "common/ceph_json.h" +#include "include/types.h" +#include "include/str_list.h" + +#include "rgw_common.h" +#include "rgw_keystone.h" +#include "rgw_auth_keystone.h" +#include "rgw_keystone.h" + +#include "common/ceph_crypto_cms.h" +#include "common/armor.h" +#include "common/Cond.h" + +#define dout_subsys ceph_subsys_rgw + + +namespace rgw { +namespace auth { +namespace keystone { + +bool +TokenEngine::is_applicable(const std::string& token) const noexcept +{ + return ! token.empty() && ! cct->_conf->rgw_keystone_url.empty(); +} + +TokenEngine::token_envelope_t +TokenEngine::decode_pki_token(const std::string& token) const +{ + ceph::buffer::list token_body_bl; + int ret = rgw_decode_b64_cms(cct, token, token_body_bl); + if (ret < 0) { + ldout(cct, 20) << "cannot decode pki token" << dendl; + throw ret; + } else { + ldout(cct, 20) << "successfully decoded pki token" << dendl; + } + + TokenEngine::token_envelope_t token_body; + ret = token_body.parse(cct, token, token_body_bl, config.get_api_version()); + if (ret < 0) { + throw ret; + } + + return token_body; +} + +TokenEngine::token_envelope_t +TokenEngine::get_from_keystone(const std::string& token) const +{ + using RGWValidateKeystoneToken + = rgw::keystone::Service::RGWValidateKeystoneToken; + + /* The container for plain response obtained from Keystone. It will be + * parsed token_envelope_t::parse method. */ + ceph::bufferlist token_body_bl; + RGWValidateKeystoneToken validate(cct, &token_body_bl); + + std::string url = config.get_endpoint_url(); + if (url.empty()) { + throw -EINVAL; + } + + const auto keystone_version = config.get_api_version(); + if (keystone_version == rgw::keystone::ApiVersion::VER_2) { + url.append("v2.0/tokens/" + token); + } else if (keystone_version == rgw::keystone::ApiVersion::VER_3) { + url.append("v3/auth/tokens"); + validate.append_header("X-Subject-Token", token); + } + + std::string admin_token; + if (rgw::keystone::Service::get_admin_token(cct, token_cache, config, + admin_token) < 0) { + throw -EINVAL; + } + + validate.append_header("X-Auth-Token", admin_token); + validate.set_send_length(0); + + int ret = validate.process(url.c_str()); + if (ret < 0) { + throw ret; + } + + /* NULL terminate for debug output. */ + token_body_bl.append(static_cast(0)); + ldout(cct, 20) << "received response status=" << validate.get_http_status() + << ", body=" << token_body_bl.c_str() << dendl; + + /* Detect Keystone rejection earlier than during the token parsing. + * Although failure at the parsing phase doesn't impose a threat, + * this allows to return proper error code (EACCESS instead of EINVAL + * or similar) and thus improves logging. */ + if (validate.get_http_status() == + /* Most likely: wrong admin credentials or admin token. */ + RGWValidateKeystoneToken::HTTP_STATUS_UNAUTHORIZED || + validate.get_http_status() == + /* Most likely: non-existent token supplied by the client. */ + RGWValidateKeystoneToken::HTTP_STATUS_NOTFOUND) { + throw -EACCES; + } + + TokenEngine::token_envelope_t token_body; + ret = token_body.parse(cct, token, token_body_bl, config.get_api_version()); + if (ret < 0) { + throw ret; + } + + return token_body; +} + +TokenEngine::auth_info_t +TokenEngine::get_creds_info(const TokenEngine::token_envelope_t& token, + const std::vector& admin_roles + ) const noexcept +{ + using acct_privilege_t = rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t; + + /* Check whether the user has an admin status. */ + acct_privilege_t level = acct_privilege_t::IS_PLAIN_ACCT; + for (const auto& admin_role : admin_roles) { + if (token.has_role(admin_role)) { + level = acct_privilege_t::IS_ADMIN_ACCT; + break; + } + } + + return auth_info_t { + /* Suggested account name for the authenticated user. */ + rgw_user(token.get_project_id()), + /* User's display name (aka real name). */ + token.get_project_name(), + /* Keystone doesn't support RGW's subuser concept, so we cannot cut down + * the access rights through the perm_mask. At least at this layer. */ + RGW_PERM_FULL_CONTROL, + level, + TYPE_KEYSTONE, + }; +} + +static inline const std::string +make_spec_item(const std::string& tenant, const std::string& id) +{ + return tenant + ":" + id; +} + +TokenEngine::acl_strategy_t +TokenEngine::get_acl_strategy(const TokenEngine::token_envelope_t& token) const +{ + /* The primary identity is constructed upon UUIDs. */ + const auto& tenant_uuid = token.get_project_id(); + const auto& user_uuid = token.get_user_id(); + + /* For Keystone v2 an alias may be also used. */ + const auto& tenant_name = token.get_project_name(); + const auto& user_name = token.get_user_name(); + + /* Construct all possible combinations including Swift's wildcards. */ + const std::array allowed_items = { + make_spec_item(tenant_uuid, user_uuid), + make_spec_item(tenant_name, user_name), + + /* Wildcards. */ + make_spec_item(tenant_uuid, "*"), + make_spec_item(tenant_name, "*"), + make_spec_item("*", user_uuid), + make_spec_item("*", user_name), + }; + + /* Lambda will obtain a copy of (not a reference to!) allowed_items. */ + return [allowed_items](const RGWIdentityApplier::aclspec_t& aclspec) { + uint32_t perm = 0; + + for (const auto& allowed_item : allowed_items) { + const auto iter = aclspec.find(allowed_item); + + if (std::end(aclspec) != iter) { + perm |= iter->second; + } + } + + return perm; + }; +} + +TokenEngine::result_t +TokenEngine::authenticate(const std::string& token) const +{ + TokenEngine::token_envelope_t t; + + /* This will be initialized on the first call to this method. In C++11 it's + * also thread-safe. */ + static const struct RolesCacher { + RolesCacher(CephContext* const cct) { + get_str_vec(cct->_conf->rgw_keystone_accepted_roles, plain); + get_str_vec(cct->_conf->rgw_keystone_accepted_admin_roles, admin); + + /* Let's suppose that having an admin role implies also a regular one. */ + plain.insert(std::end(plain), std::begin(admin), std::end(admin)); + } + + std::vector plain; + std::vector admin; + } roles(cct); + + if (! is_applicable(token)) { + return result_t::deny(); + } + + /* Token ID is a concept that makes dealing with PKI tokens more effective. + * Instead of storing several kilobytes, a short hash can be burried. */ + const auto& token_id = rgw_get_token_id(token); + ldout(cct, 20) << "token_id=" << token_id << dendl; + + /* Check cache first. */ + if (RGWKeystoneTokenCache::get_instance().find(token_id, t)) { + ldout(cct, 20) << "cached token.project.id=" << t.get_project_id() + << dendl; + auto apl = apl_factory->create_apl_remote(cct, get_acl_strategy(t), + get_creds_info(t, roles.admin)); + return std::make_pair(std::move(apl), nullptr); + } + + /* Retrieve token. */ + if (rgw_is_pki_token(token)) { + try { + t = decode_pki_token(token); + } catch (...) { + /* Last resort. */ + t = get_from_keystone(token); + } + } else { + /* Can't decode, just go to the Keystone server for validation. */ + t = get_from_keystone(token); + } + + /* Verify expiration. */ + if (t.expired()) { + ldout(cct, 0) << "got expired token: " << t.get_project_name() + << ":" << t.get_user_name() + << " expired: " << t.get_expires() << dendl; + return std::make_pair(nullptr, nullptr); + } + + /* Check for necessary roles. */ + for (const auto& role : roles.plain) { + if (t.has_role(role) == true) { + ldout(cct, 0) << "validated token: " << t.get_project_name() + << ":" << t.get_user_name() + << " expires: " << t.get_expires() << dendl; + RGWKeystoneTokenCache::get_instance().add(token_id, t); + auto apl = apl_factory->create_apl_remote(cct, get_acl_strategy(t), + get_creds_info(t, roles.admin)); + return std::make_pair(std::move(apl), nullptr); + } + } + + ldout(cct, 0) << "user does not hold a matching role; required roles: " + << g_conf->rgw_keystone_accepted_roles << dendl; + + return std::make_pair(nullptr, nullptr); +} + +}; /* namespace keystone */ +}; /* namespace auth */ +}; /* namespace rgw */ diff --git a/src/rgw/rgw_auth_keystone.h b/src/rgw/rgw_auth_keystone.h new file mode 100644 index 00000000000..8d6ea25799f --- /dev/null +++ b/src/rgw/rgw_auth_keystone.h @@ -0,0 +1,66 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab + + +#ifndef CEPH_RGW_AUTH_KEYSTONE_H +#define CEPH_RGW_AUTH_KEYSTONE_H + +#include "rgw_auth.h" +#include "rgw_common.h" +#include "rgw_keystone.h" + +namespace rgw { +namespace auth { +namespace keystone { + +/* Dedicated namespace for Keystone-related auth engines. We need it because + * Keystone offers three different authentication mechanisms (token, EC2 and + * regular user/pass). RadosGW actually does support the first two. */ + +class TokenEngine : public rgw::auth::Engine { + CephContext* const cct; + + using acl_strategy_t = rgw::auth::RemoteApplier::acl_strategy_t; + using auth_info_t = rgw::auth::RemoteApplier::AuthInfo; + using result_t = rgw::auth::Engine::result_t; + using token_envelope_t = rgw::keystone::TokenEnvelope; + + const rgw::auth::TokenExtractor* const extractor; + const rgw::auth::RemoteApplier::Factory* const apl_factory; + rgw::keystone::Config& config; + + /* Helper methods. */ + bool is_applicable(const std::string& token) const noexcept; + token_envelope_t decode_pki_token(const std::string& token) const; + token_envelope_t get_from_keystone(const std::string& token) const; + acl_strategy_t get_acl_strategy(const token_envelope_t& token) const; + auth_info_t get_creds_info(const token_envelope_t& token, + const std::vector& admin_roles + ) const noexcept; + result_t authenticate(const std::string& token) const; + +public: + TokenEngine(CephContext* const cct, + const rgw::auth::TokenExtractor* const extractor, + const rgw::auth::RemoteApplier::Factory* const apl_factory, + rgw::keystone::Config& config) + : cct(cct), + extractor(extractor), + apl_factory(apl_factory), + config(config) { + } + + const char* get_name() const noexcept override { + return "rgw::auth::keystone::TokenEngine"; + } + + result_t authenticate(const req_state* const s) const override { + return authenticate(extractor->get_token(s)); + } +}; /* class TokenEngine */ + +}; /* namespace keystone */ +}; /* namespace auth */ +}; /* namespace rgw */ + +#endif /* CEPH_RGW_AUTH_KEYSTONE_H */ -- 2.39.5