]> git.proxmox.com Git - ceph.git/blame - ceph/src/rgw/rgw_keystone.cc
import ceph quincy 17.2.6
[ceph.git] / ceph / src / rgw / rgw_keystone.cc
CommitLineData
7c673cae 1// -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
9f95a23c 2// vim: ts=8 sw=2 smarttab ft=cpp
7c673cae
FG
3
4#include <errno.h>
5#include <fnmatch.h>
6
7#include <boost/algorithm/string/predicate.hpp>
11fdf7f2
TL
8#include <boost/algorithm/string.hpp>
9#include <fstream>
7c673cae
FG
10
11#include "common/errno.h"
12#include "common/ceph_json.h"
13#include "include/types.h"
14#include "include/str_list.h"
15
16#include "rgw_common.h"
17#include "rgw_keystone.h"
7c673cae
FG
18#include "common/armor.h"
19#include "common/Cond.h"
11fdf7f2 20#include "rgw_perf_counters.h"
7c673cae
FG
21
22#define dout_context g_ceph_context
23#define dout_subsys ceph_subsys_rgw
7c673cae
FG
24#define PKI_ANS1_PREFIX "MII"
25
20effc67
TL
26using namespace std;
27
7c673cae
FG
28bool rgw_is_pki_token(const string& token)
29{
30 return token.compare(0, sizeof(PKI_ANS1_PREFIX) - 1, PKI_ANS1_PREFIX) == 0;
31}
32
33void rgw_get_token_id(const string& token, string& token_id)
34{
35 if (!rgw_is_pki_token(token)) {
36 token_id = token;
37 return;
38 }
39
40 unsigned char m[CEPH_CRYPTO_MD5_DIGESTSIZE];
41
42 MD5 hash;
20effc67
TL
43 // Allow use of MD5 digest in FIPS mode for non-cryptographic purposes
44 hash.SetFlags(EVP_MD_CTX_FLAG_NON_FIPS_ALLOW);
11fdf7f2 45 hash.Update((const unsigned char *)token.c_str(), token.size());
7c673cae
FG
46 hash.Final(m);
47
48 char calc_md5[CEPH_CRYPTO_MD5_DIGESTSIZE * 2 + 1];
49 buf_to_hex(m, CEPH_CRYPTO_MD5_DIGESTSIZE, calc_md5);
50 token_id = calc_md5;
51}
52
7c673cae
FG
53
54namespace rgw {
55namespace keystone {
56
57ApiVersion CephCtxConfig::get_api_version() const noexcept
58{
59 switch (g_ceph_context->_conf->rgw_keystone_api_version) {
60 case 3:
61 return ApiVersion::VER_3;
62 case 2:
63 return ApiVersion::VER_2;
64 default:
65 dout(0) << "ERROR: wrong Keystone API version: "
66 << g_ceph_context->_conf->rgw_keystone_api_version
67 << "; falling back to v2" << dendl;
68 return ApiVersion::VER_2;
69 }
70}
71
72std::string CephCtxConfig::get_endpoint_url() const noexcept
73{
74 static const std::string url = g_ceph_context->_conf->rgw_keystone_url;
75
76 if (url.empty() || boost::algorithm::ends_with(url, "/")) {
77 return url;
78 } else {
79 static const std::string url_normalised = url + '/';
80 return url_normalised;
81 }
82}
83
11fdf7f2
TL
84/* secrets */
85const std::string CephCtxConfig::empty{""};
86
87static inline std::string read_secret(const std::string& file_path)
88{
89 using namespace std;
90
91 constexpr int16_t size{1024};
92 char buf[size];
93 string s;
94
95 s.reserve(size);
96 ifstream ifs(file_path, ios::in | ios::binary);
97 if (ifs) {
98 while (true) {
99 auto sbuf = ifs.rdbuf();
100 auto len = sbuf->sgetn(buf, size);
101 if (!len)
102 break;
103 s.append(buf, len);
104 }
105 boost::algorithm::trim(s);
106 if (s.back() == '\n')
107 s.pop_back();
108 }
109 return s;
110}
111
112std::string CephCtxConfig::get_admin_token() const noexcept
113{
114 auto& atv = g_ceph_context->_conf->rgw_keystone_admin_token_path;
115 if (!atv.empty()) {
116 return read_secret(atv);
117 } else {
118 auto& atv = g_ceph_context->_conf->rgw_keystone_admin_token;
119 if (!atv.empty()) {
120 return atv;
121 }
122 }
123 return empty;
124}
125
126std::string CephCtxConfig::get_admin_password() const noexcept {
127 auto& apv = g_ceph_context->_conf->rgw_keystone_admin_password_path;
128 if (!apv.empty()) {
129 return read_secret(apv);
130 } else {
131 auto& apv = g_ceph_context->_conf->rgw_keystone_admin_password;
132 if (!apv.empty()) {
133 return apv;
134 }
135 }
136 return empty;
137}
138
20effc67
TL
139int Service::get_admin_token(const DoutPrefixProvider *dpp,
140 CephContext* const cct,
7c673cae
FG
141 TokenCache& token_cache,
142 const Config& config,
143 std::string& token)
144{
145 /* Let's check whether someone uses the deprecated "admin token" feauture
146 * based on a shared secret from keystone.conf file. */
147 const auto& admin_token = config.get_admin_token();
148 if (! admin_token.empty()) {
149 token = std::string(admin_token.data(), admin_token.length());
150 return 0;
151 }
152
153 TokenEnvelope t;
154
155 /* Try cache first before calling Keystone for a new admin token. */
156 if (token_cache.find_admin(t)) {
20effc67 157 ldpp_dout(dpp, 20) << "found cached admin token" << dendl;
7c673cae
FG
158 token = t.token.id;
159 return 0;
160 }
161
162 /* Call Keystone now. */
20effc67 163 const auto ret = issue_admin_token_request(dpp, cct, config, t);
7c673cae
FG
164 if (! ret) {
165 token_cache.add_admin(t);
166 token = t.token.id;
167 }
168
169 return ret;
170}
171
20effc67
TL
172int Service::issue_admin_token_request(const DoutPrefixProvider *dpp,
173 CephContext* const cct,
7c673cae
FG
174 const Config& config,
175 TokenEnvelope& t)
176{
177 std::string token_url = config.get_endpoint_url();
178 if (token_url.empty()) {
179 return -EINVAL;
180 }
181
182 bufferlist token_bl;
11fdf7f2 183 RGWGetKeystoneAdminToken token_req(cct, "POST", "", &token_bl);
7c673cae
FG
184 token_req.append_header("Content-Type", "application/json");
185 JSONFormatter jf;
186
187 const auto keystone_version = config.get_api_version();
188 if (keystone_version == ApiVersion::VER_2) {
189 AdminTokenRequestVer2 req_serializer(config);
190 req_serializer.dump(&jf);
191
192 std::stringstream ss;
193 jf.flush(ss);
194 token_req.set_post_data(ss.str());
195 token_req.set_send_length(ss.str().length());
196 token_url.append("v2.0/tokens");
197
198 } else if (keystone_version == ApiVersion::VER_3) {
199 AdminTokenRequestVer3 req_serializer(config);
200 req_serializer.dump(&jf);
201
202 std::stringstream ss;
203 jf.flush(ss);
204 token_req.set_post_data(ss.str());
205 token_req.set_send_length(ss.str().length());
206 token_url.append("v3/auth/tokens");
207 } else {
208 return -ENOTSUP;
209 }
210
11fdf7f2
TL
211 token_req.set_url(token_url);
212
9f95a23c 213 const int ret = token_req.process(null_yield);
7c673cae
FG
214 if (ret < 0) {
215 return ret;
216 }
217
218 /* Detect rejection earlier than during the token parsing step. */
219 if (token_req.get_http_status() ==
220 RGWGetKeystoneAdminToken::HTTP_STATUS_UNAUTHORIZED) {
221 return -EACCES;
222 }
223
20effc67 224 if (t.parse(dpp, cct, token_req.get_subject_token(), token_bl,
7c673cae
FG
225 keystone_version) != 0) {
226 return -EINVAL;
227 }
228
229 return 0;
230}
231
20effc67
TL
232int Service::get_keystone_barbican_token(const DoutPrefixProvider *dpp,
233 CephContext * const cct,
7c673cae
FG
234 std::string& token)
235{
236 using keystone_config_t = rgw::keystone::CephCtxConfig;
237 using keystone_cache_t = rgw::keystone::TokenCache;
238
239 auto& config = keystone_config_t::get_instance();
240 auto& token_cache = keystone_cache_t::get_instance<keystone_config_t>();
241
242 std::string token_url = config.get_endpoint_url();
243 if (token_url.empty()) {
244 return -EINVAL;
245 }
246
247 rgw::keystone::TokenEnvelope t;
248
249 /* Try cache first. */
250 if (token_cache.find_barbican(t)) {
20effc67 251 ldpp_dout(dpp, 20) << "found cached barbican token" << dendl;
7c673cae
FG
252 token = t.token.id;
253 return 0;
254 }
255
256 bufferlist token_bl;
11fdf7f2 257 RGWKeystoneHTTPTransceiver token_req(cct, "POST", "", &token_bl);
7c673cae
FG
258 token_req.append_header("Content-Type", "application/json");
259 JSONFormatter jf;
260
261 const auto keystone_version = config.get_api_version();
262 if (keystone_version == ApiVersion::VER_2) {
263 rgw::keystone::BarbicanTokenRequestVer2 req_serializer(cct);
264 req_serializer.dump(&jf);
265
266 std::stringstream ss;
267 jf.flush(ss);
268 token_req.set_post_data(ss.str());
269 token_req.set_send_length(ss.str().length());
270 token_url.append("v2.0/tokens");
271
272 } else if (keystone_version == ApiVersion::VER_3) {
273 BarbicanTokenRequestVer3 req_serializer(cct);
274 req_serializer.dump(&jf);
275
276 std::stringstream ss;
277 jf.flush(ss);
278 token_req.set_post_data(ss.str());
279 token_req.set_send_length(ss.str().length());
280 token_url.append("v3/auth/tokens");
281 } else {
282 return -ENOTSUP;
283 }
284
11fdf7f2
TL
285 token_req.set_url(token_url);
286
20effc67 287 ldpp_dout(dpp, 20) << "Requesting secret from barbican url=" << token_url << dendl;
9f95a23c 288 const int ret = token_req.process(null_yield);
7c673cae 289 if (ret < 0) {
20effc67 290 ldpp_dout(dpp, 20) << "Barbican process error:" << token_bl.c_str() << dendl;
7c673cae
FG
291 return ret;
292 }
293
294 /* Detect rejection earlier than during the token parsing step. */
295 if (token_req.get_http_status() ==
296 RGWKeystoneHTTPTransceiver::HTTP_STATUS_UNAUTHORIZED) {
297 return -EACCES;
298 }
299
20effc67 300 if (t.parse(dpp, cct, token_req.get_subject_token(), token_bl,
7c673cae
FG
301 keystone_version) != 0) {
302 return -EINVAL;
303 }
304
305 token_cache.add_barbican(t);
306 token = t.token.id;
307 return 0;
308}
309
310
311bool TokenEnvelope::has_role(const std::string& r) const
312{
313 list<Role>::const_iterator iter;
314 for (iter = roles.cbegin(); iter != roles.cend(); ++iter) {
315 if (fnmatch(r.c_str(), ((*iter).name.c_str()), 0) == 0) {
316 return true;
317 }
318 }
319 return false;
320}
321
20effc67
TL
322int TokenEnvelope::parse(const DoutPrefixProvider *dpp,
323 CephContext* const cct,
7c673cae
FG
324 const std::string& token_str,
325 ceph::bufferlist& bl,
326 const ApiVersion version)
327{
328 JSONParser parser;
329 if (! parser.parse(bl.c_str(), bl.length())) {
20effc67 330 ldpp_dout(dpp, 0) << "Keystone token parse error: malformed json" << dendl;
7c673cae
FG
331 return -EINVAL;
332 }
333
334 JSONObjIter token_iter = parser.find_first("token");
335 JSONObjIter access_iter = parser.find_first("access");
336
337 try {
338 if (version == rgw::keystone::ApiVersion::VER_2) {
339 if (! access_iter.end()) {
340 decode_v2(*access_iter);
341 } else if (! token_iter.end()) {
342 /* TokenEnvelope structure doesn't follow Identity API v2, so let's
343 * fallback to v3. Otherwise we can assume it's wrongly formatted.
344 * The whole mechanism is a workaround for s3_token middleware that
345 * speaks in v2 disregarding the promise to go with v3. */
346 decode_v3(*token_iter);
347
348 /* Identity v3 conveys the token inforamtion not as a part of JSON but
349 * in the X-Subject-Token HTTP header we're getting from caller. */
350 token.id = token_str;
351 } else {
352 return -EINVAL;
353 }
354 } else if (version == rgw::keystone::ApiVersion::VER_3) {
355 if (! token_iter.end()) {
356 decode_v3(*token_iter);
357 /* v3 suceeded. We have to fill token.id from external input as it
358 * isn't a part of the JSON response anymore. It has been moved
359 * to X-Subject-Token HTTP header instead. */
360 token.id = token_str;
361 } else if (! access_iter.end()) {
362 /* If the token cannot be parsed according to V3, try V2. */
363 decode_v2(*access_iter);
364 } else {
365 return -EINVAL;
366 }
367 } else {
368 return -ENOTSUP;
369 }
9f95a23c 370 } catch (const JSONDecoder::err& err) {
20effc67 371 ldpp_dout(dpp, 0) << "Keystone token parse error: " << err.what() << dendl;
7c673cae
FG
372 return -EINVAL;
373 }
374
375 return 0;
376}
377
378bool TokenCache::find(const std::string& token_id,
379 rgw::keystone::TokenEnvelope& token)
380{
9f95a23c 381 std::lock_guard l{lock};
7c673cae
FG
382 return find_locked(token_id, token);
383}
384
385bool TokenCache::find_locked(const std::string& token_id,
386 rgw::keystone::TokenEnvelope& token)
387{
9f95a23c 388 ceph_assert(ceph_mutex_is_locked_by_me(lock));
7c673cae
FG
389 map<string, token_entry>::iterator iter = tokens.find(token_id);
390 if (iter == tokens.end()) {
391 if (perfcounter) perfcounter->inc(l_rgw_keystone_token_cache_miss);
392 return false;
393 }
394
395 token_entry& entry = iter->second;
396 tokens_lru.erase(entry.lru_iter);
397
398 if (entry.token.expired()) {
399 tokens.erase(iter);
400 if (perfcounter) perfcounter->inc(l_rgw_keystone_token_cache_hit);
401 return false;
402 }
403 token = entry.token;
404
405 tokens_lru.push_front(token_id);
406 entry.lru_iter = tokens_lru.begin();
407
408 if (perfcounter) perfcounter->inc(l_rgw_keystone_token_cache_hit);
409
410 return true;
411}
412
413bool TokenCache::find_admin(rgw::keystone::TokenEnvelope& token)
414{
9f95a23c 415 std::lock_guard l{lock};
7c673cae
FG
416
417 return find_locked(admin_token_id, token);
418}
419
420bool TokenCache::find_barbican(rgw::keystone::TokenEnvelope& token)
421{
9f95a23c 422 std::lock_guard l{lock};
7c673cae
FG
423
424 return find_locked(barbican_token_id, token);
425}
426
427void TokenCache::add(const std::string& token_id,
428 const rgw::keystone::TokenEnvelope& token)
429{
9f95a23c 430 std::lock_guard l{lock};
7c673cae
FG
431 add_locked(token_id, token);
432}
433
434void TokenCache::add_locked(const std::string& token_id,
435 const rgw::keystone::TokenEnvelope& token)
436{
9f95a23c 437 ceph_assert(ceph_mutex_is_locked_by_me(lock));
7c673cae
FG
438 map<string, token_entry>::iterator iter = tokens.find(token_id);
439 if (iter != tokens.end()) {
440 token_entry& e = iter->second;
441 tokens_lru.erase(e.lru_iter);
442 }
443
444 tokens_lru.push_front(token_id);
445 token_entry& entry = tokens[token_id];
446 entry.token = token;
447 entry.lru_iter = tokens_lru.begin();
448
449 while (tokens_lru.size() > max) {
450 list<string>::reverse_iterator riter = tokens_lru.rbegin();
451 iter = tokens.find(*riter);
11fdf7f2 452 ceph_assert(iter != tokens.end());
7c673cae
FG
453 tokens.erase(iter);
454 tokens_lru.pop_back();
455 }
456}
457
458void TokenCache::add_admin(const rgw::keystone::TokenEnvelope& token)
459{
9f95a23c 460 std::lock_guard l{lock};
7c673cae
FG
461
462 rgw_get_token_id(token.token.id, admin_token_id);
463 add_locked(admin_token_id, token);
464}
465
466void TokenCache::add_barbican(const rgw::keystone::TokenEnvelope& token)
467{
9f95a23c 468 std::lock_guard l{lock};
7c673cae
FG
469
470 rgw_get_token_id(token.token.id, barbican_token_id);
471 add_locked(barbican_token_id, token);
472}
473
20effc67 474void TokenCache::invalidate(const DoutPrefixProvider *dpp, const std::string& token_id)
7c673cae 475{
9f95a23c 476 std::lock_guard l{lock};
7c673cae
FG
477 map<string, token_entry>::iterator iter = tokens.find(token_id);
478 if (iter == tokens.end())
479 return;
480
20effc67 481 ldpp_dout(dpp, 20) << "invalidating revoked token id=" << token_id << dendl;
7c673cae
FG
482 token_entry& e = iter->second;
483 tokens_lru.erase(e.lru_iter);
484 tokens.erase(iter);
485}
486
7c673cae
FG
487bool TokenCache::going_down() const
488{
489 return down_flag;
490}
491
7c673cae
FG
492}; /* namespace keystone */
493}; /* namespace rgw */
20effc67
TL
494
495void rgw::keystone::TokenEnvelope::Token::decode_json(JSONObj *obj)
496{
497 string expires_iso8601;
498 struct tm t;
499
500 JSONDecoder::decode_json("id", id, obj, true);
501 JSONDecoder::decode_json("tenant", tenant_v2, obj, true);
502 JSONDecoder::decode_json("expires", expires_iso8601, obj, true);
503
504 if (parse_iso8601(expires_iso8601.c_str(), &t)) {
505 expires = internal_timegm(&t);
506 } else {
507 expires = 0;
508 throw JSONDecoder::err("Failed to parse ISO8601 expiration date from Keystone response.");
509 }
510}
511
512void rgw::keystone::TokenEnvelope::Role::decode_json(JSONObj *obj)
513{
514 JSONDecoder::decode_json("id", id, obj);
515 JSONDecoder::decode_json("name", name, obj, true);
516}
517
518void rgw::keystone::TokenEnvelope::Domain::decode_json(JSONObj *obj)
519{
520 JSONDecoder::decode_json("id", id, obj, true);
521 JSONDecoder::decode_json("name", name, obj, true);
522}
523
524void rgw::keystone::TokenEnvelope::Project::decode_json(JSONObj *obj)
525{
526 JSONDecoder::decode_json("id", id, obj, true);
527 JSONDecoder::decode_json("name", name, obj, true);
528 JSONDecoder::decode_json("domain", domain, obj);
529}
530
531void rgw::keystone::TokenEnvelope::User::decode_json(JSONObj *obj)
532{
533 JSONDecoder::decode_json("id", id, obj, true);
534 JSONDecoder::decode_json("name", name, obj, true);
535 JSONDecoder::decode_json("domain", domain, obj);
536 JSONDecoder::decode_json("roles", roles_v2, obj);
537}
538
539void rgw::keystone::TokenEnvelope::decode_v3(JSONObj* const root_obj)
540{
541 std::string expires_iso8601;
542
543 JSONDecoder::decode_json("user", user, root_obj, true);
544 JSONDecoder::decode_json("expires_at", expires_iso8601, root_obj, true);
545 JSONDecoder::decode_json("roles", roles, root_obj, true);
546 JSONDecoder::decode_json("project", project, root_obj, true);
547
548 struct tm t;
549 if (parse_iso8601(expires_iso8601.c_str(), &t)) {
550 token.expires = internal_timegm(&t);
551 } else {
552 token.expires = 0;
553 throw JSONDecoder::err("Failed to parse ISO8601 expiration date"
554 "from Keystone response.");
555 }
556}
557
558void rgw::keystone::TokenEnvelope::decode_v2(JSONObj* const root_obj)
559{
560 JSONDecoder::decode_json("user", user, root_obj, true);
561 JSONDecoder::decode_json("token", token, root_obj, true);
562
563 roles = user.roles_v2;
564 project = token.tenant_v2;
565}
566
567/* This utility function shouldn't conflict with the overload of std::to_string
568 * provided by string_ref since Boost 1.54 as it's defined outside of the std
569 * namespace. I hope we'll remove it soon - just after merging the Matt's PR
570 * for bundled Boost. It would allow us to forget that CentOS 7 has Boost 1.53. */
571static inline std::string to_string(const std::string_view& s)
572{
573 return std::string(s.data(), s.length());
574}
575
576void rgw::keystone::AdminTokenRequestVer2::dump(Formatter* const f) const
577{
578 f->open_object_section("token_request");
579 f->open_object_section("auth");
580 f->open_object_section("passwordCredentials");
581 encode_json("username", ::to_string(conf.get_admin_user()), f);
582 encode_json("password", ::to_string(conf.get_admin_password()), f);
583 f->close_section();
584 encode_json("tenantName", ::to_string(conf.get_admin_tenant()), f);
585 f->close_section();
586 f->close_section();
587}
588
589void rgw::keystone::AdminTokenRequestVer3::dump(Formatter* const f) const
590{
591 f->open_object_section("token_request");
592 f->open_object_section("auth");
593 f->open_object_section("identity");
594 f->open_array_section("methods");
595 f->dump_string("", "password");
596 f->close_section();
597 f->open_object_section("password");
598 f->open_object_section("user");
599 f->open_object_section("domain");
600 encode_json("name", ::to_string(conf.get_admin_domain()), f);
601 f->close_section();
602 encode_json("name", ::to_string(conf.get_admin_user()), f);
603 encode_json("password", ::to_string(conf.get_admin_password()), f);
604 f->close_section();
605 f->close_section();
606 f->close_section();
607 f->open_object_section("scope");
608 f->open_object_section("project");
609 if (! conf.get_admin_project().empty()) {
610 encode_json("name", ::to_string(conf.get_admin_project()), f);
611 } else {
612 encode_json("name", ::to_string(conf.get_admin_tenant()), f);
613 }
614 f->open_object_section("domain");
615 encode_json("name", ::to_string(conf.get_admin_domain()), f);
616 f->close_section();
617 f->close_section();
618 f->close_section();
619 f->close_section();
620 f->close_section();
621}
622
623void rgw::keystone::BarbicanTokenRequestVer2::dump(Formatter* const f) const
624{
625 f->open_object_section("token_request");
626 f->open_object_section("auth");
627 f->open_object_section("passwordCredentials");
628 encode_json("username", cct->_conf->rgw_keystone_barbican_user, f);
629 encode_json("password", cct->_conf->rgw_keystone_barbican_password, f);
630 f->close_section();
631 encode_json("tenantName", cct->_conf->rgw_keystone_barbican_tenant, f);
632 f->close_section();
633 f->close_section();
634}
635
636void rgw::keystone::BarbicanTokenRequestVer3::dump(Formatter* const f) const
637{
638 f->open_object_section("token_request");
639 f->open_object_section("auth");
640 f->open_object_section("identity");
641 f->open_array_section("methods");
642 f->dump_string("", "password");
643 f->close_section();
644 f->open_object_section("password");
645 f->open_object_section("user");
646 f->open_object_section("domain");
647 encode_json("name", cct->_conf->rgw_keystone_barbican_domain, f);
648 f->close_section();
649 encode_json("name", cct->_conf->rgw_keystone_barbican_user, f);
650 encode_json("password", cct->_conf->rgw_keystone_barbican_password, f);
651 f->close_section();
652 f->close_section();
653 f->close_section();
654 f->open_object_section("scope");
655 f->open_object_section("project");
656 if (!cct->_conf->rgw_keystone_barbican_project.empty()) {
657 encode_json("name", cct->_conf->rgw_keystone_barbican_project, f);
658 } else {
659 encode_json("name", cct->_conf->rgw_keystone_barbican_tenant, f);
660 }
661 f->open_object_section("domain");
662 encode_json("name", cct->_conf->rgw_keystone_barbican_domain, f);
663 f->close_section();
664 f->close_section();
665 f->close_section();
666 f->close_section();
667 f->close_section();
668}
669
670