summaryrefslogtreecommitdiffstats
path: root/src/rgw/rgw_keystone.cc
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/rgw/rgw_keystone.cc684
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();
+}
+
+