]> git.proxmox.com Git - ceph.git/blob - ceph/src/rgw/rgw_auth_keystone.cc
import 15.2.0 Octopus source
[ceph.git] / ceph / src / rgw / rgw_auth_keystone.cc
1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab ft=cpp
3
4 #include <string>
5 #include <vector>
6
7 #include <errno.h>
8 #include <fnmatch.h>
9
10 #include "rgw_b64.h"
11
12 #include "common/errno.h"
13 #include "common/ceph_json.h"
14 #include "include/types.h"
15 #include "include/str_list.h"
16
17 #include "rgw_common.h"
18 #include "rgw_keystone.h"
19 #include "rgw_auth_keystone.h"
20 #include "rgw_rest_s3.h"
21 #include "rgw_auth_s3.h"
22
23 #include "common/ceph_crypto.h"
24 #include "common/Cond.h"
25
26 #define dout_subsys ceph_subsys_rgw
27
28
29 namespace rgw {
30 namespace auth {
31 namespace keystone {
32
33 bool
34 TokenEngine::is_applicable(const std::string& token) const noexcept
35 {
36 return ! token.empty() && ! cct->_conf->rgw_keystone_url.empty();
37 }
38
39 boost::optional<TokenEngine::token_envelope_t>
40 TokenEngine::get_from_keystone(const DoutPrefixProvider* dpp, const std::string& token) const
41 {
42 /* Unfortunately, we can't use the short form of "using" here. It's because
43 * we're aliasing a class' member, not namespace. */
44 using RGWValidateKeystoneToken = \
45 rgw::keystone::Service::RGWValidateKeystoneToken;
46
47 /* The container for plain response obtained from Keystone. It will be
48 * parsed token_envelope_t::parse method. */
49 ceph::bufferlist token_body_bl;
50 RGWValidateKeystoneToken validate(cct, "GET", "", &token_body_bl);
51
52 std::string url = config.get_endpoint_url();
53 if (url.empty()) {
54 throw -EINVAL;
55 }
56
57 const auto keystone_version = config.get_api_version();
58 if (keystone_version == rgw::keystone::ApiVersion::VER_2) {
59 url.append("v2.0/tokens/" + token);
60 } else if (keystone_version == rgw::keystone::ApiVersion::VER_3) {
61 url.append("v3/auth/tokens");
62 validate.append_header("X-Subject-Token", token);
63 }
64
65 std::string admin_token;
66 if (rgw::keystone::Service::get_admin_token(cct, token_cache, config,
67 admin_token) < 0) {
68 throw -EINVAL;
69 }
70
71 validate.append_header("X-Auth-Token", admin_token);
72 validate.set_send_length(0);
73
74 validate.set_url(url);
75
76 int ret = validate.process(null_yield);
77 if (ret < 0) {
78 throw ret;
79 }
80
81 /* NULL terminate for debug output. */
82 token_body_bl.append(static_cast<char>(0));
83
84 /* Detect Keystone rejection earlier than during the token parsing.
85 * Although failure at the parsing phase doesn't impose a threat,
86 * this allows to return proper error code (EACCESS instead of EINVAL
87 * or similar) and thus improves logging. */
88 if (validate.get_http_status() ==
89 /* Most likely: wrong admin credentials or admin token. */
90 RGWValidateKeystoneToken::HTTP_STATUS_UNAUTHORIZED ||
91 validate.get_http_status() ==
92 /* Most likely: non-existent token supplied by the client. */
93 RGWValidateKeystoneToken::HTTP_STATUS_NOTFOUND) {
94 ldpp_dout(dpp, 5) << "Failed keystone auth from " << url << " with "
95 << validate.get_http_status() << dendl;
96 return boost::none;
97 }
98
99 ldpp_dout(dpp, 20) << "received response status=" << validate.get_http_status()
100 << ", body=" << token_body_bl.c_str() << dendl;
101
102 TokenEngine::token_envelope_t token_body;
103 ret = token_body.parse(cct, token, token_body_bl, config.get_api_version());
104 if (ret < 0) {
105 throw ret;
106 }
107
108 return token_body;
109 }
110
111 TokenEngine::auth_info_t
112 TokenEngine::get_creds_info(const TokenEngine::token_envelope_t& token,
113 const std::vector<std::string>& admin_roles
114 ) const noexcept
115 {
116 using acct_privilege_t = rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t;
117
118 /* Check whether the user has an admin status. */
119 acct_privilege_t level = acct_privilege_t::IS_PLAIN_ACCT;
120 for (const auto& admin_role : admin_roles) {
121 if (token.has_role(admin_role)) {
122 level = acct_privilege_t::IS_ADMIN_ACCT;
123 break;
124 }
125 }
126
127 return auth_info_t {
128 /* Suggested account name for the authenticated user. */
129 rgw_user(token.get_project_id()),
130 /* User's display name (aka real name). */
131 token.get_project_name(),
132 /* Keystone doesn't support RGW's subuser concept, so we cannot cut down
133 * the access rights through the perm_mask. At least at this layer. */
134 RGW_PERM_FULL_CONTROL,
135 level,
136 TYPE_KEYSTONE,
137 };
138 }
139
140 static inline const std::string
141 make_spec_item(const std::string& tenant, const std::string& id)
142 {
143 return tenant + ":" + id;
144 }
145
146 TokenEngine::acl_strategy_t
147 TokenEngine::get_acl_strategy(const TokenEngine::token_envelope_t& token) const
148 {
149 /* The primary identity is constructed upon UUIDs. */
150 const auto& tenant_uuid = token.get_project_id();
151 const auto& user_uuid = token.get_user_id();
152
153 /* For Keystone v2 an alias may be also used. */
154 const auto& tenant_name = token.get_project_name();
155 const auto& user_name = token.get_user_name();
156
157 /* Construct all possible combinations including Swift's wildcards. */
158 const std::array<std::string, 6> allowed_items = {
159 make_spec_item(tenant_uuid, user_uuid),
160 make_spec_item(tenant_name, user_name),
161
162 /* Wildcards. */
163 make_spec_item(tenant_uuid, "*"),
164 make_spec_item(tenant_name, "*"),
165 make_spec_item("*", user_uuid),
166 make_spec_item("*", user_name),
167 };
168
169 /* Lambda will obtain a copy of (not a reference to!) allowed_items. */
170 return [allowed_items](const rgw::auth::Identity::aclspec_t& aclspec) {
171 uint32_t perm = 0;
172
173 for (const auto& allowed_item : allowed_items) {
174 const auto iter = aclspec.find(allowed_item);
175
176 if (std::end(aclspec) != iter) {
177 perm |= iter->second;
178 }
179 }
180
181 return perm;
182 };
183 }
184
185 TokenEngine::result_t
186 TokenEngine::authenticate(const DoutPrefixProvider* dpp,
187 const std::string& token,
188 const req_state* const s) const
189 {
190 boost::optional<TokenEngine::token_envelope_t> t;
191
192 /* This will be initialized on the first call to this method. In C++11 it's
193 * also thread-safe. */
194 static const struct RolesCacher {
195 explicit RolesCacher(CephContext* const cct) {
196 get_str_vec(cct->_conf->rgw_keystone_accepted_roles, plain);
197 get_str_vec(cct->_conf->rgw_keystone_accepted_admin_roles, admin);
198
199 /* Let's suppose that having an admin role implies also a regular one. */
200 plain.insert(std::end(plain), std::begin(admin), std::end(admin));
201 }
202
203 std::vector<std::string> plain;
204 std::vector<std::string> admin;
205 } roles(cct);
206
207 if (! is_applicable(token)) {
208 return result_t::deny();
209 }
210
211 /* Token ID is a legacy of supporting the service-side validation
212 * of PKI/PKIz token type which are already-removed-in-OpenStack.
213 * The idea was to bury in cache only a short hash instead of few
214 * kilobytes. RadosGW doesn't do the local validation anymore. */
215 const auto& token_id = rgw_get_token_id(token);
216 ldpp_dout(dpp, 20) << "token_id=" << token_id << dendl;
217
218 /* Check cache first. */
219 t = token_cache.find(token_id);
220 if (t) {
221 ldpp_dout(dpp, 20) << "cached token.project.id=" << t->get_project_id()
222 << dendl;
223 auto apl = apl_factory->create_apl_remote(cct, s, get_acl_strategy(*t),
224 get_creds_info(*t, roles.admin));
225 return result_t::grant(std::move(apl));
226 }
227
228 /* Not in cache. Go to the Keystone for validation. This happens even
229 * for the legacy PKI/PKIz token types. That's it, after the PKI/PKIz
230 * RadosGW-side validation has been removed, we always ask Keystone. */
231 t = get_from_keystone(dpp, token);
232
233 if (! t) {
234 return result_t::deny(-EACCES);
235 }
236
237 /* Verify expiration. */
238 if (t->expired()) {
239 ldpp_dout(dpp, 0) << "got expired token: " << t->get_project_name()
240 << ":" << t->get_user_name()
241 << " expired: " << t->get_expires() << dendl;
242 return result_t::deny(-EPERM);
243 }
244
245 /* Check for necessary roles. */
246 for (const auto& role : roles.plain) {
247 if (t->has_role(role) == true) {
248 ldpp_dout(dpp, 0) << "validated token: " << t->get_project_name()
249 << ":" << t->get_user_name()
250 << " expires: " << t->get_expires() << dendl;
251 token_cache.add(token_id, *t);
252 auto apl = apl_factory->create_apl_remote(cct, s, get_acl_strategy(*t),
253 get_creds_info(*t, roles.admin));
254 return result_t::grant(std::move(apl));
255 }
256 }
257
258 ldpp_dout(dpp, 0) << "user does not hold a matching role; required roles: "
259 << g_conf()->rgw_keystone_accepted_roles << dendl;
260
261 return result_t::deny(-EPERM);
262 }
263
264
265 /*
266 * Try to validate S3 auth against keystone s3token interface
267 */
268 std::pair<boost::optional<rgw::keystone::TokenEnvelope>, int>
269 EC2Engine::get_from_keystone(const DoutPrefixProvider* dpp, const boost::string_view& access_key_id,
270 const std::string& string_to_sign,
271 const boost::string_view& signature) const
272 {
273 /* prepare keystone url */
274 std::string keystone_url = config.get_endpoint_url();
275 if (keystone_url.empty()) {
276 throw -EINVAL;
277 }
278
279 const auto api_version = config.get_api_version();
280 if (api_version == rgw::keystone::ApiVersion::VER_3) {
281 keystone_url.append("v3/s3tokens");
282 } else {
283 keystone_url.append("v2.0/s3tokens");
284 }
285
286 /* get authentication token for Keystone. */
287 std::string admin_token;
288 int ret = rgw::keystone::Service::get_admin_token(cct, token_cache, config,
289 admin_token);
290 if (ret < 0) {
291 ldpp_dout(dpp, 2) << "s3 keystone: cannot get token for keystone access"
292 << dendl;
293 throw ret;
294 }
295
296 using RGWValidateKeystoneToken
297 = rgw::keystone::Service::RGWValidateKeystoneToken;
298
299 /* The container for plain response obtained from Keystone. It will be
300 * parsed token_envelope_t::parse method. */
301 ceph::bufferlist token_body_bl;
302 RGWValidateKeystoneToken validate(cct, "POST", keystone_url, &token_body_bl);
303
304 /* set required headers for keystone request */
305 validate.append_header("X-Auth-Token", admin_token);
306 validate.append_header("Content-Type", "application/json");
307
308 /* check if we want to verify keystone's ssl certs */
309 validate.set_verify_ssl(cct->_conf->rgw_keystone_verify_ssl);
310
311 /* create json credentials request body */
312 JSONFormatter credentials(false);
313 credentials.open_object_section("");
314 credentials.open_object_section("credentials");
315 credentials.dump_string("access", sview2cstr(access_key_id).data());
316 credentials.dump_string("token", rgw::to_base64(string_to_sign));
317 credentials.dump_string("signature", sview2cstr(signature).data());
318 credentials.close_section();
319 credentials.close_section();
320
321 std::stringstream os;
322 credentials.flush(os);
323 validate.set_post_data(os.str());
324 validate.set_send_length(os.str().length());
325
326 /* send request */
327 ret = validate.process(null_yield);
328 if (ret < 0) {
329 ldpp_dout(dpp, 2) << "s3 keystone: token validation ERROR: "
330 << token_body_bl.c_str() << dendl;
331 throw ret;
332 }
333
334 /* if the supplied signature is wrong, we will get 401 from Keystone */
335 if (validate.get_http_status() ==
336 decltype(validate)::HTTP_STATUS_UNAUTHORIZED) {
337 return std::make_pair(boost::none, -ERR_SIGNATURE_NO_MATCH);
338 } else if (validate.get_http_status() ==
339 decltype(validate)::HTTP_STATUS_NOTFOUND) {
340 return std::make_pair(boost::none, -ERR_INVALID_ACCESS_KEY);
341 }
342
343 /* now parse response */
344 rgw::keystone::TokenEnvelope token_envelope;
345 ret = token_envelope.parse(cct, std::string(), token_body_bl, api_version);
346 if (ret < 0) {
347 ldpp_dout(dpp, 2) << "s3 keystone: token parsing failed, ret=0" << ret
348 << dendl;
349 throw ret;
350 }
351
352 return std::make_pair(std::move(token_envelope), 0);
353 }
354
355 std::pair<boost::optional<std::string>, int> EC2Engine::get_secret_from_keystone(const DoutPrefixProvider* dpp,
356 const std::string& user_id,
357 const boost::string_view& access_key_id) const
358 {
359 /* Fetch from /users/{USER_ID}/credentials/OS-EC2/{ACCESS_KEY_ID} */
360 /* Should return json with response key "credential" which contains entry "secret"*/
361
362 /* prepare keystone url */
363 std::string keystone_url = config.get_endpoint_url();
364 if (keystone_url.empty()) {
365 return make_pair(boost::none, -EINVAL);
366 }
367
368 const auto api_version = config.get_api_version();
369 if (api_version == rgw::keystone::ApiVersion::VER_3) {
370 keystone_url.append("v3/");
371 } else {
372 keystone_url.append("v2.0/");
373 }
374 keystone_url.append("users/");
375 keystone_url.append(user_id);
376 keystone_url.append("/credentials/OS-EC2/");
377 keystone_url.append(access_key_id.to_string());
378
379 /* get authentication token for Keystone. */
380 std::string admin_token;
381 int ret = rgw::keystone::Service::get_admin_token(cct, token_cache, config,
382 admin_token);
383 if (ret < 0) {
384 ldpp_dout(dpp, 2) << "s3 keystone: cannot get token for keystone access"
385 << dendl;
386 return make_pair(boost::none, ret);
387 }
388
389 using RGWGetAccessSecret
390 = rgw::keystone::Service::RGWKeystoneHTTPTransceiver;
391
392 /* The container for plain response obtained from Keystone.*/
393 ceph::bufferlist token_body_bl;
394 RGWGetAccessSecret secret(cct, "GET", keystone_url, &token_body_bl);
395
396 /* set required headers for keystone request */
397 secret.append_header("X-Auth-Token", admin_token);
398
399 /* check if we want to verify keystone's ssl certs */
400 secret.set_verify_ssl(cct->_conf->rgw_keystone_verify_ssl);
401
402 /* send request */
403 ret = secret.process(null_yield);
404 if (ret < 0) {
405 ldpp_dout(dpp, 2) << "s3 keystone: secret fetching error: "
406 << token_body_bl.c_str() << dendl;
407 return make_pair(boost::none, ret);
408 }
409
410 /* if the supplied signature is wrong, we will get 401 from Keystone */
411 if (secret.get_http_status() ==
412 decltype(secret)::HTTP_STATUS_NOTFOUND) {
413 return make_pair(boost::none, -EINVAL);
414 }
415
416 /* now parse response */
417
418 JSONParser parser;
419 if (! parser.parse(token_body_bl.c_str(), token_body_bl.length())) {
420 ldpp_dout(dpp, 0) << "Keystone credential parse error: malformed json" << dendl;
421 return make_pair(boost::none, -EINVAL);
422 }
423
424 JSONObjIter credential_iter = parser.find_first("credential");
425 std::string secret_string;
426
427 try {
428 if (!credential_iter.end()) {
429 JSONDecoder::decode_json("secret", secret_string, *credential_iter, true);
430 } else {
431 ldpp_dout(dpp, 0) << "Keystone credential not present in return from server" << dendl;
432 return make_pair(boost::none, -EINVAL);
433 }
434 } catch (const JSONDecoder::err& err) {
435 ldpp_dout(dpp, 0) << "Keystone credential parse error: " << err.what() << dendl;
436 return make_pair(boost::none, -EINVAL);
437 }
438
439 return make_pair(secret_string, 0);
440 }
441
442 /*
443 * Try to get a token for S3 authentication, using a secret cache if available
444 */
445 std::pair<boost::optional<rgw::keystone::TokenEnvelope>, int>
446 EC2Engine::get_access_token(const DoutPrefixProvider* dpp,
447 const boost::string_view& access_key_id,
448 const std::string& string_to_sign,
449 const boost::string_view& signature,
450 const signature_factory_t& signature_factory) const
451 {
452 using server_signature_t = VersionAbstractor::server_signature_t;
453 boost::optional<rgw::keystone::TokenEnvelope> token;
454 int failure_reason;
455
456 /* Get a token from the cache if one has already been stored */
457 boost::optional<boost::tuple<rgw::keystone::TokenEnvelope, std::string>>
458 t = secret_cache.find(access_key_id.to_string());
459
460 /* Check that credentials can correctly be used to sign data */
461 if (t) {
462 std::string sig(signature);
463 server_signature_t server_signature = signature_factory(cct, t->get<1>(), string_to_sign);
464 if (sig.compare(server_signature) == 0) {
465 return std::make_pair(t->get<0>(), 0);
466 } else {
467 ldpp_dout(dpp, 0) << "Secret string does not correctly sign payload, cache miss" << dendl;
468 }
469 } else {
470 ldpp_dout(dpp, 0) << "No stored secret string, cache miss" << dendl;
471 }
472
473 /* No cached token, token expired, or secret invalid: fall back to keystone */
474 std::tie(token, failure_reason) = get_from_keystone(dpp, access_key_id, string_to_sign, signature);
475
476 if (token) {
477 /* Fetch secret from keystone for the access_key_id */
478 boost::optional<std::string> secret;
479 std::tie(secret, failure_reason) = get_secret_from_keystone(dpp, token->get_user_id(), access_key_id);
480
481 if (secret) {
482 /* Add token, secret pair to cache, and set timeout */
483 secret_cache.add(access_key_id.to_string(), *token, *secret);
484 }
485 }
486
487 return std::make_pair(token, failure_reason);
488 }
489
490 EC2Engine::acl_strategy_t
491 EC2Engine::get_acl_strategy(const EC2Engine::token_envelope_t&) const
492 {
493 /* This is based on the assumption that the default acl strategy in
494 * get_perms_from_aclspec, will take care. Extra acl spec is not required. */
495 return nullptr;
496 }
497
498 EC2Engine::auth_info_t
499 EC2Engine::get_creds_info(const EC2Engine::token_envelope_t& token,
500 const std::vector<std::string>& admin_roles
501 ) const noexcept
502 {
503 using acct_privilege_t = \
504 rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t;
505
506 /* Check whether the user has an admin status. */
507 acct_privilege_t level = acct_privilege_t::IS_PLAIN_ACCT;
508 for (const auto& admin_role : admin_roles) {
509 if (token.has_role(admin_role)) {
510 level = acct_privilege_t::IS_ADMIN_ACCT;
511 break;
512 }
513 }
514
515 return auth_info_t {
516 /* Suggested account name for the authenticated user. */
517 rgw_user(token.get_project_id()),
518 /* User's display name (aka real name). */
519 token.get_project_name(),
520 /* Keystone doesn't support RGW's subuser concept, so we cannot cut down
521 * the access rights through the perm_mask. At least at this layer. */
522 RGW_PERM_FULL_CONTROL,
523 level,
524 TYPE_KEYSTONE,
525 };
526 }
527
528 rgw::auth::Engine::result_t EC2Engine::authenticate(
529 const DoutPrefixProvider* dpp,
530 const boost::string_view& access_key_id,
531 const boost::string_view& signature,
532 const boost::string_view& session_token,
533 const string_to_sign_t& string_to_sign,
534 const signature_factory_t& signature_factory,
535 const completer_factory_t& completer_factory,
536 /* Passthorugh only! */
537 const req_state* s) const
538 {
539 /* This will be initialized on the first call to this method. In C++11 it's
540 * also thread-safe. */
541 static const struct RolesCacher {
542 explicit RolesCacher(CephContext* const cct) {
543 get_str_vec(cct->_conf->rgw_keystone_accepted_roles, plain);
544 get_str_vec(cct->_conf->rgw_keystone_accepted_admin_roles, admin);
545
546 /* Let's suppose that having an admin role implies also a regular one. */
547 plain.insert(std::end(plain), std::begin(admin), std::end(admin));
548 }
549
550 std::vector<std::string> plain;
551 std::vector<std::string> admin;
552 } accepted_roles(cct);
553
554 boost::optional<token_envelope_t> t;
555 int failure_reason;
556 std::tie(t, failure_reason) = \
557 get_access_token(dpp, access_key_id, string_to_sign, signature, signature_factory);
558 if (! t) {
559 return result_t::deny(failure_reason);
560 }
561
562 /* Verify expiration. */
563 if (t->expired()) {
564 ldpp_dout(dpp, 0) << "got expired token: " << t->get_project_name()
565 << ":" << t->get_user_name()
566 << " expired: " << t->get_expires() << dendl;
567 return result_t::deny();
568 }
569
570 /* check if we have a valid role */
571 bool found = false;
572 for (const auto& role : accepted_roles.plain) {
573 if (t->has_role(role) == true) {
574 found = true;
575 break;
576 }
577 }
578
579 if (! found) {
580 ldpp_dout(dpp, 5) << "s3 keystone: user does not hold a matching role;"
581 " required roles: "
582 << cct->_conf->rgw_keystone_accepted_roles << dendl;
583 return result_t::deny();
584 } else {
585 /* everything seems fine, continue with this user */
586 ldpp_dout(dpp, 5) << "s3 keystone: validated token: " << t->get_project_name()
587 << ":" << t->get_user_name()
588 << " expires: " << t->get_expires() << dendl;
589
590 auto apl = apl_factory->create_apl_remote(cct, s, get_acl_strategy(*t),
591 get_creds_info(*t, accepted_roles.admin));
592 return result_t::grant(std::move(apl), completer_factory(boost::none));
593 }
594 }
595
596 bool SecretCache::find(const std::string& token_id,
597 SecretCache::token_envelope_t& token,
598 std::string &secret)
599 {
600 std::lock_guard<std::mutex> l(lock);
601
602 map<std::string, secret_entry>::iterator iter = secrets.find(token_id);
603 if (iter == secrets.end()) {
604 return false;
605 }
606
607 secret_entry& entry = iter->second;
608 secrets_lru.erase(entry.lru_iter);
609
610 const utime_t now = ceph_clock_now();
611 if (entry.token.expired() || now > entry.expires) {
612 secrets.erase(iter);
613 return false;
614 }
615 token = entry.token;
616 secret = entry.secret;
617
618 secrets_lru.push_front(token_id);
619 entry.lru_iter = secrets_lru.begin();
620
621 return true;
622 }
623
624 void SecretCache::add(const std::string& token_id,
625 const SecretCache::token_envelope_t& token,
626 const std::string& secret)
627 {
628 std::lock_guard<std::mutex> l(lock);
629
630 map<string, secret_entry>::iterator iter = secrets.find(token_id);
631 if (iter != secrets.end()) {
632 secret_entry& e = iter->second;
633 secrets_lru.erase(e.lru_iter);
634 }
635
636 const utime_t now = ceph_clock_now();
637 secrets_lru.push_front(token_id);
638 secret_entry& entry = secrets[token_id];
639 entry.token = token;
640 entry.secret = secret;
641 entry.expires = now + s3_token_expiry_length;
642 entry.lru_iter = secrets_lru.begin();
643
644 while (secrets_lru.size() > max) {
645 list<string>::reverse_iterator riter = secrets_lru.rbegin();
646 iter = secrets.find(*riter);
647 assert(iter != secrets.end());
648 secrets.erase(iter);
649 secrets_lru.pop_back();
650 }
651 }
652
653 }; /* namespace keystone */
654 }; /* namespace auth */
655 }; /* namespace rgw */