Loading data/admin.html +48 −1 Changes for data/admin.html: 48 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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>'; } Loading Loading @@ -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) { Loading src/admin.cpp +88 −1 Changes for src/admin.cpp: 88 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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!"); } Loading Loading @@ -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)) Loading Loading @@ -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"); Loading src/authdb.cpp +7 −5 Changes for src/authdb.cpp: 7 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -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() Loading Loading @@ -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; } Loading Loading @@ -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{ Loading Loading @@ -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; Loading Loading @@ -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; Loading tests/test_backend.cpp +31 −0 Changes for tests/test_backend.cpp: 31 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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) { Loading Loading
data/admin.html +48 −1 Changes for data/admin.html: 48 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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>'; } Loading Loading @@ -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) { Loading
src/admin.cpp +88 −1 Changes for src/admin.cpp: 88 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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!"); } Loading Loading @@ -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)) Loading Loading @@ -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"); Loading
src/authdb.cpp +7 −5 Changes for src/authdb.cpp: 7 added lines, 5 removed lines. Original line number Diff line number Diff line Loading @@ -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() Loading Loading @@ -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; } Loading Loading @@ -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{ Loading Loading @@ -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; Loading Loading @@ -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; Loading
tests/test_backend.cpp +31 −0 Changes for tests/test_backend.cpp: 31 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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) { Loading