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