XRootD
Loading...
Searching...
No Matches
XrdMacaroonsHandler.cc
Go to the documentation of this file.
2
8
9#include <cstring>
10#include <iostream>
11#include <set>
12#include <sstream>
13#include <string>
14
15#include <json.h>
16#include <macaroons.h>
17#include <uuid/uuid.h>
18
19using namespace Macaroons;
20
21char *unquote(const char *str) {
22 int l = strlen(str);
23 char *r = (char *) malloc(l + 1);
24 r[0] = '\0';
25 int i, j = 0;
26
27 for (i = 0; i < l; i++) {
28
29 if (str[i] == '%') {
30 char savec[3];
31 if (l <= i + 3) {
32 free(r);
33 return nullptr;
34 }
35 savec[0] = str[i + 1];
36 savec[1] = str[i + 2];
37 savec[2] = '\0';
38
39 r[j] = strtol(savec, 0, 16);
40
41 i += 2;
42 } else if (str[i] == '+') r[j] = ' ';
43 else r[j] = str[i];
44
45 j++;
46 }
47
48 r[j] = '\0';
49
50 return r;
51
52}
53
54std::string Macaroons::NormalizeSlashes(const std::string &input)
55{
56 std::string output;
57 // In most cases, the output should be "about as large"
58 // as the input
59 output.reserve(input.size());
60 char prior_chr = '\0';
61 size_t output_idx = 0;
62 for (size_t idx = 0; idx < input.size(); idx++) {
63 char chr = input[idx];
64 if (prior_chr == '/' && chr == '/') {
65 output_idx++;
66 continue;
67 }
68 output += input[output_idx];
69 prior_chr = chr;
70 output_idx++;
71 }
72 return output;
73}
74
75static bool is_reserved_caveat(const std::string &cv)
76{
77 return cv.compare(0, 5, "name:") == 0 ||
78 cv.compare(0, 5, "path:") == 0 ||
79 cv.compare(0, 7, "before:") == 0;
80}
81
82static bool is_supported_caveat(const std::string &cv)
83{
84 return cv.compare(0, 9, "activity:") == 0;
85}
86
87static
88ssize_t determine_validity(const std::string& input)
89{
90 ssize_t duration = 0;
91 if (input.find("PT") != 0)
92 {
93 return -1;
94 }
95 size_t pos = 2;
96 std::string remaining = input;
97 do
98 {
99 remaining = remaining.substr(pos);
100 if (remaining.size() == 0) break;
101 long cur_duration;
102 try
103 {
104 cur_duration = stol(remaining, &pos);
105 } catch (...)
106 {
107 return -1;
108 }
109 if (pos >= remaining.size())
110 {
111 return -1;
112 }
113 char unit = remaining[pos];
114 switch (unit) {
115 case 'S':
116 break;
117 case 'M':
118 cur_duration *= 60;
119 break;
120 case 'H':
121 cur_duration *= 3600;
122 break;
123 default:
124 return -1;
125 };
126 pos ++;
127 duration += cur_duration;
128 } while (1);
129 return duration;
130}
131
133{
134 delete m_chain;
135}
136
137
138std::string
139Handler::GenerateID(const std::string &resource,
140 const XrdSecEntity &entity,
141 const std::string &activities,
142 const std::vector<std::string> &other_caveats,
143 const std::string &before)
144{
145 uuid_t uu;
146 uuid_generate_random(uu);
147 char uuid_buf[37];
148 uuid_unparse(uu, uuid_buf);
149 std::string result(uuid_buf);
150
151// The following code shoul have been strictly for debugging purposes. This
152// added code skips it unless debug logging has been enabled. Due to the code
153// structure, indentation is a bit of a struggle as this is a minimal fix.
154//
155if (m_log->getMsgMask() & LogMask::Debug)
156 {
157 std::stringstream ss;
158 ss << "ID=" << result << ", ";
159 ss << "resource=" << NormalizeSlashes(resource) << ", ";
160 if (entity.prot[0] != '\0') {ss << "protocol=" << entity.prot << ", ";}
161 if (entity.name) {ss << "name=" << entity.name << ", ";}
162 if (entity.host) {ss << "host=" << entity.host << ", ";}
163 if (entity.vorg) {ss << "vorg=" << entity.vorg << ", ";}
164 if (entity.role) {ss << "role=" << entity.role << ", ";}
165 if (entity.grps) {ss << "groups=" << entity.grps << ", ";}
166 if (entity.endorsements) {ss << "endorsements=" << entity.endorsements << ", ";}
167 if (activities.size()) {ss << "base_activities=" << activities << ", ";}
168
169 for (std::vector<std::string>::const_iterator iter = other_caveats.begin();
170 iter != other_caveats.end();
171 iter++)
172 {
173 ss << "user_caveat=" << *iter << ", ";
174 }
175
176 ss << "expires=" << before;
177
178 m_log->Emsg("MacaroonGen", ss.str().c_str()); // Mask::Debug
179 }
180 return result;
181}
182
183std::string
184Handler::GenerateActivities(const XrdHttpExtReq & req, const std::string &resource) const
185{
186 std::string result = "activity:READ_METADATA";
187 // TODO - generate environment object that includes the Authorization header.
188 XrdAccPrivs privs = m_chain ? m_chain->Access(&req.GetSecEntity(), resource.c_str(), AOP_Any, nullptr) : XrdAccPriv_None;
189 if ((privs & XrdAccPriv_Create) == XrdAccPriv_Create) {result += ",UPLOAD";}
190 if (privs & XrdAccPriv_Read) {result += ",DOWNLOAD";}
191 if (privs & XrdAccPriv_Delete) {result += ",DELETE";}
192 if ((privs & XrdAccPriv_Chown) == XrdAccPriv_Chown) {result += ",MANAGE,UPDATE_METADATA";}
193 if (privs & XrdAccPriv_Readdir) {result += ",LIST";}
194 return result;
195}
196
197// See if the macaroon handler is interested in this request.
198// We intercept all POST requests as we will be looking for a particular
199// header.
200bool
201Handler::MatchesPath(const char *verb, const char *path)
202{
203 return !strcmp(verb, "POST") || !strncmp(path, "/.well-known/", 13) ||
204 !strncmp(path, "/.oauth2/", 9);
205}
206
207int Handler::ProcessOAuthConfig(XrdHttpExtReq &req) {
208 if (req.verb != "GET")
209 {
210 return req.SendSimpleResp(405, nullptr, nullptr, "Only GET is valid for oauth config.", 0);
211 }
212 auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"host");
213 if (header == req.headers.end())
214 {
215 return req.SendSimpleResp(400, nullptr, nullptr, "Host header is required.", 0);
216 }
217
218 json_object *response_obj = json_object_new_object();
219 if (!response_obj)
220 {
221 return req.SendSimpleResp(500, nullptr, nullptr, "Unable to create new JSON response object.", 0);
222 }
223 std::string token_endpoint = "https://" + header->second + "/.oauth2/token";
224 json_object *endpoint_obj =
225 json_object_new_string_len(token_endpoint.c_str(), token_endpoint.size());
226 if (!endpoint_obj)
227 {
228 return req.SendSimpleResp(500, nullptr, nullptr, "Unable to create a new JSON macaroon string.", 0);
229 }
230 json_object_object_add(response_obj, "token_endpoint", endpoint_obj);
231
232 const char *response_result = json_object_to_json_string_ext(response_obj, JSON_C_TO_STRING_PRETTY);
233 int retval = req.SendSimpleResp(200, nullptr, nullptr, response_result, 0);
234 json_object_put(response_obj);
235 return retval;
236}
237
238int Handler::ProcessTokenRequest(XrdHttpExtReq &req)
239{
240 if (req.verb != "POST")
241 return req.SendSimpleResp(405, nullptr, "allow: POST",
242 "Only POST method is allowed to request a macaroon", false);
243
244 auto header = XrdOucTUtils::caseInsensitiveFind(req.headers, "content-type");
245 if (header == req.headers.end() || header->second != "application/x-www-form-urlencoded")
246 return req.SendSimpleResp(415, nullptr, "accept: application/x-www-form-urlencoded",
247 "Content-Type must be 'application/macaroon-request' to request a macaroon", false);
248
249 if (req.length > 4096)
250 return req.SendSimpleResp(413, nullptr, nullptr, "Macaroon request too large (must be less than 4KB)", false);
251
252 // Note: this does not null-terminate the buffer contents.
253 char *request_data_raw = nullptr;
254
255 if (req.length <= 0 || req.BuffgetData(req.length, &request_data_raw, true) != req.length)
256 return req.SendSimpleResp(400, nullptr, nullptr, "Missing or invalid body of request.", 0);
257
258 std::string request_data(request_data_raw, req.length);
259 bool found_grant_type = false;
260 ssize_t validity = -1;
261 std::string scope;
262 std::string token;
263 std::istringstream token_stream(request_data);
264 while (std::getline(token_stream, token, '&'))
265 {
266 std::string::size_type eq = token.find("=");
267 if (eq == std::string::npos)
268 {
269 return req.SendSimpleResp(400, nullptr, nullptr, "Invalid format for form-encoding", 0);
270 }
271 std::string key = token.substr(0, eq);
272 std::string value = token.substr(eq + 1);
273 //std::cout << "Found key " << key << ", value " << value << std::endl;
274 if (key == "grant_type")
275 {
276 found_grant_type = true;
277 if (value != "client_credentials")
278 {
279 return req.SendSimpleResp(400, nullptr, nullptr, "Invalid grant type specified.", 0);
280 }
281 }
282 else if (key == "expire_in")
283 {
284 if ((validity = std::strtoll(value.c_str(), nullptr, 10)) <= 0)
285 return req.SendSimpleResp(400, nullptr, nullptr, "Expiration request has invalid value.", 0);
286 }
287 else if (key == "scope")
288 {
289 char *value_raw = unquote(value.c_str());
290 if (value_raw == nullptr)
291 {
292 return req.SendSimpleResp(400, nullptr, nullptr, "Unable to unquote scope.", 0);
293 }
294 scope = value_raw;
295 free(value_raw);
296 }
297 }
298 if (!found_grant_type)
299 {
300 return req.SendSimpleResp(400, nullptr, nullptr, "Grant type not specified.", 0);
301 }
302 if (scope.empty())
303 {
304 return req.SendSimpleResp(400, nullptr, nullptr, "Scope was not specified.", 0);
305 }
306 std::istringstream token_stream_scope(scope);
307 std::string path;
308 std::vector<std::string> other_caveats;
309 while (std::getline(token_stream_scope, token, ' '))
310 {
311 std::string::size_type col = token.find(":");
312 if (col == std::string::npos)
313 {
314 return req.SendSimpleResp(400, nullptr, nullptr, "Invalid format for requested scope", 0);
315 }
316 std::string key = token.substr(0, col);
317 std::string value = token.substr(col + 1);
318 //std::cout << "Found activity " << key << ", path " << value << std::endl;
319 if (path.empty())
320 {
321 path = value;
322 }
323 else if (value != path)
324 {
325 if (m_log->getMsgMask() & LogMask::Error) {
326 std::stringstream ss;
327 ss << "Encountered requested scope request for authorization " << key
328 << " with resource path " << value << "; however, prior request had path "
329 << path;
330 m_log->Emsg("MacaroonRequest", ss.str().c_str()); // Mask::Error
331 }
332 return req.SendSimpleResp(500, nullptr, nullptr, "Server only supports all scopes having the same path", 0);
333 }
334 other_caveats.push_back(key);
335 }
336 if (path.empty())
337 {
338 path = "/";
339 }
340 std::vector<std::string> other_caveats_final;
341 if (!other_caveats.empty()) {
342 std::stringstream ss;
343 ss << "activity:";
344 for (std::vector<std::string>::const_iterator iter = other_caveats.begin();
345 iter != other_caveats.end();
346 iter++)
347 {
348 ss << *iter << ",";
349 }
350 const std::string &final_str = ss.str();
351 other_caveats_final.push_back(final_str.substr(0, final_str.size() - 1));
352 }
353 return GenerateMacaroonResponse(req, path, other_caveats_final, validity, true);
354}
355
356// Process a macaroon request.
358{
359 if (req.resource == "/.well-known/oauth-authorization-server") {
360 return ProcessOAuthConfig(req);
361 } else if (req.resource == "/.oauth2/token") {
362 return ProcessTokenRequest(req);
363 }
364
365 auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-type");
366 if (header == req.headers.end() || header->second != "application/macaroon-request")
367 return req.SendSimpleResp(415, nullptr, "accept: application/macaroon-request",
368 "Content-Type must be 'application/macaroon-request' to request a macaroon", false);
369
370 header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-length");
371 if (header == req.headers.end())
372 return req.SendSimpleResp(411, nullptr, nullptr, "Content-Length missing; not a valid POST", false);
373
374 ssize_t blen = std::strtoll(header->second.c_str(), nullptr, 10);
375
376 if (blen <= 0)
377 return req.SendSimpleResp(400, nullptr, nullptr, "Content-Length has invalid value.", false);
378
379 if (blen > 4096)
380 return req.SendSimpleResp(413, nullptr, nullptr, "Macaroon request too large (must be less than 4KB)", false);
381
382 // request_data is not necessarily null-terminated; hence, we use the more advanced _ex variant
383 // of the tokener to avoid making a copy of the character buffer.
384 char *request_data;
385 if (req.BuffgetData(blen, &request_data, true) != blen)
386 {
387 return req.SendSimpleResp(400, nullptr, nullptr, "Missing or invalid body of request.", 0);
388 }
389 json_tokener *tokener = json_tokener_new();
390 if (!tokener)
391 {
392 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error when allocating token parser.", 0);
393 }
394 json_object *macaroon_req = json_tokener_parse_ex(tokener, request_data, blen);
395 enum json_tokener_error err = json_tokener_get_error(tokener);
396 json_tokener_free(tokener);
397 if (err != json_tokener_success)
398 {
399 if (macaroon_req) json_object_put(macaroon_req);
400 return req.SendSimpleResp(400, nullptr, nullptr, "Invalid JSON serialization of macaroon request.", 0);
401 }
402 json_object *validity_obj;
403 if (!json_object_object_get_ex(macaroon_req, "validity", &validity_obj))
404 {
405 json_object_put(macaroon_req);
406 return req.SendSimpleResp(400, nullptr, nullptr, "JSON request does not include a `validity`", 0);
407 }
408 const char *validity_cstr = json_object_get_string(validity_obj);
409 if (!validity_cstr)
410 {
411 json_object_put(macaroon_req);
412 return req.SendSimpleResp(400, nullptr, nullptr, "validity key cannot be cast to a string", 0);
413 }
414 std::string validity_str(validity_cstr);
415 ssize_t validity = determine_validity(validity_str);
416 if (validity <= 0)
417 {
418 json_object_put(macaroon_req);
419 return req.SendSimpleResp(400, nullptr, nullptr, "Invalid ISO 8601 duration for validity key", 0);
420 }
421 json_object *caveats_obj;
422 std::vector<std::string> other_caveats;
423 if (json_object_object_get_ex(macaroon_req, "caveats", &caveats_obj))
424 {
425 if (json_object_is_type(caveats_obj, json_type_array))
426 { // Caveats were provided. Let's record them.
427 // TODO - could just add these in-situ. No need for the other_caveats vector.
428 int array_length = json_object_array_length(caveats_obj);
429 other_caveats.reserve(array_length);
430 for (int idx=0; idx<array_length; idx++)
431 {
432 json_object *caveat_item = json_object_array_get_idx(caveats_obj, idx);
433 if (caveat_item)
434 {
435 const char *caveat_item_str = json_object_get_string(caveat_item);
436
437 if (!caveat_item_str) {
438 json_object_put(macaroon_req);
439 return req.SendSimpleResp(400, nullptr, nullptr, "Malformed or invalid caveat", 0);
440 }
441
442 if (is_reserved_caveat(caveat_item_str)) {
443 json_object_put(macaroon_req);
444 return req.SendSimpleResp(400, nullptr, nullptr,
445 "Cannot accept caveat with reserved key (name, path, before)\n", 0);
446 }
447
448 if (!is_supported_caveat(caveat_item_str)) {
449 json_object_put(macaroon_req);
450 return req.SendSimpleResp(400, nullptr, nullptr,
451 "Cannot accept caveat of unsupported type (supported types: activity)\n", 0);
452 }
453
454 other_caveats.emplace_back(caveat_item_str);
455 }
456 }
457 }
458 }
459 json_object_put(macaroon_req);
460
461 return GenerateMacaroonResponse(req, req.resource, other_caveats, validity, false);
462}
463
464
465int
466Handler::GenerateMacaroonResponse(XrdHttpExtReq &req, const std::string &resource,
467 const std::vector<std::string> &other_caveats, ssize_t validity, bool oauth_response)
468{
469 time_t now;
470 time(&now);
471 if (m_max_duration > 0)
472 {
473 validity = (validity > m_max_duration) ? m_max_duration : validity;
474 }
475 now += validity;
476
477 char utc_time_buf[21];
478 if (!strftime(utc_time_buf, 21, "%FT%TZ", gmtime(&now)))
479 {
480 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error constructing UTC time", 0);
481 }
482 std::string utc_time_str(utc_time_buf);
483 std::stringstream ss;
484 ss << "before:" << utc_time_str;
485 std::string utc_time_caveat = ss.str();
486
487 std::string activities = GenerateActivities(req, resource);
488
489 // Intersect user-requested activities with those the authz chain permits.
490 // A caveat can only attenuate privileges, never grant new ones.
491 for (const auto &caveat : other_caveats) {
492 if (caveat.compare(0, 9, "activity:") == 0) {
493 std::set<std::string> allowed;
494 { std::stringstream ss(activities.substr(9));
495 for (std::string a; std::getline(ss, a, ','); )
496 allowed.insert(a); }
497 std::string result = "activity:";
498 bool first = true;
499 std::stringstream ss(caveat.substr(9));
500 for (std::string a; std::getline(ss, a, ','); ) {
501 if (allowed.count(a)) {
502 if (!first) result += ',';
503 result += a;
504 first = false;
505 }
506 }
507 if (result.size() > 9)
508 activities = result;
509 }
510 }
511
512 std::string macaroon_id = GenerateID(resource, req.GetSecEntity(), activities, other_caveats, utc_time_str);
513 enum macaroon_returncode mac_err;
514
515 struct macaroon *mac = macaroon_create(reinterpret_cast<const unsigned char*>(m_location.c_str()),
516 m_location.size(),
517 reinterpret_cast<const unsigned char*>(m_secret.c_str()),
518 m_secret.size(),
519 reinterpret_cast<const unsigned char*>(macaroon_id.c_str()),
520 macaroon_id.size(), &mac_err);
521 if (!mac) {
522 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error constructing the macaroon", 0);
523 }
524
525 // Embed the SecEntity name, if present.
526 struct macaroon *mac_with_name;
527 const char * sec_name = req.GetSecEntity().name;
528 if (sec_name) {
529 std::stringstream name_caveat_ss;
530 name_caveat_ss << "name:" << sec_name;
531 std::string name_caveat = name_caveat_ss.str();
532 mac_with_name = macaroon_add_first_party_caveat(mac,
533 reinterpret_cast<const unsigned char*>(name_caveat.c_str()),
534 name_caveat.size(),
535 &mac_err);
536 macaroon_destroy(mac);
537 } else {
538 mac_with_name = mac;
539 }
540 if (!mac_with_name)
541 {
542 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error adding 'name' caveat to macaroon", 0);
543 }
544
545 struct macaroon *mac_with_activities = macaroon_add_first_party_caveat(mac_with_name,
546 reinterpret_cast<const unsigned char*>(activities.c_str()),
547 activities.size(),
548 &mac_err);
549 macaroon_destroy(mac_with_name);
550 if (!mac_with_activities)
551 {
552 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error adding 'activity' caveat to macaroon", 0);
553 }
554
555 // Note we don't call `NormalizeSlashes` here; for backward compatibility reasons, we ensure the
556 // token issued is identical to what was working with prior versions of XRootD. This allows for a
557 // mix of old/new versions in a single cluster to interoperate. In a few years, it might be reasonable
558 // to invoke it here as well.
559 std::string path_caveat = "path:" + resource;
560 struct macaroon *mac_with_path = macaroon_add_first_party_caveat(mac_with_activities,
561 reinterpret_cast<const unsigned char*>(path_caveat.c_str()),
562 path_caveat.size(),
563 &mac_err);
564 macaroon_destroy(mac_with_activities);
565 if (!mac_with_path) {
566 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error adding 'path' caveat to macaroon", 0);
567 }
568
569 struct macaroon *mac_with_date = macaroon_add_first_party_caveat(mac_with_path,
570 reinterpret_cast<const unsigned char*>(utc_time_caveat.c_str()),
571 utc_time_caveat.size(),
572 &mac_err);
573 macaroon_destroy(mac_with_path);
574 if (!mac_with_date) {
575 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error adding date to macaroon", 0);
576 }
577
578 size_t size_hint = macaroon_serialize_size_hint(mac_with_date);
579
580 std::vector<char> macaroon_resp; macaroon_resp.resize(size_hint);
581 if (macaroon_serialize(mac_with_date, &macaroon_resp[0], size_hint, &mac_err))
582 {
583 printf("Returned macaroon_serialize code: %zu\n", size_hint);
584 return req.SendSimpleResp(500, nullptr, nullptr, "Internal error serializing macaroon", 0);
585 }
586 macaroon_destroy(mac_with_date);
587
588 json_object *response_obj = json_object_new_object();
589 if (!response_obj)
590 {
591 return req.SendSimpleResp(500, nullptr, nullptr, "Unable to create new JSON response object.", 0);
592 }
593 json_object *macaroon_obj = json_object_new_string_len(&macaroon_resp[0], strlen(&macaroon_resp[0]));
594 if (!macaroon_obj)
595 {
596 return req.SendSimpleResp(500, nullptr, nullptr, "Unable to create a new JSON macaroon string.", 0);
597 }
598 json_object_object_add(response_obj, oauth_response ? "access_token" : "macaroon", macaroon_obj);
599
600 json_object *expire_in_obj = json_object_new_int64(validity);
601 if (!expire_in_obj)
602 {
603 return req.SendSimpleResp(500, nullptr, nullptr, "Unable to create a new JSON validity object.", 0);
604 }
605 json_object_object_add(response_obj, "expires_in", expire_in_obj);
606
607 const char *macaroon_result = json_object_to_json_string_ext(response_obj, JSON_C_TO_STRING_PRETTY);
608 int retval = req.SendSimpleResp(200, nullptr, nullptr, macaroon_result, 0);
609 json_object_put(response_obj);
610 return retval;
611}
@ AOP_Any
Special for getting privs.
XrdAccPrivs
@ XrdAccPriv_Chown
@ XrdAccPriv_Read
@ XrdAccPriv_None
@ XrdAccPriv_Delete
@ XrdAccPriv_Create
@ XrdAccPriv_Readdir
char * unquote(char *str)
char * unquote(const char *str)
static bool is_supported_caveat(const std::string &cv)
static bool is_reserved_caveat(const std::string &cv)
static ssize_t determine_validity(const std::string &input)
virtual bool MatchesPath(const char *verb, const char *path) override
Tells if the incoming path is recognized as one of the paths that have to be processed.
virtual int ProcessReq(XrdHttpExtReq &req) override
std::map< std::string, std::string > & headers
std::string resource
int BuffgetData(int blen, char **data, bool wait)
Get a pointer to data read from the client, valid for up to blen bytes from the buffer....
const XrdSecEntity & GetSecEntity() const
int SendSimpleResp(int code, const char *desc, const char *header_to_add, const char *body, long long bodylen)
Sends a basic response. If the length is < 0 then it is calculated internally.
static std::map< std::string, T >::const_iterator caseInsensitiveFind(const std::map< std::string, T > &m, const std::string &lowerCaseSearchKey)
char * vorg
Entity's virtual organization(s)
char prot[XrdSecPROTOIDSIZE]
Auth protocol used (e.g. krb5)
char * grps
Entity's group name(s)
char * name
Entity's name.
char * role
Entity's role(s)
char * endorsements
Protocol specific endorsements.
char * host
Entity's host name dnr dependent.
std::string NormalizeSlashes(const std::string &)