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

test

parent 8a9e69c6
Loading
Loading
Loading
Loading
+20 −2
Changes for CMakeLists.txt: 20 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -6,8 +6,10 @@ project(adconnect VERSION 0.1.0 LANGUAGES CXX)
# systempp's <userdb.h>).
#
#   adconnectd    the daemon (root): reads /etc/adconnect/adconnect.yaml
#                 (libconfplus), binds to a domain controller over LDAPS or
#                 StartTLS (libnetplus), answers on /run/adconnect.sock and
#                 (libconfplus), binds to a domain controller -- simple bind
#                 over LDAPS or StartTLS (libnetplus), or Kerberos: SASL
#                 GSSAPI with its own Kerberos client (src/krb5*.cpp,
#                 gssapi.cpp, sasl.cpp) -- answers on /run/adconnect.sock and
#                 caches what it was told -- on disk too, so that users and
#                 groups stay known while no domain controller is reachable
#   adconnect.so  the userdb module /etc/userdb.yaml loads into every
@@ -50,6 +52,10 @@ add_library(adconnect_core STATIC
    src/config.cpp
    src/directory.cpp
    src/cache.cpp
    src/krb5crypto.cpp
    src/krb5.cpp
    src/gssapi.cpp
    src/sasl.cpp
)
target_link_libraries(adconnect_core PUBLIC adconnect_proto confplus::confplus netplus::netplus Threads::Threads)

@@ -78,3 +84,15 @@ if(BUILD_TESTING)
    add_dependencies(adconnect_test adconnectd adconnect)
    add_test(NAME adconnect_test COMMAND adconnect_test)
endif()

# Interoperability with MIT krb5 (KDC and GSS-API acceptor): needs MIT's
# gssapi headers and library, so not on FenrirOS -- test/mit_interop.sh
# sets up a throw-away KDC and runs it.
option(ADCONNECT_MIT_INTEROP "build test/mit_interop against MIT krb5" OFF)
if(ADCONNECT_MIT_INTEROP)
    find_library(GSSAPI_KRB5 gssapi_krb5 REQUIRED)
    find_path(GSSAPI_INCLUDE gssapi/gssapi.h REQUIRED)
    add_executable(mit_interop test/mit_interop.cpp)
    target_include_directories(mit_interop PRIVATE ${GSSAPI_INCLUDE})
    target_link_libraries(mit_interop PRIVATE adconnect_core ${GSSAPI_KRB5})
endif()
+16 −2
Changes for dist/adconnect.yaml: 16 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -14,18 +14,32 @@ adconnect:
  # domain controllers, tried in turn (default: the domain name itself)
  servers: [dc1.example.com, dc2.example.com]

  # ldaps (port 636, default), starttls (389) or none (389, the bind
  # password in the clear: needs insecure: true)
  # ldaps (port 636, default), starttls (389) or none (389): with
  # auth: kerberos Kerberos seals the connection; with a simple bind the
  # password would go in the clear -- that needs insecure: true
  tls: ldaps
  # port: 636
  # the CA that issued the DCs' certificates, PEM (default: the system bundle)
  # ca_file: /etc/adconnect/ca.pem

  # simple: bind_dn and its password, over LDAPS/StartTLS. kerberos: a
  # Kerberos ticket for the account, SASL GSSAPI -- the connection is
  # sealed by Kerberos, no TLS and no DC certificate needed (tls: none is
  # fine then); the clock must be within 5 minutes of the DCs' (NTP).
  # servers must be the DCs' DNS names then: tickets are for ldap/<name>.
  auth: simple
  # a service account allowed to read users and groups; as user@domain
  # or a DN. The password from a file of its own is better than here.
  bind_dn: svc-fenriros@example.com
  bind_password_file: /etc/adconnect/bind.secret
  # bind_password: secret
  # kerberos: the account (default: the user part of bind_dn), its realm
  # (default: the domain in upper case), a keytab instead of the password,
  # the KDCs (default: the servers)
  # kerberos_principal: svc-fenriros
  # kerberos_realm: EXAMPLE.COM
  # keytab: /etc/adconnect/svc.keytab
  # kdcs: [dc1.example.com, dc2.example.com]

  # ids: rid (id_base + the RID of the account's SID), rfc2307 (uidNumber
  # and gidNumber; accounts without them are left out) or guid (the
+38 −2
Changes for src/config.cpp: 38 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -170,8 +170,6 @@ Config loadConfig(const std::string &path) {
        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);
        if (cfg.tls == ldap::Tls::None && !cfg.insecure)
            throw std::runtime_error("tls: none sends the bind password in the clear -- set insecure: true to allow it");
        readString(conf, "ca_file", cfg.caFile);

        readString(conf, "bind_dn", cfg.bindDn);
