Loading data/admin.html +6 −0 Changes for data/admin.html: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -283,7 +283,10 @@ function createUserForm() { html += '<tr><td><label for="username">Username:</label></td><td><input type="text" name="username" id="cu_username"></td></tr>'; html += '<tr><td><label for="firstname">Firstname:</label></td><td><input type="text" name="firstname" id="cu_firstname"></td></tr>'; html += '<tr><td><label for="lastname">Lastname:</label></td><td><input type="text" name="lastname" id="cu_lastname"></td></tr>'; html += '<tr><td><label for="displayname">Anzeigename:</label></td><td><input type="text" name="displayname" id="cu_displayname"></td></tr>'; html += '<tr><td><label for="mail">Mail:</label></td><td><input type="text" name="mail" id="cu_mail"></td></tr>'; html += '<tr><td><label for="userprincipalname">UPN:</label></td><td><input type="text" name="userprincipalname" id="cu_upn" placeholder="user@domain"></td></tr>'; html += '<tr><td><label for="description">Beschreibung:</label></td><td><input type="text" name="description" id="cu_description"></td></tr>'; html += '<tr><td><label for="pwhash">Password:</label></td><td><input type="password" name="pwhash" id="cu_password"></td></tr>'; html += '<tr><td><label for="role">Rolle:</label></td><td><select name="role" id="cu_role">'; html += '<option value="mitarbeiter">Mitarbeiter</option>'; Loading @@ -301,7 +304,10 @@ function submitCreateUser() { formData.append("username", document.getElementById("cu_username").value); formData.append("firstname", document.getElementById("cu_firstname").value); formData.append("lastname", document.getElementById("cu_lastname").value); formData.append("displayname", document.getElementById("cu_displayname").value); formData.append("mail", document.getElementById("cu_mail").value); formData.append("userprincipalname", document.getElementById("cu_upn").value); formData.append("description", document.getElementById("cu_description").value); formData.append("pwhash", document.getElementById("cu_password").value); formData.append("role", document.getElementById("cu_role").value); if (document.getElementById("cu_windowslogin").checked) Loading plugin/ad/ad.cpp +310 −1 Changes for plugin/ad/ad.cpp: 310 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ #include "fenrirds.h" #include "domains.h" #include "backend.h" #include "replication.h" #include "user.h" #include "group.h" #include "client.h" Loading Loading @@ -396,7 +397,7 @@ void ActiveDirectory::run() { _Running = true; _CldapThread = std::thread([this]() { while (_Running) { if (!waitReadable(_Cldap->fd(), 500)) if (!_Cldap->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; Loading Loading @@ -526,6 +527,309 @@ bool ActiveDirectory::createWindowsGroup(const std::string &domain, const std::s return true; } /****************************************************************************** * Unified directory CRUD bridge (AdPlugin). The directory is the single * source of truth for users and groups; the admin daemon / web API / session * store reach the live per-domain directory through these methods. Each one * takes _Lock (the same mutex loadDomains / the LDAP handlers use) so writes * stay consistent with the live cache and the shared RID allocator. *****************************************************************************/ ActiveDirectory::Dom *ActiveDirectory::domainByName(const std::string &dnsName) { for (auto &dd : _Domains) if (dsdb::iequals(dd.meta.dnsName, dnsName)) return ⅆ return nullptr; } bool ActiveDirectory::dirList(const std::string &domain, int kind, std::vector<dsdb::Entry> &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } dsdb::Filter f; f.type = dsdb::Filter::And; f.children.push_back(dsdb::Filter::equal("objectClass", kind == DirGroup ? "group" : "user")); if (kind == DirUser) { /* 'user' also matches computer accounts (they are users too) — exclude them */ dsdb::Filter notComp; notComp.type = dsdb::Filter::Not; notComp.children.push_back(dsdb::Filter::equal("objectClass", "computer")); f.children.push_back(notComp); } dsdb::Dn base = d->meta.baseDn.child("CN", "Users"); out.clear(); d->dir->search(base, LDAP_SCOPE_SUBTREE, f, [&](const dsdb::Entry &e) -> bool { out.push_back(e); return true; }); return true; } bool ActiveDirectory::dirGet(const std::string &domain, const uuid::uuid &guid, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } out = *e; return true; } bool ActiveDirectory::dirGetBySam(const std::string &domain, const std::string &sam, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->bySamAccountName(sam); if (!e) { err = "object not found"; return false; } out = *e; return true; } bool ActiveDirectory::dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } std::string sam = dsdb::value(attrs, "sAMAccountName"); if (sam.empty()) { err = "sAMAccountName required"; return false; } dsdb::Dn users = d->meta.baseDn.child("CN", "Users"); dsdb::Dn dn = users.child("CN", sam); if (d->dir->byDn(dn) || d->dir->bySamAccountName(sam)) { err = "an object named '" + sam + "' already exists"; return false; } if (kind == DirGroup) attrs["objectClass"] = {"top", "group"}; else attrs["objectClass"] = {"top", "person", "organizationalPerson", "user"}; attrs["objectCategory"] = {"CN=" + std::string(kind == DirGroup ? "Group" : "Person") + ",CN=Schema,CN=Configuration," + d->meta.baseDn.str()}; attrs["cn"] = {sam}; attrs["name"] = {sam}; attrs["distinguishedName"] = {dn.str()}; std::unique_ptr<dsdb::Secrets> sec; if (kind == DirUser && !pw.empty()) { std::string salt = dsdb::kerberosSalt(dsdb::PrincipalKind::User, d->meta.realm, sam, d->meta.dnsName); sec = std::make_unique<dsdb::Secrets>(dsdb::deriveSecrets(pw, salt, 0)); dsdb::setInt(attrs, "msDS-KeyVersionNumber", sec->kvno); dsdb::setInt(attrs, "pwdLastSet", sec->pwdLastSet); } if (windowsLogin) { uint32_t rid = d->dir->allocateRid(); attrs["objectSid"] = {d->meta.sid.withRid(rid).binary()}; if (kind == DirGroup) { dsdb::setInt(attrs, "groupType", int32_t(dsdb::GROUP_UNIVERSAL | dsdb::GROUP_SECURITY)); dsdb::setInt(attrs, "sAMAccountType", dsdb::SAM_GROUP_OBJECT); } else { dsdb::setInt(attrs, "sAMAccountType", dsdb::SAM_USER_OBJECT); if (!attrs.count("userAccountControl")) dsdb::setInt(attrs, "userAccountControl", dsdb::UAC_NORMAL_ACCOUNT | dsdb::UAC_DONT_EXPIRE_PASSWORD); if (!attrs.count("primaryGroupID")) dsdb::setInt(attrs, "primaryGroupID", dsdb::RID_DOMAIN_USERS); if (!attrs.count("userPrincipalName")) attrs["userPrincipalName"] = {sam + "@" + d->meta.dnsName}; } } try { outGuid = d->dir->create(dn, attrs, sec.get()); } catch (std::exception &e) { err = e.what(); return false; } std::cerr << "AD: created " << (kind == DirGroup ? "group" : "user") << " '" << sam << "' in " << d->meta.dnsName << (windowsLogin ? " (Windows login)" : " (no SID)") << std::endl; return true; } bool ActiveDirectory::dirModify(const std::string &domain, const uuid::uuid &guid, const dsdb::AttrMap &attrs, const std::string &pw, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } dsdb::AttrMap merged = attrs; std::unique_ptr<dsdb::Secrets> sec; if (!pw.empty() && e->isClass("user")) { std::string sam = e->str("sAMAccountName"); std::string salt = dsdb::kerberosSalt(dsdb::PrincipalKind::User, d->meta.realm, sam, d->meta.dnsName); dsdb::Secrets old = d->dir->secretsOf(guid); sec = std::make_unique<dsdb::Secrets>(dsdb::deriveSecrets(pw, salt, old.kvno)); dsdb::setInt(merged, "msDS-KeyVersionNumber", sec->kvno); dsdb::setInt(merged, "pwdLastSet", sec->pwdLastSet); } try { d->dir->modify(guid, merged, sec.get()); } catch (std::exception &e2) { err = e2.what(); return false; } return true; } bool ActiveDirectory::dirRemove(const std::string &domain, const uuid::uuid &guid, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } if (!d->dir->byGuid(guid)) { err = "object not found"; return false; } try { d->dir->remove(guid); } catch (std::exception &e) { err = e.what(); return false; } return true; } bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::string &sam, const std::string &pw, uuid::uuid &outGuid, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } 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; } outGuid = e->objectGuid; return true; } bool ActiveDirectory::dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } groups.clear(); /* transitive walk over memberOf: a user's groups, plus the groups those * groups belong to (nested) */ std::vector<std::string> work; if (auto *mo = e->get("memberOf")) work = *mo; std::map<std::string, bool, dsdb::CaseLess> seen; while (!work.empty()) { std::string cur = work.back(); work.pop_back(); if (seen.count(cur)) continue; seen[cur] = true; dsdb::Dn gdn; if (!dsdb::Dn::parse(cur, gdn)) continue; auto g = d->dir->byDn(gdn); if (!g) continue; groups.push_back(g->objectGuid); if (auto *mo2 = g->get("memberOf")) for (auto &v : *mo2) work.push_back(v); } return true; } bool ActiveDirectory::dirAddMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); if (!g || !m) { err = "group or member not found"; return false; } std::string gdn = g->dn.str(), mdn = m->dn.str(); /* maintain both forward (group.member) and back link (member.memberOf) */ dsdb::AttrMap ga; dsdb::Values members = g->get("member") ? *g->get("member") : dsdb::Values{}; if (std::find(members.begin(), members.end(), mdn) == members.end()) members.push_back(mdn); ga["member"] = members; dsdb::AttrMap ma; dsdb::Values memberOf = m->get("memberOf") ? *m->get("memberOf") : dsdb::Values{}; if (std::find(memberOf.begin(), memberOf.end(), gdn) == memberOf.end()) memberOf.push_back(gdn); ma["memberOf"] = memberOf; try { d->dir->modify(group, ga, nullptr); d->dir->modify(member, ma, nullptr); } catch (std::exception &e) { err = e.what(); return false; } return true; } bool ActiveDirectory::dirRemoveMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); if (!g || !m) { err = "group or member not found"; return false; } std::string gdn = g->dn.str(), mdn = m->dn.str(); dsdb::AttrMap ga; dsdb::Values members = g->get("member") ? *g->get("member") : dsdb::Values{}; members.erase(std::remove(members.begin(), members.end(), mdn), members.end()); ga["member"] = members; dsdb::AttrMap ma; dsdb::Values memberOf = m->get("memberOf") ? *m->get("memberOf") : dsdb::Values{}; memberOf.erase(std::remove(memberOf.begin(), memberOf.end(), gdn), memberOf.end()); ma["memberOf"] = memberOf; try { d->dir->modify(group, ga, nullptr); d->dir->modify(member, ma, nullptr); } catch (std::exception &e) { err = e.what(); return false; } return true; } /****************************************************************************** * Domain loading: every domain of the registry becomes a provisioned * directory naming context. Loading @@ -545,6 +849,10 @@ void ActiveDirectory::loadDomains() { d.meta.baseDn = dsdb::dnFromDomain(e.name); d.meta.realm = dsdb::upper(e.name); d.dir = std::make_unique<dsdb::Directory>(*bck, d.meta); /* A replication slave is read-only — it must not provision (that would * allocate RIDs locally and fight the master's snapshot). Its directory * is populated by replication; skip provisioning here. */ if (!(g_Replication && g_Replication->isSlave())) { try { d.dir->provision(_AdminPassword); if (!_DcHost.empty()) Loading @@ -552,6 +860,7 @@ void ActiveDirectory::loadDomains() { } catch (std::exception &ex) { std::cerr << "AD: provision '" << e.name << "' failed: " << ex.what() << std::endl; } } /* pick up the domain SID the head carries */ if (auto head = d.dir->byDn(d.meta.baseDn)) d.meta.sid = head->objectSid; Loading plugin/ad/ad.h +28 −0 Changes for plugin/ad/ad.h: 28 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -277,6 +277,32 @@ namespace fenrirds { bool createWindowsGroup(const std::string &domain, const std::string &sam, std::string &err) override; /* unified directory CRUD (see AdPlugin for the contract) */ void reloadDomains() override { loadDomains(); } 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, dsdb::Entry &out, std::string &err) override; bool dirGetBySam(const std::string &domain, const std::string &sam, dsdb::Entry &out, std::string &err) override; bool dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, std::string &err) override; bool dirModify(const std::string &domain, const uuid::uuid &guid, const dsdb::AttrMap &attrs, const std::string &pw, std::string &err) override; bool dirRemove(const std::string &domain, const uuid::uuid &guid, std::string &err) override; bool dirAuthenticate(const std::string &domain, const std::string &sam, const std::string &pw, uuid::uuid &outGuid, std::string &err) override; bool dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) override; bool dirAddMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, std::string &err) override; bool dirRemoveMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, std::string &err) override; /* netplus::eventapi overrides */ void RequestEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override; void ResponseEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override; Loading Loading @@ -329,6 +355,8 @@ namespace fenrirds { void loadDomains(); /* the domain whose base DN contains dn (longest match), or nullptr */ Dom *domainForDn(const dsdb::Dn &dn); /* the domain by dnsName (case-insensitive), or nullptr; caller holds _Lock */ Dom *domainByName(const std::string &dnsName); /* render a directory entry as an LdapEntry honoring the requested * attribute list (empty / "*" = all user attrs; "+" = operational; * "1.1" = none; secret attributes are never returned) */ Loading plugin/dns/dns.cpp +2 −2 Changes for plugin/dns/dns.cpp: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -334,7 +334,7 @@ void DnsServer::run() { /* TCP responder in its own thread (prefixed with a 2-byte length) */ _TcpThread = std::thread([this]() { while (_Running) { if (!waitReadable(_Tcp->fd(), 500)) if (!_Tcp->waitReadable(500)) continue; std::unique_ptr<netplus::socket> cs; try { Loading Loading @@ -365,7 +365,7 @@ void DnsServer::run() { /* UDP request/response loop (recvmmsg is non-blocking, so poll first) */ while (_Running) { if (!waitReadable(_Udp->fd(), 500)) if (!_Udp->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; Loading plugin/krb5/krb5.cpp +1 −1 Changes for plugin/krb5/krb5.cpp: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -2628,7 +2628,7 @@ void Krb5KDC::init() { * 4-byte length prefix). Process it and send the bare response back. */ void Krb5KDC::udpLoop() { while (_Running) { if (!waitReadable(_Udp->fd(), 500)) if (!_Udp->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; Loading Loading
data/admin.html +6 −0 Changes for data/admin.html: 6 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -283,7 +283,10 @@ function createUserForm() { html += '<tr><td><label for="username">Username:</label></td><td><input type="text" name="username" id="cu_username"></td></tr>'; html += '<tr><td><label for="firstname">Firstname:</label></td><td><input type="text" name="firstname" id="cu_firstname"></td></tr>'; html += '<tr><td><label for="lastname">Lastname:</label></td><td><input type="text" name="lastname" id="cu_lastname"></td></tr>'; html += '<tr><td><label for="displayname">Anzeigename:</label></td><td><input type="text" name="displayname" id="cu_displayname"></td></tr>'; html += '<tr><td><label for="mail">Mail:</label></td><td><input type="text" name="mail" id="cu_mail"></td></tr>'; html += '<tr><td><label for="userprincipalname">UPN:</label></td><td><input type="text" name="userprincipalname" id="cu_upn" placeholder="user@domain"></td></tr>'; html += '<tr><td><label for="description">Beschreibung:</label></td><td><input type="text" name="description" id="cu_description"></td></tr>'; html += '<tr><td><label for="pwhash">Password:</label></td><td><input type="password" name="pwhash" id="cu_password"></td></tr>'; html += '<tr><td><label for="role">Rolle:</label></td><td><select name="role" id="cu_role">'; html += '<option value="mitarbeiter">Mitarbeiter</option>'; Loading @@ -301,7 +304,10 @@ function submitCreateUser() { formData.append("username", document.getElementById("cu_username").value); formData.append("firstname", document.getElementById("cu_firstname").value); formData.append("lastname", document.getElementById("cu_lastname").value); formData.append("displayname", document.getElementById("cu_displayname").value); formData.append("mail", document.getElementById("cu_mail").value); formData.append("userprincipalname", document.getElementById("cu_upn").value); formData.append("description", document.getElementById("cu_description").value); formData.append("pwhash", document.getElementById("cu_password").value); formData.append("role", document.getElementById("cu_role").value); if (document.getElementById("cu_windowslogin").checked) Loading
plugin/ad/ad.cpp +310 −1 Changes for plugin/ad/ad.cpp: 310 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -47,6 +47,7 @@ #include "fenrirds.h" #include "domains.h" #include "backend.h" #include "replication.h" #include "user.h" #include "group.h" #include "client.h" Loading Loading @@ -396,7 +397,7 @@ void ActiveDirectory::run() { _Running = true; _CldapThread = std::thread([this]() { while (_Running) { if (!waitReadable(_Cldap->fd(), 500)) if (!_Cldap->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; Loading Loading @@ -526,6 +527,309 @@ bool ActiveDirectory::createWindowsGroup(const std::string &domain, const std::s return true; } /****************************************************************************** * Unified directory CRUD bridge (AdPlugin). The directory is the single * source of truth for users and groups; the admin daemon / web API / session * store reach the live per-domain directory through these methods. Each one * takes _Lock (the same mutex loadDomains / the LDAP handlers use) so writes * stay consistent with the live cache and the shared RID allocator. *****************************************************************************/ ActiveDirectory::Dom *ActiveDirectory::domainByName(const std::string &dnsName) { for (auto &dd : _Domains) if (dsdb::iequals(dd.meta.dnsName, dnsName)) return ⅆ return nullptr; } bool ActiveDirectory::dirList(const std::string &domain, int kind, std::vector<dsdb::Entry> &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } dsdb::Filter f; f.type = dsdb::Filter::And; f.children.push_back(dsdb::Filter::equal("objectClass", kind == DirGroup ? "group" : "user")); if (kind == DirUser) { /* 'user' also matches computer accounts (they are users too) — exclude them */ dsdb::Filter notComp; notComp.type = dsdb::Filter::Not; notComp.children.push_back(dsdb::Filter::equal("objectClass", "computer")); f.children.push_back(notComp); } dsdb::Dn base = d->meta.baseDn.child("CN", "Users"); out.clear(); d->dir->search(base, LDAP_SCOPE_SUBTREE, f, [&](const dsdb::Entry &e) -> bool { out.push_back(e); return true; }); return true; } bool ActiveDirectory::dirGet(const std::string &domain, const uuid::uuid &guid, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } out = *e; return true; } bool ActiveDirectory::dirGetBySam(const std::string &domain, const std::string &sam, dsdb::Entry &out, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->bySamAccountName(sam); if (!e) { err = "object not found"; return false; } out = *e; return true; } bool ActiveDirectory::dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } std::string sam = dsdb::value(attrs, "sAMAccountName"); if (sam.empty()) { err = "sAMAccountName required"; return false; } dsdb::Dn users = d->meta.baseDn.child("CN", "Users"); dsdb::Dn dn = users.child("CN", sam); if (d->dir->byDn(dn) || d->dir->bySamAccountName(sam)) { err = "an object named '" + sam + "' already exists"; return false; } if (kind == DirGroup) attrs["objectClass"] = {"top", "group"}; else attrs["objectClass"] = {"top", "person", "organizationalPerson", "user"}; attrs["objectCategory"] = {"CN=" + std::string(kind == DirGroup ? "Group" : "Person") + ",CN=Schema,CN=Configuration," + d->meta.baseDn.str()}; attrs["cn"] = {sam}; attrs["name"] = {sam}; attrs["distinguishedName"] = {dn.str()}; std::unique_ptr<dsdb::Secrets> sec; if (kind == DirUser && !pw.empty()) { std::string salt = dsdb::kerberosSalt(dsdb::PrincipalKind::User, d->meta.realm, sam, d->meta.dnsName); sec = std::make_unique<dsdb::Secrets>(dsdb::deriveSecrets(pw, salt, 0)); dsdb::setInt(attrs, "msDS-KeyVersionNumber", sec->kvno); dsdb::setInt(attrs, "pwdLastSet", sec->pwdLastSet); } if (windowsLogin) { uint32_t rid = d->dir->allocateRid(); attrs["objectSid"] = {d->meta.sid.withRid(rid).binary()}; if (kind == DirGroup) { dsdb::setInt(attrs, "groupType", int32_t(dsdb::GROUP_UNIVERSAL | dsdb::GROUP_SECURITY)); dsdb::setInt(attrs, "sAMAccountType", dsdb::SAM_GROUP_OBJECT); } else { dsdb::setInt(attrs, "sAMAccountType", dsdb::SAM_USER_OBJECT); if (!attrs.count("userAccountControl")) dsdb::setInt(attrs, "userAccountControl", dsdb::UAC_NORMAL_ACCOUNT | dsdb::UAC_DONT_EXPIRE_PASSWORD); if (!attrs.count("primaryGroupID")) dsdb::setInt(attrs, "primaryGroupID", dsdb::RID_DOMAIN_USERS); if (!attrs.count("userPrincipalName")) attrs["userPrincipalName"] = {sam + "@" + d->meta.dnsName}; } } try { outGuid = d->dir->create(dn, attrs, sec.get()); } catch (std::exception &e) { err = e.what(); return false; } std::cerr << "AD: created " << (kind == DirGroup ? "group" : "user") << " '" << sam << "' in " << d->meta.dnsName << (windowsLogin ? " (Windows login)" : " (no SID)") << std::endl; return true; } bool ActiveDirectory::dirModify(const std::string &domain, const uuid::uuid &guid, const dsdb::AttrMap &attrs, const std::string &pw, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } dsdb::AttrMap merged = attrs; std::unique_ptr<dsdb::Secrets> sec; if (!pw.empty() && e->isClass("user")) { std::string sam = e->str("sAMAccountName"); std::string salt = dsdb::kerberosSalt(dsdb::PrincipalKind::User, d->meta.realm, sam, d->meta.dnsName); dsdb::Secrets old = d->dir->secretsOf(guid); sec = std::make_unique<dsdb::Secrets>(dsdb::deriveSecrets(pw, salt, old.kvno)); dsdb::setInt(merged, "msDS-KeyVersionNumber", sec->kvno); dsdb::setInt(merged, "pwdLastSet", sec->pwdLastSet); } try { d->dir->modify(guid, merged, sec.get()); } catch (std::exception &e2) { err = e2.what(); return false; } return true; } bool ActiveDirectory::dirRemove(const std::string &domain, const uuid::uuid &guid, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } if (!d->dir->byGuid(guid)) { err = "object not found"; return false; } try { d->dir->remove(guid); } catch (std::exception &e) { err = e.what(); return false; } return true; } bool ActiveDirectory::dirAuthenticate(const std::string &domain, const std::string &sam, const std::string &pw, uuid::uuid &outGuid, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } 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; } outGuid = e->objectGuid; return true; } bool ActiveDirectory::dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) { std::lock_guard<std::mutex> lock(_Lock); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto e = d->dir->byGuid(guid); if (!e) { err = "object not found"; return false; } groups.clear(); /* transitive walk over memberOf: a user's groups, plus the groups those * groups belong to (nested) */ std::vector<std::string> work; if (auto *mo = e->get("memberOf")) work = *mo; std::map<std::string, bool, dsdb::CaseLess> seen; while (!work.empty()) { std::string cur = work.back(); work.pop_back(); if (seen.count(cur)) continue; seen[cur] = true; dsdb::Dn gdn; if (!dsdb::Dn::parse(cur, gdn)) continue; auto g = d->dir->byDn(gdn); if (!g) continue; groups.push_back(g->objectGuid); if (auto *mo2 = g->get("memberOf")) for (auto &v : *mo2) work.push_back(v); } return true; } bool ActiveDirectory::dirAddMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); if (!g || !m) { err = "group or member not found"; return false; } std::string gdn = g->dn.str(), mdn = m->dn.str(); /* maintain both forward (group.member) and back link (member.memberOf) */ dsdb::AttrMap ga; dsdb::Values members = g->get("member") ? *g->get("member") : dsdb::Values{}; if (std::find(members.begin(), members.end(), mdn) == members.end()) members.push_back(mdn); ga["member"] = members; dsdb::AttrMap ma; dsdb::Values memberOf = m->get("memberOf") ? *m->get("memberOf") : dsdb::Values{}; if (std::find(memberOf.begin(), memberOf.end(), gdn) == memberOf.end()) memberOf.push_back(gdn); ma["memberOf"] = memberOf; try { d->dir->modify(group, ga, nullptr); d->dir->modify(member, ma, nullptr); } catch (std::exception &e) { err = e.what(); return false; } return true; } bool ActiveDirectory::dirRemoveMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, 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); Dom *d = domainByName(domain); if (!d) { err = "unknown domain '" + domain + "'"; return false; } auto g = d->dir->byGuid(group); auto m = d->dir->byGuid(member); if (!g || !m) { err = "group or member not found"; return false; } std::string gdn = g->dn.str(), mdn = m->dn.str(); dsdb::AttrMap ga; dsdb::Values members = g->get("member") ? *g->get("member") : dsdb::Values{}; members.erase(std::remove(members.begin(), members.end(), mdn), members.end()); ga["member"] = members; dsdb::AttrMap ma; dsdb::Values memberOf = m->get("memberOf") ? *m->get("memberOf") : dsdb::Values{}; memberOf.erase(std::remove(memberOf.begin(), memberOf.end(), gdn), memberOf.end()); ma["memberOf"] = memberOf; try { d->dir->modify(group, ga, nullptr); d->dir->modify(member, ma, nullptr); } catch (std::exception &e) { err = e.what(); return false; } return true; } /****************************************************************************** * Domain loading: every domain of the registry becomes a provisioned * directory naming context. Loading @@ -545,6 +849,10 @@ void ActiveDirectory::loadDomains() { d.meta.baseDn = dsdb::dnFromDomain(e.name); d.meta.realm = dsdb::upper(e.name); d.dir = std::make_unique<dsdb::Directory>(*bck, d.meta); /* A replication slave is read-only — it must not provision (that would * allocate RIDs locally and fight the master's snapshot). Its directory * is populated by replication; skip provisioning here. */ if (!(g_Replication && g_Replication->isSlave())) { try { d.dir->provision(_AdminPassword); if (!_DcHost.empty()) Loading @@ -552,6 +860,7 @@ void ActiveDirectory::loadDomains() { } catch (std::exception &ex) { std::cerr << "AD: provision '" << e.name << "' failed: " << ex.what() << std::endl; } } /* pick up the domain SID the head carries */ if (auto head = d.dir->byDn(d.meta.baseDn)) d.meta.sid = head->objectSid; Loading
plugin/ad/ad.h +28 −0 Changes for plugin/ad/ad.h: 28 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -277,6 +277,32 @@ namespace fenrirds { bool createWindowsGroup(const std::string &domain, const std::string &sam, std::string &err) override; /* unified directory CRUD (see AdPlugin for the contract) */ void reloadDomains() override { loadDomains(); } 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, dsdb::Entry &out, std::string &err) override; bool dirGetBySam(const std::string &domain, const std::string &sam, dsdb::Entry &out, std::string &err) override; bool dirCreate(const std::string &domain, int kind, dsdb::AttrMap attrs, const std::string &pw, bool windowsLogin, uuid::uuid &outGuid, std::string &err) override; bool dirModify(const std::string &domain, const uuid::uuid &guid, const dsdb::AttrMap &attrs, const std::string &pw, std::string &err) override; bool dirRemove(const std::string &domain, const uuid::uuid &guid, std::string &err) override; bool dirAuthenticate(const std::string &domain, const std::string &sam, const std::string &pw, uuid::uuid &outGuid, std::string &err) override; bool dirMemberships(const std::string &domain, const uuid::uuid &guid, std::vector<uuid::uuid> &groups, std::string &err) override; bool dirAddMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, std::string &err) override; bool dirRemoveMember(const std::string &domain, const uuid::uuid &group, const uuid::uuid &member, std::string &err) override; /* netplus::eventapi overrides */ void RequestEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override; void ResponseEvent(netplus::con &curcon, const int tid, ULONG_PTR args) override; Loading Loading @@ -329,6 +355,8 @@ namespace fenrirds { void loadDomains(); /* the domain whose base DN contains dn (longest match), or nullptr */ Dom *domainForDn(const dsdb::Dn &dn); /* the domain by dnsName (case-insensitive), or nullptr; caller holds _Lock */ Dom *domainByName(const std::string &dnsName); /* render a directory entry as an LdapEntry honoring the requested * attribute list (empty / "*" = all user attrs; "+" = operational; * "1.1" = none; secret attributes are never returned) */ Loading
plugin/dns/dns.cpp +2 −2 Changes for plugin/dns/dns.cpp: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -334,7 +334,7 @@ void DnsServer::run() { /* TCP responder in its own thread (prefixed with a 2-byte length) */ _TcpThread = std::thread([this]() { while (_Running) { if (!waitReadable(_Tcp->fd(), 500)) if (!_Tcp->waitReadable(500)) continue; std::unique_ptr<netplus::socket> cs; try { Loading Loading @@ -365,7 +365,7 @@ void DnsServer::run() { /* UDP request/response loop (recvmmsg is non-blocking, so poll first) */ while (_Running) { if (!waitReadable(_Udp->fd(), 500)) if (!_Udp->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; Loading
plugin/krb5/krb5.cpp +1 −1 Changes for plugin/krb5/krb5.cpp: 1 added line, 1 removed line. Original line number Diff line number Diff line Loading @@ -2628,7 +2628,7 @@ void Krb5KDC::init() { * 4-byte length prefix). Process it and send the bare response back. */ void Krb5KDC::udpLoop() { while (_Running) { if (!waitReadable(_Udp->fd(), 500)) if (!_Udp->waitReadable(500)) continue; std::vector<std::vector<uint8_t>> dgrams; std::vector<sockaddr_storage> addrs; Loading