diff options
Diffstat (limited to '')
-rw-r--r-- | src/rgw/rgw_keystone.cc | 684 |
1 files changed, 684 insertions, 0 deletions
diff --git a/src/rgw/rgw_keystone.cc b/src/rgw/rgw_keystone.cc new file mode 100644 index 000000000..2df417bd0 --- /dev/null +++ b/src/rgw/rgw_keystone.cc @@ -0,0 +1,684 @@ +// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- +// vim: ts=8 sw=2 smarttab ft=cpp + +#include <errno.h> +#include <fnmatch.h> + +#include <boost/algorithm/string/predicate.hpp> +#include <boost/algorithm/string.hpp> +#include <fstream> + +#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 "common/armor.h" +#include "common/Cond.h" +#include "rgw_perf_counters.h" + +#define dout_context g_ceph_context +#define dout_subsys ceph_subsys_rgw +#define PKI_ANS1_PREFIX "MII" + +using namespace std; + +bool rgw_is_pki_token(const string& token) +{ + return token.compare(0, sizeof(PKI_ANS1_PREFIX) - 1, PKI_ANS1_PREFIX) == 0; +} + +void rgw_get_token_id(const string& token, string& token_id) +{ + if (!rgw_is_pki_token(token)) { + token_id = token; + return; + } + + unsigned char m[CEPH_CRYPTO_MD5_DIGESTSIZE]; + + MD5 hash; + // Allow use of MD5 digest in FIPS mode for non-cryptographic purposes + hash.SetFlags(EVP_MD_CTX_FLAG_NON_FIPS_ALLOW); + hash.Update((const unsigned char *)token.c_str(), token.size()); + hash.Final(m); + + char calc_md5[CEPH_CRYPTO_MD5_DIGESTSIZE * 2 + 1]; + buf_to_hex(m, CEPH_CRYPTO_MD5_DIGESTSIZE, calc_md5); + token_id = calc_md5; +} + + +namespace rgw { +namespace keystone { + +ApiVersion CephCtxConfig::get_api_version() const noexcept +{ + switch (g_ceph_context->_conf->rgw_keystone_api_version) { + case 3: + return ApiVersion::VER_3; + case 2: + return ApiVersion::VER_2; + default: + dout(0) << "ERROR: wrong Keystone API version: " + << g_ceph_context->_conf->rgw_keystone_api_version + << "; falling back to v2" << dendl; + return ApiVersion::VER_2; + } +} + +std::string CephCtxConfig::get_endpoint_url() const noexcept +{ + static const std::string url = g_ceph_context->_conf->rgw_keystone_url; + + if (url.empty() || boost::algorithm::ends_with(url, "/")) { + return url; + } else { + static const std::string url_normalised = url + '/'; + return url_normalised; + } +} + +/* secrets */ +const std::string CephCtxConfig::empty{""}; + +static inline std::string read_secret(const std::string& file_path) +{ + using namespace std; + + constexpr int16_t size{1024}; + char buf[size]; + string s; + + s.reserve(size); + ifstream ifs(file_path, ios::in | ios::binary); + if (ifs) { + while (true) { + auto sbuf = ifs.rdbuf(); + auto len = sbuf->sgetn(buf, size); + if (!len) + break; + s.append(buf, len); + } + boost::algorithm::trim(s); + if (s.back() == '\n') + s.pop_back(); + } + return s; +} + +std::string CephCtxConfig::get_admin_token() const noexcept +{ + auto& atv = g_ceph_context->_conf->rgw_keystone_admin_token_path; + if (!atv.empty()) { + return read_secret(atv); + } else { + auto& atv = g_ceph_context->_conf->rgw_keystone_admin_token; + if (!atv.empty()) { + return atv; + } + } + return empty; +} + +std::string CephCtxConfig::get_admin_password() const noexcept { + auto& apv = g_ceph_context->_conf->rgw_keystone_admin_password_path; + if (!apv.empty()) { + return read_secret(apv); + } else { + auto& apv = g_ceph_context->_conf->rgw_keystone_admin_password; + if (!apv.empty()) { + return apv; + } + } + return empty; +} + +int Service::get_admin_token(const DoutPrefixProvider *dpp, + CephContext* const cct, + TokenCache& token_cache, + const Config& config, + std::string& token) +{ + /* Let's check whether someone uses the deprecated "admin token" feauture + * based on a shared secret from keystone.conf file. */ + const auto& admin_token = config.get_admin_token(); + if (! admin_token.empty()) { + token = std::string(admin_token.data(), admin_token.length()); + return 0; + } + + TokenEnvelope t; + + /* Try cache first before calling Keystone for a new admin token. */ + if (token_cache.find_admin(t)) { + ldpp_dout(dpp, 20) << "found cached admin token" << dendl; + token = t.token.id; + return 0; + } + + /* Call Keystone now. */ + const auto ret = issue_admin_token_request(dpp, cct, config, t); + if (! ret) { + token_cache.add_admin(t); + token = t.token.id; + } + + return ret; +} + +int Service::issue_admin_token_request(const DoutPrefixProvider *dpp, + CephContext* const cct, + const Config& config, + TokenEnvelope& t) +{ + std::string token_url = config.get_endpoint_url(); + if (token_url.empty()) { + return -EINVAL; + } + + bufferlist token_bl; + RGWGetKeystoneAdminToken token_req(cct, "POST", "", &token_bl); + token_req.append_header("Content-Type", "application/json"); + JSONFormatter jf; + + const auto keystone_version = config.get_api_version(); + if (keystone_version == ApiVersion::VER_2) { + AdminTokenRequestVer2 req_serializer(config); + req_serializer.dump(&jf); + + std::stringstream ss; + jf.flush(ss); + token_req.set_post_data(ss.str()); + token_req.set_send_length(ss.str().length()); + token_url.append("v2.0/tokens"); + + } else if (keystone_version == ApiVersion::VER_3) { + AdminTokenRequestVer3 req_serializer(config); + req_serializer.dump(&jf); + + std::stringstream ss; + jf.flush(ss); + token_req.set_post_data(ss.str()); + token_req.set_send_length(ss.str().length()); + token_url.append("v3/auth/tokens"); + } else { + return -ENOTSUP; + } + + token_req.set_url(token_url); + + const int ret = token_req.process(null_yield); + if (ret < 0) { + return ret; + } + + /* Detect rejection earlier than during the token parsing step. */ + if (token_req.get_http_status() == + RGWGetKeystoneAdminToken::HTTP_STATUS_UNAUTHORIZED) { + return -EACCES; + } + + if (t.parse(dpp, cct, token_req.get_subject_token(), token_bl, + keystone_version) != 0) { + return -EINVAL; + } + + return 0; +} + +int Service::get_keystone_barbican_token(const DoutPrefixProvider *dpp, + CephContext * const cct, + std::string& token) +{ + using keystone_config_t = rgw::keystone::CephCtxConfig; + using keystone_cache_t = rgw::keystone::TokenCache; + + auto& config = keystone_config_t::get_instance(); + auto& token_cache = keystone_cache_t::get_instance<keystone_config_t>(); + + std::string token_url = config.get_endpoint_url(); + if (token_url.empty()) { + return -EINVAL; + } + + rgw::keystone::TokenEnvelope t; + + /* Try cache first. */ + if (token_cache.find_barbican(t)) { + ldpp_dout(dpp, 20) << "found cached barbican token" << dendl; + token = t.token.id; + return 0; + } + + bufferlist token_bl; + RGWKeystoneHTTPTransceiver token_req(cct, "POST", "", &token_bl); + token_req.append_header("Content-Type", "application/json"); + JSONFormatter jf; + + const auto keystone_version = config.get_api_version(); + if (keystone_version == ApiVersion::VER_2) { + rgw::keystone::BarbicanTokenRequestVer2 req_serializer(cct); + req_serializer.dump(&jf); + + std::stringstream ss; + jf.flush(ss); + token_req.set_post_data(ss.str()); + token_req.set_send_length(ss.str().length()); + token_url.append("v2.0/tokens"); + + } else if (keystone_version == ApiVersion::VER_3) { + BarbicanTokenRequestVer3 req_serializer(cct); + req_serializer.dump(&jf); + + std::stringstream ss; + jf.flush(ss); + token_req.set_post_data(ss.str()); + token_req.set_send_length(ss.str().length()); + token_url.append("v3/auth/tokens"); + } else { + return -ENOTSUP; + } + + token_req.set_url(token_url); + + ldpp_dout(dpp, 20) << "Requesting secret from barbican url=" << token_url << dendl; + const int ret = token_req.process(null_yield); + if (ret < 0) { + ldpp_dout(dpp, 20) << "Barbican process error:" << token_bl.c_str() << dendl; + return ret; + } + + /* Detect rejection earlier than during the token parsing step. */ + if (token_req.get_http_status() == + RGWKeystoneHTTPTransceiver::HTTP_STATUS_UNAUTHORIZED) { + return -EACCES; + } + + if (t.parse(dpp, cct, token_req.get_subject_token(), token_bl, + keystone_version) != 0) { + return -EINVAL; + } + + token_cache.add_barbican(t); + token = t.token.id; + return 0; +} + + +bool TokenEnvelope::has_role(const std::string& r) const +{ + list<Role>::const_iterator iter; + for (iter = roles.cbegin(); iter != roles.cend(); ++iter) { + if (fnmatch(r.c_str(), ((*iter).name.c_str()), 0) == 0) { + return true; + } + } + return false; +} + +int TokenEnvelope::parse(const DoutPrefixProvider *dpp, + CephContext* const cct, + const std::string& token_str, + ceph::bufferlist& bl, + const ApiVersion version) +{ + JSONParser parser; + if (! parser.parse(bl.c_str(), bl.length())) { + ldpp_dout(dpp, 0) << "Keystone token parse error: malformed json" << dendl; + return -EINVAL; + } + + JSONObjIter token_iter = parser.find_first("token"); + JSONObjIter access_iter = parser.find_first("access"); + + try { + if (version == rgw::keystone::ApiVersion::VER_2) { + if (! access_iter.end()) { + decode_v2(*access_iter); + } else if (! token_iter.end()) { + /* TokenEnvelope structure doesn't follow Identity API v2, so let's + * fallback to v3. Otherwise we can assume it's wrongly formatted. + * The whole mechanism is a workaround for s3_token middleware that + * speaks in v2 disregarding the promise to go with v3. */ + decode_v3(*token_iter); + + /* Identity v3 conveys the token inforamtion not as a part of JSON but + * in the X-Subject-Token HTTP header we're getting from caller. */ + token.id = token_str; + } else { + return -EINVAL; + } + } else if (version == rgw::keystone::ApiVersion::VER_3) { + if (! token_iter.end()) { + decode_v3(*token_iter); + /* v3 suceeded. We have to fill token.id from external input as it + * isn't a part of the JSON response anymore. It has been moved + * to X-Subject-Token HTTP header instead. */ + token.id = token_str; + } else if (! access_iter.end()) { + /* If the token cannot be parsed according to V3, try V2. */ + decode_v2(*access_iter); + } else { + return -EINVAL; + } + } else { + return -ENOTSUP; + } + } catch (const JSONDecoder::err& err) { + ldpp_dout(dpp, 0) << "Keystone token parse error: " << err.what() << dendl; + return -EINVAL; + } + + return 0; +} + +bool TokenCache::find(const std::string& token_id, + rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + return find_locked(token_id, token, tokens, tokens_lru); +} + +bool TokenCache::find_service(const std::string& token_id, + rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + return find_locked(token_id, token, service_tokens, service_tokens_lru); +} + +bool TokenCache::find_locked(const std::string& token_id, rgw::keystone::TokenEnvelope& token, + std::map<std::string, token_entry>& tokens, std::list<std::string>& tokens_lru) +{ + ceph_assert(ceph_mutex_is_locked_by_me(lock)); + map<string, token_entry>::iterator iter = tokens.find(token_id); + if (iter == tokens.end()) { + if (perfcounter) perfcounter->inc(l_rgw_keystone_token_cache_miss); + return false; + } + + token_entry& entry = iter->second; + tokens_lru.erase(entry.lru_iter); + + if (entry.token.expired()) { + tokens.erase(iter); + if (perfcounter) perfcounter->inc(l_rgw_keystone_token_cache_hit); + return false; + } + token = entry.token; + + tokens_lru.push_front(token_id); + entry.lru_iter = tokens_lru.begin(); + + if (perfcounter) perfcounter->inc(l_rgw_keystone_token_cache_hit); + + return true; +} + +bool TokenCache::find_admin(rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + + return find_locked(admin_token_id, token, tokens, tokens_lru); +} + +bool TokenCache::find_barbican(rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + + return find_locked(barbican_token_id, token, tokens, tokens_lru); +} + +void TokenCache::add(const std::string& token_id, + const rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + add_locked(token_id, token, tokens, tokens_lru); +} + +void TokenCache::add_service(const std::string& token_id, + const rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + add_locked(token_id, token, service_tokens, service_tokens_lru); +} + +void TokenCache::add_locked(const std::string& token_id, const rgw::keystone::TokenEnvelope& token, + std::map<std::string, token_entry>& tokens, std::list<std::string>& tokens_lru) +{ + ceph_assert(ceph_mutex_is_locked_by_me(lock)); + map<string, token_entry>::iterator iter = tokens.find(token_id); + if (iter != tokens.end()) { + token_entry& e = iter->second; + tokens_lru.erase(e.lru_iter); + } + + tokens_lru.push_front(token_id); + token_entry& entry = tokens[token_id]; + entry.token = token; + entry.lru_iter = tokens_lru.begin(); + + while (tokens_lru.size() > max) { + list<string>::reverse_iterator riter = tokens_lru.rbegin(); + iter = tokens.find(*riter); + ceph_assert(iter != tokens.end()); + tokens.erase(iter); + tokens_lru.pop_back(); + } +} + +void TokenCache::add_admin(const rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + + rgw_get_token_id(token.token.id, admin_token_id); + add_locked(admin_token_id, token, tokens, tokens_lru); +} + +void TokenCache::add_barbican(const rgw::keystone::TokenEnvelope& token) +{ + std::lock_guard l{lock}; + + rgw_get_token_id(token.token.id, barbican_token_id); + add_locked(barbican_token_id, token, tokens, tokens_lru); +} + +void TokenCache::invalidate(const DoutPrefixProvider *dpp, const std::string& token_id) +{ + std::lock_guard l{lock}; + map<string, token_entry>::iterator iter = tokens.find(token_id); + if (iter == tokens.end()) + return; + + ldpp_dout(dpp, 20) << "invalidating revoked token id=" << token_id << dendl; + token_entry& e = iter->second; + tokens_lru.erase(e.lru_iter); + tokens.erase(iter); +} + +bool TokenCache::going_down() const +{ + return down_flag; +} + +}; /* namespace keystone */ +}; /* namespace rgw */ + +void rgw::keystone::TokenEnvelope::Token::decode_json(JSONObj *obj) +{ + string expires_iso8601; + struct tm t; + + JSONDecoder::decode_json("id", id, obj, true); + JSONDecoder::decode_json("tenant", tenant_v2, obj, true); + JSONDecoder::decode_json("expires", expires_iso8601, obj, true); + + if (parse_iso8601(expires_iso8601.c_str(), &t)) { + expires = internal_timegm(&t); + } else { + expires = 0; + throw JSONDecoder::err("Failed to parse ISO8601 expiration date from Keystone response."); + } +} + +void rgw::keystone::TokenEnvelope::Role::decode_json(JSONObj *obj) +{ + JSONDecoder::decode_json("id", id, obj); + JSONDecoder::decode_json("name", name, obj, true); +} + +void rgw::keystone::TokenEnvelope::Domain::decode_json(JSONObj *obj) +{ + JSONDecoder::decode_json("id", id, obj, true); + JSONDecoder::decode_json("name", name, obj, true); +} + +void rgw::keystone::TokenEnvelope::Project::decode_json(JSONObj *obj) +{ + JSONDecoder::decode_json("id", id, obj, true); + JSONDecoder::decode_json("name", name, obj, true); + JSONDecoder::decode_json("domain", domain, obj); +} + +void rgw::keystone::TokenEnvelope::User::decode_json(JSONObj *obj) +{ + JSONDecoder::decode_json("id", id, obj, true); + JSONDecoder::decode_json("name", name, obj, true); + JSONDecoder::decode_json("domain", domain, obj); + JSONDecoder::decode_json("roles", roles_v2, obj); +} + +void rgw::keystone::TokenEnvelope::decode_v3(JSONObj* const root_obj) +{ + std::string expires_iso8601; + + JSONDecoder::decode_json("user", user, root_obj, true); + JSONDecoder::decode_json("expires_at", expires_iso8601, root_obj, true); + JSONDecoder::decode_json("roles", roles, root_obj, true); + JSONDecoder::decode_json("project", project, root_obj, true); + + struct tm t; + if (parse_iso8601(expires_iso8601.c_str(), &t)) { + token.expires = internal_timegm(&t); + } else { + token.expires = 0; + throw JSONDecoder::err("Failed to parse ISO8601 expiration date" + "from Keystone response."); + } +} + +void rgw::keystone::TokenEnvelope::decode_v2(JSONObj* const root_obj) +{ + JSONDecoder::decode_json("user", user, root_obj, true); + JSONDecoder::decode_json("token", token, root_obj, true); + + roles = user.roles_v2; + project = token.tenant_v2; +} + +/* This utility function shouldn't conflict with the overload of std::to_string + * provided by string_ref since Boost 1.54 as it's defined outside of the std + * namespace. I hope we'll remove it soon - just after merging the Matt's PR + * for bundled Boost. It would allow us to forget that CentOS 7 has Boost 1.53. */ +static inline std::string to_string(const std::string_view& s) +{ + return std::string(s.data(), s.length()); +} + +void rgw::keystone::AdminTokenRequestVer2::dump(Formatter* const f) const +{ + f->open_object_section("token_request"); + f->open_object_section("auth"); + f->open_object_section("passwordCredentials"); + encode_json("username", ::to_string(conf.get_admin_user()), f); + encode_json("password", ::to_string(conf.get_admin_password()), f); + f->close_section(); + encode_json("tenantName", ::to_string(conf.get_admin_tenant()), f); + f->close_section(); + f->close_section(); +} + +void rgw::keystone::AdminTokenRequestVer3::dump(Formatter* const f) const +{ + f->open_object_section("token_request"); + f->open_object_section("auth"); + f->open_object_section("identity"); + f->open_array_section("methods"); + f->dump_string("", "password"); + f->close_section(); + f->open_object_section("password"); + f->open_object_section("user"); + f->open_object_section("domain"); + encode_json("name", ::to_string(conf.get_admin_domain()), f); + f->close_section(); + encode_json("name", ::to_string(conf.get_admin_user()), f); + encode_json("password", ::to_string(conf.get_admin_password()), f); + f->close_section(); + f->close_section(); + f->close_section(); + f->open_object_section("scope"); + f->open_object_section("project"); + if (! conf.get_admin_project().empty()) { + encode_json("name", ::to_string(conf.get_admin_project()), f); + } else { + encode_json("name", ::to_string(conf.get_admin_tenant()), f); + } + f->open_object_section("domain"); + encode_json("name", ::to_string(conf.get_admin_domain()), f); + f->close_section(); + f->close_section(); + f->close_section(); + f->close_section(); + f->close_section(); +} + +void rgw::keystone::BarbicanTokenRequestVer2::dump(Formatter* const f) const +{ + f->open_object_section("token_request"); + f->open_object_section("auth"); + f->open_object_section("passwordCredentials"); + encode_json("username", cct->_conf->rgw_keystone_barbican_user, f); + encode_json("password", cct->_conf->rgw_keystone_barbican_password, f); + f->close_section(); + encode_json("tenantName", cct->_conf->rgw_keystone_barbican_tenant, f); + f->close_section(); + f->close_section(); +} + +void rgw::keystone::BarbicanTokenRequestVer3::dump(Formatter* const f) const +{ + f->open_object_section("token_request"); + f->open_object_section("auth"); + f->open_object_section("identity"); + f->open_array_section("methods"); + f->dump_string("", "password"); + f->close_section(); + f->open_object_section("password"); + f->open_object_section("user"); + f->open_object_section("domain"); + encode_json("name", cct->_conf->rgw_keystone_barbican_domain, f); + f->close_section(); + encode_json("name", cct->_conf->rgw_keystone_barbican_user, f); + encode_json("password", cct->_conf->rgw_keystone_barbican_password, f); + f->close_section(); + f->close_section(); + f->close_section(); + f->open_object_section("scope"); + f->open_object_section("project"); + if (!cct->_conf->rgw_keystone_barbican_project.empty()) { + encode_json("name", cct->_conf->rgw_keystone_barbican_project, f); + } else { + encode_json("name", cct->_conf->rgw_keystone_barbican_tenant, f); + } + f->open_object_section("domain"); + encode_json("name", cct->_conf->rgw_keystone_barbican_domain, f); + f->close_section(); + f->close_section(); + f->close_section(); + f->close_section(); + f->close_section(); +} + + |