// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*- // vim: ts=8 sw=2 smarttab ft=cpp #include #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 "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(); 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::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& tokens, std::list& tokens_lru) { ceph_assert(ceph_mutex_is_locked_by_me(lock)); map::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& tokens, std::list& tokens_lru) { ceph_assert(ceph_mutex_is_locked_by_me(lock)); map::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::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::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(); }