]> git.proxmox.com Git - ceph.git/blob - ceph/src/rgw/rgw_auth_keystone.cc
update sources to v12.2.3
[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
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_keystone.h"
21 #include "rgw_rest_s3.h"
22 #include "rgw_auth_s3.h"
23
24 #include "common/ceph_crypto_cms.h"
25 #include "common/armor.h"
26 #include "common/Cond.h"
27
28 #define dout_subsys ceph_subsys_rgw
29
30
31 namespace rgw {
32 namespace auth {
33 namespace keystone {
34
35 bool
36 TokenEngine::is_applicable(const std::string& token) const noexcept
37 {
38 return ! token.empty() && ! cct->_conf->rgw_keystone_url.empty();
39 }
40
41 TokenEngine::token_envelope_t
42 TokenEngine::decode_pki_token(const std::string& token) const
43 {
44 ceph::buffer::list token_body_bl;
45 int ret = rgw_decode_b64_cms(cct, token, token_body_bl);
46 if (ret < 0) {
47 ldout(cct, 20) << "cannot decode pki token" << dendl;
48 throw ret;
49 } else {
50 ldout(cct, 20) << "successfully decoded pki token" << dendl;
51 }
52
53 TokenEngine::token_envelope_t token_body;
54 ret = token_body.parse(cct, token, token_body_bl, config.get_api_version());
55 if (ret < 0) {
56 throw ret;
57 }
58
59 return token_body;
60 }
61
62 boost::optional<TokenEngine::token_envelope_t>
63 TokenEngine::get_from_keystone(const std::string& token) const
64 {
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;
69
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);
74
75 std::string url = config.get_endpoint_url();
76 if (url.empty()) {
77 throw -EINVAL;
78 }
79
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);
86 }
87
88 std::string admin_token;
89 if (rgw::keystone::Service::get_admin_token(cct, token_cache, config,
90 admin_token) < 0) {
91 throw -EINVAL;
92 }
93
94 validate.append_header("X-Auth-Token", admin_token);
95 validate.set_send_length(0);
96
97 int ret = validate.process(url.c_str());
98 if (ret < 0) {
99 throw ret;
100 }
101
102 /* NULL terminate for debug output. */
103 token_body_bl.append(static_cast<char>(0));
104
105 /* Detect Keystone rejection earlier than during the token parsing.
106 * Although failure at the parsing phase doesn't impose a threat,
107 * this allows to return proper error code (EACCESS instead of EINVAL
108 * or similar) and thus improves logging. */
109 if (validate.get_http_status() ==
110 /* Most likely: wrong admin credentials or admin token. */
111 RGWValidateKeystoneToken::HTTP_STATUS_UNAUTHORIZED ||
112 validate.get_http_status() ==
113 /* Most likely: non-existent token supplied by the client. */
114 RGWValidateKeystoneToken::HTTP_STATUS_NOTFOUND) {
115 ldout(cct, 5) << "Failed keystone auth from " << url << " with "
116 << validate.get_http_status() << dendl;
117 return boost::none;
118 }
119
120 ldout(cct, 20) << "received response status=" << validate.get_http_status()
121 << ", body=" << token_body_bl.c_str() << dendl;
122
123 TokenEngine::token_envelope_t token_body;
124 ret = token_body.parse(cct, token, token_body_bl, config.get_api_version());
125 if (ret < 0) {
126 throw ret;
127 }
128
129 return token_body;
130 }
131
132 TokenEngine::auth_info_t
133 TokenEngine::get_creds_info(const TokenEngine::token_envelope_t& token,
134 const std::vector<std::string>& admin_roles
135 ) const noexcept
136 {
137 using acct_privilege_t = rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t;
138
139 /* Check whether the user has an admin status. */
140 acct_privilege_t level = acct_privilege_t::IS_PLAIN_ACCT;
141 for (const auto& admin_role : admin_roles) {
142 if (token.has_role(admin_role)) {
143 level = acct_privilege_t::IS_ADMIN_ACCT;
144 break;
145 }
146 }
147
148 return auth_info_t {
149 /* Suggested account name for the authenticated user. */
150 rgw_user(token.get_project_id()),
151 /* User's display name (aka real name). */
152 token.get_project_name(),
153 /* Keystone doesn't support RGW's subuser concept, so we cannot cut down
154 * the access rights through the perm_mask. At least at this layer. */
155 RGW_PERM_FULL_CONTROL,
156 level,
157 TYPE_KEYSTONE,
158 };
159 }
160
161 static inline const std::string
162 make_spec_item(const std::string& tenant, const std::string& id)
163 {
164 return tenant + ":" + id;
165 }
166
167 TokenEngine::acl_strategy_t
168 TokenEngine::get_acl_strategy(const TokenEngine::token_envelope_t& token) const
169 {
170 /* The primary identity is constructed upon UUIDs. */
171 const auto& tenant_uuid = token.get_project_id();
172 const auto& user_uuid = token.get_user_id();
173
174 /* For Keystone v2 an alias may be also used. */
175 const auto& tenant_name = token.get_project_name();
176 const auto& user_name = token.get_user_name();
177
178 /* Construct all possible combinations including Swift's wildcards. */
179 const std::array<std::string, 6> allowed_items = {
180 make_spec_item(tenant_uuid, user_uuid),
181 make_spec_item(tenant_name, user_name),
182
183 /* Wildcards. */
184 make_spec_item(tenant_uuid, "*"),
185 make_spec_item(tenant_name, "*"),
186 make_spec_item("*", user_uuid),
187 make_spec_item("*", user_name),
188 };
189
190 /* Lambda will obtain a copy of (not a reference to!) allowed_items. */
191 return [allowed_items](const rgw::auth::Identity::aclspec_t& aclspec) {
192 uint32_t perm = 0;
193
194 for (const auto& allowed_item : allowed_items) {
195 const auto iter = aclspec.find(allowed_item);
196
197 if (std::end(aclspec) != iter) {
198 perm |= iter->second;
199 }
200 }
201
202 return perm;
203 };
204 }
205
206 TokenEngine::result_t
207 TokenEngine::authenticate(const std::string& token,
208 const req_state* const s) const
209 {
210 boost::optional<TokenEngine::token_envelope_t> t;
211
212 /* This will be initialized on the first call to this method. In C++11 it's
213 * also thread-safe. */
214 static const struct RolesCacher {
215 RolesCacher(CephContext* const cct) {
216 get_str_vec(cct->_conf->rgw_keystone_accepted_roles, plain);
217 get_str_vec(cct->_conf->rgw_keystone_accepted_admin_roles, admin);
218
219 /* Let's suppose that having an admin role implies also a regular one. */
220 plain.insert(std::end(plain), std::begin(admin), std::end(admin));
221 }
222
223 std::vector<std::string> plain;
224 std::vector<std::string> admin;
225 } roles(cct);
226
227 if (! is_applicable(token)) {
228 return result_t::deny();
229 }
230
231 /* Token ID is a concept that makes dealing with PKI tokens more effective.
232 * Instead of storing several kilobytes, a short hash can be burried. */
233 const auto& token_id = rgw_get_token_id(token);
234 ldout(cct, 20) << "token_id=" << token_id << dendl;
235
236 /* Check cache first. */
237 t = token_cache.find(token_id);
238 if (t) {
239 ldout(cct, 20) << "cached token.project.id=" << t->get_project_id()
240 << dendl;
241 auto apl = apl_factory->create_apl_remote(cct, s, get_acl_strategy(*t),
242 get_creds_info(*t, roles.admin));
243 return result_t::grant(std::move(apl));
244 }
245
246 /* Retrieve token. */
247 if (rgw_is_pki_token(token)) {
248 try {
249 t = decode_pki_token(token);
250 } catch (...) {
251 /* Last resort. */
252 t = get_from_keystone(token);
253 }
254 } else {
255 /* Can't decode, just go to the Keystone server for validation. */
256 t = get_from_keystone(token);
257 }
258
259 if (! t) {
260 return result_t::deny(-EACCES);
261 }
262
263 /* Verify expiration. */
264 if (t->expired()) {
265 ldout(cct, 0) << "got expired token: " << t->get_project_name()
266 << ":" << t->get_user_name()
267 << " expired: " << t->get_expires() << dendl;
268 return result_t::deny(-EPERM);
269 }
270
271 /* Check for necessary roles. */
272 for (const auto& role : roles.plain) {
273 if (t->has_role(role) == true) {
274 ldout(cct, 0) << "validated token: " << t->get_project_name()
275 << ":" << t->get_user_name()
276 << " expires: " << t->get_expires() << dendl;
277 token_cache.add(token_id, *t);
278 auto apl = apl_factory->create_apl_remote(cct, s, get_acl_strategy(*t),
279 get_creds_info(*t, roles.admin));
280 return result_t::grant(std::move(apl));
281 }
282 }
283
284 ldout(cct, 0) << "user does not hold a matching role; required roles: "
285 << g_conf->rgw_keystone_accepted_roles << dendl;
286
287 return result_t::deny(-EPERM);
288 }
289
290
291 /*
292 * Try to validate S3 auth against keystone s3token interface
293 */
294 std::pair<boost::optional<rgw::keystone::TokenEnvelope>, int>
295 EC2Engine::get_from_keystone(const boost::string_view& access_key_id,
296 const std::string& string_to_sign,
297 const boost::string_view& signature) const
298 {
299 /* prepare keystone url */
300 std::string keystone_url = config.get_endpoint_url();
301 if (keystone_url.empty()) {
302 throw -EINVAL;
303 }
304
305 const auto api_version = config.get_api_version();
306 if (config.get_api_version() == rgw::keystone::ApiVersion::VER_3) {
307 keystone_url.append("v3/s3tokens");
308 } else {
309 keystone_url.append("v2.0/s3tokens");
310 }
311
312 /* get authentication token for Keystone. */
313 std::string admin_token;
314 int ret = rgw::keystone::Service::get_admin_token(cct, token_cache, config,
315 admin_token);
316 if (ret < 0) {
317 ldout(cct, 2) << "s3 keystone: cannot get token for keystone access"
318 << dendl;
319 throw ret;
320 }
321
322 using RGWValidateKeystoneToken
323 = rgw::keystone::Service::RGWValidateKeystoneToken;
324
325 /* The container for plain response obtained from Keystone. It will be
326 * parsed token_envelope_t::parse method. */
327 ceph::bufferlist token_body_bl;
328 RGWValidateKeystoneToken validate(cct, &token_body_bl);
329
330 /* set required headers for keystone request */
331 validate.append_header("X-Auth-Token", admin_token);
332 validate.append_header("Content-Type", "application/json");
333
334 /* check if we want to verify keystone's ssl certs */
335 validate.set_verify_ssl(cct->_conf->rgw_keystone_verify_ssl);
336
337 /* create json credentials request body */
338 JSONFormatter credentials(false);
339 credentials.open_object_section("");
340 credentials.open_object_section("credentials");
341 credentials.dump_string("access", sview2cstr(access_key_id).data());
342 credentials.dump_string("token", rgw::to_base64(string_to_sign));
343 credentials.dump_string("signature", sview2cstr(signature).data());
344 credentials.close_section();
345 credentials.close_section();
346
347 std::stringstream os;
348 credentials.flush(os);
349 validate.set_post_data(os.str());
350 validate.set_send_length(os.str().length());
351
352 /* send request */
353 ret = validate.process("POST", keystone_url.c_str());
354 if (ret < 0) {
355 ldout(cct, 2) << "s3 keystone: token validation ERROR: "
356 << token_body_bl.c_str() << dendl;
357 throw ret;
358 }
359
360 /* if the supplied signature is wrong, we will get 401 from Keystone */
361 if (validate.get_http_status() ==
362 decltype(validate)::HTTP_STATUS_UNAUTHORIZED) {
363 return std::make_pair(boost::none, -ERR_SIGNATURE_NO_MATCH);
364 } else if (validate.get_http_status() ==
365 decltype(validate)::HTTP_STATUS_NOTFOUND) {
366 return std::make_pair(boost::none, -ERR_INVALID_ACCESS_KEY);
367 }
368
369 /* now parse response */
370 rgw::keystone::TokenEnvelope token_envelope;
371 ret = token_envelope.parse(cct, std::string(), token_body_bl, api_version);
372 if (ret < 0) {
373 ldout(cct, 2) << "s3 keystone: token parsing failed, ret=0" << ret
374 << dendl;
375 throw ret;
376 }
377
378 return std::make_pair(std::move(token_envelope), 0);
379 }
380
381 EC2Engine::acl_strategy_t
382 EC2Engine::get_acl_strategy(const EC2Engine::token_envelope_t&) const
383 {
384 /* This is based on the assumption that the default acl strategy in
385 * get_perms_from_aclspec, will take care. Extra acl spec is not required. */
386 return nullptr;
387 }
388
389 EC2Engine::auth_info_t
390 EC2Engine::get_creds_info(const EC2Engine::token_envelope_t& token,
391 const std::vector<std::string>& admin_roles
392 ) const noexcept
393 {
394 using acct_privilege_t = \
395 rgw::auth::RemoteApplier::AuthInfo::acct_privilege_t;
396
397 /* Check whether the user has an admin status. */
398 acct_privilege_t level = acct_privilege_t::IS_PLAIN_ACCT;
399 for (const auto& admin_role : admin_roles) {
400 if (token.has_role(admin_role)) {
401 level = acct_privilege_t::IS_ADMIN_ACCT;
402 break;
403 }
404 }
405
406 return auth_info_t {
407 /* Suggested account name for the authenticated user. */
408 rgw_user(token.get_project_id()),
409 /* User's display name (aka real name). */
410 token.get_project_name(),
411 /* Keystone doesn't support RGW's subuser concept, so we cannot cut down
412 * the access rights through the perm_mask. At least at this layer. */
413 RGW_PERM_FULL_CONTROL,
414 level,
415 TYPE_KEYSTONE,
416 };
417 }
418
419 rgw::auth::Engine::result_t EC2Engine::authenticate(
420 const boost::string_view& access_key_id,
421 const boost::string_view& signature,
422 const string_to_sign_t& string_to_sign,
423 const signature_factory_t&,
424 const completer_factory_t& completer_factory,
425 /* Passthorugh only! */
426 const req_state* s) const
427 {
428 /* This will be initialized on the first call to this method. In C++11 it's
429 * also thread-safe. */
430 static const struct RolesCacher {
431 RolesCacher(CephContext* const cct) {
432 get_str_vec(cct->_conf->rgw_keystone_accepted_roles, plain);
433 get_str_vec(cct->_conf->rgw_keystone_accepted_admin_roles, admin);
434
435 /* Let's suppose that having an admin role implies also a regular one. */
436 plain.insert(std::end(plain), std::begin(admin), std::end(admin));
437 }
438
439 std::vector<std::string> plain;
440 std::vector<std::string> admin;
441 } accepted_roles(cct);
442
443 boost::optional<token_envelope_t> t;
444 int failure_reason;
445 std::tie(t, failure_reason) = \
446 get_from_keystone(access_key_id, string_to_sign, signature);
447 if (! t) {
448 return result_t::deny(failure_reason);
449 }
450
451 /* Verify expiration. */
452 if (t->expired()) {
453 ldout(cct, 0) << "got expired token: " << t->get_project_name()
454 << ":" << t->get_user_name()
455 << " expired: " << t->get_expires() << dendl;
456 return result_t::deny();
457 }
458
459 /* check if we have a valid role */
460 bool found = false;
461 for (const auto& role : accepted_roles.plain) {
462 if (t->has_role(role) == true) {
463 found = true;
464 break;
465 }
466 }
467
468 if (! found) {
469 ldout(cct, 5) << "s3 keystone: user does not hold a matching role;"
470 " required roles: "
471 << cct->_conf->rgw_keystone_accepted_roles << dendl;
472 return result_t::deny();
473 } else {
474 /* everything seems fine, continue with this user */
475 ldout(cct, 5) << "s3 keystone: validated token: " << t->get_project_name()
476 << ":" << t->get_user_name()
477 << " expires: " << t->get_expires() << dendl;
478
479 auto apl = apl_factory->create_apl_remote(cct, s, get_acl_strategy(*t),
480 get_creds_info(*t, accepted_roles.admin));
481 return result_t::grant(std::move(apl), completer_factory(boost::none));
482 }
483 }
484
485 }; /* namespace keystone */
486 }; /* namespace auth */
487 }; /* namespace rgw */