@@ -179,8 +177,42 @@ Config loadConfig(const std::string &path) {
        std::string secretFile;
        readString(conf, "bind_password_file", secretFile);
        if (!secretFile.empty()) cfg.bindPassword = readSecret(secretFile);
        std::string auth = "simple";
        readString(conf, "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");

        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");
        } 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)");
                cfg.krbPrincipal = cfg.bindDn.substr(0, at);
            }
            size_t at = cfg.krbPrincipal.find('@');
            if (at != std::string::npos) {
                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.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)");
            if (cfg.kdcs.empty()) cfg.kdcs = cfg.servers;
        }

        std::string mapping = "rid";
        readString(conf, "id_mapping", mapping);
@@ -203,6 +235,10 @@ Config loadConfig(const std::string &path) {
        checkFilter("user_filter", cfg.userFilter);
        checkFilter("group_filter", cfg.groupFilter);

        /* 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(conf, "socket", cfg.socket);
        readInt(conf, "cache_ttl", cfg.cacheTtl, 0, 86400);
        readInt(conf, "negative_ttl", cfg.negativeTtl, 0, 86400);
+11 −0
Changes for src/config.h: 11 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -37,6 +37,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

namespace adconnect {

enum class Auth {
    Simple,     /* bind_dn and its password (LDAPS or StartTLS) */
    Kerberos,   /* SASL GSSAPI with a ticket for the service account */
};

enum class IdMapping {
    Rid,        /* id_base + the RID of the objectSid (classic ids) */
    Rfc2307,    /* the uidNumber / gidNumber attributes */
@@ -54,6 +59,12 @@ struct Config {
    std::string bindDn;
    std::string bindPassword;

    Auth auth = Auth::Simple;
    std::string krbPrincipal;           /* svc-fenriros (default: bind_dn's user@ part) */
    std::string krbRealm;               /* default: the domain in upper case */
    std::string keytab;                 /* instead of the password */
    std::vector<std::string> kdcs;      /* default: the servers */

    IdMapping idMapping = IdMapping::Rid;
    unsigned long idBase = 200000;
    unsigned long idRange = 2000000;
+30 −1
Changes for src/directory.cpp: 30 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -32,6 +32,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <set>
#include <stdexcept>

#include "sasl.h"

namespace adconnect {

namespace {
@@ -74,6 +76,33 @@ bool sidValid(const std::string &b) {

/* ---- LdapSource ---- */

LdapSource::LdapSource(const Config &cfg) : _cfg(cfg), _conn(cfg.timeout * 1000) {
    if (cfg.auth != Auth::Kerberos) return;
    _krb = std::make_unique<krb5::Client>(cfg.kdcs, cfg.timeout * 1000);
    krb5::Principal me{ krb5::NtPrincipal, { cfg.krbPrincipal }, cfg.krbRealm };
    if (!cfg.keytab.empty()) _krb->setKeytab(me, cfg.keytab);
    else _krb->setPassword(me, cfg.bindPassword);
}

/* a simple bind, or GSSAPI with a ticket for ldap/<the server's name> */
void LdapSource::bind(const std::string &host) {
    if (_krb) {
        std::string h = host;
        for (char &c : h) c = (char)tolower((unsigned char)c);
        krb5::Credentials ticket;
        try {
            ticket = _krb->serviceTicket(krb5::Principal{ krb5::SrvInst, { "ldap", h }, _cfg.krbRealm });
        } catch (const krb5::Error &e) {
            _conn.close();
            /* a KDC that answered with an error is no transport failure */
            throw ldap::Error(e.code() == -1 ? -1 : 49, e.what());
        }
        gssapiBind(_conn, ticket);
    } else if (!_cfg.bindDn.empty()) {
        _conn.bind(_cfg.bindDn, _cfg.bindPassword);
    }
}

void LdapSource::ensure() {
    if (_conn.connected()) return;
    netplus::TlsTrustPolicy trust;
@@ -86,7 +115,7 @@ void LdapSource::ensure() {
        try {
            trust.expectedHostname = host;
            _conn.connect(host, _cfg.ldapPort(), _cfg.tls, trust);
            if (!_cfg.bindDn.empty()) _conn.bind(_cfg.bindDn, _cfg.bindPassword);
            bind(host);
            _server = n;
            return;
        } catch (const ldap::Error &e) {
Loading