XRootD
Loading...
Searching...
No Matches
XrdMacaroonsAuthz.cc
Go to the documentation of this file.
3
4#include "XrdOuc/XrdOucEnv.hh"
9
10#include <ctime>
11#include <sstream>
12#include <stdexcept>
13
14#include <macaroons.h>
15
16using namespace Macaroons;
17
18namespace {
19
20class AuthzCheck
21{
22public:
23 AuthzCheck(const char *req_path, const Access_Operation req_oper, ssize_t max_duration, XrdSysError &log);
24
25 const std::string &GetSecName() const {return m_sec_name;}
26 const std::string &GetErrorMessage() const {return m_emsg;}
27
28 static int verify_before_s(void *authz_ptr,
29 const unsigned char *pred,
30 size_t pred_sz);
31
32 static int verify_activity_s(void *authz_ptr,
33 const unsigned char *pred,
34 size_t pred_sz);
35
36 static int verify_path_s(void *authz_ptr,
37 const unsigned char *pred,
38 size_t pred_sz);
39
40 static int verify_name_s(void *authz_ptr,
41 const unsigned char *pred,
42 size_t pred_sz);
43
44private:
45 int verify_before(const unsigned char *pred, size_t pred_sz);
46 int verify_activity(const unsigned char *pred, size_t pred_sz);
47 int verify_path(const unsigned char *pred, size_t pred_sz);
48 int verify_name(const unsigned char *pred, size_t pred_sz);
49
50 ssize_t m_max_duration;
51 XrdSysError &m_log;
52 std::string m_emsg;
53 const std::string m_path;
54 std::string m_desired_activity;
55 std::string m_sec_name;
56 Access_Operation m_oper;
57 time_t m_now;
58};
59
60static XrdAccPrivs AddPriv(Access_Operation op, XrdAccPrivs privs)
61{
62 int new_privs = privs;
63 switch (op) {
64 case AOP_Any:
65 break;
66 case AOP_Chmod:
67 new_privs |= static_cast<int>(XrdAccPriv_Chmod);
68 break;
69 case AOP_Chown:
70 new_privs |= static_cast<int>(XrdAccPriv_Chown);
71 break;
72 case AOP_Excl_Create: // fallthrough
73 case AOP_Create:
74 new_privs |= static_cast<int>(XrdAccPriv_Create);
75 break;
76 case AOP_Delete:
77 new_privs |= static_cast<int>(XrdAccPriv_Delete);
78 break;
79 case AOP_Excl_Insert: // fallthrough
80 case AOP_Insert:
81 new_privs |= static_cast<int>(XrdAccPriv_Insert);
82 break;
83 case AOP_Lock:
84 new_privs |= static_cast<int>(XrdAccPriv_Lock);
85 break;
86 case AOP_Mkdir:
87 new_privs |= static_cast<int>(XrdAccPriv_Mkdir);
88 break;
89 case AOP_Read:
90 new_privs |= static_cast<int>(XrdAccPriv_Read);
91 break;
92 case AOP_Readdir:
93 new_privs |= static_cast<int>(XrdAccPriv_Readdir);
94 break;
95 case AOP_Rename:
96 new_privs |= static_cast<int>(XrdAccPriv_Rename);
97 break;
98 case AOP_Stat:
99 new_privs |= static_cast<int>(XrdAccPriv_Lookup);
100 break;
101 case AOP_Update:
102 new_privs |= static_cast<int>(XrdAccPriv_Update);
103 break;
104 case AOP_Stage:
105 new_privs |= static_cast<int>(XrdAccPriv_Stage);
106 break;
107 case AOP_Poll:
108 new_privs |= static_cast<int>(XrdAccPriv_Poll);
109 break;
110 };
111 return static_cast<XrdAccPrivs>(new_privs);
112}
113
114// Accept any value of the path, name, or activity caveats
115int validate_verify_empty(void *emsg_ptr,
116 const unsigned char *pred,
117 size_t pred_sz)
118{
119 if ((pred_sz >= 5) && (!memcmp(reinterpret_cast<const char *>(pred), "path:", 5) ||
120 !memcmp(reinterpret_cast<const char *>(pred), "name:", 5)))
121 {
122 return 0;
123 }
124 if ((pred_sz >= 9) && (!memcmp(reinterpret_cast<const char *>(pred), "activity:", 9)))
125 {
126 return 0;
127 }
128 return 1;
129}
130
131} // unnamed namespace
132
133Authz::Authz(XrdSysLogger *log, char const *config, XrdAccAuthorize *chain)
134 : m_max_duration(86400),
135 m_chain(chain),
136 m_log(log, "macarons_"),
137 m_authz_behavior(static_cast<int>(Handler::AuthzBehavior::PASSTHROUGH))
138{
140 XrdOucEnv env;
141 if (!Handler::Config(config, &env, &m_log, m_location, m_secret, m_max_duration, behavior))
142 {
143 throw std::runtime_error("Macaroon authorization config failed.");
144 }
145 m_authz_behavior = static_cast<int>(behavior);
146}
147
149Authz::OnMissing(const XrdSecEntity *Entity, const char *path,
150 const Access_Operation oper, XrdOucEnv *env)
151{
152 switch (m_authz_behavior) {
154 return m_chain ? m_chain->Access(Entity, path, oper, env) : XrdAccPriv_None;
156 return AddPriv(oper, XrdAccPriv_None);;
158 return XrdAccPriv_None;
159 }
160 // Code should be unreachable.
161 return XrdAccPriv_None;
162}
163
165Authz::Access(const XrdSecEntity *Entity, const char *path,
166 const Access_Operation oper, XrdOucEnv *env)
167{
168 // We don't allow any testing to occur in this authz module, preventing
169 // a macaroon to be used to receive further macaroons.
170 if (oper == AOP_Any)
171 {
172 return m_chain ? m_chain->Access(Entity, path, oper, env) : XrdAccPriv_None;
173 }
174
175 const char *authz = env ? env->Get("authz") : nullptr;
176 if (authz && !strncmp(authz, "Bearer%20", 9))
177 {
178 authz += 9;
179 }
180 else if (!authz && (authz = env ? env->Get("access_token") : nullptr) && !strncmp(authz, "Bearer%20", 9))
181 {
182 authz += 9;
183 }
184
185 // If there's no request-specific token, check for a ZTN session token
186 if (!authz && Entity && !strcmp("ztn", Entity->prot) && Entity->creds &&
187 Entity->credslen && Entity->creds[Entity->credslen] == '\0')
188 {
189 authz = Entity->creds;
190 }
191
192 if (!authz) {
193 return OnMissing(Entity, path, oper, env);
194 }
195
196 macaroon_returncode mac_err = MACAROON_SUCCESS;
197 struct macaroon* macaroon = macaroon_deserialize(
198 authz,
199 &mac_err);
200 if (!macaroon)
201 {
202 // Do not log - might be other token type!
203 //m_log.Emsg("Access", "Failed to parse the macaroon");
204 return OnMissing(Entity, path, oper, env);
205 }
206
207 struct macaroon_verifier *verifier = macaroon_verifier_create();
208 if (!verifier)
209 {
210 m_log.Emsg("Access", "Failed to create a new macaroon verifier");
211 return XrdAccPriv_None;
212 }
213 if (!path)
214 {
215 m_log.Emsg("Access", "Request with no provided path.");
216 macaroon_verifier_destroy(verifier);
217 return XrdAccPriv_None;
218 }
219
220 AuthzCheck check_helper(path, oper, m_max_duration, m_log);
221
222 if (macaroon_verifier_satisfy_general(verifier, AuthzCheck::verify_before_s, &check_helper, &mac_err) ||
223 macaroon_verifier_satisfy_general(verifier, AuthzCheck::verify_activity_s, &check_helper, &mac_err) ||
224 macaroon_verifier_satisfy_general(verifier, AuthzCheck::verify_name_s, &check_helper, &mac_err) ||
225 macaroon_verifier_satisfy_general(verifier, AuthzCheck::verify_path_s, &check_helper, &mac_err))
226 {
227 m_log.Emsg("Access", "Failed to configure caveat verifier:");
228 macaroon_verifier_destroy(verifier);
229 return XrdAccPriv_None;
230 }
231
232 const unsigned char *macaroon_loc;
233 size_t location_sz;
234 macaroon_location(macaroon, &macaroon_loc, &location_sz);
235 if (strncmp(reinterpret_cast<const char *>(macaroon_loc), m_location.c_str(), location_sz))
236 {
237 std::string location_str(reinterpret_cast<const char *>(macaroon_loc), location_sz);
238 m_log.Emsg("Access", "Macaroon is for incorrect location", location_str.c_str());
239 macaroon_verifier_destroy(verifier);
240 macaroon_destroy(macaroon);
241 return m_chain ? m_chain->Access(Entity, path, oper, env) : XrdAccPriv_None;
242 }
243
244 if (macaroon_verify(verifier, macaroon,
245 reinterpret_cast<const unsigned char *>(m_secret.c_str()),
246 m_secret.size(),
247 nullptr, 0, // discharge macaroons
248 &mac_err))
249 {
250 m_log.Log(LogMask::Debug, "Access", "Macaroon verification failed");
251 macaroon_verifier_destroy(verifier);
252 macaroon_destroy(macaroon);
253 // This token is from our server (location matched) but caveats or HMAC
254 // failed. Falling through to the chain would let a path-restricted
255 // macaroon bypass its own restrictions. Deny unconditionally.
256 return XrdAccPriv_None;
257 }
258 macaroon_verifier_destroy(verifier);
259
260 const unsigned char *macaroon_id;
261 size_t id_sz;
262 macaroon_identifier(macaroon, &macaroon_id, &id_sz);
263
264 std::string macaroon_id_str(reinterpret_cast<const char *>(macaroon_id), id_sz);
265 m_log.Log(LogMask::Info, "Access", "Macaroon verification successful; ID", macaroon_id_str.c_str());
266 macaroon_destroy(macaroon);
267
268 // Copy the name, if present into the macaroon, into the credential object.
269 if (Entity && check_helper.GetSecName().size()) {
270 const std::string &username = check_helper.GetSecName();
271 m_log.Log(LogMask::Debug, "Access", "Setting the request name to", username.c_str());
272 Entity->eaAPI->Add("request.name", username,true);
273 }
274
275 // We passed verification - give the correct privilege.
276 return AddPriv(oper, XrdAccPriv_None);
277}
278
279bool Authz::Validate(const char *token,
280 std::string &emsg,
281 long long *expT,
282 XrdSecEntity *entP)
283{
284 macaroon_returncode mac_err = MACAROON_SUCCESS;
285 std::unique_ptr<struct macaroon, decltype(&macaroon_destroy)> macaroon(
286 macaroon_deserialize(token, &mac_err),
287 &macaroon_destroy);
288
289 if (!macaroon)
290 {
291 emsg = "Failed to deserialize the token as a macaroon";
292 // Purposely log at debug level in case if this validation is ever
293 // chained so we don't have overly-chatty logs.
294 m_log.Log(LogMask::Debug, "Validate", emsg.c_str());
295 return false;
296 }
297
298 std::unique_ptr<struct macaroon_verifier, decltype(&macaroon_verifier_destroy)> verifier(
299 macaroon_verifier_create(), &macaroon_verifier_destroy);
300 if (!verifier)
301 {
302 emsg = "Internal error: failed to create a verifier.";
303 m_log.Log(LogMask::Error, "Validate", emsg.c_str());
304 return false;
305 }
306
307 // Note the path and operation here are ignored as we won't use those validators
308 AuthzCheck check_helper("/", AOP_Read, m_max_duration, m_log);
309
310 if (macaroon_verifier_satisfy_general(verifier.get(), AuthzCheck::verify_before_s, &check_helper, &mac_err) ||
311 macaroon_verifier_satisfy_general(verifier.get(), validate_verify_empty, nullptr, &mac_err))
312 {
313 emsg = "Failed to configure the verifier";
314 m_log.Log(LogMask::Error, "Validate", emsg.c_str());
315 return false;
316 }
317
318 const unsigned char *macaroon_loc;
319 size_t location_sz;
320 macaroon_location(macaroon.get(), &macaroon_loc, &location_sz);
321 if (strncmp(reinterpret_cast<const char *>(macaroon_loc), m_location.c_str(), location_sz))
322 {
323 emsg = "Macaroon contains incorrect location: " +
324 std::string(reinterpret_cast<const char *>(macaroon_loc), location_sz);
325 m_log.Log(LogMask::Warning, "Validate", emsg.c_str(), ("all.sitename is " + m_location).c_str());
326 return false;
327 }
328
329 if (macaroon_verify(verifier.get(), macaroon.get(),
330 reinterpret_cast<const unsigned char *>(m_secret.c_str()),
331 m_secret.size(),
332 nullptr, 0,
333 &mac_err))
334 {
335 emsg = "Macaroon verification error" + (check_helper.GetErrorMessage().size() ?
336 (", " + check_helper.GetErrorMessage()) : "");
337 m_log.Log(LogMask::Warning, "Validate", emsg.c_str());
338 return false;
339 }
340
341 const unsigned char *macaroon_id;
342 size_t id_sz;
343 macaroon_identifier(macaroon.get(), &macaroon_id, &id_sz);
344 m_log.Log(LogMask::Info, "Validate", ("Macaroon verification successful; ID " +
345 std::string(reinterpret_cast<const char *>(macaroon_id), id_sz)).c_str());
346
347 return true;
348}
349
350AuthzCheck::AuthzCheck(const char *req_path, const Access_Operation req_oper, ssize_t max_duration, XrdSysError &log)
351 : m_max_duration(max_duration),
352 m_log(log),
353 m_path(XrdOucUtils::NormalizePath(req_path)),
354 m_oper(req_oper),
355 m_now(time(nullptr))
356{
357 switch (m_oper)
358 {
359 case AOP_Any:
360 break;
361 case AOP_Chmod:
362 case AOP_Chown:
363 m_desired_activity = "UPDATE_METADATA";
364 break;
365 case AOP_Insert:
366 case AOP_Lock:
367 case AOP_Mkdir:
368 case AOP_Update:
369 case AOP_Create:
370 m_desired_activity = "MANAGE";
371 break;
372 case AOP_Rename:
373 case AOP_Excl_Create:
374 case AOP_Excl_Insert:
375 m_desired_activity = "UPLOAD";
376 break;
377 case AOP_Delete:
378 m_desired_activity = "DELETE";
379 break;
380 case AOP_Read:
381 m_desired_activity = "DOWNLOAD";
382 break;
383 case AOP_Readdir:
384 m_desired_activity = "LIST";
385 break;
386 case AOP_Stat:
387 m_desired_activity = "READ_METADATA";
388 break;
389 case AOP_Stage:
390 case AOP_Poll:
391 break;
392 };
393}
394
395int
396AuthzCheck::verify_before_s(void *authz_ptr,
397 const unsigned char *pred,
398 size_t pred_sz)
399{
400 return static_cast<AuthzCheck*>(authz_ptr)->verify_before(pred, pred_sz);
401}
402
403int
404AuthzCheck::verify_activity_s(void *authz_ptr,
405 const unsigned char *pred,
406 size_t pred_sz)
407{
408 return static_cast<AuthzCheck*>(authz_ptr)->verify_activity(pred, pred_sz);
409}
410
411int
412AuthzCheck::verify_path_s(void *authz_ptr,
413 const unsigned char *pred,
414 size_t pred_sz)
415{
416 return static_cast<AuthzCheck*>(authz_ptr)->verify_path(pred, pred_sz);
417}
418
419int
420AuthzCheck::verify_name_s(void *authz_ptr,
421 const unsigned char *pred,
422 size_t pred_sz)
423{
424 return static_cast<AuthzCheck*>(authz_ptr)->verify_name(pred, pred_sz);
425}
426
427int
428AuthzCheck::verify_before(const unsigned char * pred, size_t pred_sz)
429{
430 std::string pred_str(reinterpret_cast<const char *>(pred), pred_sz);
431 if (strncmp("before:", pred_str.c_str(), 7))
432 {
433 return 1;
434 }
435 m_log.Log(LogMask::Debug, "AuthzCheck", "Checking macaroon for expiration; caveat:", pred_str.c_str());
436
437 struct tm caveat_tm;
438 if (strptime(&pred_str[7], "%Y-%m-%dT%H:%M:%SZ", &caveat_tm) == nullptr)
439 {
440 m_emsg = "Failed to parse time string: " + pred_str.substr(7);
441 m_log.Log(LogMask::Warning, "AuthzCheck", m_emsg.c_str());
442 return 1;
443 }
444 caveat_tm.tm_isdst = -1;
445
446 time_t caveat_time = timegm(&caveat_tm);
447 if (-1 == caveat_time)
448 {
449 m_emsg = "Failed to generate unix time: " + pred_str.substr(7);
450 m_log.Log(LogMask::Warning, "AuthzCheck", m_emsg.c_str());
451 return 1;
452 }
453 if ((m_max_duration > 0) && (caveat_time > m_now + m_max_duration))
454 {
455 m_emsg = "Max token age is greater than configured max duration; rejecting";
456 m_log.Log(LogMask::Warning, "AuthzCheck", m_emsg.c_str());
457 return 1;
458 }
459
460 int result = (m_now >= caveat_time);
461 if (!result)
462 {
463 m_log.Log(LogMask::Debug, "AuthzCheck", "Macaroon has not expired.");
464 }
465 else
466 {
467 m_emsg = "Macaroon expired at " + pred_str.substr(7);
468 m_log.Log(LogMask::Debug, "AuthzCheck", m_emsg.c_str());
469 }
470 return result;
471}
472
473int
474AuthzCheck::verify_activity(const unsigned char * pred, size_t pred_sz)
475{
476 if (!m_desired_activity.size()) {return 1;}
477 std::string pred_str(reinterpret_cast<const char *>(pred), pred_sz);
478 if (strncmp("activity:", pred_str.c_str(), 9)) {return 1;}
479 m_log.Log(LogMask::Debug, "AuthzCheck", "running verify activity", pred_str.c_str());
480
481 std::stringstream ss(pred_str.substr(9));
482 for (std::string activity; std::getline(ss, activity, ','); )
483 {
484 // Any allowed activity also implies "READ_METADATA"
485 if (m_desired_activity == "READ_METADATA") {return 0;}
486 if ((activity == m_desired_activity) || ((m_desired_activity == "UPLOAD") && (activity == "MANAGE")))
487 {
488 m_log.Log(LogMask::Debug, "AuthzCheck", "macaroon has desired activity", activity.c_str());
489 return 0;
490 }
491 }
492 m_log.Log(LogMask::Info, "AuthzCheck", "macaroon does NOT have desired activity", m_desired_activity.c_str());
493 return 1;
494}
495
496int
497AuthzCheck::verify_path(const unsigned char * pred, size_t pred_sz)
498{
499 std::string pred_str_raw(reinterpret_cast<const char *>(pred), pred_sz);
500 if (strncmp("path:", pred_str_raw.c_str(), 5)) {return 1;}
501 std::string pred_str = XrdOucUtils::NormalizePath(pred_str_raw.substr(5));
502 m_log.Log(LogMask::Debug, "AuthzCheck", "running verify path", pred_str.c_str());
503
504 if ((m_path.find("/./") != std::string::npos) ||
505 (m_path.find("/../") != std::string::npos))
506 {
507 m_log.Log(LogMask::Info, "AuthzCheck", "invalid requested path", m_path.c_str());
508 return 1;
509 }
510
511 // Allow operations under subdirectories and not substrings
512 // For e.g. pred_str = "/data/sudir/mydir"
513 // Allows m_path = /data/subdir/mydir/newdir
514 // But rejects, m_path = /data/subdir/mydirmycoolname/newdir
515 int is_subdir = is_subdirectory(pred_str, m_path);
516 if (is_subdir)
517 {
518 m_log.Log(LogMask::Debug, "AuthzCheck", "path request verified for", m_path.c_str());
519 }
520
521 // READ_METADATA (i.e AOP_Stat) permission for /foo/bar automatically implies permission
522 // to READ_METADATA for /foo.
523 // Similarly, MKDIR Pemissions for a parent path is implied.
524 else if (m_oper == AOP_Stat || m_oper == AOP_Mkdir)
525 {
526 is_subdir = is_subdirectory(m_path, pred_str);
527 const char *opName = (m_oper == AOP_Stat) ? "READ_METADATA" : "MKDIR";
528 m_log.Log(LogMask::Debug, "AuthzCheck",
529 (std::string(opName) + (is_subdir? " Path request verified for" : " Path request NOT allowed for")).c_str(),
530 m_path.c_str());
531 }
532 else
533 {
534 m_log.Log(LogMask::Debug, "AuthzCheck", "path request NOT allowed", m_path.c_str());
535 }
536
537 return !is_subdir;
538}
539
540int
541AuthzCheck::verify_name(const unsigned char * pred, size_t pred_sz)
542{
543 std::string pred_str(reinterpret_cast<const char *>(pred), pred_sz);
544 if (strncmp("name:", pred_str.c_str(), 5)) {return 1;}
545 if (pred_str.size() < 6) {return 1;}
546 m_log.Log(LogMask::Debug, "AuthzCheck", "Verifying macaroon with", pred_str.c_str());
547
548 // Make a copy of the name for the XrdSecEntity; this will be used later.
549 m_sec_name = pred_str.substr(5);
550
551 return 0;
552}
Access_Operation
The following are supported operations.
@ AOP_Delete
rm() or rmdir()
@ AOP_Mkdir
mkdir()
@ AOP_Update
open() r/w or append
@ AOP_Create
open() with create
@ AOP_Readdir
opendir()
@ AOP_Chmod
chmod()
@ AOP_Any
Special for getting privs.
@ AOP_Stat
exists(), stat()
@ AOP_Poll
stage polling operations
@ AOP_Rename
mv() for source
@ AOP_Read
open() r/o, prepare()
@ AOP_Excl_Create
open() with O_EXCL|O_CREAT
@ AOP_Insert
mv() for target
@ AOP_Lock
n/a
@ AOP_Chown
chown()
@ AOP_Stage
stage and or read data, plus related operations
@ AOP_Excl_Insert
mv() where destination doesn't exist.
XrdAccPrivs
@ XrdAccPriv_Mkdir
@ XrdAccPriv_Chown
@ XrdAccPriv_Insert
@ XrdAccPriv_Lookup
@ XrdAccPriv_Rename
@ XrdAccPriv_Poll
@ XrdAccPriv_Update
@ XrdAccPriv_Read
@ XrdAccPriv_Lock
@ XrdAccPriv_None
@ XrdAccPriv_Stage
@ XrdAccPriv_Delete
@ XrdAccPriv_Create
@ XrdAccPriv_Readdir
@ XrdAccPriv_Chmod
static bool is_subdirectory(const std::string_view dir, const std::string_view subdir)
int emsg(int rc, char *msg)
virtual bool Validate(const char *token, std::string &emsg, long long *expT, XrdSecEntity *entP) override
Authz(XrdSysLogger *lp, const char *parms, XrdAccAuthorize *chain)
virtual XrdAccPrivs Access(const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *env) override
static bool Config(const char *config, XrdOucEnv *env, XrdSysError *log, std::string &location, std::string &secret, ssize_t &max_duration, AuthzBehavior &behavior)
XrdAccAuthorize()
Constructor.
virtual XrdAccPrivs Access(const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *Env=0)=0
char * Get(const char *varname)
Definition XrdOucEnv.hh:69
static std::string NormalizePath(const std::string &path)
bool Add(XrdSecAttr &attr)
int credslen
Length of the 'creds' data.
XrdSecEntityAttr * eaAPI
non-const API to attributes
char prot[XrdSecPROTOIDSIZE]
Auth protocol used (e.g. krb5)
char * creds
Raw entity credentials or cert.