Loading src/auth.cpp +32 −16 Original line number Diff line number Diff line Loading @@ -173,6 +173,22 @@ sess.addSessionData(*ssid,"uid",uid.c_str()); } } void blogi::Auth::_doLoginWithFailover(const std::vector<const AuthSource*> &srcs, const char *username, const char *password, uuid::uuid &authid, const int tid, const std::string *ssid){ for(size_t i = 0; i < srcs.size(); ++i){ try { _doLogin(*srcs[i], username, password, authid, tid, ssid); return; } catch (...) { if(i + 1 == srcs.size()) throw; // Endpoint unreachable/rejected -- try the next failover entry // for this realm before giving up. } } } void blogi::Auth::login(const int tid,const char* username, const char* password,uuid::uuid &authid, const std::string &ssid, const std::string &domain){ libhttppp::HTTPException httpException; Loading @@ -194,22 +210,22 @@ void blogi::Auth::login(const int tid,const char* username, const char* password user = userNameOnly; } const AuthSource *src = _config.findAuthSource(dom); if(!src && !userDomain.empty()){ src = _config.findAuthSource(userDomain); if(src) std::vector<const AuthSource*> srcs = _config.findAuthSources(dom); if(srcs.empty() && !userDomain.empty()){ srcs = _config.findAuthSources(userDomain); if(!srcs.empty()) user = userNameOnly; } if(!src){ src = _config.findAuthSource(""); if(srcs.empty()){ srcs = _config.findAuthSources(""); } if(!src){ if(srcs.empty()){ httpException[libhttppp::HTTPException::Error] << "No auth source found for domain: " << dom; throw httpException; } _doLogin(*src, user.c_str(), password, authid, tid, &ssid); _doLoginWithFailover(srcs, user.c_str(), password, authid, tid, &ssid); } void blogi::Auth::Apilogin(const int tid,const char *username,const char *password,uuid::uuid &authid, const std::string &domain){ Loading @@ -233,22 +249,22 @@ void blogi::Auth::Apilogin(const int tid,const char *username,const char *passwo user = userNameOnly; } const AuthSource *src = _config.findAuthSource(dom); if(!src && !userDomain.empty()){ src = _config.findAuthSource(userDomain); if(src) std::vector<const AuthSource*> srcs = _config.findAuthSources(dom); if(srcs.empty() && !userDomain.empty()){ srcs = _config.findAuthSources(userDomain); if(!srcs.empty()) user = userNameOnly; } if(!src){ src = _config.findAuthSource(""); if(srcs.empty()){ srcs = _config.findAuthSources(""); } if(!src){ if(srcs.empty()){ httpException[libhttppp::HTTPException::Error] << "No auth source found for domain: " << dom; throw httpException; } _doLogin(*src, user.c_str(), password, authid, tid, nullptr); _doLoginWithFailover(srcs, user.c_str(), password, authid, tid, nullptr); } bool blogi::Auth::isLoggedIn(const int tid,const uuid::uuid &authid){ Loading src/auth.h +6 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,12 @@ namespace blogi { private: void _doLogin(const AuthSource &src, const char *username, const char *password, uuid::uuid &authid, const int tid, const std::string *ssid); // Try each source in turn (failover chain for one realm), returning // on the first that accepts the credentials; rethrows the last // failure if none do. srcs must be non-empty. void _doLoginWithFailover(const std::vector<const AuthSource*> &srcs, const char *username, const char *password, uuid::uuid &authid, const int tid, const std::string *ssid); std::vector<std::unique_ptr<dbpp::ReplicatedDatabase>> &_dbconn; const blogi::Config &_config; }; Loading src/blogi.cpp +276 −3 Original line number Diff line number Diff line Loading @@ -410,6 +410,12 @@ blogi::Blogi::Blogi(std::vector<netplus::socket *> serversocket, bool debug) return; } // Load this domain's auth/media connection list from its own DB, // migrating the YAML-seeded single entry in on first run. Must run // before Auth is constructed below, since Auth reads the source list // via ctx.config. ctx.config->loadSourcesFromDB(*initdb); ctx.plgArgs->smtp = std::make_unique<SmtpSettings>(); try Loading Loading @@ -1319,9 +1325,9 @@ static void renderSettingsJson(json_object *jroot, libhtmlpp::HtmlString &out, c out << "</div>"; } // Standalone table json_object *jtable = nullptr; if (json_object_object_get_ex(jroot, "table", &jtable)) // Render one {headers, rows} table object; shared by the standalone // "table" key and the "tables" (plural, each with its own title) key. auto renderTable = [&](json_object *jtable) { json_object *jheaders = nullptr, *jrows = nullptr; json_object_object_get_ex(jtable, "headers", &jheaders); Loading Loading @@ -1379,6 +1385,29 @@ static void renderSettingsJson(json_object *jroot, libhtmlpp::HtmlString &out, c out << "</tbody>"; } out << "</table>"; }; // Standalone table json_object *jtable = nullptr; if (json_object_object_get_ex(jroot, "table", &jtable)) { renderTable(jtable); } // Multiple titled tables on one page (e.g. settings/connections showing // both the auth and the media source list) json_object *jtables = nullptr; if (json_object_object_get_ex(jroot, "tables", &jtables)) { int tlen = json_object_array_length(jtables); for (int t = 0; t < tlen; ++t) { json_object *jt = json_object_array_get_idx(jtables, t); json_object *jttitle = nullptr; if (json_object_object_get_ex(jt, "title", &jttitle)) out << "<h4 class=\"set-heading\">" << json_object_get_string(jttitle) << "</h4>"; renderTable(jt); } } // Info display (read-only key/value) Loading Loading @@ -2091,6 +2120,11 @@ void blogi::Blogi::settingsApi(libhttppp::HttpRequest &curreq, const int tid, co json_object_object_add(jmail, "name", json_object_new_string("Mail")); json_object_array_add(jnav, jmail); json_object *jconn = json_object_new_object(); json_object_object_add(jconn, "id", json_object_new_string("connections")); json_object_object_add(jconn, "name", json_object_new_string("Connections")); json_object_array_add(jnav, jconn); for (const blogi::Plugin::PluginData *curplg = BlogiPlg->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()) { if (curplg->getInstace()->haveSettings()) Loading Loading @@ -2254,6 +2288,239 @@ static json_object *SettingsLanguage(dbpp::ReplicatedDatabase &db, libhttppp::Ht return jroot; } // Admin UI for this domain's DB-backed auth/media connection lists (see // Config::loadSourcesFromDB). Each list is a failover chain tried in order; // "Domain" on an auth entry additionally tags which named realm it belongs // to (matched by plugins such as schimmeldoc's admin/moderator distinction), // so several entries sharing one domain are redundant endpoints for the same // realm rather than separate realms. static json_object *SettingsConnections(dbpp::ReplicatedDatabase &db, libhttppp::HttpRequest &req, blogi::Config &cfg) { libhttppp::HttpForm form; form.parse(req); std::string message, messageType; std::string removeAuthIdx, removeMediaIdx; std::string connType, connDomain, connUrl, connClientName, connClientSecret, connTls; bool haveAdd = false; for (const auto &cur : form.urlData()) { if (cur.key == "remove_auth") removeAuthIdx = cur.value; else if (cur.key == "remove_media") removeMediaIdx = cur.value; else if (cur.key == "conn_type") { connType = cur.value; haveAdd = true; } else if (cur.key == "conn_domain") connDomain = cur.value; else if (cur.key == "conn_url") connUrl = cur.value; else if (cur.key == "conn_clientname") connClientName = cur.value; else if (cur.key == "conn_clientsecret") connClientSecret = cur.value; else if (cur.key == "conn_tlsverify") connTls = cur.value; } if (!removeAuthIdx.empty()) { size_t idx = static_cast<size_t>(atoi(removeAuthIdx.c_str())); std::vector<blogi::AuthSource> srcs; for (size_t i = 0; i < cfg.getAuthSourceCount(); ++i) if (i != idx) srcs.push_back(cfg.getAuthSource(i)); cfg.saveAuthSources(db, srcs); message = "Auth-Verbindung entfernt."; messageType = "success"; } else if (!removeMediaIdx.empty()) { size_t idx = static_cast<size_t>(atoi(removeMediaIdx.c_str())); std::vector<blogi::MediaSource> srcs; for (size_t i = 0; i < cfg.getMediaSourceCount(); ++i) if (i != idx) srcs.push_back(cfg.getMediaSource(i)); cfg.saveMediaSources(db, srcs); message = "Media-Verbindung entfernt."; messageType = "success"; } else if (haveAdd && !connUrl.empty()) { bool tlsVerify = (connTls == "on" || connTls == "true" || connTls == "1"); if (connType == "media") { std::vector<blogi::MediaSource> srcs; for (size_t i = 0; i < cfg.getMediaSourceCount(); ++i) srcs.push_back(cfg.getMediaSource(i)); blogi::MediaSource ms; ms.url = connUrl; ms.tlsVerify = tlsVerify; srcs.push_back(ms); cfg.saveMediaSources(db, srcs); } else { std::vector<blogi::AuthSource> srcs; for (size_t i = 0; i < cfg.getAuthSourceCount(); ++i) srcs.push_back(cfg.getAuthSource(i)); blogi::AuthSource as; as.url = connUrl; as.domain = connDomain; as.clientName = connClientName; as.clientSecret = connClientSecret; as.tlsVerify = tlsVerify; srcs.push_back(as); cfg.saveAuthSources(db, srcs); } message = "Verbindung hinzugefuegt."; messageType = "success"; } json_object *jroot = json_object_new_object(); json_object_object_add(jroot, "title", json_object_new_string("Connections")); if (!message.empty()) { json_object_object_add(jroot, "message", json_object_new_string(message.c_str())); json_object_object_add(jroot, "message_type", json_object_new_string(messageType.c_str())); } json_object *jtables = json_object_new_array(); json_object *jauthTable = json_object_new_object(); json_object_object_add(jauthTable, "title", json_object_new_string( "Auth-Verbindungen (Failover-Kette je Domain -- mehrere Eintraege mit gleicher Domain werden der Reihe nach versucht)")); { json_object *jheaders = json_object_new_array(); for (const char *h : {"Domain", "URL", "Client", "TLS", ""}) json_object_array_add(jheaders, json_object_new_string(h)); json_object_object_add(jauthTable, "headers", jheaders); json_object *jrows = json_object_new_array(); for (size_t i = 0; i < cfg.getAuthSourceCount(); ++i) { const auto &s = cfg.getAuthSource(i); json_object *jrow = json_object_new_object(); json_object *jcells = json_object_new_array(); json_object_array_add(jcells, json_object_new_string(s.domain.c_str())); json_object_array_add(jcells, json_object_new_string(s.url.c_str())); json_object_array_add(jcells, json_object_new_string(s.clientName.c_str())); json_object_array_add(jcells, json_object_new_string(s.tlsVerify ? "ja" : "nein")); json_object_object_add(jrow, "cells", jcells); json_object *jactions = json_object_new_array(); json_object *jrem = json_object_new_object(); json_object_object_add(jrem, "label", json_object_new_string("Entfernen")); json_object_object_add(jrem, "url", json_object_new_string( (req.getRequestURL() + "?remove_auth=" + std::to_string(i)).c_str())); json_object_array_add(jactions, jrem); json_object_object_add(jrow, "actions", jactions); json_object_array_add(jrows, jrow); } json_object_object_add(jauthTable, "rows", jrows); } json_object_array_add(jtables, jauthTable); json_object *jmediaTable = json_object_new_object(); json_object_object_add(jmediaTable, "title", json_object_new_string( "Media-Verbindungen (Failover-Kette -- der Reihe nach versucht)")); { json_object *jheaders = json_object_new_array(); for (const char *h : {"URL", "TLS", ""}) json_object_array_add(jheaders, json_object_new_string(h)); json_object_object_add(jmediaTable, "headers", jheaders); json_object *jrows = json_object_new_array(); for (size_t i = 0; i < cfg.getMediaSourceCount(); ++i) { const auto &s = cfg.getMediaSource(i); json_object *jrow = json_object_new_object(); json_object *jcells = json_object_new_array(); json_object_array_add(jcells, json_object_new_string(s.url.c_str())); json_object_array_add(jcells, json_object_new_string(s.tlsVerify ? "ja" : "nein")); json_object_object_add(jrow, "cells", jcells); json_object *jactions = json_object_new_array(); json_object *jrem = json_object_new_object(); json_object_object_add(jrem, "label", json_object_new_string("Entfernen")); json_object_object_add(jrem, "url", json_object_new_string( (req.getRequestURL() + "?remove_media=" + std::to_string(i)).c_str())); json_object_array_add(jactions, jrem); json_object_object_add(jrow, "actions", jactions); json_object_array_add(jrows, jrow); } json_object_object_add(jmediaTable, "rows", jrows); } json_object_array_add(jtables, jmediaTable); json_object_object_add(jroot, "tables", jtables); // Add-connection form (one form covers both types; irrelevant fields for // the chosen type are simply ignored on save) json_object *jform = json_object_new_object(); json_object_object_add(jform, "method", json_object_new_string("POST")); json_object_object_add(jform, "action", json_object_new_string(req.getRequestURL().c_str())); json_object *jfields = json_object_new_array(); { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_type")); json_object_object_add(jf, "label", json_object_new_string("Typ")); json_object_object_add(jf, "type", json_object_new_string("select")); json_object *jopts = json_object_new_array(); json_object *jo1 = json_object_new_object(); json_object_object_add(jo1, "value", json_object_new_string("auth")); json_object_object_add(jo1, "label", json_object_new_string("Auth (authdb)")); json_object_array_add(jopts, jo1); json_object *jo2 = json_object_new_object(); json_object_object_add(jo2, "value", json_object_new_string("media")); json_object_object_add(jo2, "label", json_object_new_string("Media (mediadb)")); json_object_array_add(jopts, jo2); json_object_object_add(jf, "options", jopts); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_domain")); json_object_object_add(jf, "label", json_object_new_string("Domain (nur Auth -- Realm-Name, z.B. \"admin\"; leer = Standard)")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_url")); json_object_object_add(jf, "label", json_object_new_string("URL")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_clientname")); json_object_object_add(jf, "label", json_object_new_string("Client Name (nur Auth)")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_clientsecret")); json_object_object_add(jf, "label", json_object_new_string("Client Secret (nur Auth)")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_tlsverify")); json_object_object_add(jf, "label", json_object_new_string("TLS Verify")); json_object_object_add(jf, "type", json_object_new_string("checkbox")); json_object_array_add(jfields, jf); } json_object_object_add(jform, "fields", jfields); json_object_object_add(jform, "submit", json_object_new_string("Hinzufuegen")); json_object_object_add(jroot, "form", jform); return jroot; } void blogi::Blogi::Settings(libhttppp::HttpRequest &curreq, const int tid, const std::string &sessiondid, libhtmlpp::HtmlElement &index, DomainContext &ctx) { auto &PlgArgs = ctx.plgArgs; Loading Loading @@ -2307,6 +2574,11 @@ void blogi::Blogi::Settings(libhttppp::HttpRequest &curreq, const int tid, const { jformdata = SettingsMail(*PlgArgs->database[tid], curreq, sessiondid); } else if (url.compare(0, ctx.buildurl("settings/connections").length(), ctx.buildurl("settings/connections")) == 0) { jformdata = SettingsConnections(*PlgArgs->database[tid], curreq, *ctx.config); } else if (url.compare(0, ctx.buildurl("settings/theme").length(), ctx.buildurl("settings/theme")) == 0) { Loading Loading @@ -2595,6 +2867,7 @@ void blogi::Blogi::Settings(libhttppp::HttpRequest &curreq, const int tid, const << "</div>" << "<ul class=\"set-nav\" id=\"set-nav\">" << "<li data-url=\"" << ctx.buildurl("settings/mail") << "\">Mail</li>" << "<li data-url=\"" << ctx.buildurl("settings/connections") << "\">Connections</li>" << "<li data-url=\"" << ctx.buildurl("settings/theme") << "\">Theme</li>" << "<li data-url=\"" << ctx.buildurl("settings/language") << "\">" << blogi::tr(_lang, "Language") << "</li>"; Loading src/conf.cpp +157 −1 File changed.Preview size limit exceeded, changes collapsed. Show changes src/conf.h +48 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,12 @@ namespace blogi { bool tlsVerify = true; }; struct MediaSource { std::string url; // See DomainConfig::mediaTlsVerify. bool tlsVerify = false; }; struct ReplicaConfig { std::string driver; std::string connection; Loading @@ -64,6 +70,14 @@ namespace blogi { // Peer verification for the authdb connection (AUTH_TLS_VERIFY). Defaults to on; // set false for an authdb endpoint reached by bare IP whose cert SAN doesn't cover it. bool authTlsVerify = true; // Failover chain of authdb endpoints for this domain, tried in order // (see e.g. Auth::login / isLoggedIn). Multiple entries may share the // same .domain tag (redundant endpoints for one realm) or carry // different .domain tags (distinct named realms such as "admin"/ // "moderator" a plugin can select between, see schimmeldoc). Seeded // with one entry from authUrl/clientName/clientSecret/authTlsVerify // on first load, then overridden from the domain's own DB (see // Config::loadSourcesFromDB). std::vector<AuthSource> authSources; std::string siteUrl; std::vector<std::string> siteUrls; Loading @@ -75,6 +89,10 @@ namespace blogi { // Peer verification for the mediadb connection (MEDIA_TLS_VERIFY). Defaults to off, // matching mediadb's typical same-host/self-signed deployment. bool mediaTlsVerify = false; // Failover chain of mediadb endpoints for this domain, tried in order. // Seeded with one entry from mediaDBUrl/mediaTlsVerify on first load, // then overridden from the domain's own DB (see Config::loadSourcesFromDB). std::vector<MediaSource> mediaSources; std::string tmpDir; const std::string buildurl(const std::string &url) const; Loading Loading @@ -112,6 +130,12 @@ namespace blogi { size_t getAuthSourceCount() const; const AuthSource &getAuthSource(size_t idx) const; const AuthSource *findAuthSource(const std::string &domain) const; // All auth sources tagged with domain, in configured (failover) order. // Empty if none match. Used where a single credential submission must // be retried against every endpoint of one named realm (see // Auth::login/Apilogin), as opposed to findAuthSource()'s single best // match used for read-only checks that just need any match. std::vector<const AuthSource*> findAuthSources(const std::string &domain) const; int gethttpport() const; const std::string &gethttpaddr() const; Loading @@ -137,9 +161,15 @@ namespace blogi { const std::string &getTmpDir() const; // Primary (first) mediadb endpoint -- kept for callers that don't // need failover. New code should prefer getMediaSourceCount()/ // getMediaSource() and try each in order. const std::string &getMediaDBUrl() const; bool getMediaTlsVerify() const; size_t getMediaSourceCount() const; const MediaSource &getMediaSource(size_t idx) const; // Multi-domain support size_t getDomainCount() const; const DomainConfig &getDomainConfig(size_t idx) const; Loading @@ -155,6 +185,23 @@ namespace blogi { // Used by per-domain worker processes spawned by the supervisor. void filterDomains(size_t domainIndex); // ---- DB-backed auth/media connections (SCHIMMELDOC-style options table) ---- // // AUTH_URL/AUTH_CLIENTNAME/.../MEDIA_URL in config.yaml only ever seed // ONE endpoint per domain and require a restart to change. This loads // the real (possibly multi-entry, failover) lists from this domain's // own `options` table instead, migrating the YAML-seeded single entry // into the DB the first time a domain starts with no DB entries yet. // Call once per domain, after the `options` table exists and before // Auth/plugins are constructed (see blogi.cpp's initCtx). void loadSourcesFromDB(dbpp::ReplicatedDatabase &db); // Persist the given list as this domain's auth/media sources (used by // the settings/connections admin page) and update the in-memory // lists so the change is live without a restart. void saveAuthSources(dbpp::ReplicatedDatabase &db, const std::vector<AuthSource> &srcs); void saveMediaSources(dbpp::ReplicatedDatabase &db, const std::vector<MediaSource> &srcs); private: std::string _ConfigPath; std::vector<std::string> _PlgDir; Loading Loading @@ -183,6 +230,7 @@ namespace blogi { std::string _TmpDir; std::string _MediaDBUrl; bool _MediaTlsVerify = false; std::vector<MediaSource> _MediaSources; std::vector<DomainConfig> _Domains; }; Loading Loading
src/auth.cpp +32 −16 Original line number Diff line number Diff line Loading @@ -173,6 +173,22 @@ sess.addSessionData(*ssid,"uid",uid.c_str()); } } void blogi::Auth::_doLoginWithFailover(const std::vector<const AuthSource*> &srcs, const char *username, const char *password, uuid::uuid &authid, const int tid, const std::string *ssid){ for(size_t i = 0; i < srcs.size(); ++i){ try { _doLogin(*srcs[i], username, password, authid, tid, ssid); return; } catch (...) { if(i + 1 == srcs.size()) throw; // Endpoint unreachable/rejected -- try the next failover entry // for this realm before giving up. } } } void blogi::Auth::login(const int tid,const char* username, const char* password,uuid::uuid &authid, const std::string &ssid, const std::string &domain){ libhttppp::HTTPException httpException; Loading @@ -194,22 +210,22 @@ void blogi::Auth::login(const int tid,const char* username, const char* password user = userNameOnly; } const AuthSource *src = _config.findAuthSource(dom); if(!src && !userDomain.empty()){ src = _config.findAuthSource(userDomain); if(src) std::vector<const AuthSource*> srcs = _config.findAuthSources(dom); if(srcs.empty() && !userDomain.empty()){ srcs = _config.findAuthSources(userDomain); if(!srcs.empty()) user = userNameOnly; } if(!src){ src = _config.findAuthSource(""); if(srcs.empty()){ srcs = _config.findAuthSources(""); } if(!src){ if(srcs.empty()){ httpException[libhttppp::HTTPException::Error] << "No auth source found for domain: " << dom; throw httpException; } _doLogin(*src, user.c_str(), password, authid, tid, &ssid); _doLoginWithFailover(srcs, user.c_str(), password, authid, tid, &ssid); } void blogi::Auth::Apilogin(const int tid,const char *username,const char *password,uuid::uuid &authid, const std::string &domain){ Loading @@ -233,22 +249,22 @@ void blogi::Auth::Apilogin(const int tid,const char *username,const char *passwo user = userNameOnly; } const AuthSource *src = _config.findAuthSource(dom); if(!src && !userDomain.empty()){ src = _config.findAuthSource(userDomain); if(src) std::vector<const AuthSource*> srcs = _config.findAuthSources(dom); if(srcs.empty() && !userDomain.empty()){ srcs = _config.findAuthSources(userDomain); if(!srcs.empty()) user = userNameOnly; } if(!src){ src = _config.findAuthSource(""); if(srcs.empty()){ srcs = _config.findAuthSources(""); } if(!src){ if(srcs.empty()){ httpException[libhttppp::HTTPException::Error] << "No auth source found for domain: " << dom; throw httpException; } _doLogin(*src, user.c_str(), password, authid, tid, nullptr); _doLoginWithFailover(srcs, user.c_str(), password, authid, tid, nullptr); } bool blogi::Auth::isLoggedIn(const int tid,const uuid::uuid &authid){ Loading
src/auth.h +6 −0 Original line number Diff line number Diff line Loading @@ -53,6 +53,12 @@ namespace blogi { private: void _doLogin(const AuthSource &src, const char *username, const char *password, uuid::uuid &authid, const int tid, const std::string *ssid); // Try each source in turn (failover chain for one realm), returning // on the first that accepts the credentials; rethrows the last // failure if none do. srcs must be non-empty. void _doLoginWithFailover(const std::vector<const AuthSource*> &srcs, const char *username, const char *password, uuid::uuid &authid, const int tid, const std::string *ssid); std::vector<std::unique_ptr<dbpp::ReplicatedDatabase>> &_dbconn; const blogi::Config &_config; }; Loading
src/blogi.cpp +276 −3 Original line number Diff line number Diff line Loading @@ -410,6 +410,12 @@ blogi::Blogi::Blogi(std::vector<netplus::socket *> serversocket, bool debug) return; } // Load this domain's auth/media connection list from its own DB, // migrating the YAML-seeded single entry in on first run. Must run // before Auth is constructed below, since Auth reads the source list // via ctx.config. ctx.config->loadSourcesFromDB(*initdb); ctx.plgArgs->smtp = std::make_unique<SmtpSettings>(); try Loading Loading @@ -1319,9 +1325,9 @@ static void renderSettingsJson(json_object *jroot, libhtmlpp::HtmlString &out, c out << "</div>"; } // Standalone table json_object *jtable = nullptr; if (json_object_object_get_ex(jroot, "table", &jtable)) // Render one {headers, rows} table object; shared by the standalone // "table" key and the "tables" (plural, each with its own title) key. auto renderTable = [&](json_object *jtable) { json_object *jheaders = nullptr, *jrows = nullptr; json_object_object_get_ex(jtable, "headers", &jheaders); Loading Loading @@ -1379,6 +1385,29 @@ static void renderSettingsJson(json_object *jroot, libhtmlpp::HtmlString &out, c out << "</tbody>"; } out << "</table>"; }; // Standalone table json_object *jtable = nullptr; if (json_object_object_get_ex(jroot, "table", &jtable)) { renderTable(jtable); } // Multiple titled tables on one page (e.g. settings/connections showing // both the auth and the media source list) json_object *jtables = nullptr; if (json_object_object_get_ex(jroot, "tables", &jtables)) { int tlen = json_object_array_length(jtables); for (int t = 0; t < tlen; ++t) { json_object *jt = json_object_array_get_idx(jtables, t); json_object *jttitle = nullptr; if (json_object_object_get_ex(jt, "title", &jttitle)) out << "<h4 class=\"set-heading\">" << json_object_get_string(jttitle) << "</h4>"; renderTable(jt); } } // Info display (read-only key/value) Loading Loading @@ -2091,6 +2120,11 @@ void blogi::Blogi::settingsApi(libhttppp::HttpRequest &curreq, const int tid, co json_object_object_add(jmail, "name", json_object_new_string("Mail")); json_object_array_add(jnav, jmail); json_object *jconn = json_object_new_object(); json_object_object_add(jconn, "id", json_object_new_string("connections")); json_object_object_add(jconn, "name", json_object_new_string("Connections")); json_object_array_add(jnav, jconn); for (const blogi::Plugin::PluginData *curplg = BlogiPlg->getFirstPlugin(); curplg; curplg = curplg->getNextPlg()) { if (curplg->getInstace()->haveSettings()) Loading Loading @@ -2254,6 +2288,239 @@ static json_object *SettingsLanguage(dbpp::ReplicatedDatabase &db, libhttppp::Ht return jroot; } // Admin UI for this domain's DB-backed auth/media connection lists (see // Config::loadSourcesFromDB). Each list is a failover chain tried in order; // "Domain" on an auth entry additionally tags which named realm it belongs // to (matched by plugins such as schimmeldoc's admin/moderator distinction), // so several entries sharing one domain are redundant endpoints for the same // realm rather than separate realms. static json_object *SettingsConnections(dbpp::ReplicatedDatabase &db, libhttppp::HttpRequest &req, blogi::Config &cfg) { libhttppp::HttpForm form; form.parse(req); std::string message, messageType; std::string removeAuthIdx, removeMediaIdx; std::string connType, connDomain, connUrl, connClientName, connClientSecret, connTls; bool haveAdd = false; for (const auto &cur : form.urlData()) { if (cur.key == "remove_auth") removeAuthIdx = cur.value; else if (cur.key == "remove_media") removeMediaIdx = cur.value; else if (cur.key == "conn_type") { connType = cur.value; haveAdd = true; } else if (cur.key == "conn_domain") connDomain = cur.value; else if (cur.key == "conn_url") connUrl = cur.value; else if (cur.key == "conn_clientname") connClientName = cur.value; else if (cur.key == "conn_clientsecret") connClientSecret = cur.value; else if (cur.key == "conn_tlsverify") connTls = cur.value; } if (!removeAuthIdx.empty()) { size_t idx = static_cast<size_t>(atoi(removeAuthIdx.c_str())); std::vector<blogi::AuthSource> srcs; for (size_t i = 0; i < cfg.getAuthSourceCount(); ++i) if (i != idx) srcs.push_back(cfg.getAuthSource(i)); cfg.saveAuthSources(db, srcs); message = "Auth-Verbindung entfernt."; messageType = "success"; } else if (!removeMediaIdx.empty()) { size_t idx = static_cast<size_t>(atoi(removeMediaIdx.c_str())); std::vector<blogi::MediaSource> srcs; for (size_t i = 0; i < cfg.getMediaSourceCount(); ++i) if (i != idx) srcs.push_back(cfg.getMediaSource(i)); cfg.saveMediaSources(db, srcs); message = "Media-Verbindung entfernt."; messageType = "success"; } else if (haveAdd && !connUrl.empty()) { bool tlsVerify = (connTls == "on" || connTls == "true" || connTls == "1"); if (connType == "media") { std::vector<blogi::MediaSource> srcs; for (size_t i = 0; i < cfg.getMediaSourceCount(); ++i) srcs.push_back(cfg.getMediaSource(i)); blogi::MediaSource ms; ms.url = connUrl; ms.tlsVerify = tlsVerify; srcs.push_back(ms); cfg.saveMediaSources(db, srcs); } else { std::vector<blogi::AuthSource> srcs; for (size_t i = 0; i < cfg.getAuthSourceCount(); ++i) srcs.push_back(cfg.getAuthSource(i)); blogi::AuthSource as; as.url = connUrl; as.domain = connDomain; as.clientName = connClientName; as.clientSecret = connClientSecret; as.tlsVerify = tlsVerify; srcs.push_back(as); cfg.saveAuthSources(db, srcs); } message = "Verbindung hinzugefuegt."; messageType = "success"; } json_object *jroot = json_object_new_object(); json_object_object_add(jroot, "title", json_object_new_string("Connections")); if (!message.empty()) { json_object_object_add(jroot, "message", json_object_new_string(message.c_str())); json_object_object_add(jroot, "message_type", json_object_new_string(messageType.c_str())); } json_object *jtables = json_object_new_array(); json_object *jauthTable = json_object_new_object(); json_object_object_add(jauthTable, "title", json_object_new_string( "Auth-Verbindungen (Failover-Kette je Domain -- mehrere Eintraege mit gleicher Domain werden der Reihe nach versucht)")); { json_object *jheaders = json_object_new_array(); for (const char *h : {"Domain", "URL", "Client", "TLS", ""}) json_object_array_add(jheaders, json_object_new_string(h)); json_object_object_add(jauthTable, "headers", jheaders); json_object *jrows = json_object_new_array(); for (size_t i = 0; i < cfg.getAuthSourceCount(); ++i) { const auto &s = cfg.getAuthSource(i); json_object *jrow = json_object_new_object(); json_object *jcells = json_object_new_array(); json_object_array_add(jcells, json_object_new_string(s.domain.c_str())); json_object_array_add(jcells, json_object_new_string(s.url.c_str())); json_object_array_add(jcells, json_object_new_string(s.clientName.c_str())); json_object_array_add(jcells, json_object_new_string(s.tlsVerify ? "ja" : "nein")); json_object_object_add(jrow, "cells", jcells); json_object *jactions = json_object_new_array(); json_object *jrem = json_object_new_object(); json_object_object_add(jrem, "label", json_object_new_string("Entfernen")); json_object_object_add(jrem, "url", json_object_new_string( (req.getRequestURL() + "?remove_auth=" + std::to_string(i)).c_str())); json_object_array_add(jactions, jrem); json_object_object_add(jrow, "actions", jactions); json_object_array_add(jrows, jrow); } json_object_object_add(jauthTable, "rows", jrows); } json_object_array_add(jtables, jauthTable); json_object *jmediaTable = json_object_new_object(); json_object_object_add(jmediaTable, "title", json_object_new_string( "Media-Verbindungen (Failover-Kette -- der Reihe nach versucht)")); { json_object *jheaders = json_object_new_array(); for (const char *h : {"URL", "TLS", ""}) json_object_array_add(jheaders, json_object_new_string(h)); json_object_object_add(jmediaTable, "headers", jheaders); json_object *jrows = json_object_new_array(); for (size_t i = 0; i < cfg.getMediaSourceCount(); ++i) { const auto &s = cfg.getMediaSource(i); json_object *jrow = json_object_new_object(); json_object *jcells = json_object_new_array(); json_object_array_add(jcells, json_object_new_string(s.url.c_str())); json_object_array_add(jcells, json_object_new_string(s.tlsVerify ? "ja" : "nein")); json_object_object_add(jrow, "cells", jcells); json_object *jactions = json_object_new_array(); json_object *jrem = json_object_new_object(); json_object_object_add(jrem, "label", json_object_new_string("Entfernen")); json_object_object_add(jrem, "url", json_object_new_string( (req.getRequestURL() + "?remove_media=" + std::to_string(i)).c_str())); json_object_array_add(jactions, jrem); json_object_object_add(jrow, "actions", jactions); json_object_array_add(jrows, jrow); } json_object_object_add(jmediaTable, "rows", jrows); } json_object_array_add(jtables, jmediaTable); json_object_object_add(jroot, "tables", jtables); // Add-connection form (one form covers both types; irrelevant fields for // the chosen type are simply ignored on save) json_object *jform = json_object_new_object(); json_object_object_add(jform, "method", json_object_new_string("POST")); json_object_object_add(jform, "action", json_object_new_string(req.getRequestURL().c_str())); json_object *jfields = json_object_new_array(); { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_type")); json_object_object_add(jf, "label", json_object_new_string("Typ")); json_object_object_add(jf, "type", json_object_new_string("select")); json_object *jopts = json_object_new_array(); json_object *jo1 = json_object_new_object(); json_object_object_add(jo1, "value", json_object_new_string("auth")); json_object_object_add(jo1, "label", json_object_new_string("Auth (authdb)")); json_object_array_add(jopts, jo1); json_object *jo2 = json_object_new_object(); json_object_object_add(jo2, "value", json_object_new_string("media")); json_object_object_add(jo2, "label", json_object_new_string("Media (mediadb)")); json_object_array_add(jopts, jo2); json_object_object_add(jf, "options", jopts); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_domain")); json_object_object_add(jf, "label", json_object_new_string("Domain (nur Auth -- Realm-Name, z.B. \"admin\"; leer = Standard)")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_url")); json_object_object_add(jf, "label", json_object_new_string("URL")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_clientname")); json_object_object_add(jf, "label", json_object_new_string("Client Name (nur Auth)")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_clientsecret")); json_object_object_add(jf, "label", json_object_new_string("Client Secret (nur Auth)")); json_object_object_add(jf, "type", json_object_new_string("text")); json_object_object_add(jf, "value", json_object_new_string("")); json_object_array_add(jfields, jf); } { json_object *jf = json_object_new_object(); json_object_object_add(jf, "name", json_object_new_string("conn_tlsverify")); json_object_object_add(jf, "label", json_object_new_string("TLS Verify")); json_object_object_add(jf, "type", json_object_new_string("checkbox")); json_object_array_add(jfields, jf); } json_object_object_add(jform, "fields", jfields); json_object_object_add(jform, "submit", json_object_new_string("Hinzufuegen")); json_object_object_add(jroot, "form", jform); return jroot; } void blogi::Blogi::Settings(libhttppp::HttpRequest &curreq, const int tid, const std::string &sessiondid, libhtmlpp::HtmlElement &index, DomainContext &ctx) { auto &PlgArgs = ctx.plgArgs; Loading Loading @@ -2307,6 +2574,11 @@ void blogi::Blogi::Settings(libhttppp::HttpRequest &curreq, const int tid, const { jformdata = SettingsMail(*PlgArgs->database[tid], curreq, sessiondid); } else if (url.compare(0, ctx.buildurl("settings/connections").length(), ctx.buildurl("settings/connections")) == 0) { jformdata = SettingsConnections(*PlgArgs->database[tid], curreq, *ctx.config); } else if (url.compare(0, ctx.buildurl("settings/theme").length(), ctx.buildurl("settings/theme")) == 0) { Loading Loading @@ -2595,6 +2867,7 @@ void blogi::Blogi::Settings(libhttppp::HttpRequest &curreq, const int tid, const << "</div>" << "<ul class=\"set-nav\" id=\"set-nav\">" << "<li data-url=\"" << ctx.buildurl("settings/mail") << "\">Mail</li>" << "<li data-url=\"" << ctx.buildurl("settings/connections") << "\">Connections</li>" << "<li data-url=\"" << ctx.buildurl("settings/theme") << "\">Theme</li>" << "<li data-url=\"" << ctx.buildurl("settings/language") << "\">" << blogi::tr(_lang, "Language") << "</li>"; Loading
src/conf.h +48 −0 Original line number Diff line number Diff line Loading @@ -48,6 +48,12 @@ namespace blogi { bool tlsVerify = true; }; struct MediaSource { std::string url; // See DomainConfig::mediaTlsVerify. bool tlsVerify = false; }; struct ReplicaConfig { std::string driver; std::string connection; Loading @@ -64,6 +70,14 @@ namespace blogi { // Peer verification for the authdb connection (AUTH_TLS_VERIFY). Defaults to on; // set false for an authdb endpoint reached by bare IP whose cert SAN doesn't cover it. bool authTlsVerify = true; // Failover chain of authdb endpoints for this domain, tried in order // (see e.g. Auth::login / isLoggedIn). Multiple entries may share the // same .domain tag (redundant endpoints for one realm) or carry // different .domain tags (distinct named realms such as "admin"/ // "moderator" a plugin can select between, see schimmeldoc). Seeded // with one entry from authUrl/clientName/clientSecret/authTlsVerify // on first load, then overridden from the domain's own DB (see // Config::loadSourcesFromDB). std::vector<AuthSource> authSources; std::string siteUrl; std::vector<std::string> siteUrls; Loading @@ -75,6 +89,10 @@ namespace blogi { // Peer verification for the mediadb connection (MEDIA_TLS_VERIFY). Defaults to off, // matching mediadb's typical same-host/self-signed deployment. bool mediaTlsVerify = false; // Failover chain of mediadb endpoints for this domain, tried in order. // Seeded with one entry from mediaDBUrl/mediaTlsVerify on first load, // then overridden from the domain's own DB (see Config::loadSourcesFromDB). std::vector<MediaSource> mediaSources; std::string tmpDir; const std::string buildurl(const std::string &url) const; Loading Loading @@ -112,6 +130,12 @@ namespace blogi { size_t getAuthSourceCount() const; const AuthSource &getAuthSource(size_t idx) const; const AuthSource *findAuthSource(const std::string &domain) const; // All auth sources tagged with domain, in configured (failover) order. // Empty if none match. Used where a single credential submission must // be retried against every endpoint of one named realm (see // Auth::login/Apilogin), as opposed to findAuthSource()'s single best // match used for read-only checks that just need any match. std::vector<const AuthSource*> findAuthSources(const std::string &domain) const; int gethttpport() const; const std::string &gethttpaddr() const; Loading @@ -137,9 +161,15 @@ namespace blogi { const std::string &getTmpDir() const; // Primary (first) mediadb endpoint -- kept for callers that don't // need failover. New code should prefer getMediaSourceCount()/ // getMediaSource() and try each in order. const std::string &getMediaDBUrl() const; bool getMediaTlsVerify() const; size_t getMediaSourceCount() const; const MediaSource &getMediaSource(size_t idx) const; // Multi-domain support size_t getDomainCount() const; const DomainConfig &getDomainConfig(size_t idx) const; Loading @@ -155,6 +185,23 @@ namespace blogi { // Used by per-domain worker processes spawned by the supervisor. void filterDomains(size_t domainIndex); // ---- DB-backed auth/media connections (SCHIMMELDOC-style options table) ---- // // AUTH_URL/AUTH_CLIENTNAME/.../MEDIA_URL in config.yaml only ever seed // ONE endpoint per domain and require a restart to change. This loads // the real (possibly multi-entry, failover) lists from this domain's // own `options` table instead, migrating the YAML-seeded single entry // into the DB the first time a domain starts with no DB entries yet. // Call once per domain, after the `options` table exists and before // Auth/plugins are constructed (see blogi.cpp's initCtx). void loadSourcesFromDB(dbpp::ReplicatedDatabase &db); // Persist the given list as this domain's auth/media sources (used by // the settings/connections admin page) and update the in-memory // lists so the change is live without a restart. void saveAuthSources(dbpp::ReplicatedDatabase &db, const std::vector<AuthSource> &srcs); void saveMediaSources(dbpp::ReplicatedDatabase &db, const std::vector<MediaSource> &srcs); private: std::string _ConfigPath; std::vector<std::string> _PlgDir; Loading Loading @@ -183,6 +230,7 @@ namespace blogi { std::string _TmpDir; std::string _MediaDBUrl; bool _MediaTlsVerify = false; std::vector<MediaSource> _MediaSources; std::vector<DomainConfig> _Domains; }; Loading