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 
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 
41 namespace {
42 
43 enum LogMask {
44  Debug = 0x01,
45  Info = 0x02,
46  Warning = 0x04,
47  Error = 0x08,
48  All = 0xff
49 };
50 
51 std::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 
75 inline 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 
85 XrdAccPrivs AddPriv(Access_Operation op, XrdAccPrivs privs)
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 
133 const 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 
154 std::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.
179 bool 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 
188 bool 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 
220 void 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 
237 struct 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 
279 class OverrideINIReader: public INIReader {
280 public:
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  }
288 protected:
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 
316 void
317 ParseTokenString(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 
340 std::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.
346 const 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.
385 bool 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 
448 class XrdAccSciTokens;
449 
451 
453  public XrdSciTokensMon
454 {
455 
456  enum class AuthzBehavior {
457  PASSTHROUGH,
458  ALLOW,
459  DENY
460  };
461 
462 public:
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 
477  virtual ~XrdAccSciTokens() {
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 
776 private:
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.
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 
1487 void 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 
1497 extern "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
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_Update
Definition: XrdAccPrivs.hh:52
@ XrdAccPriv_Read
Definition: XrdAccPrivs.hh:49
@ XrdAccPriv_Lock
Definition: XrdAccPrivs.hh:45
@ XrdAccPriv_None
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:95
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:141
void setMsgMask(int mask)
Definition: XrdSysError.hh:154
int getMsgMask()
Definition: XrdSysError.hh:156
void Log(int mask, const char *esfx, const char *text1, const char *text2=0, const char *text3=0)
Definition: XrdSysError.hh:133
const CTX_Params * GetParams()
@ Warning
XrdTlsContext * tlsCtx
Definition: XrdGlobals.cc:52
std::string LogMaskToString(int mask)
XrdOucEnv * envP
Definition: XrdPss.cc:109