Loading plugin/ad/ad.cpp +106 −1 Changes for plugin/ad/ad.cpp: 106 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -725,10 +725,25 @@ bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::stri auto e = d->dir->bySamAccountName(sam); if (!e) { err = "no such user"; return false; } dsdb::Secrets sec = d->dir->secretsOf(e->objectGuid); if (!dsdb::verifyPassword(sec, pw)) { err = "wrong password"; return false; } if (dsdb::verifyPassword(sec, pw)) { outGuid = e->objectGuid; return true; } /* migrated native account: verify the preserved sha512 web hash (no NT * hash / clear password was available at migration time) */ std::string wh = e->str("@webHash"); if (!wh.empty()) { sha512 sha; std::string h; sha.hash(pw, h); if (h == wh) { outGuid = e->objectGuid; return true; } } err = "wrong password"; return false; } bool ActiveDirectory::dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) { Loading Loading @@ -832,6 +847,96 @@ bool ActiveDirectory::dirRemoveMember(const std::string &domain, const uuid::uui return true; } bool ActiveDirectory::dirImportNative(const std::string &domain, int &migrated, std::string &err) { if (g_Replication && g_Replication->isSlave()) { err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); migrated = 0; Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } AuthBackend *bck = DomainRegistry::instance().getByName(domain, nullptr); if (!bck) { err = "no backend for '" + domain + "'"; return false; } dsdb::Dn users = d->meta.baseDn.child("CN", "Users"); /* native UserData -> SID-less directory user, sha512 hash kept in @webHash */ { User user; std::vector<uuid::uuid> uids; try { user.list(*bck, uids); } catch (...) {} for (const auto &uid : uids) { class UserData udat(uid); try { size_t p = sizeof(AuthHeader); user.info(*bck, udat, p); } catch (...) { continue; } std::string sam = udat.getUsername(); if (sam.empty()) continue; if (d->dir->bySamAccountName(sam)) continue; dsdb::Dn dn = users.child("CN", sam); if (d->dir->byDn(dn)) continue; dsdb::AttrMap a; a["objectClass"] = {"top", "person", "organizationalPerson", "user"}; a["objectCategory"] = {"CN=Person,CN=Schema,CN=Configuration," + d->meta.baseDn.str()}; a["sAMAccountName"] = {sam}; a["cn"] = {sam}; a["name"] = {sam}; a["distinguishedName"] = {dn.str()}; try { std::string v = udat.getFirstname(); if (!v.empty()) a["givenName"] = {v}; } catch (...) {} try { std::string v = udat.getLastname(); if (!v.empty()) a["sn"] = {v}; } catch (...) {} try { std::string v = udat.getMail(); if (!v.empty()) a["mail"] = {v}; } catch (...) {} try { std::string v = udat.getPwHash(); if (!v.empty()) a["@webHash"] = {v}; } catch (...) {} try { d->dir->create(dn, a, nullptr); ++migrated; } catch (...) {} } } /* native GroupData -> SID-less directory group + members (by username) */ { Group group; std::vector<uuid::uuid> gids; try { group.list(*bck, gids); } catch (...) {} for (const auto &gid : gids) { class GroupData gdat(gid); try { size_t p = sizeof(AuthHeader); group.info(*bck, gdat, p); } catch (...) { continue; } std::string gname = gdat.getName(); if (gname.empty()) continue; if (d->dir->bySamAccountName(gname)) continue; dsdb::Dn dn = users.child("CN", gname); if (d->dir->byDn(dn)) continue; dsdb::AttrMap a; a["objectClass"] = {"top", "group"}; a["objectCategory"] = {"CN=Group,CN=Schema,CN=Configuration," + d->meta.baseDn.str()}; a["sAMAccountName"] = {gname}; a["cn"] = {gname}; a["name"] = {gname}; a["distinguishedName"] = {dn.str()}; std::vector<uuid::uuid> mems; gdat.getMembers(mems); dsdb::Values memberDns; for (const auto &muid : mems) { class UserData mu(muid); User u; try { size_t p = sizeof(AuthHeader); u.info(*bck, mu, p); std::string msam = mu.getUsername(); if (!msam.empty()) memberDns.push_back(users.child("CN", msam).str()); } catch (...) {} } if (!memberDns.empty()) a["member"] = memberDns; try { d->dir->create(dn, a, nullptr); for (auto &mdn : memberDns) { dsdb::Dn md; if (!dsdb::Dn::parse(mdn, md)) continue; auto me = d->dir->byDn(md); if (!me) continue; dsdb::AttrMap up; dsdb::Values mo = me->get("memberOf") ? *me->get("memberOf") : dsdb::Values{}; mo.push_back(dn.str()); up["memberOf"] = mo; try { d->dir->modify(me->objectGuid, up, nullptr); } catch (...) {} } ++migrated; } catch (...) {} } } std::cerr << "[AD] dirImportNative domain=" << domain << " migrated=" << migrated << std::endl; return true; } /****************************************************************************** * Domain loading: every domain of the registry becomes a provisioned * directory naming context. Loading plugin/ad/ad.h +2 −0 Changes for plugin/ad/ad.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -279,6 +279,8 @@ namespace fenrirds { /* unified directory CRUD (see AdPlugin for the contract) */ void reloadDomains() override { loadDomains(); } bool dirImportNative(const std::string &domain, int &migrated, std::string &err) override; bool dirList(const std::string &domain, int kind, std::vector<dsdb::Entry> &out, std::string &err) override; bool dirGet(const std::string &domain, const uuid::uuid &guid, Loading src/admin.cpp +19 −2 Changes for src/admin.cpp: 19 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -2056,18 +2056,35 @@ namespace fenrirds { void apiImportDb(libhttppp::HttpRequest &curreq, const char *cdid) { AuthBackend *backend = getDomain(cdid); if (!backend) throw AuthBackendError("import: could not resolve domain!"); AuthBackend::Guard guard(*backend); std::string reqbody = getRequestBody(curreq); json_object *jreq = json_tokener_parse(reqbody.c_str()); if (!jreq) throw AuthBackendError("import: invalid JSON!"); int count = Import::fromJson(*backend, jreq); int count = 0; { /* scoped: release the backend write lock before migrating, so * the directory's own locking (below) can't deadlock against it */ AuthBackend::Guard guard(*backend); count = Import::fromJson(*backend, jreq); } json_object_put(jreq); /* Import restores raw native records. For a real domain, fold any * native UserData/GroupData into the directory (single source) so * imported users/groups show up and can log in. */ int migrated = 0; if (strcmp(cdid, "admin") != 0 && g_AdPlugin) { std::string domain = domainNameFor(cdid); std::string err; if (!domain.empty() && !g_AdPlugin->dirImportNative(domain, migrated, err)) std::cerr << "import: dirImportNative failed: " << err << std::endl; } json_object *jobj = json_object_new_object(); json_object_object_add(jobj, "status", json_object_new_string("ok")); json_object_object_add(jobj, "imported", json_object_new_int(count)); json_object_object_add(jobj, "migrated", json_object_new_int(migrated)); sendJson(curreq, jobj); json_object_put(jobj); } Loading src/fenrirds.cpp +29 −2 Changes for src/fenrirds.cpp: 29 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ #include "fenrirds.h" #include "user.h" #include "domain.h" #include "profile_webp.h" #include "group.h" #include "types.h" #include "hash/sha512.h" Loading Loading @@ -661,9 +662,34 @@ namespace fenrirds { } catch (AuthBackendError &) { } rep.setState(HTTP404); rep.send(curreq,"",0); /* directory user (no native record): serve its thumbnailPhoto */ if (g_AdPlugin && strcmp(domain, "admin") != 0) { std::string dnName; try { class DomainData ddat(did); Domain dom; size_t dp = sizeof(AuthHeader); dom.info(_AdminBackend, ddat, dp); dnName = ddat.getDomainName(); } catch (...) {} if (!dnName.empty()) { dsdb::Entry e; std::string err; if (g_AdPlugin->dirGet(dnName, uid, e, err)) { std::string tp = e.str("thumbnailPhoto"); if (!tp.empty()) { rep.setContentType("image/jpeg"); rep.send(curreq, tp.data(), tp.size()); return; } } } } /* fallback: the built-in default avatar, so the UI never shows a * broken image for accounts without a picture */ rep.setContentType("image/webp"); rep.send(curreq, reinterpret_cast<const char*>(profile_webp), sizeof(profile_webp)); }; AuthBackend *getDomainBackend(const ::uuid::uuid &did){ Loading Loading @@ -1182,6 +1208,7 @@ int main(int argc,char *argv[]){ /* Start each plugin in its own background thread (runs as unprivileged user) */ std::vector<std::thread> pluginThreads; for (auto &[pname, lp] : pluginMgr.plugins()) { if (!lp.ready) continue; // its init() failed (see above) pluginThreads.emplace_back([&lp, &pname](){ try { lp.instance->run(); Loading src/plugin.cpp +1 −0 Changes for src/plugin.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -203,6 +203,7 @@ void PluginManager::initAll() { if (lp.instance) { try { lp.instance->init(); lp.ready = true; } catch (const netplus::NetException &e) { std::cerr << "Plugin init failed: " << name << ": " << e.what() << std::endl; Loading Loading
plugin/ad/ad.cpp +106 −1 Changes for plugin/ad/ad.cpp: 106 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -725,10 +725,25 @@ bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::stri auto e = d->dir->bySamAccountName(sam); if (!e) { err = "no such user"; return false; } dsdb::Secrets sec = d->dir->secretsOf(e->objectGuid); if (!dsdb::verifyPassword(sec, pw)) { err = "wrong password"; return false; } if (dsdb::verifyPassword(sec, pw)) { outGuid = e->objectGuid; return true; } /* migrated native account: verify the preserved sha512 web hash (no NT * hash / clear password was available at migration time) */ std::string wh = e->str("@webHash"); if (!wh.empty()) { sha512 sha; std::string h; sha.hash(pw, h); if (h == wh) { outGuid = e->objectGuid; return true; } } err = "wrong password"; return false; } bool ActiveDirectory::dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) { Loading Loading @@ -832,6 +847,96 @@ bool ActiveDirectory::dirRemoveMember(const std::string &domain, const uuid::uui return true; } bool ActiveDirectory::dirImportNative(const std::string &domain, int &migrated, std::string &err) { if (g_Replication && g_Replication->isSlave()) { err = "this node is a replication slave (read-only); make changes on the master"; return false; } std::lock_guard<std::mutex> lock(_Lock); migrated = 0; Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } AuthBackend *bck = DomainRegistry::instance().getByName(domain, nullptr); if (!bck) { err = "no backend for '" + domain + "'"; return false; } dsdb::Dn users = d->meta.baseDn.child("CN", "Users"); /* native UserData -> SID-less directory user, sha512 hash kept in @webHash */ { User user; std::vector<uuid::uuid> uids; try { user.list(*bck, uids); } catch (...) {} for (const auto &uid : uids) { class UserData udat(uid); try { size_t p = sizeof(AuthHeader); user.info(*bck, udat, p); } catch (...) { continue; } std::string sam = udat.getUsername(); if (sam.empty()) continue; if (d->dir->bySamAccountName(sam)) continue; dsdb::Dn dn = users.child("CN", sam); if (d->dir->byDn(dn)) continue; dsdb::AttrMap a; a["objectClass"] = {"top", "person", "organizationalPerson", "user"}; a["objectCategory"] = {"CN=Person,CN=Schema,CN=Configuration," + d->meta.baseDn.str()}; a["sAMAccountName"] = {sam}; a["cn"] = {sam}; a["name"] = {sam}; a["distinguishedName"] = {dn.str()}; try { std::string v = udat.getFirstname(); if (!v.empty()) a["givenName"] = {v}; } catch (...) {} try { std::string v = udat.getLastname(); if (!v.empty()) a["sn"] = {v}; } catch (...) {} try { std::string v = udat.getMail(); if (!v.empty()) a["mail"] = {v}; } catch (...) {} try { std::string v = udat.getPwHash(); if (!v.empty()) a["@webHash"] = {v}; } catch (...) {} try { d->dir->create(dn, a, nullptr); ++migrated; } catch (...) {} } } /* native GroupData -> SID-less directory group + members (by username) */ { Group group; std::vector<uuid::uuid> gids; try { group.list(*bck, gids); } catch (...) {} for (const auto &gid : gids) { class GroupData gdat(gid); try { size_t p = sizeof(AuthHeader); group.info(*bck, gdat, p); } catch (...) { continue; } std::string gname = gdat.getName(); if (gname.empty()) continue; if (d->dir->bySamAccountName(gname)) continue; dsdb::Dn dn = users.child("CN", gname); if (d->dir->byDn(dn)) continue; dsdb::AttrMap a; a["objectClass"] = {"top", "group"}; a["objectCategory"] = {"CN=Group,CN=Schema,CN=Configuration," + d->meta.baseDn.str()}; a["sAMAccountName"] = {gname}; a["cn"] = {gname}; a["name"] = {gname}; a["distinguishedName"] = {dn.str()}; std::vector<uuid::uuid> mems; gdat.getMembers(mems); dsdb::Values memberDns; for (const auto &muid : mems) { class UserData mu(muid); User u; try { size_t p = sizeof(AuthHeader); u.info(*bck, mu, p); std::string msam = mu.getUsername(); if (!msam.empty()) memberDns.push_back(users.child("CN", msam).str()); } catch (...) {} } if (!memberDns.empty()) a["member"] = memberDns; try { d->dir->create(dn, a, nullptr); for (auto &mdn : memberDns) { dsdb::Dn md; if (!dsdb::Dn::parse(mdn, md)) continue; auto me = d->dir->byDn(md); if (!me) continue; dsdb::AttrMap up; dsdb::Values mo = me->get("memberOf") ? *me->get("memberOf") : dsdb::Values{}; mo.push_back(dn.str()); up["memberOf"] = mo; try { d->dir->modify(me->objectGuid, up, nullptr); } catch (...) {} } ++migrated; } catch (...) {} } } std::cerr << "[AD] dirImportNative domain=" << domain << " migrated=" << migrated << std::endl; return true; } /****************************************************************************** * Domain loading: every domain of the registry becomes a provisioned * directory naming context. Loading
plugin/ad/ad.h +2 −0 Changes for plugin/ad/ad.h: 2 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -279,6 +279,8 @@ namespace fenrirds { /* unified directory CRUD (see AdPlugin for the contract) */ void reloadDomains() override { loadDomains(); } bool dirImportNative(const std::string &domain, int &migrated, std::string &err) override; bool dirList(const std::string &domain, int kind, std::vector<dsdb::Entry> &out, std::string &err) override; bool dirGet(const std::string &domain, const uuid::uuid &guid, Loading
src/admin.cpp +19 −2 Changes for src/admin.cpp: 19 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -2056,18 +2056,35 @@ namespace fenrirds { void apiImportDb(libhttppp::HttpRequest &curreq, const char *cdid) { AuthBackend *backend = getDomain(cdid); if (!backend) throw AuthBackendError("import: could not resolve domain!"); AuthBackend::Guard guard(*backend); std::string reqbody = getRequestBody(curreq); json_object *jreq = json_tokener_parse(reqbody.c_str()); if (!jreq) throw AuthBackendError("import: invalid JSON!"); int count = Import::fromJson(*backend, jreq); int count = 0; { /* scoped: release the backend write lock before migrating, so * the directory's own locking (below) can't deadlock against it */ AuthBackend::Guard guard(*backend); count = Import::fromJson(*backend, jreq); } json_object_put(jreq); /* Import restores raw native records. For a real domain, fold any * native UserData/GroupData into the directory (single source) so * imported users/groups show up and can log in. */ int migrated = 0; if (strcmp(cdid, "admin") != 0 && g_AdPlugin) { std::string domain = domainNameFor(cdid); std::string err; if (!domain.empty() && !g_AdPlugin->dirImportNative(domain, migrated, err)) std::cerr << "import: dirImportNative failed: " << err << std::endl; } json_object *jobj = json_object_new_object(); json_object_object_add(jobj, "status", json_object_new_string("ok")); json_object_object_add(jobj, "imported", json_object_new_int(count)); json_object_object_add(jobj, "migrated", json_object_new_int(migrated)); sendJson(curreq, jobj); json_object_put(jobj); } Loading
src/fenrirds.cpp +29 −2 Changes for src/fenrirds.cpp: 29 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -52,6 +52,7 @@ #include "fenrirds.h" #include "user.h" #include "domain.h" #include "profile_webp.h" #include "group.h" #include "types.h" #include "hash/sha512.h" Loading Loading @@ -661,9 +662,34 @@ namespace fenrirds { } catch (AuthBackendError &) { } rep.setState(HTTP404); rep.send(curreq,"",0); /* directory user (no native record): serve its thumbnailPhoto */ if (g_AdPlugin && strcmp(domain, "admin") != 0) { std::string dnName; try { class DomainData ddat(did); Domain dom; size_t dp = sizeof(AuthHeader); dom.info(_AdminBackend, ddat, dp); dnName = ddat.getDomainName(); } catch (...) {} if (!dnName.empty()) { dsdb::Entry e; std::string err; if (g_AdPlugin->dirGet(dnName, uid, e, err)) { std::string tp = e.str("thumbnailPhoto"); if (!tp.empty()) { rep.setContentType("image/jpeg"); rep.send(curreq, tp.data(), tp.size()); return; } } } } /* fallback: the built-in default avatar, so the UI never shows a * broken image for accounts without a picture */ rep.setContentType("image/webp"); rep.send(curreq, reinterpret_cast<const char*>(profile_webp), sizeof(profile_webp)); }; AuthBackend *getDomainBackend(const ::uuid::uuid &did){ Loading Loading @@ -1182,6 +1208,7 @@ int main(int argc,char *argv[]){ /* Start each plugin in its own background thread (runs as unprivileged user) */ std::vector<std::thread> pluginThreads; for (auto &[pname, lp] : pluginMgr.plugins()) { if (!lp.ready) continue; // its init() failed (see above) pluginThreads.emplace_back([&lp, &pname](){ try { lp.instance->run(); Loading
src/plugin.cpp +1 −0 Changes for src/plugin.cpp: 1 added line, 0 removed lines. Original line number Diff line number Diff line Loading @@ -203,6 +203,7 @@ void PluginManager::initAll() { if (lp.instance) { try { lp.instance->init(); lp.ready = true; } catch (const netplus::NetException &e) { std::cerr << "Plugin init failed: " << name << ": " << e.what() << std::endl; Loading