Loading dist/adconnect.yaml +4 −3 Changes for dist/adconnect.yaml: 4 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -49,9 +49,10 @@ adconnect: id_base: 200000 id_range: 2000000 # %u the user name, %d the domain; unixHomeDirectory and loginShell win # where an account has them home: /home/%u # %d the domain, %n the account name, %u the name as shown (with # @domain when qualified); unixHomeDirectory and loginShell win where an # account has them home: /home/%d/%n shell: /bin/sh lowercase_names: true Loading src/config.cpp +151 −81 Changes for src/config.cpp: 151 added lines, 81 removed lines. Original line number Diff line number Diff line Loading @@ -29,7 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <cctype> #include <fstream> #include <sstream> #include <set> #include <stdexcept> #include <confplus/conf.h> Loading @@ -41,40 +41,61 @@ namespace { const std::string Root = "/adconnect"; /* what a domain has of its own; every other key a domain doesn't set * comes from the top level */ const std::set<std::string> OwnKeys = { "domain", "base", "servers", "bind_dn", "bind_password", "bind_password_file", "kerberos_principal", "kerberos_realm", "keytab", "kdcs", "id_base", "id_range", "user_filter", "group_filter", "qualified_names", }; std::string lower(std::string s) { for (char &c : s) c = (char)tolower((unsigned char)c); return s; } bool readValue(confplus::Config &conf, const std::string &key, std::string &out) { auto *k = conf.getKey(Root + "/" + key); /* where keys are looked up: a domain's entry (domains: [...]), then for * shared keys the top level; or the top level alone */ struct Scope { confplus::Config &conf; std::string prefix; std::string label; /* "domains[1]." in messages */ confplus::Config::ConfigData *key(const std::string &name) const { if (auto *k = conf.getKey(prefix + "/" + name)) return k; if (prefix != Root && !OwnKeys.count(name)) return conf.getKey(Root + "/" + name); return nullptr; } bool own(const std::string &name) const { return conf.getKey(prefix + "/" + name) != nullptr; } }; bool readValue(const Scope &s, const std::string &key, std::string &out) { auto *k = s.key(key); if (!k) return false; try { out = conf.getValue(k, 0); out = s.conf.getValue(k, 0); return true; } catch (const confplus::ConfException &) { return false; } } void readString(confplus::Config &conf, const std::string &key, std::string &out) { void readString(const Scope &s, const std::string &key, std::string &out) { std::string v; if (readValue(conf, key, v)) out = v; if (readValue(s, key, v)) out = v; } void readBool(confplus::Config &conf, const std::string &key, bool &out) { void readBool(const Scope &s, const std::string &key, bool &out) { std::string v; if (!readValue(conf, key, v)) return; if (!readValue(s, key, v)) return; v = lower(v); if (v == "true" || v == "yes" || v == "1" || v == "on") out = true; else if (v == "false" || v == "no" || v == "0" || v == "off") out = false; else throw std::runtime_error(key + ": not a boolean: " + v); else throw std::runtime_error(s.label + key + ": not a boolean: " + v); } void readNumber(confplus::Config &conf, const std::string &key, unsigned long &out, unsigned long min, unsigned long max) { void readNumber(const Scope &s, const std::string &key, unsigned long &out, unsigned long min, unsigned long max) { std::string v; if (!readValue(conf, key, v)) return; if (!readValue(s, key, v)) return; size_t used = 0; unsigned long n = 0; try { Loading @@ -83,24 +104,24 @@ void readNumber(confplus::Config &conf, const std::string &key, unsigned long &o used = 0; } if (v.empty() || used != v.size() || n < min || n > max) throw std::runtime_error(key + ": expected a number from " + std::to_string(min) + " to " + throw std::runtime_error(s.label + key + ": expected a number from " + std::to_string(min) + " to " + std::to_string(max) + ", got \"" + v + "\""); out = n; } void readInt(confplus::Config &conf, const std::string &key, int &out, int min, int max) { void readInt(const Scope &s, const std::string &key, int &out, int min, int max) { unsigned long n = (unsigned long)out; readNumber(conf, key, n, (unsigned long)min, (unsigned long)max); readNumber(s, key, n, (unsigned long)min, (unsigned long)max); out = (int)n; } std::vector<std::string> readList(confplus::Config &conf, const std::string &key) { std::vector<std::string> readList(const Scope &s, const std::string &key) { std::vector<std::string> out; auto *k = conf.getKey(Root + "/" + key); auto *k = s.key(key); if (!k) return out; for (size_t i = 0; i < k->getElements(); ++i) { try { std::string v = conf.getValue(k, i); std::string v = s.conf.getValue(k, i); if (!v.empty()) out.push_back(v); } catch (const confplus::ConfException &) { break; Loading Loading @@ -133,70 +154,75 @@ void checkFilter(const std::string &key, const std::string &filter) { } } std::string readSecret(const std::string &path) { std::string readSecret(const std::string &key, const std::string &path) { std::ifstream in(path); if (!in) throw std::runtime_error("bind_password_file: cannot read " + path); if (!in) throw std::runtime_error(key + ": cannot read " + path); std::string s; std::getline(in, s); while (!s.empty() && (s.back() == '\r' || s.back() == '\n')) s.pop_back(); return s; } } // namespace Config loadConfig(const std::string &path) { Config cfg; { std::ifstream probe(path); if (!probe) throw std::runtime_error("cannot read " + path); /* the settings every domain shares */ void readGlobal(const Scope &s, Config &cfg) { readString(s, "socket", cfg.socket); readInt(s, "cache_ttl", cfg.cacheTtl, 0, 86400); readInt(s, "negative_ttl", cfg.negativeTtl, 0, 86400); unsigned long offline = (unsigned long)cfg.offlineTtl; readNumber(s, "offline_ttl", offline, 0, 31536000); cfg.offlineTtl = (long)offline; readInt(s, "retry_interval", cfg.retryInterval, 1, 3600); readString(s, "cache_file", cfg.cacheFile); } try { confplus::Config conf("yaml:" + path); readString(conf, "domain", cfg.domain); /* one domain; idBaseSet: whether it set id_base itself */ void readDomain(const Scope &s, Config &cfg, bool multi, bool &idBaseSet) { const std::string &l = s.label; readString(s, "domain", cfg.domain); cfg.domain = lower(cfg.domain); if (cfg.domain.empty()) throw std::runtime_error("domain: missing"); readString(conf, "base", cfg.base); if (cfg.domain.empty()) throw std::runtime_error(l + "domain: missing"); readString(s, "base", cfg.base); if (cfg.base.empty()) cfg.base = baseFromDomain(cfg.domain); cfg.servers = readList(conf, "servers"); cfg.servers = readList(s, "servers"); if (cfg.servers.empty()) cfg.servers.push_back(cfg.domain); readInt(conf, "port", cfg.port, 1, 65535); readInt(s, "port", cfg.port, 1, 65535); std::string tls = "ldaps"; readString(conf, "tls", tls); readString(s, "tls", tls); tls = lower(tls); if (tls == "ldaps") cfg.tls = ldap::Tls::Ldaps; else if (tls == "starttls") cfg.tls = ldap::Tls::StartTls; else if (tls == "none") cfg.tls = ldap::Tls::None; else throw std::runtime_error("tls: expected ldaps, starttls or none, got \"" + tls + "\""); readBool(conf, "insecure", cfg.insecure); readString(conf, "ca_file", cfg.caFile); else throw std::runtime_error(l + "tls: expected ldaps, starttls or none, got \"" + tls + "\""); readBool(s, "insecure", cfg.insecure); readString(s, "ca_file", cfg.caFile); readString(conf, "bind_dn", cfg.bindDn); readString(conf, "bind_password", cfg.bindPassword); readString(s, "bind_dn", cfg.bindDn); readString(s, "bind_password", cfg.bindPassword); std::string secretFile; readString(conf, "bind_password_file", secretFile); if (!secretFile.empty()) cfg.bindPassword = readSecret(secretFile); readString(s, "bind_password_file", secretFile); if (!secretFile.empty()) cfg.bindPassword = readSecret(l + "bind_password_file", secretFile); std::string auth = "simple"; readString(conf, "auth", auth); readString(s, "auth", auth); auth = lower(auth); if (auth == "simple") cfg.auth = Auth::Simple; else if (auth == "kerberos") cfg.auth = Auth::Kerberos; else throw std::runtime_error("auth: expected simple or kerberos, got \"" + auth + "\""); readString(conf, "kerberos_principal", cfg.krbPrincipal); readString(conf, "kerberos_realm", cfg.krbRealm); readString(conf, "keytab", cfg.keytab); cfg.kdcs = readList(conf, "kdcs"); else throw std::runtime_error(l + "auth: expected simple or kerberos, got \"" + auth + "\""); readString(s, "kerberos_principal", cfg.krbPrincipal); readString(s, "kerberos_realm", cfg.krbRealm); readString(s, "keytab", cfg.keytab); cfg.kdcs = readList(s, "kdcs"); if (cfg.auth == Auth::Simple) { if (cfg.bindDn.empty() != cfg.bindPassword.empty()) throw std::runtime_error("bind_dn and bind_password (or bind_password_file) go together"); throw std::runtime_error(l + "bind_dn and bind_password (or bind_password_file) go together"); } else { /* svc@EXAMPLE.COM, or the user part of a bind_dn user@domain */ if (cfg.krbPrincipal.empty()) { size_t at = cfg.bindDn.find('@'); if (at == std::string::npos || cfg.bindDn.find('=') != std::string::npos) throw std::runtime_error("auth: kerberos needs kerberos_principal (or bind_dn as user@domain)"); throw std::runtime_error(l + "auth: kerberos needs kerberos_principal (or bind_dn as user@domain)"); cfg.krbPrincipal = cfg.bindDn.substr(0, at); } size_t at = cfg.krbPrincipal.find('@'); Loading @@ -204,55 +230,99 @@ Config loadConfig(const std::string &path) { if (cfg.krbRealm.empty()) cfg.krbRealm = cfg.krbPrincipal.substr(at + 1); cfg.krbPrincipal.erase(at); } if (cfg.krbPrincipal.empty()) throw std::runtime_error("kerberos_principal: empty"); if (cfg.krbPrincipal.empty()) throw std::runtime_error(l + "kerberos_principal: empty"); if (cfg.krbRealm.empty()) { cfg.krbRealm = cfg.domain; for (char &c : cfg.krbRealm) c = (char)toupper((unsigned char)c); } if (cfg.keytab.empty() && cfg.bindPassword.empty()) throw std::runtime_error("auth: kerberos needs a keytab or bind_password (bind_password_file)"); throw std::runtime_error(l + "auth: kerberos needs a keytab or bind_password (bind_password_file)"); if (cfg.kdcs.empty()) cfg.kdcs = cfg.servers; } /* Kerberos protects the connection itself; a simple bind needs TLS */ if (cfg.tls == ldap::Tls::None && cfg.auth == Auth::Simple && !cfg.insecure) throw std::runtime_error(l + "tls: none sends the bind password in the clear -- use auth: kerberos, or set insecure: true"); std::string mapping = "rid"; readString(conf, "id_mapping", mapping); readString(s, "id_mapping", mapping); mapping = lower(mapping); if (mapping == "rid") cfg.idMapping = IdMapping::Rid; else if (mapping == "rfc2307") cfg.idMapping = IdMapping::Rfc2307; else if (mapping == "guid") cfg.idMapping = IdMapping::Guid; else throw std::runtime_error("id_mapping: expected rid, rfc2307 or guid, got \"" + mapping + "\""); readNumber(conf, "id_base", cfg.idBase, 1000, 0xfffffffeUL); readNumber(conf, "id_range", cfg.idRange, 1, 0xfffffffeUL); if (cfg.idBase + cfg.idRange > 0xfffffffeUL) throw std::runtime_error("id_base + id_range: past the largest id (4294967294)"); readString(conf, "home", cfg.home); readString(conf, "shell", cfg.shell); readBool(conf, "lowercase_names", cfg.lowercase); readBool(conf, "enumerate", cfg.enumerate); readString(conf, "user_filter", cfg.userFilter); readString(conf, "group_filter", cfg.groupFilter); checkFilter("user_filter", cfg.userFilter); checkFilter("group_filter", cfg.groupFilter); else throw std::runtime_error(l + "id_mapping: expected rid, rfc2307 or guid, got \"" + mapping + "\""); idBaseSet = s.own("id_base"); readNumber(s, "id_base", cfg.idBase, 1000, 0xfffffffeUL); readNumber(s, "id_range", cfg.idRange, 1, 0xfffffffeUL); /* Kerberos protects the connection itself; a simple bind needs TLS */ if (cfg.tls == ldap::Tls::None && cfg.auth == Auth::Simple && !cfg.insecure) throw std::runtime_error("tls: none sends the bind password in the clear -- use auth: kerberos, or set insecure: true"); readString(s, "home", cfg.home); readString(s, "shell", cfg.shell); readBool(s, "lowercase_names", cfg.lowercase); cfg.qualified = multi; readBool(s, "qualified_names", cfg.qualified); readBool(s, "enumerate", cfg.enumerate); readString(s, "user_filter", cfg.userFilter); readString(s, "group_filter", cfg.groupFilter); checkFilter(l + "user_filter", cfg.userFilter); checkFilter(l + "group_filter", cfg.groupFilter); readInt(s, "timeout", cfg.timeout, 1, 300); readInt(s, "page_size", cfg.pageSize, 1, 1000); } readString(conf, "socket", cfg.socket); readInt(conf, "cache_ttl", cfg.cacheTtl, 0, 86400); readInt(conf, "negative_ttl", cfg.negativeTtl, 0, 86400); unsigned long offline = (unsigned long)cfg.offlineTtl; readNumber(conf, "offline_ttl", offline, 0, 31536000); cfg.offlineTtl = (long)offline; readInt(conf, "retry_interval", cfg.retryInterval, 1, 3600); readString(conf, "cache_file", cfg.cacheFile); readInt(conf, "timeout", cfg.timeout, 1, 300); readInt(conf, "page_size", cfg.pageSize, 1, 1000); } // namespace std::vector<Config> loadConfigs(const std::string &path) { { std::ifstream probe(path); if (!probe) throw std::runtime_error("cannot read " + path); } std::vector<Config> out; std::vector<bool> idBaseSet; try { confplus::Config conf("yaml:" + path); Scope top{ conf, Root, "" }; Config global; readGlobal(top, global); bool multi = conf.getKey(Root + "/domains/0") != nullptr; for (size_t i = 0;; ++i) { std::string prefix = Root + "/domains/" + std::to_string(i); if (multi && !conf.getKey(prefix)) break; Config c = global; bool set = false; readDomain(multi ? Scope{ conf, prefix, "domains[" + std::to_string(i) + "]." } : top, c, multi, set); out.push_back(c); idBaseSet.push_back(set); if (!multi) break; } } catch (const confplus::ConfException &e) { throw std::runtime_error(path + ": " + e.what()); } return cfg; /* one name and one id per account, across the domains */ std::set<std::string> names; int plain = 0; unsigned long nextBase = 0; for (size_t i = 0; i < out.size(); i++) { Config &c = out[i]; if (!names.insert(c.domain).second) throw std::runtime_error("domain " + c.domain + " twice"); if (!c.qualified && ++plain > 1) throw std::runtime_error("only one domain can go without qualified_names (user@domain)"); if (c.idMapping != IdMapping::Rid) continue; if (!idBaseSet[i] && i > 0 && nextBase) c.idBase = nextBase; /* after the one before */ if (c.idBase + c.idRange > 0xfffffffeUL) throw std::runtime_error(c.domain + ": id_base + id_range past the largest id (4294967294)"); for (size_t k = 0; k < i; k++) { const Config &o = out[k]; if (o.idMapping == IdMapping::Rid && c.idBase < o.idBase + o.idRange && o.idBase < c.idBase + c.idRange) throw std::runtime_error(c.domain + ": its id range overlaps " + o.domain + "'s"); } nextBase = c.idBase + c.idRange; } return out; } Config loadConfig(const std::string &path) { return loadConfigs(path).front(); } } // namespace adconnect src/config.h +9 −3 Changes for src/config.h: 9 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -26,7 +26,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ /* adconnectd's configuration, /etc/adconnect/adconnect.yaml (libconfplus): * see dist/adconnect.yaml for every key. */ * see dist/adconnect.yaml for every key. One Config per domain: several * under "domains:", each taking what it doesn't set from the top level * (but not what names the domain, its account or its ids); the cache * settings are the same in all. */ #pragma once Loading Loading @@ -69,9 +72,10 @@ struct Config { unsigned long idBase = 200000; unsigned long idRange = 2000000; std::string home = "/home/%u"; /* %u user, %d domain */ std::string home = "/home/%d/%n"; /* %d domain, %n the account name, %u the name (with @domain if qualified) */ std::string shell = "/bin/sh"; bool lowercase = true; bool qualified = false; /* names user@domain (default with several domains) */ bool enumerate = false; std::string userFilter; /* ANDed to every user search */ std::string groupFilter; /* ANDed to every group search */ Loading @@ -90,7 +94,9 @@ struct Config { int ldapPort() const { return port ? port : tls == ldap::Tls::Ldaps ? 636 : 389; } }; /* throws std::runtime_error naming the bad key */ /* the domains, in order; throws std::runtime_error naming the bad key */ std::vector<Config> loadConfigs(const std::string &path); /* the first of them */ Config loadConfig(const std::string &path); } // namespace adconnect src/daemon.cpp +118 −49 File changed.Preview size limit exceeded, changes collapsed. Show changes src/directory.cpp +17 −7 Changes for src/directory.cpp: 17 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -228,15 +228,24 @@ std::string Directory::groupFilter(const std::string &extra) const { _cfg.groupFilter + extra + ")"; } /* lowercase_names: "Jan" isn't "jan" -- one name per account */ /* lowercase_names: "Jan" isn't "jan" -- one name per account; * qualified_names: only user@domain, with our domain */ bool Directory::validName(const std::string &n) const { return clean(n) && (!_cfg.lowercase || n == lower(n)); if (!clean(n) || (_cfg.lowercase && n != lower(n))) return false; size_t at = n.rfind('@'); if (!_cfg.qualified) return at == std::string::npos; return at != std::string::npos && at > 0 && lower(n.substr(at + 1)) == _cfg.domain; } std::string Directory::account(const std::string &n) const { return _cfg.qualified ? n.substr(0, n.rfind('@')) : n; } std::string Directory::name(const ldap::Entry &e) const { const std::string *n = e.first("samaccountname"); if (!n) return std::string(); return _cfg.lowercase ? lower(*n) : *n; if (!n || n->find('@') != std::string::npos) return std::string(); std::string s = _cfg.lowercase ? lower(*n) : *n; return _cfg.qualified ? s + "@" + _cfg.domain : s; } const std::string &Directory::domainSid() { Loading Loading @@ -326,6 +335,7 @@ std::string Directory::expandHome(const std::string &user) const { } char c = _cfg.home[++i]; if (c == 'u') out += user; else if (c == 'n') out += account(user); else if (c == 'd') out += _cfg.domain; else if (c == '%') out += '%'; else { Loading Loading @@ -369,7 +379,7 @@ bool Directory::toGroup(const ldap::Entry &e, Group &g) { std::string Directory::userByName(const std::string &n) { if (!validName(n)) return End; for (const ldap::Entry &e : find(userFilter("(sAMAccountName=" + ldap::escape(n) + ")"), UserAttrs)) { for (const ldap::Entry &e : find(userFilter("(sAMAccountName=" + ldap::escape(account(n)) + ")"), UserAttrs)) { User u; if (toUser(e, u)) return formatUser(u) + End; } Loading @@ -388,7 +398,7 @@ std::string Directory::userById(const std::string &id) { std::string Directory::groupByName(const std::string &n) { if (!validName(n)) return End; for (const ldap::Entry &e : find(groupFilter("(sAMAccountName=" + ldap::escape(n) + ")"), GroupAttrs)) { for (const ldap::Entry &e : find(groupFilter("(sAMAccountName=" + ldap::escape(account(n)) + ")"), GroupAttrs)) { Group g; if (!toGroup(e, g)) continue; g.members = members(e.dn); Loading Loading @@ -442,7 +452,7 @@ std::string Directory::allGroups() { std::string Directory::groupsOf(const std::string &n) { if (!validName(n)) return End; std::vector<ldap::Entry> users = find(userFilter("(sAMAccountName=" + ldap::escape(n) + ")"), UserAttrs); std::vector<ldap::Entry> users = find(userFilter("(sAMAccountName=" + ldap::escape(account(n)) + ")"), UserAttrs); if (users.empty()) return End; const ldap::Entry &user = users.front(); Loading Loading
dist/adconnect.yaml +4 −3 Changes for dist/adconnect.yaml: 4 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -49,9 +49,10 @@ adconnect: id_base: 200000 id_range: 2000000 # %u the user name, %d the domain; unixHomeDirectory and loginShell win # where an account has them home: /home/%u # %d the domain, %n the account name, %u the name as shown (with # @domain when qualified); unixHomeDirectory and loginShell win where an # account has them home: /home/%d/%n shell: /bin/sh lowercase_names: true Loading
src/config.cpp +151 −81 Changes for src/config.cpp: 151 added lines, 81 removed lines. Original line number Diff line number Diff line Loading @@ -29,7 +29,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include <cctype> #include <fstream> #include <sstream> #include <set> #include <stdexcept> #include <confplus/conf.h> Loading @@ -41,40 +41,61 @@ namespace { const std::string Root = "/adconnect"; /* what a domain has of its own; every other key a domain doesn't set * comes from the top level */ const std::set<std::string> OwnKeys = { "domain", "base", "servers", "bind_dn", "bind_password", "bind_password_file", "kerberos_principal", "kerberos_realm", "keytab", "kdcs", "id_base", "id_range", "user_filter", "group_filter", "qualified_names", }; std::string lower(std::string s) { for (char &c : s) c = (char)tolower((unsigned char)c); return s; } bool readValue(confplus::Config &conf, const std::string &key, std::string &out) { auto *k = conf.getKey(Root + "/" + key); /* where keys are looked up: a domain's entry (domains: [...]), then for * shared keys the top level; or the top level alone */ struct Scope { confplus::Config &conf; std::string prefix; std::string label; /* "domains[1]." in messages */ confplus::Config::ConfigData *key(const std::string &name) const { if (auto *k = conf.getKey(prefix + "/" + name)) return k; if (prefix != Root && !OwnKeys.count(name)) return conf.getKey(Root + "/" + name); return nullptr; } bool own(const std::string &name) const { return conf.getKey(prefix + "/" + name) != nullptr; } }; bool readValue(const Scope &s, const std::string &key, std::string &out) { auto *k = s.key(key); if (!k) return false; try { out = conf.getValue(k, 0); out = s.conf.getValue(k, 0); return true; } catch (const confplus::ConfException &) { return false; } } void readString(confplus::Config &conf, const std::string &key, std::string &out) { void readString(const Scope &s, const std::string &key, std::string &out) { std::string v; if (readValue(conf, key, v)) out = v; if (readValue(s, key, v)) out = v; } void readBool(confplus::Config &conf, const std::string &key, bool &out) { void readBool(const Scope &s, const std::string &key, bool &out) { std::string v; if (!readValue(conf, key, v)) return; if (!readValue(s, key, v)) return; v = lower(v); if (v == "true" || v == "yes" || v == "1" || v == "on") out = true; else if (v == "false" || v == "no" || v == "0" || v == "off") out = false; else throw std::runtime_error(key + ": not a boolean: " + v); else throw std::runtime_error(s.label + key + ": not a boolean: " + v); } void readNumber(confplus::Config &conf, const std::string &key, unsigned long &out, unsigned long min, unsigned long max) { void readNumber(const Scope &s, const std::string &key, unsigned long &out, unsigned long min, unsigned long max) { std::string v; if (!readValue(conf, key, v)) return; if (!readValue(s, key, v)) return; size_t used = 0; unsigned long n = 0; try { Loading @@ -83,24 +104,24 @@ void readNumber(confplus::Config &conf, const std::string &key, unsigned long &o used = 0; } if (v.empty() || used != v.size() || n < min || n > max) throw std::runtime_error(key + ": expected a number from " + std::to_string(min) + " to " + throw std::runtime_error(s.label + key + ": expected a number from " + std::to_string(min) + " to " + std::to_string(max) + ", got \"" + v + "\""); out = n; } void readInt(confplus::Config &conf, const std::string &key, int &out, int min, int max) { void readInt(const Scope &s, const std::string &key, int &out, int min, int max) { unsigned long n = (unsigned long)out; readNumber(conf, key, n, (unsigned long)min, (unsigned long)max); readNumber(s, key, n, (unsigned long)min, (unsigned long)max); out = (int)n; } std::vector<std::string> readList(confplus::Config &conf, const std::string &key) { std::vector<std::string> readList(const Scope &s, const std::string &key) { std::vector<std::string> out; auto *k = conf.getKey(Root + "/" + key); auto *k = s.key(key); if (!k) return out; for (size_t i = 0; i < k->getElements(); ++i) { try { std::string v = conf.getValue(k, i); std::string v = s.conf.getValue(k, i); if (!v.empty()) out.push_back(v); } catch (const confplus::ConfException &) { break; Loading Loading @@ -133,70 +154,75 @@ void checkFilter(const std::string &key, const std::string &filter) { } } std::string readSecret(const std::string &path) { std::string readSecret(const std::string &key, const std::string &path) { std::ifstream in(path); if (!in) throw std::runtime_error("bind_password_file: cannot read " + path); if (!in) throw std::runtime_error(key + ": cannot read " + path); std::string s; std::getline(in, s); while (!s.empty() && (s.back() == '\r' || s.back() == '\n')) s.pop_back(); return s; } } // namespace Config loadConfig(const std::string &path) { Config cfg; { std::ifstream probe(path); if (!probe) throw std::runtime_error("cannot read " + path); /* the settings every domain shares */ void readGlobal(const Scope &s, Config &cfg) { readString(s, "socket", cfg.socket); readInt(s, "cache_ttl", cfg.cacheTtl, 0, 86400); readInt(s, "negative_ttl", cfg.negativeTtl, 0, 86400); unsigned long offline = (unsigned long)cfg.offlineTtl; readNumber(s, "offline_ttl", offline, 0, 31536000); cfg.offlineTtl = (long)offline; readInt(s, "retry_interval", cfg.retryInterval, 1, 3600); readString(s, "cache_file", cfg.cacheFile); } try { confplus::Config conf("yaml:" + path); readString(conf, "domain", cfg.domain); /* one domain; idBaseSet: whether it set id_base itself */ void readDomain(const Scope &s, Config &cfg, bool multi, bool &idBaseSet) { const std::string &l = s.label; readString(s, "domain", cfg.domain); cfg.domain = lower(cfg.domain); if (cfg.domain.empty()) throw std::runtime_error("domain: missing"); readString(conf, "base", cfg.base); if (cfg.domain.empty()) throw std::runtime_error(l + "domain: missing"); readString(s, "base", cfg.base); if (cfg.base.empty()) cfg.base = baseFromDomain(cfg.domain); cfg.servers = readList(conf, "servers"); cfg.servers = readList(s, "servers"); if (cfg.servers.empty()) cfg.servers.push_back(cfg.domain); readInt(conf, "port", cfg.port, 1, 65535); readInt(s, "port", cfg.port, 1, 65535); std::string tls = "ldaps"; readString(conf, "tls", tls); readString(s, "tls", tls); tls = lower(tls); if (tls == "ldaps") cfg.tls = ldap::Tls::Ldaps; else if (tls == "starttls") cfg.tls = ldap::Tls::StartTls; else if (tls == "none") cfg.tls = ldap::Tls::None; else throw std::runtime_error("tls: expected ldaps, starttls or none, got \"" + tls + "\""); readBool(conf, "insecure", cfg.insecure); readString(conf, "ca_file", cfg.caFile); else throw std::runtime_error(l + "tls: expected ldaps, starttls or none, got \"" + tls + "\""); readBool(s, "insecure", cfg.insecure); readString(s, "ca_file", cfg.caFile); readString(conf, "bind_dn", cfg.bindDn); readString(conf, "bind_password", cfg.bindPassword); readString(s, "bind_dn", cfg.bindDn); readString(s, "bind_password", cfg.bindPassword); std::string secretFile; readString(conf, "bind_password_file", secretFile); if (!secretFile.empty()) cfg.bindPassword = readSecret(secretFile); readString(s, "bind_password_file", secretFile); if (!secretFile.empty()) cfg.bindPassword = readSecret(l + "bind_password_file", secretFile); std::string auth = "simple"; readString(conf, "auth", auth); readString(s, "auth", auth); auth = lower(auth); if (auth == "simple") cfg.auth = Auth::Simple; else if (auth == "kerberos") cfg.auth = Auth::Kerberos; else throw std::runtime_error("auth: expected simple or kerberos, got \"" + auth + "\""); readString(conf, "kerberos_principal", cfg.krbPrincipal); readString(conf, "kerberos_realm", cfg.krbRealm); readString(conf, "keytab", cfg.keytab); cfg.kdcs = readList(conf, "kdcs"); else throw std::runtime_error(l + "auth: expected simple or kerberos, got \"" + auth + "\""); readString(s, "kerberos_principal", cfg.krbPrincipal); readString(s, "kerberos_realm", cfg.krbRealm); readString(s, "keytab", cfg.keytab); cfg.kdcs = readList(s, "kdcs"); if (cfg.auth == Auth::Simple) { if (cfg.bindDn.empty() != cfg.bindPassword.empty()) throw std::runtime_error("bind_dn and bind_password (or bind_password_file) go together"); throw std::runtime_error(l + "bind_dn and bind_password (or bind_password_file) go together"); } else { /* svc@EXAMPLE.COM, or the user part of a bind_dn user@domain */ if (cfg.krbPrincipal.empty()) { size_t at = cfg.bindDn.find('@'); if (at == std::string::npos || cfg.bindDn.find('=') != std::string::npos) throw std::runtime_error("auth: kerberos needs kerberos_principal (or bind_dn as user@domain)"); throw std::runtime_error(l + "auth: kerberos needs kerberos_principal (or bind_dn as user@domain)"); cfg.krbPrincipal = cfg.bindDn.substr(0, at); } size_t at = cfg.krbPrincipal.find('@'); Loading @@ -204,55 +230,99 @@ Config loadConfig(const std::string &path) { if (cfg.krbRealm.empty()) cfg.krbRealm = cfg.krbPrincipal.substr(at + 1); cfg.krbPrincipal.erase(at); } if (cfg.krbPrincipal.empty()) throw std::runtime_error("kerberos_principal: empty"); if (cfg.krbPrincipal.empty()) throw std::runtime_error(l + "kerberos_principal: empty"); if (cfg.krbRealm.empty()) { cfg.krbRealm = cfg.domain; for (char &c : cfg.krbRealm) c = (char)toupper((unsigned char)c); } if (cfg.keytab.empty() && cfg.bindPassword.empty()) throw std::runtime_error("auth: kerberos needs a keytab or bind_password (bind_password_file)"); throw std::runtime_error(l + "auth: kerberos needs a keytab or bind_password (bind_password_file)"); if (cfg.kdcs.empty()) cfg.kdcs = cfg.servers; } /* Kerberos protects the connection itself; a simple bind needs TLS */ if (cfg.tls == ldap::Tls::None && cfg.auth == Auth::Simple && !cfg.insecure) throw std::runtime_error(l + "tls: none sends the bind password in the clear -- use auth: kerberos, or set insecure: true"); std::string mapping = "rid"; readString(conf, "id_mapping", mapping); readString(s, "id_mapping", mapping); mapping = lower(mapping); if (mapping == "rid") cfg.idMapping = IdMapping::Rid; else if (mapping == "rfc2307") cfg.idMapping = IdMapping::Rfc2307; else if (mapping == "guid") cfg.idMapping = IdMapping::Guid; else throw std::runtime_error("id_mapping: expected rid, rfc2307 or guid, got \"" + mapping + "\""); readNumber(conf, "id_base", cfg.idBase, 1000, 0xfffffffeUL); readNumber(conf, "id_range", cfg.idRange, 1, 0xfffffffeUL); if (cfg.idBase + cfg.idRange > 0xfffffffeUL) throw std::runtime_error("id_base + id_range: past the largest id (4294967294)"); readString(conf, "home", cfg.home); readString(conf, "shell", cfg.shell); readBool(conf, "lowercase_names", cfg.lowercase); readBool(conf, "enumerate", cfg.enumerate); readString(conf, "user_filter", cfg.userFilter); readString(conf, "group_filter", cfg.groupFilter); checkFilter("user_filter", cfg.userFilter); checkFilter("group_filter", cfg.groupFilter); else throw std::runtime_error(l + "id_mapping: expected rid, rfc2307 or guid, got \"" + mapping + "\""); idBaseSet = s.own("id_base"); readNumber(s, "id_base", cfg.idBase, 1000, 0xfffffffeUL); readNumber(s, "id_range", cfg.idRange, 1, 0xfffffffeUL); /* Kerberos protects the connection itself; a simple bind needs TLS */ if (cfg.tls == ldap::Tls::None && cfg.auth == Auth::Simple && !cfg.insecure) throw std::runtime_error("tls: none sends the bind password in the clear -- use auth: kerberos, or set insecure: true"); readString(s, "home", cfg.home); readString(s, "shell", cfg.shell); readBool(s, "lowercase_names", cfg.lowercase); cfg.qualified = multi; readBool(s, "qualified_names", cfg.qualified); readBool(s, "enumerate", cfg.enumerate); readString(s, "user_filter", cfg.userFilter); readString(s, "group_filter", cfg.groupFilter); checkFilter(l + "user_filter", cfg.userFilter); checkFilter(l + "group_filter", cfg.groupFilter); readInt(s, "timeout", cfg.timeout, 1, 300); readInt(s, "page_size", cfg.pageSize, 1, 1000); } readString(conf, "socket", cfg.socket); readInt(conf, "cache_ttl", cfg.cacheTtl, 0, 86400); readInt(conf, "negative_ttl", cfg.negativeTtl, 0, 86400); unsigned long offline = (unsigned long)cfg.offlineTtl; readNumber(conf, "offline_ttl", offline, 0, 31536000); cfg.offlineTtl = (long)offline; readInt(conf, "retry_interval", cfg.retryInterval, 1, 3600); readString(conf, "cache_file", cfg.cacheFile); readInt(conf, "timeout", cfg.timeout, 1, 300); readInt(conf, "page_size", cfg.pageSize, 1, 1000); } // namespace std::vector<Config> loadConfigs(const std::string &path) { { std::ifstream probe(path); if (!probe) throw std::runtime_error("cannot read " + path); } std::vector<Config> out; std::vector<bool> idBaseSet; try { confplus::Config conf("yaml:" + path); Scope top{ conf, Root, "" }; Config global; readGlobal(top, global); bool multi = conf.getKey(Root + "/domains/0") != nullptr; for (size_t i = 0;; ++i) { std::string prefix = Root + "/domains/" + std::to_string(i); if (multi && !conf.getKey(prefix)) break; Config c = global; bool set = false; readDomain(multi ? Scope{ conf, prefix, "domains[" + std::to_string(i) + "]." } : top, c, multi, set); out.push_back(c); idBaseSet.push_back(set); if (!multi) break; } } catch (const confplus::ConfException &e) { throw std::runtime_error(path + ": " + e.what()); } return cfg; /* one name and one id per account, across the domains */ std::set<std::string> names; int plain = 0; unsigned long nextBase = 0; for (size_t i = 0; i < out.size(); i++) { Config &c = out[i]; if (!names.insert(c.domain).second) throw std::runtime_error("domain " + c.domain + " twice"); if (!c.qualified && ++plain > 1) throw std::runtime_error("only one domain can go without qualified_names (user@domain)"); if (c.idMapping != IdMapping::Rid) continue; if (!idBaseSet[i] && i > 0 && nextBase) c.idBase = nextBase; /* after the one before */ if (c.idBase + c.idRange > 0xfffffffeUL) throw std::runtime_error(c.domain + ": id_base + id_range past the largest id (4294967294)"); for (size_t k = 0; k < i; k++) { const Config &o = out[k]; if (o.idMapping == IdMapping::Rid && c.idBase < o.idBase + o.idRange && o.idBase < c.idBase + c.idRange) throw std::runtime_error(c.domain + ": its id range overlaps " + o.domain + "'s"); } nextBase = c.idBase + c.idRange; } return out; } Config loadConfig(const std::string &path) { return loadConfigs(path).front(); } } // namespace adconnect
src/config.h +9 −3 Changes for src/config.h: 9 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -26,7 +26,10 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *******************************************************************************/ /* adconnectd's configuration, /etc/adconnect/adconnect.yaml (libconfplus): * see dist/adconnect.yaml for every key. */ * see dist/adconnect.yaml for every key. One Config per domain: several * under "domains:", each taking what it doesn't set from the top level * (but not what names the domain, its account or its ids); the cache * settings are the same in all. */ #pragma once Loading Loading @@ -69,9 +72,10 @@ struct Config { unsigned long idBase = 200000; unsigned long idRange = 2000000; std::string home = "/home/%u"; /* %u user, %d domain */ std::string home = "/home/%d/%n"; /* %d domain, %n the account name, %u the name (with @domain if qualified) */ std::string shell = "/bin/sh"; bool lowercase = true; bool qualified = false; /* names user@domain (default with several domains) */ bool enumerate = false; std::string userFilter; /* ANDed to every user search */ std::string groupFilter; /* ANDed to every group search */ Loading @@ -90,7 +94,9 @@ struct Config { int ldapPort() const { return port ? port : tls == ldap::Tls::Ldaps ? 636 : 389; } }; /* throws std::runtime_error naming the bad key */ /* the domains, in order; throws std::runtime_error naming the bad key */ std::vector<Config> loadConfigs(const std::string &path); /* the first of them */ Config loadConfig(const std::string &path); } // namespace adconnect
src/directory.cpp +17 −7 Changes for src/directory.cpp: 17 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -228,15 +228,24 @@ std::string Directory::groupFilter(const std::string &extra) const { _cfg.groupFilter + extra + ")"; } /* lowercase_names: "Jan" isn't "jan" -- one name per account */ /* lowercase_names: "Jan" isn't "jan" -- one name per account; * qualified_names: only user@domain, with our domain */ bool Directory::validName(const std::string &n) const { return clean(n) && (!_cfg.lowercase || n == lower(n)); if (!clean(n) || (_cfg.lowercase && n != lower(n))) return false; size_t at = n.rfind('@'); if (!_cfg.qualified) return at == std::string::npos; return at != std::string::npos && at > 0 && lower(n.substr(at + 1)) == _cfg.domain; } std::string Directory::account(const std::string &n) const { return _cfg.qualified ? n.substr(0, n.rfind('@')) : n; } std::string Directory::name(const ldap::Entry &e) const { const std::string *n = e.first("samaccountname"); if (!n) return std::string(); return _cfg.lowercase ? lower(*n) : *n; if (!n || n->find('@') != std::string::npos) return std::string(); std::string s = _cfg.lowercase ? lower(*n) : *n; return _cfg.qualified ? s + "@" + _cfg.domain : s; } const std::string &Directory::domainSid() { Loading Loading @@ -326,6 +335,7 @@ std::string Directory::expandHome(const std::string &user) const { } char c = _cfg.home[++i]; if (c == 'u') out += user; else if (c == 'n') out += account(user); else if (c == 'd') out += _cfg.domain; else if (c == '%') out += '%'; else { Loading Loading @@ -369,7 +379,7 @@ bool Directory::toGroup(const ldap::Entry &e, Group &g) { std::string Directory::userByName(const std::string &n) { if (!validName(n)) return End; for (const ldap::Entry &e : find(userFilter("(sAMAccountName=" + ldap::escape(n) + ")"), UserAttrs)) { for (const ldap::Entry &e : find(userFilter("(sAMAccountName=" + ldap::escape(account(n)) + ")"), UserAttrs)) { User u; if (toUser(e, u)) return formatUser(u) + End; } Loading @@ -388,7 +398,7 @@ std::string Directory::userById(const std::string &id) { std::string Directory::groupByName(const std::string &n) { if (!validName(n)) return End; for (const ldap::Entry &e : find(groupFilter("(sAMAccountName=" + ldap::escape(n) + ")"), GroupAttrs)) { for (const ldap::Entry &e : find(groupFilter("(sAMAccountName=" + ldap::escape(account(n)) + ")"), GroupAttrs)) { Group g; if (!toGroup(e, g)) continue; g.members = members(e.dn); Loading Loading @@ -442,7 +452,7 @@ std::string Directory::allGroups() { std::string Directory::groupsOf(const std::string &n) { if (!validName(n)) return End; std::vector<ldap::Entry> users = find(userFilter("(sAMAccountName=" + ldap::escape(n) + ")"), UserAttrs); std::vector<ldap::Entry> users = find(userFilter("(sAMAccountName=" + ldap::escape(account(n)) + ")"), UserAttrs); if (users.empty()) return End; const ldap::Entry &user = users.front(); Loading