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