Loading modules/users/sources.cpp +134 −9 Changes for modules/users/sources.cpp: 134 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -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> Loading Loading @@ -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); Loading @@ -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 (;;) { Loading Loading @@ -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 { Loading Loading @@ -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) { Loading Loading @@ -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; } Loading modules/users/sources.h +5 −2 Changes for modules/users/sources.h: 5 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -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} * Loading Loading
modules/users/sources.cpp +134 −9 Changes for modules/users/sources.cpp: 134 added lines, 9 removed lines. Original line number Diff line number Diff line Loading @@ -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> Loading Loading @@ -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); Loading @@ -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 (;;) { Loading Loading @@ -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 { Loading Loading @@ -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) { Loading Loading @@ -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; } Loading
modules/users/sources.h +5 −2 Changes for modules/users/sources.h: 5 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -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} * Loading