Commit 21e511b5 authored by Jan Koester's avatar Jan Koester
Browse files

settings moved to db

parent 250441cc
Loading
Loading
Loading
Loading
+32 −16
Original line number Diff line number Diff line
@@ -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;

@@ -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){
@@ -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){
+6 −0
Original line number Diff line number Diff line
@@ -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;
    };
+276 −3
Original line number Diff line number Diff line
@@ -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
@@ -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);
@@ -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)
@@ -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())
@@ -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;
@@ -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)
                {
@@ -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>";

+157 −1

File changed.

Preview size limit exceeded, changes collapsed.

+48 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -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;
@@ -183,6 +230,7 @@ namespace blogi {
        std::string               _TmpDir;
        std::string               _MediaDBUrl;
        bool                      _MediaTlsVerify = false;
        std::vector<MediaSource>  _MediaSources;
        std::vector<DomainConfig> _Domains;
    };