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