Commit 7a4df7df authored by jan.koester's avatar jan.koester
Browse files

test

parent 27fb189d
Loading
Loading
Loading
Loading
+134 −9
Changes for modules/users/sources.cpp: 134 added lines, 9 removed lines.
Original line number Diff line number Diff line
@@ -38,6 +38,8 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <chrono>
#include <cstring>
#include <fstream>
#include <map>
#include <sstream>
#include <utility>
#include <vector>

@@ -536,26 +538,31 @@ struct Result {
    std::string out;
};

// argv with a clean environment, stdout and stderr together; killed after timeoutMs
Result runCommand(const std::vector<std::string> &argv, int timeoutMs) {
// argv with a clean environment, stdout and stderr together, input on
// stdin; killed after timeoutMs
Result runCommand(const std::vector<std::string> &argv, int timeoutMs, const std::string &input = "") {
    Result r;
    int p[2];
    int p[2], in[2];
    if (pipe2(p, O_CLOEXEC) != 0) {
        r.out = strerror(errno);
        return r;
    }
    pid_t pid = fork();
    if (pid < 0) {
    if (pipe2(in, O_CLOEXEC) != 0) {
        r.out = strerror(errno);
        close(p[0]);
        close(p[1]);
        return r;
    }
    pid_t pid = fork();
    if (pid < 0) {
        r.out = strerror(errno);
        for (int fd : {p[0], p[1], in[0], in[1]}) close(fd);
        return r;
    }
    if (pid == 0) {
        dup2(p[1], 1);
        dup2(p[1], 2);
        int null = open("/dev/null", O_RDONLY);
        if (null >= 0) dup2(null, 0);
        dup2(in[0], 0);
        std::vector<char *> a;
        for (auto &s : argv) a.push_back(const_cast<char *>(s.c_str()));
        a.push_back(nullptr);
@@ -565,6 +572,10 @@ Result runCommand(const std::vector<std::string> &argv, int timeoutMs) {
        _exit(127);
    }
    close(p[1]);
    close(in[0]);
    // small enough for the pipe buffer: no deadlock with the child's output
    if (!input.empty()) (void)!write(in[1], input.data(), input.size());
    close(in[1]);
    auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(timeoutMs);
    bool timedOut = false;
    for (;;) {
@@ -667,8 +678,12 @@ json_object *buildAdconnect(json_object *global, json_object *domains, bool test
        if (!validText(password)) return error(tr("invalid password", "ungültiges Passwort"));
        if (!seen.insert(name).second) return error(tr("domain %1 twice", "Domäne %1 doppelt", {name}));
        const AdDomain *was = old.find(name);
        std::string file, inlinePassword;
        if (!password.empty()) {
        std::string file, inlinePassword, joined = webctl::str(d, "passwordFile");
        if (!joined.empty() && (joined.compare(0, strlen(AdDir) + 1, std::string(AdDir) + "/") != 0 || !validText(joined)))
            return error(tr("invalid password file %1", "ungültige Passwortdatei %1", {joined}));
        if (!joined.empty()) {
            file = joined;  // the computer account's, from the join
        } else if (!password.empty()) {
            file = test ? std::string(AdDir) + "/.webctl-test-" + std::to_string(i) + ".secret" : secretFileFor(name, was);
            secrets.emplace_back(file, password);
        } else {
@@ -775,6 +790,114 @@ json_object *adconnectSave(json_object *args) {
    return webctl::ok();
}

// the domains as adconnectSave takes them, from the file (passwords kept)
json_object *savedDomains(const AdSettings &ad, const std::string &except) {
    json_object *arr = json_object_new_array();
    for (auto &d : ad.domains) {
        if (webctl::str(d.config, "domain") == except) continue;
        json_object *o = json_object_new_object();
        json_object_object_add(o, "config", json_object_get(d.config));
        json_object_array_add(arr, o);
    }
    return arr;
}

// writes the settings and turns adconnect on (or off, without domains)
json_object *applyAdconnect(json_object *global, json_object *domains) {
    std::string err;
    std::vector<Source> sources;
    if (!readSources(sources, err)) return error(err, 500);
    bool any = json_object_array_length(domains) > 0;
    if (any) {
        std::string yaml;
        std::vector<std::pair<std::string, std::string>> secrets;
        if (json_object *e = buildAdconnect(global, domains, false, yaml, secrets)) return e;
        if (json_object *e = writeChecked(AdConfig, yaml, secrets)) return e;
    } else {
        unlink(AdConfig);
    }
    if (any != hasModule(sources, AdModule)) {
        if (any) sources.push_back({AdModule, {}});
        else sources.erase(std::remove_if(sources.begin(), sources.end(),
                                          [](const Source &s) { return s.module == AdModule; }), sources.end());
        if (!writeSources(sources, err)) return error(err, 500);
    }
    return setService(any);
}

// {domain, admin, password}: join -- a computer account for this system
// (adconnectd -j); everything else from DNS and the domain's name
json_object *adconnectJoin(json_object *args) {
    if (access(Adconnectd, X_OK) != 0) return error(tr("adconnect isn't installed", "adconnect ist nicht installiert"), 404);
    std::string domain = webctl::str(args, "domain"), admin = webctl::str(args, "admin"), password = webctl::str(args, "password");
    for (char &c : domain) c = (char)tolower((unsigned char)c);
    if (!validDomain(domain)) return error(tr("invalid domain %1: its DNS name", "ungültige Domäne %1: ihr DNS-Name", {domain}));
    if (admin.empty() || !validText(admin) || admin[0] == '-') return error(tr("the administrator is missing", "der Administrator fehlt"));
    if (password.empty() || !validText(password)) return error(tr("the administrator's password is missing", "das Passwort des Administrators fehlt"));

    mkdir(AdDir, 0700);
    Result r = runCommand({Adconnectd, "-j", domain, "-u", admin, "-s", AdDir}, TestTimeoutMs, password + "\n");
    if (r.rc != 0) {
        std::string msg = r.out;
        if (msg.compare(0, 12, "adconnectd: ") == 0) msg.erase(0, 12);
        return error(msg.empty() ? tr("the join failed", "der Beitritt ist fehlgeschlagen") : msg, 502);
    }
    std::map<std::string, std::string> kv;
    std::istringstream lines(r.out);
    for (std::string line; std::getline(lines, line);) {
        size_t eq = line.find('=');
        if (eq != std::string::npos) kv[line.substr(0, eq)] = line.substr(eq + 1);
    }
    if (kv["principal"].empty() || kv["secret_file"].empty())
        return error(tr("adconnectd's answer to the join is incomplete", "die Antwort von adconnectd auf den Beitritt ist unvollständig"), 500);

    json_object *config = json_object_new_object();
    json_object_object_add(config, "domain", jstr(domain));
    json_object_object_add(config, "base", jstr(kv["base"]));
    json_object *servers = json_object_new_array();
    std::istringstream sv(kv["servers"]);
    for (std::string s; sv >> s;) json_object_array_add(servers, jstr(s));
    json_object_object_add(config, "servers", servers);
    json_object_object_add(config, "tls", jstr("none"));          // Kerberos seals the connection
    json_object_object_add(config, "auth", jstr("kerberos"));
    json_object_object_add(config, "kerberos_principal", jstr(kv["principal"]));
    json_object_object_add(config, "kerberos_realm", jstr(kv["realm"]));
    json_object_object_add(config, "qualified_names", json_object_new_boolean(1));  // user@domain

    std::string err;
    AdSettings ad;
    if (!readAd(ad, err)) return error(err, 500);
    json_object *domains = savedDomains(ad, domain);  // a domain joined again replaces its entry
    json_object *entry = json_object_new_object();
    json_object_object_add(entry, "config", config);
    json_object_object_add(entry, "passwordFile", jstr(kv["secret_file"]));
    json_object_array_add(domains, entry);
    json_object *e = applyAdconnect(ad.global, domains);
    json_object_put(domains);
    if (e) return e;
    json_object *out = webctl::ok();
    json_object_object_add(out, "principal", jstr(kv["principal"]));
    json_object_object_add(out, "servers", jstr(kv["servers"]));
    json_object_object_add(out, "created", json_object_new_boolean(kv["created"] == "yes"));
    return out;
}

// {domain}: no longer asked (its computer account stays in the domain)
json_object *adconnectRemove(json_object *args) {
    std::string domain = webctl::str(args, "domain"), err;
    AdSettings ad;
    if (!readAd(ad, err)) return error(err, 500);
    const AdDomain *d = ad.find(domain);
    if (!d) return error(tr("no domain %1", "keine Domäne %1", {domain}), 404);
    std::string secret = d->passwordFile;
    json_object *domains = savedDomains(ad, domain);
    json_object *e = applyAdconnect(ad.global, domains);
    json_object_put(domains);
    if (e) return e;
    if (secret.compare(0, strlen(AdDir) + 6, std::string(AdDir) + "/bind-") == 0) unlink(secret.c_str());
    return webctl::ok();
}

// {domain: {config, password?}, global?}: that domain alone, with the
// dialog's settings; without it: the saved configuration, every domain
json_object *adconnectTest(json_object *args) {
@@ -870,6 +993,8 @@ json_object *call(const std::string &action, json_object *args) {
    if (action == "sourcesSave") return sourcesSave(args);
    if (action == "adconnectSave") return adconnectSave(args);
    if (action == "adconnectTest") return adconnectTest(args);
    if (action == "adconnectJoin") return adconnectJoin(args);
    if (action == "adconnectRemove") return adconnectRemove(args);
    if (action == "adconnectLookup") return adconnectLookup(args);
    return nullptr;
}
+5 −2
Changes for modules/users/sources.h: 5 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -32,8 +32,11 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 *   sources                           -> {sources, available, moduleDir, adconnect}
 *   sourcesSave {sources: [{module, options: {}}]}
 *   adconnectSave {config: {...}, password?, enabled}
 *   adconnectTest {config: {...}, password?}   -> {ok, output}
 *   adconnectJoin {domain, admin, password}  join: a computer account for
 *                                     this system, the rest automatic
 *   adconnectRemove {domain}
 *   adconnectSave {global: {...}, domains: [{config: {...}, password?}], enabled}
 *   adconnectTest {domain?: {config, password?}, global?}   -> {ok, output}
 *   adconnectLookup {type: user|uid|group|gid|groups, name}
 *                                     -> {users, groups, gids}
 *