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