XRootD
Loading...
Searching...
No Matches
XrdSciTokensAccess.cc
Go to the documentation of this file.
1
3#include "XrdOuc/XrdOucEnv.hh"
10#include "XrdVersion.hh"
11
12#include <map>
13#include <memory>
14#include <mutex>
15#include <string>
16#include <vector>
17#include <sstream>
18#include <fstream>
19#include <unordered_map>
20#include <tuple>
21
22#include "fcntl.h"
23
24#include "INIReader.h"
25#include "picojson.h"
26
27#include "scitokens/scitokens.h"
31
32// The status-quo to retrieve the default object is to copy/paste the
33// linker definition and invoke directly.
36
38
39namespace {
40
41enum LogMask {
42 Debug = 0x01,
43 Info = 0x02,
44 Warning = 0x04,
45 Error = 0x08,
46 All = 0xff
47};
48
49std::string LogMaskToString(int mask) {
50 if (mask == LogMask::All) {return "all";}
51
52 bool has_entry = false;
53 std::stringstream ss;
54 if (mask & LogMask::Debug) {
55 ss << "debug";
56 has_entry = true;
57 }
58 if (mask & LogMask::Info) {
59 ss << (has_entry ? ", " : "") << "info";
60 has_entry = true;
61 }
62 if (mask & LogMask::Warning) {
63 ss << (has_entry ? ", " : "") << "warning";
64 has_entry = true;
65 }
66 if (mask & LogMask::Error) {
67 ss << (has_entry ? ", " : "") << "error";
68 has_entry = true;
69 }
70 return ss.str();
71}
72
73inline uint64_t monotonic_time() {
74 struct timespec tp;
75#ifdef CLOCK_MONOTONIC_COARSE
76 clock_gettime(CLOCK_MONOTONIC_COARSE, &tp);
77#else
78 clock_gettime(CLOCK_MONOTONIC, &tp);
79#endif
80 return tp.tv_sec + (tp.tv_nsec >= 500000000);
81}
82
84{
85 int new_privs = privs;
86 switch (op) {
87 case AOP_Any:
88 break;
89 case AOP_Chmod:
90 new_privs |= static_cast<int>(XrdAccPriv_Chmod);
91 break;
92 case AOP_Chown:
93 new_privs |= static_cast<int>(XrdAccPriv_Chown);
94 break;
95 case AOP_Excl_Create: // fallthrough
96 case AOP_Create:
97 new_privs |= static_cast<int>(XrdAccPriv_Create);
98 break;
99 case AOP_Delete:
100 new_privs |= static_cast<int>(XrdAccPriv_Delete);
101 break;
102 case AOP_Excl_Insert: // fallthrough
103 case AOP_Insert:
104 new_privs |= static_cast<int>(XrdAccPriv_Insert);
105 break;
106 case AOP_Lock:
107 new_privs |= static_cast<int>(XrdAccPriv_Lock);
108 break;
109 case AOP_Mkdir:
110 new_privs |= static_cast<int>(XrdAccPriv_Mkdir);
111 break;
112 case AOP_Read:
113 new_privs |= static_cast<int>(XrdAccPriv_Read);
114 break;
115 case AOP_Readdir:
116 new_privs |= static_cast<int>(XrdAccPriv_Readdir);
117 break;
118 case AOP_Rename:
119 new_privs |= static_cast<int>(XrdAccPriv_Rename);
120 break;
121 case AOP_Stat:
122 new_privs |= static_cast<int>(XrdAccPriv_Lookup);
123 break;
124 case AOP_Update:
125 new_privs |= static_cast<int>(XrdAccPriv_Update);
126 break;
127 };
128 return static_cast<XrdAccPrivs>(new_privs);
129}
130
131const std::string OpToName(Access_Operation op) {
132 switch (op) {
133 case AOP_Any: return "any";
134 case AOP_Chmod: return "chmod";
135 case AOP_Chown: return "chown";
136 case AOP_Create: return "create";
137 case AOP_Excl_Create: return "excl_create";
138 case AOP_Delete: return "del";
139 case AOP_Excl_Insert: return "excl_insert";
140 case AOP_Insert: return "insert";
141 case AOP_Lock: return "lock";
142 case AOP_Mkdir: return "mkdir";
143 case AOP_Read: return "read";
144 case AOP_Readdir: return "dir";
145 case AOP_Rename: return "mv";
146 case AOP_Stat: return "stat";
147 case AOP_Update: return "update";
148 };
149 return "unknown";
150}
151
152std::string AccessRuleStr(const AccessRulesRaw &rules) {
153 std::unordered_map<std::string, std::unique_ptr<std::stringstream>> rule_map;
154 for (const auto &rule : rules) {
155 auto iter = rule_map.find(rule.second);
156 if (iter == rule_map.end()) {
157 auto result = rule_map.insert(std::make_pair(rule.second, std::make_unique<std::stringstream>()));
158 iter = result.first;
159 *(iter->second) << OpToName(rule.first);
160 } else {
161 *(iter->second) << "," << OpToName(rule.first);
162 }
163 }
164 std::stringstream ss;
165 bool first = true;
166 for (const auto &val : rule_map) {
167 ss << (first ? "" : ";") << val.first << ":" << val.second->str();
168 first = false;
169 }
170 return ss.str();
171}
172
173bool MakeCanonical(const std::string &path, std::string &result)
174{
175 if (path.empty() || path[0] != '/') {return false;}
176
177 size_t pos = 0;
178 std::vector<std::string> components;
179 do {
180 while (path.size() > pos && path[pos] == '/') {pos++;}
181 auto next_pos = path.find_first_of("/", pos);
182 auto next_component = path.substr(pos, next_pos - pos);
183 pos = next_pos;
184 if (next_component.empty() || next_component == ".") {continue;}
185 else if (next_component == "..") {
186 if (!components.empty()) {
187 components.pop_back();
188 }
189 } else {
190 components.emplace_back(next_component);
191 }
192 } while (pos != std::string::npos);
193 if (components.empty()) {
194 result = "/";
195 return true;
196 }
197 std::stringstream ss;
198 for (const auto &comp : components) {
199 ss << "/" << comp;
200 }
201 result = ss.str();
202 return true;
203}
204
205void ParseCanonicalPaths(const std::string &path, std::vector<std::string> &results)
206{
207 size_t pos = 0;
208 do {
209 while (path.size() > pos && (path[pos] == ',' || path[pos] == ' ')) {pos++;}
210 auto next_pos = path.find_first_of(", ", pos);
211 auto next_path = path.substr(pos, next_pos - pos);
212 pos = next_pos;
213 if (!next_path.empty()) {
214 std::string canonical_path;
215 if (MakeCanonical(next_path, canonical_path)) {
216 results.emplace_back(std::move(canonical_path));
217 }
218 }
219 } while (pos != std::string::npos);
220}
221
222struct IssuerConfig
223{
224 IssuerConfig(const std::string &issuer_name,
225 const std::string &issuer_url,
226 const std::vector<std::string> &base_paths,
227 const std::vector<std::string> &restricted_paths,
228 bool map_subject,
229 uint32_t authz_strategy,
230 const std::string &default_user,
231 const std::string &username_claim,
232 const std::string &groups_claim,
233 const std::vector<MapRule> rules,
234 AuthzSetting acceptable_authz,
235 AuthzSetting required_authz)
236 : m_map_subject(map_subject || !username_claim.empty()),
237 m_acceptable_authz(acceptable_authz),
238 m_required_authz(required_authz),
239 m_authz_strategy(authz_strategy),
240 m_name(issuer_name),
241 m_url(issuer_url),
242 m_default_user(default_user),
243 m_username_claim(username_claim),
244 m_groups_claim(groups_claim),
245 m_base_paths(base_paths),
246 m_restricted_paths(restricted_paths),
247 m_map_rules(rules)
248 {}
249
250 const bool m_map_subject;
251 const AuthzSetting m_acceptable_authz;
252 const AuthzSetting m_required_authz;
253 const uint32_t m_authz_strategy;
254 const std::string m_name;
255 const std::string m_url;
256 const std::string m_default_user;
257 const std::string m_username_claim;
258 const std::string m_groups_claim;
259 const std::vector<std::string> m_base_paths;
260 const std::vector<std::string> m_restricted_paths;
261 const std::vector<MapRule> m_map_rules;
262};
263
264class OverrideINIReader: public INIReader {
265public:
266 OverrideINIReader() {};
267 inline OverrideINIReader(std::string filename) {
268 _error = ini_parse(filename.c_str(), ValueHandler, this);
269 }
270 inline OverrideINIReader(FILE *file) {
271 _error = ini_parse_file(file, ValueHandler, this);
272 }
273protected:
287 inline static int ValueHandler(void* user, const char* section, const char* name,
288 const char* value) {
289 OverrideINIReader* reader = (OverrideINIReader*)user;
290 std::string key = MakeKey(section, name);
291
292 // Overwrite existing values, if they exist
293 reader->_values[key] = value;
294 reader->_sections.insert(section);
295 return 1;
296 }
297
298};
299
300
301void
302ParseTokenString(const std::string &param, XrdOucEnv *env, std::vector<std::string_view> &authz_list)
303{
304 if (!env) {return;}
305 const char *authz = env->Get(param.c_str());
306 if (!authz) {return;}
307 std::string_view authz_view(authz);
308 size_t pos;
309 do {
310 // Note: this is more permissive than the plugin was previously.
311 // The prefix 'Bearer%20' used to be required as that's what HTTP
312 // required. However, to make this more pleasant for XRootD protocol
313 // users, we now simply "handle" the prefix insterad of requiring it.
314 if (authz_view.substr(0, 9) == "Bearer%20") {
315 authz_view = authz_view.substr(9);
316 }
317 pos = authz_view.find(",");
318 authz_list.push_back(authz_view.substr(0, pos));
319 authz_view = authz_view.substr(pos + 1);
320 } while (pos != std::string_view::npos);
321}
322
323} // namespace
324
325std::string
327 return AccessRuleStr(m_rules); // Returns a human-friendly representation of the access rules
328}
329
330// Convert a list of authorizations into a human-readable string.
331const std::string
333{
334 std::stringstream ss;
335 ss << "mapped_username=" << m_username << ", subject=" << m_token_subject
336 << ", issuer=" << m_issuer;
337 if (!m_groups.empty()) {
338 ss << ", groups=";
339 bool first=true;
340 for (const auto &group : m_groups) {
341 ss << (first ? "" : ",") << group;
342 first = false;
343 }
344 }
345 if (!m_matcher.empty()) {
346 ss << ", authorizations=" << m_matcher.str();
347 }
348 return ss.str();
349}
350
352{
353 return monotonic_time() > m_expiry_time;
354}
355
356// Determine whether a list of authorizations contains at least one entry
357// from each of the applicable required issuers.
358//
359// - `oper`: The operation type (read, write) to test for authorization.
360// - `path`: The requested path for the operation.
361// - `required_issuers`: A map from a list of paths to an issuer.
362// - `access_rules_list`: A list of access rules derived from the token
363//
364// If the requested path/operation matches one of the required issuers, then one
365// of the provided authorizations (e.g., the token's scopes) must come from that
366// issuer.
367//
368// The return value indicates whether the required authorization was missing, found,
369// or there was no required issuer for the path.
370bool AuthorizesRequiredIssuers(Access_Operation client_oper, const std::string_view &path,
371 const std::vector<std::pair<std::unique_ptr<SubpathMatch>, std::string>> &required_issuers,
372 const std::vector<std::shared_ptr<XrdAccRules>> &access_rules_list)
373{
374
375 // Translate the client-attempted operation to one of the simpler operations we've defined.
376 Access_Operation oper;
377 switch (client_oper) {
378 case AOP_Any:
379 return false; // Invalid request
380 break;
381 case AOP_Chmod: [[fallthrough]];
382 case AOP_Chown: [[fallthrough]];
383 case AOP_Create: [[fallthrough]];
384 case AOP_Excl_Create: [[fallthrough]];
385 case AOP_Delete: [[fallthrough]];
386 case AOP_Excl_Insert: [[fallthrough]];
387 case AOP_Insert: [[fallthrough]];
388 case AOP_Lock:
389 oper = AOP_Create;
390 break;
391 case AOP_Mkdir:
392 oper = AOP_Mkdir;
393 break;
394 case AOP_Read:
395 oper = AOP_Read;
396 break;
397 case AOP_Readdir:
398 oper = AOP_Readdir;
399 break;
400 case AOP_Rename:
401 oper = AOP_Create;
402 break;
403 case AOP_Stat:
404 oper = AOP_Stat;
405 break;
406 case AOP_Update:
407 oper = AOP_Update;
408 break;
409 default:
410 return false; // Invalid request
411 };
412
413 // Iterate through all the required issuers
414 for (const auto &info : required_issuers) {
415 // See if this issuer is required for this path/operation.
416 if (info.first->apply(oper, path)) {
417 bool has_authz = false;
418 // If so, see if one of the tokens (a) is from this issuer and (b) authorizes the request.
419 for (const auto &rules : access_rules_list) {
420 if (rules->get_issuer() == info.second && rules->apply(oper, path)) {
421 has_authz = true;
422 break;
423 }
424 }
425 if (!has_authz) {
426 return false;
427 }
428 }
429 }
430 return true;
431}
432
433class XrdAccSciTokens;
434
436
438 public XrdSciTokensMon
439{
440
441 enum class AuthzBehavior {
442 PASSTHROUGH,
443 ALLOW,
444 DENY
445 };
446
447public:
448 XrdAccSciTokens(XrdSysLogger *lp, const char *parms, XrdAccAuthorize* chain, XrdOucEnv *envP) :
449 m_chain(chain),
450 m_parms(parms ? parms : ""),
451 m_next_clean(monotonic_time() + m_expiry_secs),
452 m_log(lp, "scitokens_")
453 {
454 pthread_rwlock_init(&m_config_lock, nullptr);
455 m_config_lock_initialized = true;
456 m_log.Say("++++++ XrdAccSciTokens: Initialized SciTokens-based authorization.");
457 if (!Config(envP)) {
458 throw std::runtime_error("Failed to configure SciTokens authorization.");
459 }
460 }
461
463 if (m_config_lock_initialized) {
464 pthread_rwlock_destroy(&m_config_lock);
465 }
466 }
467
468 virtual XrdAccPrivs Access(const XrdSecEntity *Entity,
469 const char *path,
470 const Access_Operation oper,
471 XrdOucEnv *env) override
472 {
473 std::vector<std::string_view> authz_list;
474 authz_list.reserve(1);
475
476 // Parse the authz environment entry as a comma-separated list of tokens.
477 // Traditionally, `authz` has been used as the parameter for XRootD; however,
478 // RFC 6750 Section 2.3 ("URI Query Parameter") specifies that access_token
479 // is correct. We support both.
480 ParseTokenString("authz", env, authz_list);
481 ParseTokenString("access_token", env, authz_list);
482
483 if (Entity && !strcmp("ztn", Entity->prot) && Entity->creds &&
484 Entity->credslen && Entity->creds[Entity->credslen] == '\0')
485 {
486 authz_list.push_back(Entity->creds);
487 }
488
489 if (authz_list.empty()) {
490 return OnMissing(Entity, path, oper, env);
491 }
492
493 // A potential DoS would be providing a large number of tokens to consider for ACLs.
494 // Have a hardcoded assumption of <10 tokens per request.
495 if (authz_list.size() > 10) {
496 m_log.Log(LogMask::Warning, "Access", "Request had more than 10 tokens attached; ignoring");
497 return OnMissing(Entity, path, oper, env);
498 }
499
500 m_log.Log(LogMask::Debug, "Access", "Trying token-based access control");
501 std::vector<std::shared_ptr<XrdAccRules>> access_rules_list;
502 uint64_t now = monotonic_time();
503 Check(now);
504 for (const auto &authz : authz_list) {
505 std::shared_ptr<XrdAccRules> access_rules;
506 {
507 std::lock_guard<std::mutex> guard(m_mutex);
508 const auto iter = m_map.find(authz);
509 if (iter != m_map.end() && !iter->second->expired()) {
510 access_rules = iter->second;
511 }
512 }
513 if (!access_rules) {
514 m_log.Log(LogMask::Debug, "Access", "Token not found in recent cache; parsing.");
515 try {
516 uint64_t cache_expiry;
517 AccessRulesRaw rules;
518 std::string username;
519 std::string token_subject;
520 std::string issuer;
521 std::vector<MapRule> map_rules;
522 std::vector<std::string> groups;
523 uint32_t authz_strategy;
524 AuthzSetting acceptable_authz;
525 if (GenerateAcls(authz, cache_expiry, rules, username, token_subject, issuer, map_rules, groups, authz_strategy, acceptable_authz)) {
526 access_rules.reset(new XrdAccRules(now + cache_expiry, username, token_subject, issuer, map_rules, groups, authz_strategy, acceptable_authz));
527 access_rules->parse(rules);
528 } else {
529 m_log.Log(LogMask::Warning, "Access", "Failed to generate ACLs for token");
530 continue;
531 }
532 if (m_log.getMsgMask() & LogMask::Debug) {
533 m_log.Log(LogMask::Debug, "Access", "New valid token", access_rules->str().c_str());
534 }
535 } catch (std::exception &exc) {
536 m_log.Log(LogMask::Warning, "Access", "Error generating ACLs for authorization", exc.what());
537 continue;
538 }
539 std::lock_guard<std::mutex> guard(m_mutex);
540 m_map[std::string(authz)] = access_rules;
541 } else if (m_log.getMsgMask() & LogMask::Debug) {
542 m_log.Log(LogMask::Debug, "Access", "Cached token", access_rules->str().c_str());
543 }
544 access_rules_list.push_back(access_rules);
545 }
546 if (access_rules_list.empty()) {
547 return OnMissing(Entity, path, oper, env);
548 }
549 std::string_view path_view(path, strlen(path));
550
551 // Apply the logic for the required issuers.
552 if (!AuthorizesRequiredIssuers(oper, path_view, m_required_issuers, access_rules_list)) {
553 return OnMissing(Entity, path, oper, env);
554 }
555
556 // Strategy: assuming the corresponding strategy is enabled, we populate the name in
557 // the XrdSecEntity if:
558 // 1. There are scopes present in the token that authorize the request,
559 // 2. The token is mapped by some rule in the mapfile (group or subject-based mapping).
560 // The default username for the issuer is only used in (1).
561 // If the scope-based mapping is successful, authorize immediately. Otherwise, if the
562 // mapping is successful, we potentially chain to another plugin.
563 //
564 // We always populate the issuer and the groups, if present.
565
566 // Access may be authorized; populate XrdSecEntity
567 for (const auto &access_rules : access_rules_list) {
568 // Make sure this issuer is acceptable for the given operation.
569 if (!access_rules->acceptable_authz(oper)) {
570 m_log.Log(LogMask::Debug, "Access", "Issuer is not acceptable for given operation:", access_rules->get_issuer().c_str());
571 continue;
572 }
573
574 XrdSecEntity new_secentity;
575 new_secentity.vorg = nullptr;
576 new_secentity.grps = nullptr;
577 new_secentity.role = nullptr;
578 new_secentity.secMon = Entity->secMon;
579 new_secentity.addrInfo = Entity->addrInfo;
580 const auto &issuer = access_rules->get_issuer();
581 if (!issuer.empty()) {
582 new_secentity.vorg = strdup(issuer.c_str());
583 }
584 bool group_success = false;
585 if ((access_rules->get_authz_strategy() & IssuerAuthz::Group) && access_rules->groups().size()) {
586 std::stringstream ss;
587 for (const auto &grp : access_rules->groups()) {
588 ss << grp << " ";
589 }
590 const auto &groups_str = ss.str();
591 new_secentity.grps = static_cast<char*>(malloc(groups_str.size() + 1));
592 if (new_secentity.grps) {
593 memcpy(new_secentity.grps, groups_str.c_str(), groups_str.size());
594 new_secentity.grps[groups_str.size()] = '\0';
595 }
596 group_success = true;
597 }
598
599 std::string username;
600 bool mapping_success = false;
601 bool scope_success = false;
602 username = access_rules->get_username(path_view);
603
604 mapping_success = (access_rules->get_authz_strategy() & IssuerAuthz::Mapping) && !username.empty();
605 scope_success = (access_rules->get_authz_strategy() & IssuerAuthz::Capability) && access_rules->apply(oper, path_view);
606 if (scope_success && (m_log.getMsgMask() & LogMask::Debug)) {
607 std::stringstream ss;
608 ss << "Grant authorization based on scopes for operation=" << OpToName(oper) << ", path=" << path;
609 m_log.Log(LogMask::Debug, "Access", ss.str().c_str());
610 }
611
612 if (!scope_success && !mapping_success && !group_success) {
613 auto returned_accs = OnMissing(&new_secentity, path, oper, env);
614 // Clean up the new_secentity
615 if (new_secentity.vorg != nullptr) free(new_secentity.vorg);
616 if (new_secentity.grps != nullptr) free(new_secentity.grps);
617 if (new_secentity.role != nullptr) free(new_secentity.role);
618
619 return returned_accs;
620 }
621
622 // Default user only applies to scope-based mappings.
623 if (scope_success && username.empty()) {
624 username = access_rules->get_default_username();
625 }
626
627 // Setting the request.name will pass the username to the next plugin.
628 // Ensure we do that only if map-based or scope-based authorization worked.
629 if (scope_success || mapping_success) {
630 // Set scitokens.name in the extra attribute
631 Entity->eaAPI->Add("request.name", username, true);
632 new_secentity.eaAPI->Add("request.name", username, true);
633 m_log.Log(LogMask::Debug, "Access", "Request username", username.c_str());
634 }
635
636 // Make the token subject available. Even though it's a reasonably bad idea
637 // to use for *authorization* for file access, there may be other use cases.
638 // For example, the combination of (vorg, token.subject) is a reasonable
639 // approximation of a unique 'entity' (either person or a robot) and is
640 // more reasonable to use for resource fairshare in XrdThrottle.
641 const auto &token_subject = access_rules->get_token_subject();
642 if (!token_subject.empty()) {
643 Entity->eaAPI->Add("token.subject", token_subject, true);
644 }
645
646 // When the scope authorized this access, allow immediately. Otherwise, chain
647 XrdAccPrivs returned_op = scope_success ? AddPriv(oper, XrdAccPriv_None) : OnMissing(&new_secentity, path, oper, env);
648
649 // Since we are doing an early return, insert token info into the
650 // monitoring stream if monitoring is in effect and access granted
651 //
652 if (Entity->secMon && scope_success && returned_op && Mon_isIO(oper))
653 Mon_Report(new_secentity, token_subject, username);
654
655 // Cleanup the new_secentry
656 if (new_secentity.vorg != nullptr) free(new_secentity.vorg);
657 if (new_secentity.grps != nullptr) free(new_secentity.grps);
658 if (new_secentity.role != nullptr) free(new_secentity.role);
659 return returned_op;
660 }
661
662 // We iterated through all available credentials and none provided authorization; fall back
663 return OnMissing(Entity, path, oper, env);
664 }
665
666 virtual Issuers IssuerList() override
667 {
668 /*
669 Convert the m_issuers into the data structure:
670 struct ValidIssuer
671 {std::string issuer_name;
672 std::string issuer_url;
673 };
674 typedef std::vector<ValidIssuer> Issuers;
675 */
676 Issuers issuers;
677 for (auto it: m_issuers) {
678 ValidIssuer issuer_info;
679 issuer_info.issuer_name = it.first;
680 issuer_info.issuer_url = it.second.m_url;
681 issuers.push_back(issuer_info);
682 }
683 return issuers;
684
685 }
686
687 virtual bool Validate(const char *token, std::string &emsg, long long *expT,
688 XrdSecEntity *Entity) override
689 {
690 // Just check if the token is valid, no scope checking
691
692 // Deserialize the token
693 SciToken scitoken;
694 char *err_msg;
695 if (!strncmp(token, "Bearer%20", 9)) token += 9;
696 pthread_rwlock_rdlock(&m_config_lock);
697 auto retval = scitoken_deserialize(token, &scitoken, &m_valid_issuers_array[0], &err_msg);
698 pthread_rwlock_unlock(&m_config_lock);
699 if (retval) {
700 // This originally looked like a JWT so log the failure.
701 m_log.Log(LogMask::Warning, "Validate", "Failed to deserialize SciToken:", err_msg);
702 emsg = err_msg;
703 free(err_msg);
704 return false;
705 }
706
707 // If an entity was passed then we will fill it in with the subject
708 // name, should it exist. Note that we are gauranteed that all the
709 // settable entity fields are null so no need to worry setting them.
710 //
711 if (Entity)
712 {char *value = nullptr;
713 if (!scitoken_get_claim_string(scitoken, "sub", &value, &err_msg))
714 Entity->name = strdup(value);
715 }
716
717 // Return the expiration time of this token if so wanted.
718 //
719 if (expT && scitoken_get_expiration(scitoken, expT, &err_msg)) {
720 emsg = err_msg;
721 free(err_msg);
722 return false;
723 }
724
725
726 // Delete the scitokens
727 scitoken_destroy(scitoken);
728
729 // Deserialize checks the key, so we're good now.
730 return true;
731 }
732
733 virtual int Audit(const int accok,
734 const XrdSecEntity *Entity,
735 const char *path,
736 const Access_Operation oper,
737 XrdOucEnv *Env=0) override
738 {
739 return 0;
740 }
741
742 virtual int Test(const XrdAccPrivs priv,
743 const Access_Operation oper) override
744 {
745 return (m_chain ? m_chain->Test(priv, oper) : 0);
746 }
747
748 std::string GetConfigFile() {
749 return m_cfg_file;
750 }
751
752private:
753 XrdAccPrivs OnMissing(const XrdSecEntity *Entity, const char *path,
754 const Access_Operation oper, XrdOucEnv *env)
755 {
756 switch (m_authz_behavior) {
757 case AuthzBehavior::PASSTHROUGH:
758 return m_chain ? m_chain->Access(Entity, path, oper, env) : XrdAccPriv_None;
759 case AuthzBehavior::ALLOW:
760 return AddPriv(oper, XrdAccPriv_None);
761 case AuthzBehavior::DENY:
762 return XrdAccPriv_None;
763 }
764 // Code should be unreachable.
765 return XrdAccPriv_None;
766 }
767
768 bool GenerateAcls(const std::string_view &authz, uint64_t &cache_expiry, AccessRulesRaw &rules, std::string &username, std::string &token_subject, std::string &issuer, std::vector<MapRule> &map_rules, std::vector<std::string> &groups, uint32_t &authz_strategy, AuthzSetting &acceptable_authz) {
769 // Does this look like a JWT? If not, bail out early and
770 // do not pollute the log.
771 bool looks_good = true;
772 int separator_count = 0;
773 for (auto cur_char = authz.data(); *cur_char; cur_char++) {
774 if (*cur_char == '.') {
775 separator_count++;
776 if (separator_count > 2) {
777 break;
778 }
779 } else
780 if (!(*cur_char >= 65 && *cur_char <= 90) && // uppercase letters
781 !(*cur_char >= 97 && *cur_char <= 122) && // lowercase letters
782 !(*cur_char >= 48 && *cur_char <= 57) && // numbers
783 (*cur_char != 43) && (*cur_char != 47) && // + and /
784 (*cur_char != 45) && (*cur_char != 95)) // - and _
785 {
786 looks_good = false;
787 break;
788 }
789 }
790 if ((separator_count != 2) || (!looks_good)) {
791 m_log.Log(LogMask::Debug, "Parse", "Token does not appear to be a valid JWT; skipping.");
792 return false;
793 }
794
795 char *err_msg;
796 SciToken token = nullptr;
797 pthread_rwlock_rdlock(&m_config_lock);
798 auto retval = scitoken_deserialize(authz.data(), &token, &m_valid_issuers_array[0], &err_msg);
799 pthread_rwlock_unlock(&m_config_lock);
800 if (retval) {
801 // This originally looked like a JWT so log the failure.
802 m_log.Log(LogMask::Warning, "GenerateAcls", "Failed to deserialize SciToken:", err_msg);
803 free(err_msg);
804 return false;
805 }
806
807 long long expiry;
808 if (scitoken_get_expiration(token, &expiry, &err_msg)) {
809 m_log.Log(LogMask::Warning, "GenerateAcls", "Unable to determine token expiration:", err_msg);
810 free(err_msg);
811 scitoken_destroy(token);
812 return false;
813 }
814 if (expiry > 0) {
815 expiry = std::max(static_cast<int64_t>(monotonic_time() - expiry),
816 static_cast<int64_t>(60));
817 } else {
818 expiry = 60;
819 }
820
821 char *value = nullptr;
822 if (scitoken_get_claim_string(token, "iss", &value, &err_msg)) {
823 m_log.Log(LogMask::Warning, "GenerateAcls", "Failed to get issuer:", err_msg);
824 scitoken_destroy(token);
825 free(err_msg);
826 return false;
827 }
828 std::string token_issuer(value);
829 free(value);
830
831 pthread_rwlock_rdlock(&m_config_lock);
832 auto enf = enforcer_create(token_issuer.c_str(), &m_audiences_array[0], &err_msg);
833 pthread_rwlock_unlock(&m_config_lock);
834 if (!enf) {
835 m_log.Log(LogMask::Warning, "GenerateAcls", "Failed to create an enforcer:", err_msg);
836 scitoken_destroy(token);
837 free(err_msg);
838 return false;
839 }
840
841 Acl *acls = nullptr;
842 if (enforcer_generate_acls(enf, token, &acls, &err_msg)) {
843 scitoken_destroy(token);
844 enforcer_destroy(enf);
845 m_log.Log(LogMask::Warning, "GenerateAcls", "ACL generation from SciToken failed:", err_msg);
846 free(err_msg);
847 return false;
848 }
849 enforcer_destroy(enf);
850
851 pthread_rwlock_rdlock(&m_config_lock);
852 auto iter = m_issuers.find(token_issuer);
853 if (iter == m_issuers.end()) {
854 pthread_rwlock_unlock(&m_config_lock);
855 m_log.Log(LogMask::Warning, "GenerateAcls", "Authorized issuer without a config.");
856 scitoken_destroy(token);
857 return false;
858 }
859 const auto config = iter->second;
860 pthread_rwlock_unlock(&m_config_lock);
861 value = nullptr;
862
863 char **group_list;
864 std::vector<std::string> groups_parsed;
865 if (scitoken_get_claim_string_list(token, config.m_groups_claim.c_str(), &group_list, &err_msg) == 0) {
866 for (int idx=0; group_list[idx]; idx++) {
867 groups_parsed.emplace_back(group_list[idx]);
868 }
869 scitoken_free_string_list(group_list);
870 } else {
871 // Failing to parse groups is not fatal, but we should still warn about what's wrong
872 m_log.Log(LogMask::Warning, "GenerateAcls", "Failed to get token groups:", err_msg);
873 free(err_msg);
874 }
875
876 if (scitoken_get_claim_string(token, "sub", &value, &err_msg)) {
877 m_log.Log(LogMask::Warning, "GenerateAcls", "Failed to get token subject:", err_msg);
878 free(err_msg);
879 scitoken_destroy(token);
880 return false;
881 }
882 token_subject = std::string(value);
883 free(value);
884
885 auto tmp_username = token_subject;
886 if (!config.m_username_claim.empty()) {
887 if (scitoken_get_claim_string(token, config.m_username_claim.c_str(), &value, &err_msg)) {
888 m_log.Log(LogMask::Warning, "GenerateAcls", "Failed to get token username:", err_msg);
889 free(err_msg);
890 scitoken_destroy(token);
891 return false;
892 }
893 tmp_username = std::string(value);
894 free(value);
895 } else if (!config.m_map_subject) {
896 tmp_username = config.m_default_user;
897 }
898
899 for (auto rule : config.m_map_rules) {
900 for (auto path : config.m_base_paths) {
901 auto path_rule = rule;
902 path_rule.m_path_prefix = path + rule.m_path_prefix;
903 auto pos = path_rule.m_path_prefix.find("//");
904 if (pos != std::string::npos) {
905 path_rule.m_path_prefix.erase(pos + 1, 1);
906 }
907 map_rules.emplace_back(path_rule);
908 }
909 }
910
911 AccessRulesRaw xrd_rules;
912 int idx = 0;
913 std::set<std::string> paths_write_seen;
914 std::set<std::string> paths_create_or_modify_seen;
915 std::vector<std::string> acl_paths;
916 acl_paths.reserve(config.m_restricted_paths.size() + 1);
917 while (acls[idx].resource && acls[idx++].authz) {
918 acl_paths.clear();
919 const auto &acl_path = acls[idx-1].resource;
920 const auto &acl_authz = acls[idx-1].authz;
921 if (config.m_restricted_paths.empty()) {
922 acl_paths.push_back(acl_path);
923 } else {
924 auto acl_path_size = strlen(acl_path);
925 for (const auto &restricted_path : config.m_restricted_paths) {
926 // See if the acl_path is more specific than the restricted path; if so, accept it
927 // and move on to applying paths.
928 if (!strncmp(acl_path, restricted_path.c_str(), restricted_path.size())) {
929 // Only do prefix checking on full path components. If acl_path=/foobar and
930 // restricted_path=/foo, then we shouldn't authorize access to /foobar.
931 if (acl_path_size > restricted_path.size() && acl_path[restricted_path.size()] != '/') {
932 continue;
933 }
934 acl_paths.push_back(acl_path);
935 break;
936 }
937 // See if the restricted_path is more specific than the acl_path; if so, accept the
938 // restricted path as the ACL. Keep looping to see if other restricted paths add
939 // more possible authorizations.
940 if (!strncmp(acl_path, restricted_path.c_str(), acl_path_size)) {
941 // Only do prefix checking on full path components. If acl_path=/foo and
942 // restricted_path=/foobar, then we shouldn't authorize access to /foobar. Note:
943 // - The scitokens-cpp library guaranteees that acl_path is normalized and not
944 // of the form `/foo/`.
945 // - Hence, the only time that the acl_path can end in a '/' is when it is
946 // set to `/`.
947 if ((restricted_path.size() > acl_path_size && restricted_path[acl_path_size] != '/') && (acl_path_size != 1)) {
948 continue;
949 }
950 acl_paths.push_back(restricted_path);
951 }
952 }
953 }
954 for (const auto &acl_path : acl_paths) {
955 for (const auto &base_path : config.m_base_paths) {
956 if (!acl_path[0] || acl_path[0] != '/') {continue;}
957 std::string path;
958 MakeCanonical(base_path + acl_path, path);
959 if (!strcmp(acl_authz, "read")) {
960 xrd_rules.emplace_back(AOP_Read, path);
961 xrd_rules.emplace_back(AOP_Readdir, path);
962 xrd_rules.emplace_back(AOP_Stat, path);
963 } else if (!strcmp(acl_authz, "create")) {
964 paths_create_or_modify_seen.insert(path);
965 xrd_rules.emplace_back(AOP_Excl_Create, path);
966 xrd_rules.emplace_back(AOP_Mkdir, path);
967 xrd_rules.emplace_back(AOP_Rename, path);
968 xrd_rules.emplace_back(AOP_Excl_Insert, path);
969 xrd_rules.emplace_back(AOP_Stat, path);
970 } else if (!strcmp(acl_authz, "modify")) {
971 paths_create_or_modify_seen.insert(path);
972 xrd_rules.emplace_back(AOP_Create, path);
973 xrd_rules.emplace_back(AOP_Mkdir, path);
974 xrd_rules.emplace_back(AOP_Rename, path);
975 xrd_rules.emplace_back(AOP_Insert, path);
976 xrd_rules.emplace_back(AOP_Update, path);
977 xrd_rules.emplace_back(AOP_Chmod, path);
978 xrd_rules.emplace_back(AOP_Stat, path);
979 xrd_rules.emplace_back(AOP_Delete, path);
980 } else if (!strcmp(acl_authz, "write")) {
981 paths_write_seen.insert(path);
982 }
983 }
984 }
985 }
986 for (const auto &write_path : paths_write_seen) {
987 if (paths_create_or_modify_seen.find(write_path) == paths_create_or_modify_seen.end()) {
988 // This is a SciToken, add write ACLs.
989 xrd_rules.emplace_back(AOP_Create, write_path);
990 xrd_rules.emplace_back(AOP_Mkdir, write_path);
991 xrd_rules.emplace_back(AOP_Rename, write_path);
992 xrd_rules.emplace_back(AOP_Insert, write_path);
993 xrd_rules.emplace_back(AOP_Update, write_path);
994 xrd_rules.emplace_back(AOP_Stat, write_path);
995 xrd_rules.emplace_back(AOP_Chmod, write_path);
996 xrd_rules.emplace_back(AOP_Delete, write_path);
997 }
998 }
999 authz_strategy = config.m_authz_strategy;
1000
1001 cache_expiry = expiry;
1002 rules = std::move(xrd_rules);
1003 username = std::move(tmp_username);
1004 issuer = std::move(token_issuer);
1005 groups = std::move(groups_parsed);
1006 acceptable_authz = config.m_acceptable_authz;
1007
1008 return true;
1009 }
1010
1011
1012 bool Config(XrdOucEnv *envP) {
1013 // Set default mask for logging.
1014 m_log.setMsgMask(LogMask::Error | LogMask::Warning);
1015
1016 char *config_filename = nullptr;
1017 if (!XrdOucEnv::Import("XRDCONFIGFN", config_filename)) {
1018 return false;
1019 }
1020 XrdOucGatherConf scitokens_conf("scitokens.trace", &m_log);
1021 int result;
1022 if ((result = scitokens_conf.Gather(config_filename, XrdOucGatherConf::trim_lines)) < 0) {
1023 m_log.Emsg("Config", -result, "parsing config file", config_filename);
1024 return false;
1025 }
1026
1027 char *val;
1028 std::string map_filename;
1029 while (scitokens_conf.GetLine()) {
1030 m_log.setMsgMask(0);
1031 scitokens_conf.GetToken(); // Ignore the output; we asked for a single config value, trace
1032 if (!(val = scitokens_conf.GetToken())) {
1033 m_log.Emsg("Config", "scitokens.trace requires an argument. Usage: scitokens.trace [all|error|warning|info|debug|none]");
1034 return false;
1035 }
1036 do {
1037 if (!strcmp(val, "all")) {m_log.setMsgMask(m_log.getMsgMask() | LogMask::All);}
1038 else if (!strcmp(val, "error")) {m_log.setMsgMask(m_log.getMsgMask() | LogMask::Error);}
1039 else if (!strcmp(val, "warning")) {m_log.setMsgMask(m_log.getMsgMask() | LogMask::Warning);}
1040 else if (!strcmp(val, "info")) {m_log.setMsgMask(m_log.getMsgMask() | LogMask::Info);}
1041 else if (!strcmp(val, "debug")) {m_log.setMsgMask(m_log.getMsgMask() | LogMask::Debug);}
1042 else if (!strcmp(val, "none")) {m_log.setMsgMask(0);}
1043 else {m_log.Emsg("Config", "scitokens.trace encountered an unknown directive:", val); return false;}
1044 } while ((val = scitokens_conf.GetToken()));
1045 }
1046 m_log.Emsg("Config", "Logging levels enabled -", LogMaskToString(m_log.getMsgMask()).c_str());
1047
1048 auto xrdEnv = static_cast<XrdOucEnv*>(envP ? envP->GetPtr("xrdEnv*") : nullptr);
1049 auto tlsCtx = static_cast<XrdTlsContext*>(xrdEnv ? xrdEnv->GetPtr("XrdTlsContext*") : nullptr);
1050 if (tlsCtx) {
1051 auto params = tlsCtx->GetParams();
1052 if (params && !params->cafile.empty()) {
1053#ifdef HAVE_SCITOKEN_CONFIG_SET_STR
1054 scitoken_config_set_str("tls.ca_file", params->cafile.c_str(), nullptr);
1055#else
1056 m_log.Log(LogMask::Warning, "Config", "tls.ca_file is set but the platform's libscitokens.so does not support setting config parameters");
1057#endif
1058 }
1059 }
1060
1061 return Reconfig();
1062 }
1063
1064 bool ParseMapfile(const std::string &filename, std::vector<MapRule> &rules)
1065 {
1066 std::stringstream ss;
1067 std::ifstream mapfile(filename);
1068 if (!mapfile.is_open())
1069 {
1070 ss << "Error opening mapfile (" << filename << "): " << strerror(errno);
1071 m_log.Log(LogMask::Error, "ParseMapfile", ss.str().c_str());
1072 return false;
1073 }
1074 picojson::value val;
1075 auto err = picojson::parse(val, mapfile);
1076 if (!err.empty()) {
1077 ss << "Unable to parse mapfile (" << filename << ") as json: " << err;
1078 m_log.Log(LogMask::Error, "ParseMapfile", ss.str().c_str());
1079 return false;
1080 }
1081 if (!val.is<picojson::array>()) {
1082 ss << "Top-level element of the mapfile " << filename << " must be a list";
1083 m_log.Log(LogMask::Error, "ParseMapfile", ss.str().c_str());
1084 return false;
1085 }
1086 const auto& rule_list = val.get<picojson::array>();
1087 for (const auto &rule : rule_list)
1088 {
1089 if (!rule.is<picojson::object>()) {
1090 ss << "Mapfile " << filename << " must be a list of JSON objects; found non-object";
1091 m_log.Log(LogMask::Error, "ParseMapfile", ss.str().c_str());
1092 return false;
1093 }
1094 std::string path;
1095 std::string group;
1096 std::string sub;
1097 std::string username;
1098 std::string result;
1099 bool ignore = false;
1100 for (const auto &entry : rule.get<picojson::object>()) {
1101 if (!entry.second.is<std::string>()) {
1102 if (entry.first != "result" && entry.first != "group" && entry.first != "sub" && entry.first != "path") {continue;}
1103 ss << "In mapfile " << filename << ", rule entry for " << entry.first << " has non-string value";
1104 m_log.Log(LogMask::Error, "ParseMapfile", ss.str().c_str());
1105 return false;
1106 }
1107 if (entry.first == "result") {
1108 result = entry.second.get<std::string>();
1109 }
1110 else if (entry.first == "group") {
1111 group = entry.second.get<std::string>();
1112 }
1113 else if (entry.first == "sub") {
1114 sub = entry.second.get<std::string>();
1115 } else if (entry.first == "username") {
1116 username = entry.second.get<std::string>();
1117 } else if (entry.first == "path") {
1118 std::string norm_path;
1119 if (!MakeCanonical(entry.second.get<std::string>(), norm_path)) {
1120 ss << "In mapfile " << filename << " encountered a path " << entry.second.get<std::string>()
1121 << " that cannot be normalized";
1122 m_log.Log(LogMask::Error, "ParseMapfile", ss.str().c_str());
1123 return false;
1124 }
1125 path = norm_path;
1126 } else if (entry.first == "ignore") {
1127 ignore = true;
1128 break;
1129 }
1130 }
1131 if (ignore) continue;
1132 if (result.empty())
1133 {
1134 ss << "In mapfile " << filename << " encountered a rule without a 'result' attribute";
1135 m_log.Log(LogMask::Error, "ParseMapfile", ss.str().c_str());
1136 return false;
1137 }
1138 rules.emplace_back(sub, username, path, group, result);
1139 }
1140
1141 return true;
1142 }
1143
1144 // A helper function for parsing one of the authorization setting variables (required_authz, acceptable_authz).
1145 // The result object is only changed if the variable is set to a non-empty string in the configuration.
1146 //
1147 // Returns false on failure.
1148 bool ParseAuthzSetting(OverrideINIReader &reader, const std::string &section, const std::string &variable, AuthzSetting &result) {
1149 auto authz_setting_str = reader.Get(section, variable, "");
1150 AuthzSetting authz_setting(AuthzSetting::None);
1151 if (authz_setting_str == "") {
1152 return true;
1153 } else if (authz_setting_str == "none") {
1154 authz_setting = AuthzSetting::None;
1155 } else if (authz_setting_str == "all") {
1156 authz_setting = AuthzSetting::All;
1157 } else if (authz_setting_str == "read") {
1158 authz_setting = AuthzSetting::Read;
1159 } else if (authz_setting_str == "write") {
1160 authz_setting = AuthzSetting::Write;
1161 } else {
1162 std::stringstream ss;
1163 ss << "Failed to parse " << variable << " in section " << section << ": unknown authorization setting " << authz_setting_str;
1164 m_log.Log(LogMask::Error, "Reconfig", ss.str().c_str());
1165 return false;
1166 }
1167 result = authz_setting;
1168 return true;
1169 }
1170
1171 bool Reconfig()
1172 {
1173 errno = 0;
1174 m_cfg_file = "/etc/xrootd/scitokens.cfg";
1175 if (!m_parms.empty()) {
1176 size_t pos = 0;
1177 std::vector<std::string> arg_list;
1178 do {
1179 while ((m_parms.size() > pos) && (m_parms[pos] == ' ')) {pos++;}
1180 auto next_pos = m_parms.find_first_of(", ", pos);
1181 auto next_arg = m_parms.substr(pos, next_pos - pos);
1182 pos = next_pos;
1183 if (!next_arg.empty()) {
1184 arg_list.emplace_back(std::move(next_arg));
1185 }
1186 } while (pos != std::string::npos);
1187
1188 for (const auto &arg : arg_list) {
1189 if (strncmp(arg.c_str(), "config=", 7)) {
1190 m_log.Log(LogMask::Error, "Reconfig", "Ignoring unknown configuration argument:", arg.c_str());
1191 continue;
1192 }
1193 m_cfg_file = std::string(arg.c_str() + 7);
1194 }
1195 }
1196 m_log.Log(LogMask::Info, "Reconfig", "Parsing configuration file:", m_cfg_file.c_str());
1197
1198 OverrideINIReader reader(m_cfg_file);
1199 if (reader.ParseError() < 0) {
1200 std::stringstream ss;
1201 ss << "Error opening config file (" << m_cfg_file << "): " << strerror(errno);
1202 m_log.Log(LogMask::Error, "Reconfig", ss.str().c_str());
1203 return false;
1204 } else if (reader.ParseError()) {
1205 std::stringstream ss;
1206 ss << "Parse error on line " << reader.ParseError() << " of file " << m_cfg_file;
1207 m_log.Log(LogMask::Error, "Reconfig", ss.str().c_str());
1208 return false;
1209 }
1210 std::vector<std::string> audiences;
1211 std::unordered_map<std::string, IssuerConfig> issuers;
1212 for (const auto &section : reader.Sections()) {
1213 std::string section_lower;
1214 std::transform(section.begin(), section.end(), std::back_inserter(section_lower),
1215 [](unsigned char c){ return std::tolower(c); });
1216
1217 if (section_lower.substr(0, 6) == "global") {
1218 auto audience = reader.Get(section, "audience", "");
1219 if (!audience.empty()) {
1220 size_t pos = 0;
1221 do {
1222 while (audience.size() > pos && (audience[pos] == ',' || audience[pos] == ' ')) {pos++;}
1223 auto next_pos = audience.find_first_of(", ", pos);
1224 auto next_aud = audience.substr(pos, next_pos - pos);
1225 pos = next_pos;
1226 if (!next_aud.empty()) {
1227 audiences.push_back(next_aud);
1228 }
1229 } while (pos != std::string::npos);
1230 }
1231 audience = reader.Get(section, "audience_json", "");
1232 if (!audience.empty()) {
1233 picojson::value json_obj;
1234 auto err = picojson::parse(json_obj, audience);
1235 if (!err.empty()) {
1236 m_log.Log(LogMask::Error, "Reconfig", "Unable to parse audience_json:", err.c_str());
1237 return false;
1238 }
1239 if (!json_obj.is<picojson::value::array>()) {
1240 m_log.Log(LogMask::Error, "Reconfig", "audience_json must be a list of strings; not a list.");
1241 return false;
1242 }
1243 for (const auto &val : json_obj.get<picojson::value::array>()) {
1244 if (!val.is<std::string>()) {
1245 m_log.Log(LogMask::Error, "Reconfig", "audience must be a list of strings; value is not a string.");
1246 return false;
1247 }
1248 audiences.push_back(val.get<std::string>());
1249 }
1250 }
1251 auto onmissing = reader.Get(section, "onmissing", "");
1252 if (onmissing == "passthrough") {
1253 m_authz_behavior = AuthzBehavior::PASSTHROUGH;
1254 } else if (onmissing == "allow") {
1255 m_authz_behavior = AuthzBehavior::ALLOW;
1256 } else if (onmissing == "deny") {
1257 m_authz_behavior = AuthzBehavior::DENY;
1258 } else if (!onmissing.empty()) {
1259 m_log.Log(LogMask::Error, "Reconfig", "Unknown value for onmissing key:", onmissing.c_str());
1260 return false;
1261 }
1262 }
1263
1264 if (section_lower.substr(0, 7) != "issuer ") {continue;}
1265
1266 auto issuer = reader.Get(section, "issuer", "");
1267 if (issuer.empty()) {
1268 m_log.Log(LogMask::Error, "Reconfig", "Ignoring section because 'issuer' attribute is not set:",
1269 section.c_str());
1270 continue;
1271 }
1272 m_log.Log(LogMask::Debug, "Reconfig", "Configuring issuer", issuer.c_str());
1273
1274 std::vector<MapRule> rules;
1275 auto name_mapfile = reader.Get(section, "name_mapfile", "");
1276 if (!name_mapfile.empty()) {
1277 if (!ParseMapfile(name_mapfile, rules)) {
1278 m_log.Log(LogMask::Error, "Reconfig", "Failed to parse mapfile; failing (re-)configuration", name_mapfile.c_str());
1279 return false;
1280 } else {
1281 m_log.Log(LogMask::Info, "Reconfig", "Successfully parsed SciTokens mapfile:", name_mapfile.c_str());
1282 }
1283 }
1284
1285 auto base_path = reader.Get(section, "base_path", "");
1286 if (base_path.empty()) {
1287 m_log.Log(LogMask::Error, "Reconfig", "Ignoring section because 'base_path' attribute is not set:",
1288 section.c_str());
1289 continue;
1290 }
1291
1292 size_t pos = 7;
1293 while (section.size() > pos && std::isspace(section[pos])) {pos++;}
1294
1295 auto name = section.substr(pos);
1296 if (name.empty()) {
1297 m_log.Log(LogMask::Error, "Reconfig", "Invalid section name:", section.c_str());
1298 continue;
1299 }
1300
1301 std::vector<std::string> base_paths;
1302 ParseCanonicalPaths(base_path, base_paths);
1303
1304 auto restricted_path = reader.Get(section, "restricted_path", "");
1305 std::vector<std::string> restricted_paths;
1306 if (!restricted_path.empty()) {
1307 ParseCanonicalPaths(restricted_path, restricted_paths);
1308 }
1309
1310 auto default_user = reader.Get(section, "default_user", "");
1311 auto map_subject = reader.GetBoolean(section, "map_subject", false);
1312 auto username_claim = reader.Get(section, "username_claim", "");
1313 auto groups_claim = reader.Get(section, "groups_claim", "wlcg.groups");
1314
1315 AuthzSetting required_authz(AuthzSetting::None), acceptable_authz(AuthzSetting::All);
1316 if (!ParseAuthzSetting(reader, section, "required_authorization", required_authz)) {
1317 m_log.Log(LogMask::Error, "Reconfig", "Ignoring required_authorization and using default of 'none'");
1318 }
1319 if (!ParseAuthzSetting(reader, section, "acceptable_authorization", acceptable_authz)) {
1320 m_log.Log(LogMask::Error, "Reconfig", "Ignoring acceptable_authorization and using default of 'all'");
1321 }
1322
1323 auto authz_strategy_str = reader.Get(section, "authorization_strategy", "");
1324 uint32_t authz_strategy = 0;
1325 if (authz_strategy_str.empty()) {
1326 authz_strategy = IssuerAuthz::Default;
1327 } else {
1328 std::istringstream authz_strategy_stream(authz_strategy_str);
1329 std::string authz_str;
1330 while (std::getline(authz_strategy_stream, authz_str, ' ')) {
1331 if (!strcasecmp(authz_str.c_str(), "capability")) {
1332 authz_strategy |= IssuerAuthz::Capability;
1333 } else if (!strcasecmp(authz_str.c_str(), "group")) {
1334 authz_strategy |= IssuerAuthz::Group;
1335 } else if (!strcasecmp(authz_str.c_str(), "mapping")) {
1336 authz_strategy |= IssuerAuthz::Mapping;
1337 } else {
1338 m_log.Log(LogMask::Error, "Reconfig", "Unknown authorization strategy (ignoring):", authz_str.c_str());
1339 }
1340 }
1341 }
1342
1343 issuers.emplace(std::piecewise_construct,
1344 std::forward_as_tuple(issuer),
1345 std::forward_as_tuple(name, issuer, base_paths, restricted_paths,
1346 map_subject, authz_strategy, default_user, username_claim, groups_claim, rules,
1347 acceptable_authz, required_authz));
1348
1349 // If this is an issuer that is required for authorization, calculate the paths that it is
1350 // responsible for.
1351 if (required_authz != AuthzSetting::None) {
1352 AccessRulesRaw rules;
1353 for (const auto &base_path : base_paths) {
1354 if (restricted_paths.empty()) {
1355 restricted_paths.emplace_back("/");
1356 }
1357 for (const auto &restricted_path : restricted_paths) {
1358 auto full_path = base_path + "/" + restricted_path;
1359 std::string cleaned_path;
1360 MakeCanonical(full_path, cleaned_path);
1361 if (required_authz == AuthzSetting::Read || required_authz == AuthzSetting::All) {
1362 rules.emplace_back(AOP_Read, cleaned_path);
1363 rules.emplace_back(AOP_Stat, cleaned_path);
1364 } else if (required_authz == AuthzSetting::Write || required_authz == AuthzSetting::All) {
1365 rules.emplace_back(AOP_Create, cleaned_path);
1366 rules.emplace_back(AOP_Mkdir, cleaned_path);
1367 rules.emplace_back(AOP_Stat, cleaned_path);
1368 }
1369 }
1370 }
1371 m_required_issuers.emplace_back(std::make_unique<SubpathMatch>(rules), issuer);
1372 }
1373 }
1374
1375 if (issuers.empty()) {
1376 m_log.Log(LogMask::Warning, "Reconfig", "No issuers configured.");
1377 }
1378
1379 pthread_rwlock_wrlock(&m_config_lock);
1380 try {
1381 m_audiences = std::move(audiences);
1382 size_t idx = 0;
1383 m_audiences_array.resize(m_audiences.size() + 1);
1384 for (const auto &audience : m_audiences) {
1385 m_audiences_array[idx++] = audience.c_str();
1386 }
1387 m_audiences_array[idx] = nullptr;
1388
1389 m_issuers = std::move(issuers);
1390 m_valid_issuers_array.resize(m_issuers.size() + 1);
1391 idx = 0;
1392 for (const auto &issuer : m_issuers) {
1393 m_valid_issuers_array[idx++] = issuer.first.c_str();
1394 }
1395 m_valid_issuers_array[idx] = nullptr;
1396 } catch (...) {
1397 pthread_rwlock_unlock(&m_config_lock);
1398 return false;
1399 }
1400 pthread_rwlock_unlock(&m_config_lock);
1401 return true;
1402 }
1403
1404 void Check(uint64_t now)
1405 {
1406 if (now <= m_next_clean) {return;}
1407 std::lock_guard<std::mutex> guard(m_mutex);
1408
1409 for (auto iter = m_map.begin(); iter != m_map.end(); ) {
1410 if (iter->second->expired()) {
1411 iter = m_map.erase(iter);
1412 } else {
1413 ++iter;
1414 }
1415 }
1416 Reconfig();
1417
1418 m_next_clean = monotonic_time() + m_expiry_secs;
1419 }
1420
1421 bool m_config_lock_initialized{false};
1422 std::mutex m_mutex;
1423 pthread_rwlock_t m_config_lock;
1424 std::vector<std::string> m_audiences;
1425 std::vector<const char *> m_audiences_array;
1426 std::map<std::string, std::shared_ptr<XrdAccRules>, std::less<>> m_map; // Note: std::less<> is used as the comparator to enable transparent casting from std::string_view for key lookup
1427 XrdAccAuthorize* m_chain;
1428 const std::string m_parms;
1429 std::vector<const char*> m_valid_issuers_array;
1430 // Authorization from these issuers are required for any matching path. The map tracks the
1431 // base prefix to the issuer URL.
1432 std::vector<std::pair<std::unique_ptr<SubpathMatch>, std::string>> m_required_issuers;
1433 std::unordered_map<std::string, IssuerConfig> m_issuers;
1434 uint64_t m_next_clean{0};
1435 XrdSysError m_log;
1436 AuthzBehavior m_authz_behavior{AuthzBehavior::PASSTHROUGH};
1437 std::string m_cfg_file;
1438
1439 static constexpr uint64_t m_expiry_secs = 60;
1440};
1441
1442void InitAccSciTokens(XrdSysLogger *lp, const char *cfn, const char *parm,
1443 XrdAccAuthorize *accP, XrdOucEnv *envP)
1444{
1445 try {
1446 accSciTokens = new XrdAccSciTokens(lp, parm, accP, envP);
1448 } catch (std::exception &) {
1449 }
1450}
1451
1452extern "C" {
1453
1455 const char *cfn,
1456 const char *parm,
1457 XrdOucEnv *envP,
1458 XrdAccAuthorize *accP)
1459{
1460 // Record the parent authorization plugin. There is no need to use
1461 // unique_ptr as all of this happens once in the main and only thread.
1462 //
1463
1464 // If we have been initialized by a previous load, them return that result.
1465 // Otherwise, it's the first time through, get a new SciTokens authorizer.
1466 //
1467 if (!accSciTokens) InitAccSciTokens(lp, cfn, parm, accP, envP);
1468 return accSciTokens;
1469}
1470
1472 const char *cfn,
1473 const char *parm)
1474{
1475 InitAccSciTokens(lp, cfn, parm, nullptr, nullptr);
1476 return accSciTokens;
1477}
1478
1480 const char *cfn,
1481 const char *parm,
1482 XrdOucEnv *envP)
1483{
1484 InitAccSciTokens(lp, cfn, parm, nullptr, envP);
1485 return accSciTokens;
1486}
1487
1488
1489}
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_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_Excl_Insert
mv() where destination doesn't exist.
XrdAccPrivs
@ XrdAccPriv_Mkdir
@ XrdAccPriv_Chown
@ XrdAccPriv_Insert
@ XrdAccPriv_Lookup
@ XrdAccPriv_Rename
@ XrdAccPriv_Update
@ XrdAccPriv_Read
@ XrdAccPriv_Lock
@ XrdAccPriv_None
@ XrdAccPriv_Delete
@ XrdAccPriv_Create
@ XrdAccPriv_Readdir
@ XrdAccPriv_Chmod
XrdSciTokensHelper * SciTokensHelper
XrdAccAuthorize * XrdAccAuthorizeObjAdd(XrdSysLogger *log, const char *config, const char *params, XrdOucEnv *, XrdAccAuthorize *chain_authz)
XrdAccAuthorize * XrdAccAuthorizeObject(XrdSysLogger *log, const char *config, const char *parms)
XrdAccSciTokens * accSciTokens
XrdAccAuthorize * XrdAccAuthorizeObjAdd(XrdSysLogger *lp, const char *cfn, const char *parm, XrdOucEnv *envP, XrdAccAuthorize *accP)
XrdVERSIONINFO(XrdAccAuthorizeObject, XrdAccSciTokens)
void InitAccSciTokens(XrdSysLogger *lp, const char *cfn, const char *parm, XrdAccAuthorize *accP, XrdOucEnv *envP)
XrdAccAuthorize * XrdAccAuthorizeObject2(XrdSysLogger *lp, const char *cfn, const char *parm, XrdOucEnv *envP)
XrdAccAuthorize * XrdAccAuthorizeObject(XrdSysLogger *lp, const char *cfn, const char *parm)
bool AuthorizesRequiredIssuers(Access_Operation client_oper, const std::string_view &path, const std::vector< std::pair< std::unique_ptr< SubpathMatch >, std::string > > &required_issuers, const std::vector< std::shared_ptr< XrdAccRules > > &access_rules_list)
std::vector< std::pair< Access_Operation, std::string > > AccessRulesRaw
@ Capability
bool Debug
int emsg(int rc, char *msg)
std::string str() const
XrdAccAuthorize()
Constructor.
virtual XrdAccPrivs Access(const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *Env=0)=0
bool expired() const
const std::string str() const
virtual int Audit(const int accok, const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *Env=0) override
virtual XrdAccPrivs Access(const XrdSecEntity *Entity, const char *path, const Access_Operation oper, XrdOucEnv *env) override
XrdAccSciTokens(XrdSysLogger *lp, const char *parms, XrdAccAuthorize *chain, XrdOucEnv *envP)
virtual Issuers IssuerList() override
virtual bool Validate(const char *token, std::string &emsg, long long *expT, XrdSecEntity *Entity) override
virtual int Test(const XrdAccPrivs priv, const Access_Operation oper) override
std::string GetConfigFile()
static bool Import(const char *var, char *&val)
Definition XrdOucEnv.cc:222
char * Get(const char *varname)
Definition XrdOucEnv.hh:69
void * GetPtr(const char *varname)
Definition XrdOucEnv.cc:281
@ trim_lines
Prefix trimmed lines.
XrdSciTokensHelper()
Constructor and Destructor.
std::vector< ValidIssuer > Issuers
bool Mon_isIO(const Access_Operation oper)
void Mon_Report(const XrdSecEntity &Entity, const std::string &subject, const std::string &username)
bool Add(XrdSecAttr &attr)
char * vorg
Entity's virtual organization(s)
int credslen
Length of the 'creds' data.
XrdNetAddrInfo * addrInfo
Entity's connection details.
XrdSecEntityAttr * eaAPI
non-const API to attributes
char prot[XrdSecPROTOIDSIZE]
Auth protocol used (e.g. krb5)
char * creds
Raw entity credentials or cert.
XrdSecMonitor * secMon
If !0 security monitoring enabled.
char * grps
Entity's group name(s)
char * name
Entity's name.
char * role
Entity's role(s)
const CTX_Params * GetParams()
XrdTlsContext * tlsCtx
Definition XrdGlobals.cc:52
std::string LogMaskToString(int mask)
XrdOucEnv * envP
Definition XrdPss.cc:109