Commit 0222ac38 authored by jan.koester's avatar jan.koester
Browse files

test

parent ac4e440d
Loading
Loading
Loading
Loading
Loading
+48 −1
Changes for data/admin.html: 48 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -375,7 +375,8 @@ function loadDomains() {
                    html += '<li>Storageoptions: ' + (d.storageoptions || '') + '</li>';
                    html += '</ul></td>';
                    if (isSuperAdmin) {
                        html += '<td><a class="button danger" href="#" onclick="removeDomain(\'' + d.id + '\'); return false;">Remove</a></td>';
                        html += '<td><a class="button" href="#" onclick="editDomainForm(\'' + d.id + '\'); return false;">Edit</a> ';
                        html += '<a class="button danger" href="#" onclick="removeDomain(\'' + d.id + '\'); return false;">Remove</a></td>';
                    }
                    html += '</tr>';
                }
@@ -456,6 +457,52 @@ function submitCreateDomain() {
    });
}

function editDomainForm(did) {
    ajaxGet("/settings/" + currentDomain + "/api/listdomains", function(err, data) {
        if (err) { showError(err); return; }
        var d = null;
        for (var i = 0; data.domains && i < data.domains.length; i++)
            if (data.domains[i].id === did) d = data.domains[i];
        if (!d) { showError("Domain " + did + " not found."); return; }
        var html = '<h3>Edit Domain</h3>';
        html += '<form id="editdomainform" class="admin-form">';
        html += '<table>';
        html += '<tr><td><label>Domain ID:</label></td><td>' + escapeHtml(d.id) + '</td></tr>';
        html += '<tr><td><label>Domain Name:</label></td><td><input type="text" id="ed_name" class="domaintext"></td></tr>';
        html += '<tr><td><label>Domain Storage:</label></td><td><select id="ed_storage" onchange="editDomainStorageChanged()">';
        html += '<option value="0">File</option><option value="2">Cluster</option></select></td></tr>';
        html += '<tr id="ed_options_row"><td><label>Domain Options:</label></td><td><input type="text" id="ed_options" class="domaintext"></td></tr>';
        html += '<tr><td></td><td><small>Changing the storage does not move existing data. Cluster domains can\'t be renamed.</small></td></tr>';
        html += '<tr><td></td><td><input type="button" value="Save" onclick="submitEditDomain(\'' + d.id + '\')"></td></tr>';
        html += '</table></form>';
        setContent(html);
        document.getElementById("ed_name").value = d.name || '';
        document.getElementById("ed_storage").value = String(d.storagetype === 2 ? 2 : 0);
        document.getElementById("ed_options").value = d.storageoptions || '';
        editDomainStorageChanged();
    });
}

function editDomainStorageChanged() {
    var isFile = document.getElementById("ed_storage").value === "0";
    document.getElementById("ed_options_row").style.display = isFile ? '' : 'none';
}

function submitEditDomain(did) {
    var storage = parseInt(document.getElementById("ed_storage").value);
    var name = document.getElementById("ed_name").value.trim();
    var options = document.getElementById("ed_options").value.trim();
    if (!name || (storage === 0 && !options)) {
        showError("Domain Name and, for File storage, Domain Options are required.");
        return;
    }
    var obj = { domainname: name, domainstorage: storage, domainoptions: storage === 0 ? options : "" };
    ajaxPost("/settings/" + currentDomain + "/api/editdomain/" + did, obj, function(err, data) {
        if (err) { showError(err); return; }
        loadDomains();
    });
}

