1 // -*- mode:C++; tab-width:8; c-basic-offset:2; indent-tabs-mode:t -*-
2 // vim: ts=8 sw=2 smarttab
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_keystone.h"
21 #include "rgw_rest_s3.h"
22 #include "rgw_auth_s3.h"
24 #include "common/ceph_crypto_cms.h"
25 #include "common/armor.h"
26 #include "common/Cond.h"
28 #define dout_subsys ceph_subsys_rgw
36 TokenEngine::is_applicable(const std::string
& token
) const noexcept
38 return ! token
.empty() && ! cct
->_conf
->rgw_keystone_url
.empty();
41 TokenEngine::token_envelope_t
42 TokenEngine::decode_pki_token(const std::string
& token
) const
44 ceph::buffer::list token_body_bl
;
45 int ret
= rgw_decode_b64_cms(cct
, token
, token_body_bl
);
47 ldout(cct
, 20) << "cannot decode pki token" << dendl
;
50 ldout(cct
, 20) << "successfully decoded pki token" << dendl
;
53 TokenEngine::token_envelope_t token_body
;
54 ret
= token_body
.parse(cct
, token
, token_body_bl
, config
.get_api_version());
62 boost::optional
<TokenEngine::token_envelope_t
>
63 TokenEngine::get_from_keystone(const std::string
& token
) const
65 /* Unfortunately, we can't use the short form of "using" here. It's because
66 * we're aliasing a class' member, not namespace. */
67 using RGWValidateKeystoneToken
= \
68 rgw::keystone::Service::RGWValidateKeystoneToken
;
70 /* The container for plain response obtained from Keystone. It will be
71 * parsed token_envelope_t::parse method. */
72 ceph::bufferlist token_body_bl
;
73 RGWValidateKeystoneToken
validate(cct
, &token_body_bl
);
75 std::string url
= config
.get_endpoint_url();
80 const auto keystone_version
= config
.get_api_version();
81 if (keystone_version
== rgw::keystone::ApiVersion::VER_2
) {
82 url
.append("v2.0/tokens/" + token
);
83 } else if (keystone_version
== rgw::keystone::ApiVersion::VER_3
) {
84 url
.append("v3/auth/tokens");
85 validate
.append_header("X-Subject-Token", token
);
88 std::string admin_token
;
89 if (rgw::keystone::Service::get_admin_token(cct
, token_cache
, config
,
94 validate
.append_header("X-Auth-Token", admin_token
);
95 validate
.set_send_length(0);
97 int ret
= validate
.process(url
.c_str());
102 /* NULL terminate for debug output. */
103 token_body_bl
.append(static_cast<char>(0));
104 ldout(cct
, 20) << "received response status=" << validate
.get_http_status()
105 << ", body=" << token_body_bl
.c_str() << dendl
;
107 /* Detect Keystone rejection earlier than during the token parsing.
108 * Although failure at the parsing phase doesn't impose a threat,
109 * this allows to return proper error code (EACCESS instead of EINVAL
110 * or similar) and thus improves logging. */
111 if (validate
.get_http_status() ==
112 /* Most likely: wrong admin credentials or admin token. */
113 RGWValidateKeystoneToken::HTTP_STATUS_UNAUTHORIZED
||
114 validate
.get_http_status() ==
115 /* Most likely: non-existent token supplied by the client. */
116 RGWValidateKeystoneToken::HTTP_STATUS_NOTFOUND
) {
120 TokenEngine::token_envelope_t token_body
;
121 ret
= token_body
.parse(cct
, token
, token_body_bl
, config
.get_api_version());
129 TokenEngine::auth_info_t
130 TokenEngine::get_creds_info(const TokenEngine::token_envelope_t
& token
,
131 const std::vector
<std::string
>& admin_roles
134 using acct_privilege_t
= rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t
;
136 /* Check whether the user has an admin status. */
137 acct_privilege_t level
= acct_privilege_t::IS_PLAIN_ACCT
;
138 for (const auto& admin_role
: admin_roles
) {
139 if (token
.has_role(admin_role
)) {
140 level
= acct_privilege_t::IS_ADMIN_ACCT
;
146 /* Suggested account name for the authenticated user. */
147 rgw_user(token
.get_project_id()),
148 /* User's display name (aka real name). */
149 token
.get_project_name(),
150 /* Keystone doesn't support RGW's subuser concept, so we cannot cut down
151 * the access rights through the perm_mask. At least at this layer. */
152 RGW_PERM_FULL_CONTROL
,
158 static inline const std::string
159 make_spec_item(const std::string
& tenant
, const std::string
& id
)
161 return tenant
+ ":" + id
;
164 TokenEngine::acl_strategy_t
165 TokenEngine::get_acl_strategy(const TokenEngine::token_envelope_t
& token
) const
167 /* The primary identity is constructed upon UUIDs. */
168 const auto& tenant_uuid
= token
.get_project_id();
169 const auto& user_uuid
= token
.get_user_id();
171 /* For Keystone v2 an alias may be also used. */
172 const auto& tenant_name
= token
.get_project_name();
173 const auto& user_name
= token
.get_user_name();
175 /* Construct all possible combinations including Swift's wildcards. */
176 const std::array
<std::string
, 6> allowed_items
= {
177 make_spec_item(tenant_uuid
, user_uuid
),
178 make_spec_item(tenant_name
, user_name
),
181 make_spec_item(tenant_uuid
, "*"),
182 make_spec_item(tenant_name
, "*"),
183 make_spec_item("*", user_uuid
),
184 make_spec_item("*", user_name
),
187 /* Lambda will obtain a copy of (not a reference to!) allowed_items. */
188 return [allowed_items
](const rgw::auth::Identity::aclspec_t
& aclspec
) {
191 for (const auto& allowed_item
: allowed_items
) {
192 const auto iter
= aclspec
.find(allowed_item
);
194 if (std::end(aclspec
) != iter
) {
195 perm
|= iter
->second
;
203 TokenEngine::result_t
204 TokenEngine::authenticate(const std::string
& token
,
205 const req_state
* const s
) const
207 boost::optional
<TokenEngine::token_envelope_t
> t
;
209 /* This will be initialized on the first call to this method. In C++11 it's
210 * also thread-safe. */
211 static const struct RolesCacher
{
212 RolesCacher(CephContext
* const cct
) {
213 get_str_vec(cct
->_conf
->rgw_keystone_accepted_roles
, plain
);
214 get_str_vec(cct
->_conf
->rgw_keystone_accepted_admin_roles
, admin
);
216 /* Let's suppose that having an admin role implies also a regular one. */
217 plain
.insert(std::end(plain
), std::begin(admin
), std::end(admin
));
220 std::vector
<std::string
> plain
;
221 std::vector
<std::string
> admin
;
224 if (! is_applicable(token
)) {
225 return result_t::deny();
228 /* Token ID is a concept that makes dealing with PKI tokens more effective.
229 * Instead of storing several kilobytes, a short hash can be burried. */
230 const auto& token_id
= rgw_get_token_id(token
);
231 ldout(cct
, 20) << "token_id=" << token_id
<< dendl
;
233 /* Check cache first. */
234 t
= token_cache
.find(token_id
);
236 ldout(cct
, 20) << "cached token.project.id=" << t
->get_project_id()
238 auto apl
= apl_factory
->create_apl_remote(cct
, s
, get_acl_strategy(*t
),
239 get_creds_info(*t
, roles
.admin
));
240 return result_t::grant(std::move(apl
));
243 /* Retrieve token. */
244 if (rgw_is_pki_token(token
)) {
246 t
= decode_pki_token(token
);
249 t
= get_from_keystone(token
);
252 /* Can't decode, just go to the Keystone server for validation. */
253 t
= get_from_keystone(token
);
257 return result_t::deny(-EACCES
);
260 /* Verify expiration. */
262 ldout(cct
, 0) << "got expired token: " << t
->get_project_name()
263 << ":" << t
->get_user_name()
264 << " expired: " << t
->get_expires() << dendl
;
265 return result_t::deny(-EPERM
);
268 /* Check for necessary roles. */
269 for (const auto& role
: roles
.plain
) {
270 if (t
->has_role(role
) == true) {
271 ldout(cct
, 0) << "validated token: " << t
->get_project_name()
272 << ":" << t
->get_user_name()
273 << " expires: " << t
->get_expires() << dendl
;
274 token_cache
.add(token_id
, *t
);
275 auto apl
= apl_factory
->create_apl_remote(cct
, s
, get_acl_strategy(*t
),
276 get_creds_info(*t
, roles
.admin
));
277 return result_t::grant(std::move(apl
));
281 ldout(cct
, 0) << "user does not hold a matching role; required roles: "
282 << g_conf
->rgw_keystone_accepted_roles
<< dendl
;
284 return result_t::deny(-EPERM
);
289 * Try to validate S3 auth against keystone s3token interface
291 std::pair
<boost::optional
<rgw::keystone::TokenEnvelope
>, int>
292 EC2Engine::get_from_keystone(const boost::string_view
& access_key_id
,
293 const std::string
& string_to_sign
,
294 const boost::string_view
& signature
) const
296 /* prepare keystone url */
297 std::string keystone_url
= config
.get_endpoint_url();
298 if (keystone_url
.empty()) {
302 const auto api_version
= config
.get_api_version();
303 if (config
.get_api_version() == rgw::keystone::ApiVersion::VER_3
) {
304 keystone_url
.append("v3/s3tokens");
306 keystone_url
.append("v2.0/s3tokens");
309 /* get authentication token for Keystone. */
310 std::string admin_token
;
311 int ret
= rgw::keystone::Service::get_admin_token(cct
, token_cache
, config
,
314 ldout(cct
, 2) << "s3 keystone: cannot get token for keystone access"
319 using RGWValidateKeystoneToken
320 = rgw::keystone::Service::RGWValidateKeystoneToken
;
322 /* The container for plain response obtained from Keystone. It will be
323 * parsed token_envelope_t::parse method. */
324 ceph::bufferlist token_body_bl
;
325 RGWValidateKeystoneToken
validate(cct
, &token_body_bl
);
327 /* set required headers for keystone request */
328 validate
.append_header("X-Auth-Token", admin_token
);
329 validate
.append_header("Content-Type", "application/json");
331 /* check if we want to verify keystone's ssl certs */
332 validate
.set_verify_ssl(cct
->_conf
->rgw_keystone_verify_ssl
);
334 /* create json credentials request body */
335 JSONFormatter
credentials(false);
336 credentials
.open_object_section("");
337 credentials
.open_object_section("credentials");
338 credentials
.dump_string("access", sview2cstr(access_key_id
).data());
339 credentials
.dump_string("token", rgw::to_base64(string_to_sign
));
340 credentials
.dump_string("signature", sview2cstr(signature
).data());
341 credentials
.close_section();
342 credentials
.close_section();
344 std::stringstream os
;
345 credentials
.flush(os
);
346 validate
.set_post_data(os
.str());
347 validate
.set_send_length(os
.str().length());
350 ret
= validate
.process("POST", keystone_url
.c_str());
352 ldout(cct
, 2) << "s3 keystone: token validation ERROR: "
353 << token_body_bl
.c_str() << dendl
;
357 /* if the supplied signature is wrong, we will get 401 from Keystone */
358 if (validate
.get_http_status() ==
359 decltype(validate
)::HTTP_STATUS_UNAUTHORIZED
) {
360 return std::make_pair(boost::none
, -ERR_SIGNATURE_NO_MATCH
);
361 } else if (validate
.get_http_status() ==
362 decltype(validate
)::HTTP_STATUS_NOTFOUND
) {
363 return std::make_pair(boost::none
, -ERR_INVALID_ACCESS_KEY
);
366 /* now parse response */
367 rgw::keystone::TokenEnvelope token_envelope
;
368 ret
= token_envelope
.parse(cct
, std::string(), token_body_bl
, api_version
);
370 ldout(cct
, 2) << "s3 keystone: token parsing failed, ret=0" << ret
375 return std::make_pair(std::move(token_envelope
), 0);
378 EC2Engine::acl_strategy_t
379 EC2Engine::get_acl_strategy(const EC2Engine::token_envelope_t
&) const
381 /* This is based on the assumption that the default acl strategy in
382 * get_perms_from_aclspec, will take care. Extra acl spec is not required. */
386 EC2Engine::auth_info_t
387 EC2Engine::get_creds_info(const EC2Engine::token_envelope_t
& token
,
388 const std::vector
<std::string
>& admin_roles
391 using acct_privilege_t
= \
392 rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t
;
394 /* Check whether the user has an admin status. */
395 acct_privilege_t level
= acct_privilege_t::IS_PLAIN_ACCT
;
396 for (const auto& admin_role
: admin_roles
) {
397 if (token
.has_role(admin_role
)) {
398 level
= acct_privilege_t::IS_ADMIN_ACCT
;
404 /* Suggested account name for the authenticated user. */
405 rgw_user(token
.get_project_id()),
406 /* User's display name (aka real name). */
407 token
.get_project_name(),
408 /* Keystone doesn't support RGW's subuser concept, so we cannot cut down
409 * the access rights through the perm_mask. At least at this layer. */
410 RGW_PERM_FULL_CONTROL
,
416 rgw::auth::Engine::result_t
EC2Engine::authenticate(
417 const boost::string_view
& access_key_id
,
418 const boost::string_view
& signature
,
419 const string_to_sign_t
& string_to_sign
,
420 const signature_factory_t
&,
421 const completer_factory_t
& completer_factory
,
422 /* Passthorugh only! */
423 const req_state
* s
) const
425 /* This will be initialized on the first call to this method. In C++11 it's
426 * also thread-safe. */
427 static const struct RolesCacher
{
428 RolesCacher(CephContext
* const cct
) {
429 get_str_vec(cct
->_conf
->rgw_keystone_accepted_roles
, plain
);
430 get_str_vec(cct
->_conf
->rgw_keystone_accepted_admin_roles
, admin
);
432 /* Let's suppose that having an admin role implies also a regular one. */
433 plain
.insert(std::end(plain
), std::begin(admin
), std::end(admin
));
436 std::vector
<std::string
> plain
;
437 std::vector
<std::string
> admin
;
438 } accepted_roles(cct
);
440 boost::optional
<token_envelope_t
> t
;
442 std::tie(t
, failure_reason
) = \
443 get_from_keystone(access_key_id
, string_to_sign
, signature
);
445 return result_t::deny(failure_reason
);
448 /* Verify expiration. */
450 ldout(cct
, 0) << "got expired token: " << t
->get_project_name()
451 << ":" << t
->get_user_name()
452 << " expired: " << t
->get_expires() << dendl
;
453 return result_t::deny();
456 /* check if we have a valid role */
458 for (const auto& role
: accepted_roles
.plain
) {
459 if (t
->has_role(role
) == true) {
466 ldout(cct
, 5) << "s3 keystone: user does not hold a matching role;"
468 << cct
->_conf
->rgw_keystone_accepted_roles
<< dendl
;
469 return result_t::deny();
471 /* everything seems fine, continue with this user */
472 ldout(cct
, 5) << "s3 keystone: validated token: " << t
->get_project_name()
473 << ":" << t
->get_user_name()
474 << " expires: " << t
->get_expires() << dendl
;
476 auto apl
= apl_factory
->create_apl_remote(cct
, s
, get_acl_strategy(*t
),
477 get_creds_info(*t
, accepted_roles
.admin
));
478 return result_t::grant(std::move(apl
), completer_factory(boost::none
));
482 }; /* namespace keystone */
483 }; /* namespace auth */
484 }; /* namespace rgw */