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