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 bool is_reserved_caveat(const std::string &cv)
79{
80 return cv.compare(0, 5, "name:") == 0 ||
81 cv.compare(0, 5, "path:") == 0 ||
82 cv.compare(0, 9, "activity:") == 0 ||
83 cv.compare(0, 7, "before:") == 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
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
183
184std::string
185Handler::GenerateActivities(const XrdHttpExtReq & req, const std::string &resource) const
186{
187 std::string result = "activity:READ_METADATA";
188 // TODO - generate environment object that includes the Authorization header.
189 XrdAccPrivs privs = m_chain ? m_chain->Access(&req.GetSecEntity(), resource.c_str(), AOP_Any, NULL) : XrdAccPriv_None;
190 if ((privs & XrdAccPriv_Create) == XrdAccPriv_Create) {result += ",UPLOAD";}
191 if (privs & XrdAccPriv_Read) {result += ",DOWNLOAD";}
192 if (privs & XrdAccPriv_Delete) {result += ",DELETE";}
193 if ((privs & XrdAccPriv_Chown) == XrdAccPriv_Chown) {result += ",MANAGE,UPDATE_METADATA";}
194 if (privs & XrdAccPriv_Readdir) {result += ",LIST";}
195 return result;
196}
197
198
199// See if the macaroon handler is interested in this request.
200// We intercept all POST requests as we will be looking for a particular
201// header.
202bool
203Handler::MatchesPath(const char *verb, const char *path)
204{
205 return !strcmp(verb, "POST") || !strncmp(path, "/.well-known/", 13) ||
206 !strncmp(path, "/.oauth2/", 9);
207}
208
209
210int Handler::ProcessOAuthConfig(XrdHttpExtReq &req) {
211 if (req.verb != "GET")
212 {
213 return req.SendSimpleResp(405, NULL, NULL, "Only GET is valid for oauth config.", 0);
214 }
215 auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"host");
216 if (header == req.headers.end())
217 {
218 return req.SendSimpleResp(400, NULL, NULL, "Host header is required.", 0);
219 }
220
221 json_object *response_obj = json_object_new_object();
222 if (!response_obj)
223 {
224 return req.SendSimpleResp(500, NULL, NULL, "Unable to create new JSON response object.", 0);
225 }
226 std::string token_endpoint = "https://" + header->second + "/.oauth2/token";
227 json_object *endpoint_obj =
228 json_object_new_string_len(token_endpoint.c_str(), token_endpoint.size());
229 if (!endpoint_obj)
230 {
231 return req.SendSimpleResp(500, NULL, NULL, "Unable to create a new JSON macaroon string.", 0);
232 }
233 json_object_object_add(response_obj, "token_endpoint", endpoint_obj);
234
235 const char *response_result = json_object_to_json_string_ext(response_obj, JSON_C_TO_STRING_PRETTY);
236 int retval = req.SendSimpleResp(200, NULL, NULL, response_result, 0);
237 json_object_put(response_obj);
238 return retval;
239}
240
241
242int Handler::ProcessTokenRequest(XrdHttpExtReq &req)
243{
244 if (req.verb != "POST")
245 {
246 return req.SendSimpleResp(405, NULL, NULL, "Only POST is valid for token request.", 0);
247 }
248 auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-type");
249 if (header == req.headers.end())
250 {
251 return req.SendSimpleResp(400, NULL, NULL, "Content-Type missing; not a valid macaroon request?", 0);
252 }
253 if (header->second != "application/x-www-form-urlencoded")
254 {
255 return req.SendSimpleResp(400, NULL, NULL, "Content-Type must be set to `application/macaroon-request' to request a macaroon", 0);
256 }
257 char *request_data_raw;
258 // Note: this does not null-terminate the buffer contents.
259 if (req.BuffgetData(req.length, &request_data_raw, true) != req.length)
260 {
261 return req.SendSimpleResp(400, NULL, NULL, "Missing or invalid body of request.", 0);
262 }
263 std::string request_data(request_data_raw, req.length);
264 bool found_grant_type = false;
265 ssize_t validity = -1;
266 std::string scope;
267 std::string token;
268 std::istringstream token_stream(request_data);
269 while (std::getline(token_stream, token, '&'))
270 {
271 std::string::size_type eq = token.find("=");
272 if (eq == std::string::npos)
273 {
274 return req.SendSimpleResp(400, NULL, NULL, "Invalid format for form-encoding", 0);
275 }
276 std::string key = token.substr(0, eq);
277 std::string value = token.substr(eq + 1);
278 //std::cout << "Found key " << key << ", value " << value << std::endl;
279 if (key == "grant_type")
280 {
281 found_grant_type = true;
282 if (value != "client_credentials")
283 {
284 return req.SendSimpleResp(400, NULL, NULL, "Invalid grant type specified.", 0);
285 }
286 }
287 else if (key == "expire_in")
288 {
289 try
290 {
291 validity = std::stoll(value);
292 }
293 catch (...)
294 {
295 return req.SendSimpleResp(400, NULL, NULL, "Expiration request not parseable.", 0);
296 }
297 if (validity <= 0)
298 {
299 return req.SendSimpleResp(400, NULL, NULL, "Expiration request has invalid value.", 0);
300 }
301 }
302 else if (key == "scope")
303 {
304 char *value_raw = unquote(value.c_str());
305 if (value_raw == NULL)
306 {
307 return req.SendSimpleResp(400, NULL, NULL, "Unable to unquote scope.", 0);
308 }
309 scope = value_raw;
310 free(value_raw);
311 }
312 }
313 if (!found_grant_type)
314 {
315 return req.SendSimpleResp(400, NULL, NULL, "Grant type not specified.", 0);
316 }
317 if (scope.empty())
318 {
319 return req.SendSimpleResp(400, NULL, NULL, "Scope was not specified.", 0);
320 }
321 std::istringstream token_stream_scope(scope);
322 std::string path;
323 std::vector<std::string> other_caveats;
324 while (std::getline(token_stream_scope, token, ' '))
325 {
326 std::string::size_type col = token.find(":");
327 if (col == std::string::npos)
328 {
329 return req.SendSimpleResp(400, NULL, NULL, "Invalid format for requested scope", 0);
330 }
331 std::string key = token.substr(0, col);
332 std::string value = token.substr(col + 1);
333 //std::cout << "Found activity " << key << ", path " << value << std::endl;
334 if (path.empty())
335 {
336 path = value;
337 }
338 else if (value != path)
339 {
340 if (m_log->getMsgMask() & LogMask::Error) {
341 std::stringstream ss;
342 ss << "Encountered requested scope request for authorization " << key
343 << " with resource path " << value << "; however, prior request had path "
344 << path;
345 m_log->Emsg("MacaroonRequest", ss.str().c_str()); // Mask::Error
346 }
347 return req.SendSimpleResp(500, NULL, NULL, "Server only supports all scopes having the same path", 0);
348 }
349 other_caveats.push_back(key);
350 }
351 if (path.empty())
352 {
353 path = "/";
354 }
355 std::vector<std::string> other_caveats_final;
356 if (!other_caveats.empty()) {
357 std::stringstream ss;
358 ss << "activity:";
359 for (std::vector<std::string>::const_iterator iter = other_caveats.begin();
360 iter != other_caveats.end();
361 iter++)
362 {
363 ss << *iter << ",";
364 }
365 const std::string &final_str = ss.str();
366 other_caveats_final.push_back(final_str.substr(0, final_str.size() - 1));
367 }
368 return GenerateMacaroonResponse(req, path, other_caveats_final, validity, true);
369}
370
371
372// Process a macaroon request.
374{
375 if (req.resource == "/.well-known/oauth-authorization-server") {
376 return ProcessOAuthConfig(req);
377 } else if (req.resource == "/.oauth2/token") {
378 return ProcessTokenRequest(req);
379 }
380
381 auto header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-type");
382 if (header == req.headers.end())
383 {
384 return req.SendSimpleResp(400, NULL, NULL, "Content-Type missing; not a valid macaroon request?", 0);
385 }
386 if (header->second != "application/macaroon-request")
387 {
388 return req.SendSimpleResp(400, NULL, NULL, "Content-Type must be set to `application/macaroon-request' to request a macaroon", 0);
389 }
390 header = XrdOucTUtils::caseInsensitiveFind(req.headers,"content-length");
391 if (header == req.headers.end())
392 {
393 return req.SendSimpleResp(400, NULL, NULL, "Content-Length missing; not a valid POST", 0);
394 }
395 ssize_t blen;
396 try
397 {
398 blen = std::stoll(header->second);
399 }
400 catch (...)
401 {
402 return req.SendSimpleResp(400, NULL, NULL, "Content-Length not parseable.", 0);
403 }
404 if (blen <= 0)
405 {
406 return req.SendSimpleResp(400, NULL, NULL, "Content-Length has invalid value.", 0);
407 }
408 //for (const auto &header : req.headers) { printf("** Request header: %s=%s\n", header.first.c_str(), header.second.c_str()); }
409
410 // request_data is not necessarily null-terminated; hence, we use the more advanced _ex variant
411 // of the tokener to avoid making a copy of the character buffer.
412 char *request_data;
413 if (req.BuffgetData(blen, &request_data, true) != blen)
414 {
415 return req.SendSimpleResp(400, NULL, NULL, "Missing or invalid body of request.", 0);
416 }
417 json_tokener *tokener = json_tokener_new();
418 if (!tokener)
419 {
420 return req.SendSimpleResp(500, NULL, NULL, "Internal error when allocating token parser.", 0);
421 }
422 json_object *macaroon_req = json_tokener_parse_ex(tokener, request_data, blen);
423 enum json_tokener_error err = json_tokener_get_error(tokener);
424 json_tokener_free(tokener);
425 if (err != json_tokener_success)
426 {
427 if (macaroon_req) json_object_put(macaroon_req);
428 return req.SendSimpleResp(400, NULL, NULL, "Invalid JSON serialization of macaroon request.", 0);
429 }
430 json_object *validity_obj;
431 if (!json_object_object_get_ex(macaroon_req, "validity", &validity_obj))
432 {
433 json_object_put(macaroon_req);
434 return req.SendSimpleResp(400, NULL, NULL, "JSON request does not include a `validity`", 0);
435 }
436 const char *validity_cstr = json_object_get_string(validity_obj);
437 if (!validity_cstr)
438 {
439 json_object_put(macaroon_req);
440 return req.SendSimpleResp(400, NULL, NULL, "validity key cannot be cast to a string", 0);
441 }
442 std::string validity_str(validity_cstr);
443 ssize_t validity = determine_validity(validity_str);
444 if (validity <= 0)
445 {
446 json_object_put(macaroon_req);
447 return req.SendSimpleResp(400, NULL, NULL, "Invalid ISO 8601 duration for validity key", 0);
448 }
449 json_object *caveats_obj;
450 std::vector<std::string> other_caveats;
451 if (json_object_object_get_ex(macaroon_req, "caveats", &caveats_obj))
452 {
453 if (json_object_is_type(caveats_obj, json_type_array))
454 { // Caveats were provided. Let's record them.
455 // TODO - could just add these in-situ. No need for the other_caveats vector.
456 int array_length = json_object_array_length(caveats_obj);
457 other_caveats.reserve(array_length);
458 for (int idx=0; idx<array_length; idx++)
459 {
460 json_object *caveat_item = json_object_array_get_idx(caveats_obj, idx);
461 if (caveat_item)
462 {
463 const char *caveat_item_str = json_object_get_string(caveat_item);
464 if (caveat_item_str && is_reserved_caveat(caveat_item_str)) {
465 json_object_put(macaroon_req);
466 return req.SendSimpleResp(400, NULL, NULL,
467 "Caveat uses a reserved predicate key (name:/path:/activity:/before:)", 0);
468 }
469 other_caveats.emplace_back(caveat_item_str);
470 }
471 }
472 }
473 }
474 json_object_put(macaroon_req);
475
476 return GenerateMacaroonResponse(req, req.resource, other_caveats, validity, false);
477}
478
479
480int
481Handler::GenerateMacaroonResponse(XrdHttpExtReq &req, const std::string &resource,
482 const std::vector<std::string> &other_caveats, ssize_t validity, bool oauth_response)
483{
484 time_t now;
485 time(&now);
486 if (m_max_duration > 0)
487 {
488 validity = (validity > m_max_duration) ? m_max_duration : validity;
489 }
490 now += validity;
491
492 char utc_time_buf[21];
493 if (!strftime(utc_time_buf, 21, "%FT%TZ", gmtime(&now)))
494 {
495 return req.SendSimpleResp(500, NULL, NULL, "Internal error constructing UTC time", 0);
496 }
497 std::string utc_time_str(utc_time_buf);
498 std::stringstream ss;
499 ss << "before:" << utc_time_str;
500 std::string utc_time_caveat = ss.str();
501
502 std::string activities = GenerateActivities(req, resource);
503 std::string macaroon_id = GenerateID(resource, req.GetSecEntity(), activities, other_caveats, utc_time_str);
504 enum macaroon_returncode mac_err;
505
506 struct macaroon *mac = macaroon_create(reinterpret_cast<const unsigned char*>(m_location.c_str()),
507 m_location.size(),
508 reinterpret_cast<const unsigned char*>(m_secret.c_str()),
509 m_secret.size(),
510 reinterpret_cast<const unsigned char*>(macaroon_id.c_str()),
511 macaroon_id.size(), &mac_err);
512 if (!mac) {
513 return req.SendSimpleResp(500, NULL, NULL, "Internal error constructing the macaroon", 0);
514 }
515
516 // Embed the SecEntity name, if present.
517 struct macaroon *mac_with_name;
518 const char * sec_name = req.GetSecEntity().name;
519 if (sec_name) {
520 std::stringstream name_caveat_ss;
521 name_caveat_ss << "name:" << sec_name;
522 std::string name_caveat = name_caveat_ss.str();
523 mac_with_name = macaroon_add_first_party_caveat(mac,
524 reinterpret_cast<const unsigned char*>(name_caveat.c_str()),
525 name_caveat.size(),
526 &mac_err);
527 macaroon_destroy(mac);
528 } else {
529 mac_with_name = mac;
530 }
531 if (!mac_with_name)
532 {
533 return req.SendSimpleResp(500, NULL, NULL, "Internal error adding default activities to macaroon", 0);
534 }
535
536 struct macaroon *mac_with_activities = macaroon_add_first_party_caveat(mac_with_name,
537 reinterpret_cast<const unsigned char*>(activities.c_str()),
538 activities.size(),
539 &mac_err);
540 macaroon_destroy(mac_with_name);
541 if (!mac_with_activities)
542 {
543 return req.SendSimpleResp(500, NULL, NULL, "Internal error adding default activities to macaroon", 0);
544 }
545
546
547 for (const auto &caveat : other_caveats)
548 {
549 struct macaroon *mac_tmp = mac_with_activities;
550 mac_with_activities = macaroon_add_first_party_caveat(mac_tmp,
551 reinterpret_cast<const unsigned char*>(caveat.c_str()),
552 caveat.size(),
553 &mac_err);
554 macaroon_destroy(mac_tmp);
555 if (!mac_with_activities)
556 {
557 return req.SendSimpleResp(500, NULL, NULL, "Internal error adding user caveat to macaroon", 0);
558 }
559 }
560
561 // Note we don't call `NormalizeSlashes` here; for backward compatibility reasons, we ensure the
562 // token issued is identical to what was working with prior versions of XRootD. This allows for a
563 // mix of old/new versions in a single cluster to interoperate. In a few years, it might be reasonable
564 // to invoke it here as well.
565 std::string path_caveat = "path:" + resource;
566 struct macaroon *mac_with_path = macaroon_add_first_party_caveat(mac_with_activities,
567 reinterpret_cast<const unsigned char*>(path_caveat.c_str()),
568 path_caveat.size(),
569 &mac_err);
570 macaroon_destroy(mac_with_activities);
571 if (!mac_with_path) {
572 return req.SendSimpleResp(500, NULL, NULL, "Internal error adding path to macaroon", 0);
573 }
574
575 struct macaroon *mac_with_date = macaroon_add_first_party_caveat(mac_with_path,
576 reinterpret_cast<const unsigned char*>(utc_time_caveat.c_str()),
577 utc_time_caveat.size(),
578 &mac_err);
579 macaroon_destroy(mac_with_path);
580 if (!mac_with_date) {
581 return req.SendSimpleResp(500, NULL, NULL, "Internal error adding date to macaroon", 0);
582 }
583
584 size_t size_hint = macaroon_serialize_size_hint(mac_with_date);
585
586 std::vector<char> macaroon_resp; macaroon_resp.resize(size_hint);
587 if (macaroon_serialize(mac_with_date, &macaroon_resp[0], size_hint, &mac_err))
588 {
589 printf("Returned macaroon_serialize code: %zu\n", size_hint);
590 return req.SendSimpleResp(500, NULL, NULL, "Internal error serializing macaroon", 0);
591 }
592 macaroon_destroy(mac_with_date);
593
594 json_object *response_obj = json_object_new_object();
595 if (!response_obj)
596 {
597 return req.SendSimpleResp(500, NULL, NULL, "Unable to create new JSON response object.", 0);
598 }
599 json_object *macaroon_obj = json_object_new_string_len(&macaroon_resp[0], strlen(&macaroon_resp[0]));
600 if (!macaroon_obj)
601 {
602 return req.SendSimpleResp(500, NULL, NULL, "Unable to create a new JSON macaroon string.", 0);
603 }
604 json_object_object_add(response_obj, oauth_response ? "access_token" : "macaroon", macaroon_obj);
605
606 json_object *expire_in_obj = json_object_new_int64(validity);
607 if (!expire_in_obj)
608 {
609 return req.SendSimpleResp(500, NULL, NULL, "Unable to create a new JSON validity object.", 0);
610 }
611 json_object_object_add(response_obj, "expires_in", expire_in_obj);
612
613 const char *macaroon_result = json_object_to_json_string_ext(response_obj, JSON_C_TO_STRING_PRETTY);
614 int retval = req.SendSimpleResp(200, NULL, NULL, macaroon_result, 0);
615 json_object_put(response_obj);
616 return retval;
617}
@ 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_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 &)