Commit a7e16360 authored by jan.koester's avatar jan.koester
Browse files

test

parent d621d696
Loading
Loading
Loading
Loading
+4 −3
Changes for dist/adconnect.yaml: 4 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -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

+151 −81
Changes for src/config.cpp: 151 added lines, 81 removed lines.
Original line number Diff line number Diff line
@@ -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>
@@ -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 {
@@ -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;
@@ -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('@');
@@ -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
+9 −3
Changes for src/config.h: 9 added lines, 3 removed lines.
Original line number Diff line number Diff line
@@ -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

@@ -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 */
@@ -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
+118 −49

File changed.

Preview size limit exceeded, changes collapsed.

+17 −7
Changes for src/directory.cpp: 17 added lines, 7 removed lines.
Original line number Diff line number Diff line
@@ -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() {
@@ -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 {
@@ -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;
    }
@@ -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);
@@ -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