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