XRootD
XrdSciTokensAccess.cc
Go to the documentation of this file.
1 
3 #include "XrdOuc/XrdOucEnv.hh"
6 #include "XrdSec/XrdSecEntity.hh"
8 #include "XrdSys/XrdSysLogger.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 
40 namespace {
41 
42 enum LogMask {
43  Debug = 0x01,
44  Info = 0x02,
45  Warning = 0x04,
46  Error = 0x08,
47  All = 0xff
48 };
49 
50 std::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 
74 inline 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 
84 XrdAccPrivs AddPriv(Access_Operation op, XrdAccPrivs privs)
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 
138 const 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 
161 std::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.
186 bool 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 
195 bool 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 
227 void 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 
244 struct 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 
286 class OverrideINIReader: public INIReader {
287 public:
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  }
295 protected:
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 
323 void
324 ParseTokenString(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 
347 std::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.
353 const 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.
392 bool 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 
455 class XrdAccSciTokens;
456 
458 
460  public XrdSciTokensMon
461 {
462 
463  enum class AuthzBehavior {
464  PASSTHROUGH,
465  ALLOW,
466  DENY
467  };
468 
469 public:
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 
484  virtual ~XrdAccSciTokens() {
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 
783 private:
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.
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 
1530 void 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 
1540 extern "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
Definition: XrdAccPrivs.hh:39
@ XrdAccPriv_Mkdir
Definition: XrdAccPrivs.hh:46
@ XrdAccPriv_Chown
Definition: XrdAccPrivs.hh:41
@ XrdAccPriv_Insert
Definition: XrdAccPrivs.hh:44
@ XrdAccPriv_Lookup
Definition: XrdAccPrivs.hh:47
@ XrdAccPriv_Rename
Definition: XrdAccPrivs.hh:48
@ XrdAccPriv_Poll
Definition: XrdAccPrivs.hh:54
@ XrdAccPriv_Update
Definition: XrdAccPrivs.hh:52
@ XrdAccPriv_Read
Definition: XrdAccPrivs.hh:49
@ XrdAccPriv_Lock
Definition: XrdAccPrivs.hh:45
@ XrdAccPriv_None
Definition: XrdAccPrivs.hh:55
@ XrdAccPriv_Stage
Definition: XrdAccPrivs.hh:53
@ XrdAccPriv_Delete
Definition: XrdAccPrivs.hh:43
@ XrdAccPriv_Create
Definition: XrdAccPrivs.hh:42
@ XrdAccPriv_Readdir
Definition: XrdAccPrivs.hh:50
@ XrdAccPriv_Chmod
Definition: XrdAccPrivs.hh:40
XrdAccSciTokens * accSciTokens
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)
XrdSciTokensHelper * SciTokensHelper
XrdVERSIONINFO(XrdAccAuthorizeObject, XrdAccSciTokens)
void InitAccSciTokens(XrdSysLogger *lp, const char *cfn, const char *parm, XrdAccAuthorize *accP, XrdOucEnv *envP)
XrdAccAuthorize * XrdAccAuthorizeObjAdd(XrdSysLogger *lp, const char *cfn, const char *parm, XrdOucEnv *envP, XrdAccAuthorize *accP)
XrdAccAuthorize * XrdAccAuthorizeObject(XrdSysLogger *lp, const char *cfn, const char *parm)
XrdAccAuthorize * XrdAccAuthorizeObject2(XrdSysLogger *lp, const char *cfn, const char *parm, XrdOucEnv *envP)
std::vector< std::pair< Access_Operation, std::string > > AccessRulesRaw
@ Default
@ Capability
@ Mapping
AuthzSetting
bool Debug
void getline(uchar *buff, int blen)
int emsg(int rc, char *msg)
@ Error
std::string str() const
bool empty() const
virtual int Test(const XrdAccPrivs priv, const Access_Operation oper)=0
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
void * GetPtr(const char *varname)
Definition: XrdOucEnv.cc:281
char * Get(const char *varname)
Definition: XrdOucEnv.hh:69
@ trim_lines
Prefix trimmed lines.
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)
Definition: XrdSecEntity.hh:71
int credslen
Length of the 'creds' data.
Definition: XrdSecEntity.hh:78
XrdNetAddrInfo * addrInfo
Entity's connection details.
Definition: XrdSecEntity.hh:80
XrdSecEntityAttr * eaAPI
non-const API to attributes
Definition: XrdSecEntity.hh:92
char prot[XrdSecPROTOIDSIZE]
Auth protocol used (e.g. krb5)
Definition: XrdSecEntity.hh:67
char * creds
Raw entity credentials or cert.
Definition: XrdSecEntity.hh:77
XrdSecMonitor * secMon
If !0 security monitoring enabled.
Definition: XrdSecEntity.hh:89
char * grps
Entity's group name(s)
Definition: XrdSecEntity.hh:73
char * name
Entity's name.
Definition: XrdSecEntity.hh:69
char * role
Entity's role(s)
Definition: XrdSecEntity.hh:72
int Emsg(const char *esfx, int ecode, const char *text1, const char *text2=0)
Definition: XrdSysError.cc:116
void Say(const char *text1, const char *text2=0, const char *txt3=0, const char *text4=0, const char *text5=0, const char *txt6=0)
Definition: XrdSysError.cc:162
void setMsgMask(int mask)
Definition: XrdSysError.hh:188
int getMsgMask()
Definition: XrdSysError.hh:190
void Log(int mask, const char *esfx, const char *text1, const char *text2=0, const char *text3=0)
Definition: XrdSysError.hh:167
const CTX_Params * GetParams()
@ Warning
XrdTlsContext * tlsCtx
Definition: XrdGlobals.cc:52
std::string LogMaskToString(int mask)
XrdOucEnv * envP
Definition: XrdPss.cc:110