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