function removeDomain(did) {
    if (!confirm("Remove domain " + did + "?")) return;
    ajaxDelete("/settings/" + currentDomain + "/api/removedomain/" + did, function(err, data) {
+88 −1
Changes for src/admin.cpp: 88 added lines, 1 removed line.
Original line number Diff line number Diff line
@@ -834,8 +834,10 @@ namespace authdb {
                throw AuthBackendError("createDomain: missing fields!");
            }

            // Cluster domains are keyed by name; only File needs a storage path.
            if (strlen(json_object_get_string(jname)) == 0 ||
                strlen(json_object_get_string(joptions)) == 0) {
                (json_object_get_int(jstorage) == AuthBackendType::File &&
                 strlen(json_object_get_string(joptions)) == 0)) {
                json_object_put(jreq);
                throw AuthBackendError("createDomain: domainname and domainoptions must not be empty!");
            }
@@ -996,6 +998,89 @@ namespace authdb {
            json_object_put(jobj);
        }

        /* ---- JSON API: Edit Domain ---- */
        void apiEditDomain(libhttppp::HttpRequest &curreq, const char *cdid, const SessionData *sdat) {
            if (sdat && !isSuperAdmin(sdat))
                throw AuthBackendError("editDomain: nur SuperAdmins von admin.local erlaubt!");

            uuid::uuid did;
            if (did.parse(cdid) != 0)
                throw AuthBackendError("editDomain: invalid domain id!");

            std::string reqbody = getRequestBody(curreq);
            json_object *jreq = json_tokener_parse(reqbody.c_str());
            if (!jreq) throw AuthBackendError("editDomain: invalid JSON!");

            json_object *jname = json_object_object_get(jreq, "domainname");
            json_object *jstorage = json_object_object_get(jreq, "domainstorage");
            json_object *joptions = json_object_object_get(jreq, "domainoptions");

            Domain domain;
            class DomainData cur(did);
            {
                size_t dpos = sizeof(authdb::AuthHeader);
                domain.info(_AdminBackend, cur, dpos);
            }
            std::string oldname;
            try {
                oldname = cur.getDomainName();
            } catch (AuthBackendError &) {
                json_object_put(jreq);
                throw AuthBackendError("editDomain: domain not found!");
            }
            // A damaged record may lack storage fields; the request then has to supply them.
            int oldstorage = -1;
            std::string oldoptions;
            try { oldstorage = cur.getStorageType(); } catch (AuthBackendError &) {}
            try { oldoptions = cur.getStorageOptions(); } catch (AuthBackendError &) {}

            std::string name = jname ? json_object_get_string(jname) : oldname;
            int storage = jstorage ? json_object_get_int(jstorage) : oldstorage;
            std::string options = joptions ? json_object_get_string(joptions) : oldoptions;
            json_object_put(jreq);

            if (name.empty())
                throw AuthBackendError("editDomain: domainname must not be empty!");
            if (storage != AuthBackendType::File && storage != AuthBackendType::ClusterStore)
                throw AuthBackendError("editDomain: unknown domainstorage!");
            if (storage == AuthBackendType::File && options.empty())
                throw AuthBackendError("editDomain: File storage needs domainoptions!");

            if (name != oldname) {
                // ClusterBackend derives its block group from the domain name:
                // a rename would orphan all of the domain's cluster data.
                if (storage == AuthBackendType::ClusterStore || oldstorage == AuthBackendType::ClusterStore)
                    throw AuthBackendError("editDomain: cluster domains can't be renamed!");
                std::vector<uuid::uuid> dids;
                domain.list(_AdminBackend, dids);
                for (const auto &odid : dids) {
                    if (odid == did) continue;
                    class DomainData odat(odid);
                    size_t dpos = sizeof(authdb::AuthHeader);
                    domain.info(_AdminBackend, odat, dpos);
                    std::string oname;
                    try { oname = odat.getDomainName(); } catch (AuthBackendError &) { continue; }
                    if (oname == name)
                        throw AuthBackendError("editDomain: Domainname already exists!");
                }
            }

            class DomainData editrec(did);
            editrec.setDomainName(name);
            editrec.setStorageType(storage);
            editrec.setStorageOptions(options);
            _AdminBackend.editRecord(editrec, DataType::DomainData);

            // Evict from domain cache so the next request opens the new storage
            if (_DomainCacheEvictor)
                _DomainCacheEvictor(cdid);

            json_object *jobj = json_object_new_object();
            json_object_object_add(jobj, "status", json_object_new_string("ok"));
            sendJson(curreq, jobj);
            json_object_put(jobj);
        }

        /* ---- JSON API: Remove Domain ---- */
        void apiRemoveDomain(libhttppp::HttpRequest &curreq, const char *cdid, const SessionData *sdat) {
            if (sdat && !isSuperAdmin(sdat))
@@ -2276,6 +2361,8 @@ namespace authdb {
                        apiCreateDomain(curreq, sdat);
                    } else if (strcmp(apiurl, "removedomain") == 0 && param[0]) {
                        apiRemoveDomain(curreq, param, sdat);
                    } else if (strcmp(apiurl, "editdomain") == 0 && param[0]) {
                        apiEditDomain(curreq, param, sdat);
                    } else if (strcmp(apiurl, "listgroups") == 0) {
                        checkDomainAccess(sdat, domain);
                        checkGPO(sdat, "a1b2c3d4-452e-11f0-b1cc-1a2b3c4d5e03");
+7 −5
Changes for src/authdb.cpp: 7 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -299,7 +299,7 @@ namespace authdb {
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP503);
                        rep.setContentType("text/html");
                        rep.send(curreq,"cluster data unavailable — waiting for peers",45);
                        rep.send(curreq,"cluster data unavailable — waiting for peers",46);
                        return;
                    }
                    std::cerr << "[AUTHDB] wizard check: end=" << _AdminBackend.end()
@@ -401,7 +401,7 @@ namespace authdb {
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP500);
                        rep.setContentType("application/json");
                        rep.send(curreq,"[{\"error\":\"can't parse domain id\"}]",36);
                        rep.send(curreq,"[{\"error\":\"can't parse domain id\"}]",35);
                        return;
                    }

@@ -481,7 +481,7 @@ namespace authdb {
                        libhttppp::HttpResponse rep;
                        rep.setState(HTTP404);
                        rep.setContentType("application/json");
                        rep.send(curreq,"[{\"error\":\"uuid not found\"}]",27);
                        rep.send(curreq,"[{\"error\":\"uuid not found\"}]",28);
                    }
                }else if(curreq.getRequestURL().compare(0,10,"/getavatar",10)==0){
                    try{
@@ -678,7 +678,9 @@ namespace authdb {
            if(!dinfo.found)
                return nullptr;

            if(dinfo.storageOptions.empty()){
            // ClusterBackend keys its data by domain name and ignores the
            // options, so only a File domain needs a storage path.
            if(dinfo.storageType == AuthBackendType::File && dinfo.storageOptions.empty()){
                std::cerr << "[AUTHDB] domain '" << dinfo.domainName
                          << "': empty storage path" << std::endl;
                return nullptr;
@@ -715,7 +717,7 @@ namespace authdb {
                    dinfo = db.resolve(did);
                }
                if (!dinfo.found) continue;
                if (dinfo.storageOptions.empty()) {
                if (dinfo.storageType == AuthBackendType::File && dinfo.storageOptions.empty()) {
                    std::cerr << "[AUTHDB] skipping domain '" << dinfo.domainName
                              << "': empty storage path" << std::endl;
                    continue;
+31 −0
Changes for tests/test_backend.cpp: 31 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -240,6 +240,37 @@ TEST_F(FileBackendTest, CreateAndReadDomain) {
    }
}

// What apiEditDomain does: a cluster domain stored without options gets
// switched to File storage with a path, and resolve() sees the new values.
TEST_F(FileBackendTest, EditDomainStorage) {
    uuid::uuid did;
    did.generate();

    {
        class authdb::DomainData d(did);
        d.setDomainName("corp.example.com");
        d.setStorageType(authdb::AuthBackendType::ClusterStore);
        d.setStorageOptions("");
        Domain dom;
        dom.create(*backend, &d);
    }

    {
        class authdb::DomainData d(did);
        d.setDomainName("corp.example.com");
        d.setStorageType(authdb::AuthBackendType::File);
        d.setStorageOptions("/tmp/corp.db");
        backend->editRecord(d, authdb::DataType::DomainData);
    }

    authdb::DomainBackend db(*backend);
    authdb::DomainInfo info = db.resolve(did);
    ASSERT_TRUE(info.found);
    EXPECT_EQ(info.domainName, "corp.example.com");
    EXPECT_EQ(info.storageType, authdb::AuthBackendType::File);
    EXPECT_EQ(info.storageOptions, "/tmp/corp.db");
}

// ── Revision tracking ──

TEST_F(FileBackendTest, RevisionIncrementsOnWrite) {