1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab ft=cpp
12 #include "common/errno.h"
13 #include "common/ceph_json.h"
14 #include "include/types.h"
15 #include "include/str_list.h"
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"
23 #include "common/ceph_crypto.h"
24 #include "common/Cond.h"
26 #define dout_subsys ceph_subsys_rgw
34 TokenEngine::is_applicable(const std::string
& token
) const noexcept
36 return ! token
.empty() && ! cct
->_conf
->rgw_keystone_url
.empty();
39 boost::optional
<TokenEngine::token_envelope_t
>
40 TokenEngine::get_from_keystone(const DoutPrefixProvider
* dpp
, const std::string
& token
) const
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
;
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
);
52 std::string url
= config
.get_endpoint_url();
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
);
65 std::string admin_token
;
66 if (rgw::keystone::Service::get_admin_token(cct
, token_cache
, config
,
71 validate
.append_header("X-Auth-Token", admin_token
);
72 validate
.set_send_length(0);
74 validate
.set_url(url
);
76 int ret
= validate
.process(null_yield
);
81 /* NULL terminate for debug output. */
82 token_body_bl
.append(static_cast<char>(0));
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
;
99 ldpp_dout(dpp
, 20) << "received response status=" << validate
.get_http_status()
100 << ", body=" << token_body_bl
.c_str() << dendl
;
102 TokenEngine::token_envelope_t token_body
;
103 ret
= token_body
.parse(cct
, token
, token_body_bl
, config
.get_api_version());
111 TokenEngine::auth_info_t
112 TokenEngine::get_creds_info(const TokenEngine::token_envelope_t
& token
,
113 const std::vector
<std::string
>& admin_roles
116 using acct_privilege_t
= rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t
;
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
;
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
,
140 static inline const std::string
141 make_spec_item(const std::string
& tenant
, const std::string
& id
)
143 return tenant
+ ":" + id
;
146 TokenEngine::acl_strategy_t
147 TokenEngine::get_acl_strategy(const TokenEngine::token_envelope_t
& token
) const
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();
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();
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
),
163 make_spec_item(tenant_uuid
, "*"),
164 make_spec_item(tenant_name
, "*"),
165 make_spec_item("*", user_uuid
),
166 make_spec_item("*", user_name
),
169 /* Lambda will obtain a copy of (not a reference to!) allowed_items. */
170 return [allowed_items
](const rgw::auth::Identity::aclspec_t
& aclspec
) {
173 for (const auto& allowed_item
: allowed_items
) {
174 const auto iter
= aclspec
.find(allowed_item
);
176 if (std::end(aclspec
) != iter
) {
177 perm
|= iter
->second
;
185 TokenEngine::result_t
186 TokenEngine::authenticate(const DoutPrefixProvider
* dpp
,
187 const std::string
& token
,
188 const req_state
* const s
) const
190 boost::optional
<TokenEngine::token_envelope_t
> t
;
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
);
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
));
203 std::vector
<std::string
> plain
;
204 std::vector
<std::string
> admin
;
207 if (! is_applicable(token
)) {
208 return result_t::deny();
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
;
218 /* Check cache first. */
219 t
= token_cache
.find(token_id
);
221 ldpp_dout(dpp
, 20) << "cached token.project.id=" << t
->get_project_id()
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
));
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
);
234 return result_t::deny(-EACCES
);
237 /* Verify expiration. */
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
);
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
));
258 ldpp_dout(dpp
, 0) << "user does not hold a matching role; required roles: "
259 << g_conf()->rgw_keystone_accepted_roles
<< dendl
;
261 return result_t::deny(-EPERM
);
266 * Try to validate S3 auth against keystone s3token interface
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
273 /* prepare keystone url */
274 std::string keystone_url
= config
.get_endpoint_url();
275 if (keystone_url
.empty()) {
279 const auto api_version
= config
.get_api_version();
280 if (api_version
== rgw::keystone::ApiVersion::VER_3
) {
281 keystone_url
.append("v3/s3tokens");
283 keystone_url
.append("v2.0/s3tokens");
286 /* get authentication token for Keystone. */
287 std::string admin_token
;
288 int ret
= rgw::keystone::Service::get_admin_token(cct
, token_cache
, config
,
291 ldpp_dout(dpp
, 2) << "s3 keystone: cannot get token for keystone access"
296 using RGWValidateKeystoneToken
297 = rgw::keystone::Service::RGWValidateKeystoneToken
;
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
);
304 /* set required headers for keystone request */
305 validate
.append_header("X-Auth-Token", admin_token
);
306 validate
.append_header("Content-Type", "application/json");
308 /* check if we want to verify keystone's ssl certs */
309 validate
.set_verify_ssl(cct
->_conf
->rgw_keystone_verify_ssl
);
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();
321 std::stringstream os
;
322 credentials
.flush(os
);
323 validate
.set_post_data(os
.str());
324 validate
.set_send_length(os
.str().length());
327 ret
= validate
.process(null_yield
);
329 ldpp_dout(dpp
, 2) << "s3 keystone: token validation ERROR: "
330 << token_body_bl
.c_str() << dendl
;
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
);
343 /* now parse response */
344 rgw::keystone::TokenEnvelope token_envelope
;
345 ret
= token_envelope
.parse(cct
, std::string(), token_body_bl
, api_version
);
347 ldpp_dout(dpp
, 2) << "s3 keystone: token parsing failed, ret=0" << ret
352 return std::make_pair(std::move(token_envelope
), 0);
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
359 /* Fetch from /users/{USER_ID}/credentials/OS-EC2/{ACCESS_KEY_ID} */
360 /* Should return json with response key "credential" which contains entry "secret"*/
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
);
368 const auto api_version
= config
.get_api_version();
369 if (api_version
== rgw::keystone::ApiVersion::VER_3
) {
370 keystone_url
.append("v3/");
372 keystone_url
.append("v2.0/");
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());
379 /* get authentication token for Keystone. */
380 std::string admin_token
;
381 int ret
= rgw::keystone::Service::get_admin_token(cct
, token_cache
, config
,
384 ldpp_dout(dpp
, 2) << "s3 keystone: cannot get token for keystone access"
386 return make_pair(boost::none
, ret
);
389 using RGWGetAccessSecret
390 = rgw::keystone::Service::RGWKeystoneHTTPTransceiver
;
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
);
396 /* set required headers for keystone request */
397 secret
.append_header("X-Auth-Token", admin_token
);
399 /* check if we want to verify keystone's ssl certs */
400 secret
.set_verify_ssl(cct
->_conf
->rgw_keystone_verify_ssl
);
403 ret
= secret
.process(null_yield
);
405 ldpp_dout(dpp
, 2) << "s3 keystone: secret fetching error: "
406 << token_body_bl
.c_str() << dendl
;
407 return make_pair(boost::none
, ret
);
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
);
416 /* now parse response */
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
);
424 JSONObjIter credential_iter
= parser
.find_first("credential");
425 std::string secret_string
;
428 if (!credential_iter
.end()) {
429 JSONDecoder::decode_json("secret", secret_string
, *credential_iter
, true);
431 ldpp_dout(dpp
, 0) << "Keystone credential not present in return from server" << dendl
;
432 return make_pair(boost::none
, -EINVAL
);
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
);
439 return make_pair(secret_string
, 0);
443 * Try to get a token for S3 authentication, using a secret cache if available
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
452 using server_signature_t
= VersionAbstractor::server_signature_t
;
453 boost::optional
<rgw::keystone::TokenEnvelope
> token
;
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());
460 /* Check that credentials can correctly be used to sign data */
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);
467 ldpp_dout(dpp
, 0) << "Secret string does not correctly sign payload, cache miss" << dendl
;
470 ldpp_dout(dpp
, 0) << "No stored secret string, cache miss" << dendl
;
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
);
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
);
482 /* Add token, secret pair to cache, and set timeout */
483 secret_cache
.add(access_key_id
.to_string(), *token
, *secret
);
487 return std::make_pair(token
, failure_reason
);
490 EC2Engine::acl_strategy_t
491 EC2Engine::get_acl_strategy(const EC2Engine::token_envelope_t
&) const
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. */
498 EC2Engine::auth_info_t
499 EC2Engine::get_creds_info(const EC2Engine::token_envelope_t
& token
,
500 const std::vector
<std::string
>& admin_roles
503 using acct_privilege_t
= \
504 rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t
;
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
;
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
,
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
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
);
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
));
550 std::vector
<std::string
> plain
;
551 std::vector
<std::string
> admin
;
552 } accepted_roles(cct
);
554 boost::optional
<token_envelope_t
> t
;
556 std::tie(t
, failure_reason
) = \
557 get_access_token(dpp
, access_key_id
, string_to_sign
, signature
, signature_factory
);
559 return result_t::deny(failure_reason
);
562 /* Verify expiration. */
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();
570 /* check if we have a valid role */
572 for (const auto& role
: accepted_roles
.plain
) {
573 if (t
->has_role(role
) == true) {
580 ldpp_dout(dpp
, 5) << "s3 keystone: user does not hold a matching role;"
582 << cct
->_conf
->rgw_keystone_accepted_roles
<< dendl
;
583 return result_t::deny();
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
;
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
));
596 bool SecretCache::find(const std::string
& token_id
,
597 SecretCache::token_envelope_t
& token
,
600 std::lock_guard
<std::mutex
> l(lock
);
602 map
<std::string
, secret_entry
>::iterator iter
= secrets
.find(token_id
);
603 if (iter
== secrets
.end()) {
607 secret_entry
& entry
= iter
->second
;
608 secrets_lru
.erase(entry
.lru_iter
);
610 const utime_t now
= ceph_clock_now();
611 if (entry
.token
.expired() || now
> entry
.expires
) {
616 secret
= entry
.secret
;
618 secrets_lru
.push_front(token_id
);
619 entry
.lru_iter
= secrets_lru
.begin();
624 void SecretCache::add(const std::string
& token_id
,
625 const SecretCache::token_envelope_t
& token
,
626 const std::string
& secret
)
628 std::lock_guard
<std::mutex
> l(lock
);
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
);
636 const utime_t now
= ceph_clock_now();
637 secrets_lru
.push_front(token_id
);
638 secret_entry
& entry
= secrets
[token_id
];
640 entry
.secret
= secret
;
641 entry
.expires
= now
+ s3_token_expiry_length
;
642 entry
.lru_iter
= secrets_lru
.begin();
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());
649 secrets_lru
.pop_back();
653 }; /* namespace keystone */
654 }; /* namespace auth */
655 }; /* namespace rgw */