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

test

parent 301c75f5
Loading
Loading
Loading
Loading
Loading
+6 −0
Changes for data/admin.html: 6 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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>';
@@ -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)
+310 −1
Changes for plugin/ad/ad.cpp: 310 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -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"
@@ -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;
@@ -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 &dd;
    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.
@@ -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())
@@ -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;
+28 −0
Changes for plugin/ad/ad.h: 28 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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;
@@ -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) */
+2 −2
Changes for plugin/dns/dns.cpp: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -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 {
@@ -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;
+1 −1
Changes for plugin/krb5/krb5.cpp: 1 added line, 1 removed line.
Original line number Diff line number Diff line
@@ -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