Loading docs/ROLES.md +21 −7 Changes for docs/ROLES.md: 21 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -79,10 +79,24 @@ Service Accounts laufen über dieselben GPOs — `GPOcheck()` funktioniert unabh Session aus `UserAuth()` oder `ClientAuth()` stammt, solange die zugehörige Gruppen-/Connection-ID Mitglied der GPO ist. ## Bekannte Lücke `authdb::client` bietet aktuell **keine** Methode, um eine neue Gruppe anzulegen (`addGroup` existiert nicht, nur `listGroups()` lesend). Bis authdb das nachrüstet, müssen die `proxyplus.*`-Gruppen einmalig durch einen authdb-Administrator direkt im Backend angelegt werden; `scripts/authdb-bootstrap.sh` bricht mit einer klaren Fehlermeldung ab und verweist hierauf, statt das über einen inoffiziellen Weg zu umgehen. ## Gruppenanlage `authdb::client::Client::addGroup(gid, name)` legt eine Gruppe an (spiegelt `GPOadd`s Vertrag: der Aufrufer liefert die UUID, siehe `authdb/src/api.cpp`s `addGroup`-Handler und `authdb/src/group.cpp`s `Group::create()`). Idempotent über den gleichen Id-Match-Fastpath wie `addGPO` -- ein erneuter Aufruf mit derselben `gid` gibt einfach `true` zurück, statt auf `Group::create()`s Name-Kollisionsprüfung zu laufen. `tools/proxyplus-bootstrap.cpp` legt beim Start alle zehn `proxyplus.*`-Standardrollen-Gruppen aus der Tabelle oben mit festen UUIDs an (`kGroups`), bevor es die GPOs anlegt und deren `allowedGroupNames` per `listGroups()` auf die echten Gruppen-UUIDs auflöst. **Frühere Lücke, jetzt geschlossen (2026-08-07):** vorher bot `authdb::client` keine Gruppenanlage an (nur lesendes `listGroups()`), sodass die `proxyplus.*`-Gruppen einmalig von einem authdb-Administrator direkt im Backend angelegt werden mussten. Ergänzt um `Client::addGroup()` + den serverseitigen `groupadd`-API-Befehl. **Verbleibende Hürde:** `proxyplus` bindet authdb aktuell über das installierte `libauthorize-dev`-Paket ein (`/usr/include/authdb/client.h`), nicht direkt gegen den authdb-Quellbaum. Bis dieses Paket neu gebaut und installiert ist, sieht `proxyplus-bootstrap` weiterhin die alte, `addGroup`-lose Client-API -- dasselbe Muster wie bei anderen Bibliotheken, die über ein Debian-Paket statt direkt aus dem Quellbaum eingebunden werden. test/CMakeLists.txt +0 −3 Changes for test/CMakeLists.txt: 0 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -19,6 +19,3 @@ add_test(NAME test_jose COMMAND test_jose) add_executable(test_loadbalancer test_loadbalancer.cpp) target_link_libraries(test_loadbalancer PRIVATE proxyplus_core) add_test(NAME test_loadbalancer COMMAND test_loadbalancer) add_executable(manual_verify_spf manual_verify_spf.cpp) target_link_libraries(manual_verify_spf PRIVATE proxyplus_core) test/manual_verify_spf.cppdeleted 100644 → 0 +0 −45 Changes for test/manual_verify_spf.cpp: 0 added lines, 45 removed lines. Original line number Diff line number Diff line // Ad-hoc manual verification, not part of the checked-in test suite (SPF evaluation needs live // DNS against real third-party domains, which would make a ctest-registered test flaky against // this environment's network and against those domains' policies changing over time -- the // same reason DkimAdapter::verify()/DmarcAdapter::evaluate() have no ctest coverage either). #include "../src/security/SpfAdapter.h" #include <iostream> using proxyplus::security::SpfAdapter; using proxyplus::security::SpfResult; const char *name(SpfResult r) { switch (r) { case SpfResult::None: return "None"; case SpfResult::Neutral: return "Neutral"; case SpfResult::Pass: return "Pass"; case SpfResult::Fail: return "Fail"; case SpfResult::SoftFail: return "SoftFail"; case SpfResult::TempError: return "TempError"; case SpfResult::PermError: return "PermError"; } return "?"; } int main() { SpfAdapter spf; // google.com's record is "v=spf1 include:_spf.google.com ~all" -- an IP with no business // being in any of Google's ranges must fail (through the ~all catch-all), and a domain // with no SPF record at all must be None. std::cout << "google.com vs 203.0.113.1 (documentation range, TEST-NET-3, RFC 5737): " << name(spf.check("google.com", "203.0.113.1")) << " (expect SoftFail)\n"; std::cout << "example.com vs 203.0.113.1 (no SPF record at all): " << name(spf.check("example.com", "203.0.113.1")) << " (expect None)\n"; // A known Google Workspace outbound source range from _spf.google.com's own published // "include:_netblocks.google.com" (ip4:172.217.0.0/16 has stayed stable across Google's // ranges for years, since it's the core Google IPv4 block) -- exercises ip4 CIDR matching // through a real "include:" chain. std::cout << "google.com vs 172.217.1.1 (inside Google's own 172.217.0.0/16): " << name(spf.check("google.com", "172.217.1.1")) << " (expect Pass)\n"; return 0; } tools/proxyplus-bootstrap.cpp +45 −12 Changes for tools/proxyplus-bootstrap.cpp: 45 added lines, 12 removed lines. Original line number Diff line number Diff line // One-shot ProxyPlus/authdb bootstrap helper. Real, compiled tool -- not a shell script // guessing at JSON payloads -- because authdb's CLI (authdb/cli/authcli.cpp) exposes // addclient/remclient/editclient/gpoexists but has no "add a GPO" subcommand; only // authdb::client::Client::GPOadd() (a C++ library call) can create one. See // docs/ROLES.md's "Bekannte Luecke" for the matching authdb-side gap (no addGroup either -- // the proxyplus.* groups this tool references as GPO members must already exist, created // by an authdb administrator through whatever means authdb currently offers for that). // addclient/remclient/editclient/gpoexists but has no "add a GPO"/"add a group" subcommand; // only authdb::client::Client::GPOadd()/addGroup() (C++ library calls) can create those. // See docs/ROLES.md's "Gruppenanlage" section for how group/GPO ids stay static across // reruns (idempotent id-match fast-path) instead of colliding on name. // // What this tool does, in order: // 1. Logs into authdb as an existing admin user (username/password given on argv). // 2. Creates ProxyPlus's own service Client-Credential (authdb's API-key equivalent, // see docs/ARCHITECTURE.md §2) and writes "<data-dir>/service-credential". // 3. Creates one GPO per permission in docs/ROLES.md's list, with the standard // proxyplus.* groups as allowed members, and records permission->GPO-id into // 3. Creates the standard-role proxyplus.* groups (docs/ROLES.md's role table). // 4. Creates one GPO per permission in docs/ROLES.md's list, with those groups // resolved to their real ids as allowed members, and records permission->GPO-id into // ProxyPlus's own binary ConfigStore at <data-dir> (never into authdb itself). // // Usage: Loading @@ -30,6 +30,27 @@ namespace { struct GroupSpec { std::string name; // Static id, same reasoning as PermissionSpec::gpoId below. std::string groupId; }; // The standard-role groups from docs/ROLES.md's "Rolle -> authdb-Gruppe" table. Service // Account has no group (client-credential only, see docs/ARCHITECTURE.md Sz2). const std::vector<GroupSpec> kGroups = { {"proxyplus.super_admin", "9e971dbf-4194-4633-a4b2-62aa1440bead"}, {"proxyplus.cluster_admin", "9f8a9656-cd77-449e-b7a7-4dbe0e183d2d"}, {"proxyplus.http_admin", "9a55e711-6b13-4802-b25b-7fca12407489"}, {"proxyplus.smtp_admin", "b4fceb46-3248-41cc-adc6-1d8fa1d38bcb"}, {"proxyplus.security_admin", "50f5a682-1c86-48fd-8c1b-c33b219ae5b6"}, {"proxyplus.user_admin", "77cab5f0-b3a5-4285-9843-051e903e504a"}, {"proxyplus.auditor", "1f0cc72c-2146-450f-af6a-ba094e9c4f5b"}, {"proxyplus.readonly_operator", "b5546181-8b5e-48ac-b6e7-3f79fc566e9e"}, {"proxyplus.http_proxy_user", "b11222ab-7ab8-46fe-920c-9dc8485af97f"}, {"proxyplus.smtp_relay_user", "1395d6f6-5cf3-4025-bee5-5aeca660f9c4"}, }; struct PermissionSpec { std::string permission; // Static per-permission GPO id, in the same spirit as authdb's own src/gpo_default.h: Loading Loading @@ -121,10 +142,23 @@ int main(int argc, char **argv) { std::cerr << "created service client '" << serviceClientName << "' (id=" << clientId << ")" << std::endl; // Create the standard-role groups (idempotent: addGroup's id-match fast-path in // authdb's api.cpp returns success if a group with this id already exists). for (const auto &g : kGroups) { uuid::uuid groupId(g.groupId.c_str()); try { admin.addGroup(groupId, g.name); } catch (const std::exception &e) { std::cerr << "addGroup(" << g.name << ") failed: " << e.what() << std::endl; continue; } std::cerr << "ensured group '" << g.name << "' (id=" << g.groupId << ")" << std::endl; } // authdb's wire format takes GPO members as group UUIDs, not names (api.cpp parses each // gpomembers entry as uuid::uuid(str)) -- resolve docs/ROLES.md's proxyplus.* group names // to their real ids here. Groups not yet created by an authdb admin (see "Bekannte Luecke" // above) are dropped from that GPO's members with a warning rather than sent as garbage. // to their real ids here. Any group that still failed to create above is dropped from // that GPO's members with a warning rather than sent as garbage. std::map<std::string, std::string> groupNameToId; for (const auto &g : admin.listGroups()) { groupNameToId[g.name] = g.gid; Loading @@ -136,9 +170,8 @@ int main(int argc, char **argv) { for (const auto &groupName : spec.allowedGroups) { auto it = groupNameToId.find(groupName); if (it == groupNameToId.end()) { std::cerr << "warning: group '" << groupName << "' does not exist yet in authdb" << " -- omitted from " << spec.permission << "'s GPO members until an authdb admin creates it" << std::endl; std::cerr << "warning: group '" << groupName << "' does not exist in authdb" << " -- omitted from " << spec.permission << "'s GPO members" << std::endl; continue; } memberIds.push_back(it->second); Loading Loading
docs/ROLES.md +21 −7 Changes for docs/ROLES.md: 21 added lines, 7 removed lines. Original line number Diff line number Diff line Loading @@ -79,10 +79,24 @@ Service Accounts laufen über dieselben GPOs — `GPOcheck()` funktioniert unabh Session aus `UserAuth()` oder `ClientAuth()` stammt, solange die zugehörige Gruppen-/Connection-ID Mitglied der GPO ist. ## Bekannte Lücke `authdb::client` bietet aktuell **keine** Methode, um eine neue Gruppe anzulegen (`addGroup` existiert nicht, nur `listGroups()` lesend). Bis authdb das nachrüstet, müssen die `proxyplus.*`-Gruppen einmalig durch einen authdb-Administrator direkt im Backend angelegt werden; `scripts/authdb-bootstrap.sh` bricht mit einer klaren Fehlermeldung ab und verweist hierauf, statt das über einen inoffiziellen Weg zu umgehen. ## Gruppenanlage `authdb::client::Client::addGroup(gid, name)` legt eine Gruppe an (spiegelt `GPOadd`s Vertrag: der Aufrufer liefert die UUID, siehe `authdb/src/api.cpp`s `addGroup`-Handler und `authdb/src/group.cpp`s `Group::create()`). Idempotent über den gleichen Id-Match-Fastpath wie `addGPO` -- ein erneuter Aufruf mit derselben `gid` gibt einfach `true` zurück, statt auf `Group::create()`s Name-Kollisionsprüfung zu laufen. `tools/proxyplus-bootstrap.cpp` legt beim Start alle zehn `proxyplus.*`-Standardrollen-Gruppen aus der Tabelle oben mit festen UUIDs an (`kGroups`), bevor es die GPOs anlegt und deren `allowedGroupNames` per `listGroups()` auf die echten Gruppen-UUIDs auflöst. **Frühere Lücke, jetzt geschlossen (2026-08-07):** vorher bot `authdb::client` keine Gruppenanlage an (nur lesendes `listGroups()`), sodass die `proxyplus.*`-Gruppen einmalig von einem authdb-Administrator direkt im Backend angelegt werden mussten. Ergänzt um `Client::addGroup()` + den serverseitigen `groupadd`-API-Befehl. **Verbleibende Hürde:** `proxyplus` bindet authdb aktuell über das installierte `libauthorize-dev`-Paket ein (`/usr/include/authdb/client.h`), nicht direkt gegen den authdb-Quellbaum. Bis dieses Paket neu gebaut und installiert ist, sieht `proxyplus-bootstrap` weiterhin die alte, `addGroup`-lose Client-API -- dasselbe Muster wie bei anderen Bibliotheken, die über ein Debian-Paket statt direkt aus dem Quellbaum eingebunden werden.
test/CMakeLists.txt +0 −3 Changes for test/CMakeLists.txt: 0 added lines, 3 removed lines. Original line number Diff line number Diff line Loading @@ -19,6 +19,3 @@ add_test(NAME test_jose COMMAND test_jose) add_executable(test_loadbalancer test_loadbalancer.cpp) target_link_libraries(test_loadbalancer PRIVATE proxyplus_core) add_test(NAME test_loadbalancer COMMAND test_loadbalancer) add_executable(manual_verify_spf manual_verify_spf.cpp) target_link_libraries(manual_verify_spf PRIVATE proxyplus_core)
test/manual_verify_spf.cppdeleted 100644 → 0 +0 −45 Changes for test/manual_verify_spf.cpp: 0 added lines, 45 removed lines. Original line number Diff line number Diff line // Ad-hoc manual verification, not part of the checked-in test suite (SPF evaluation needs live // DNS against real third-party domains, which would make a ctest-registered test flaky against // this environment's network and against those domains' policies changing over time -- the // same reason DkimAdapter::verify()/DmarcAdapter::evaluate() have no ctest coverage either). #include "../src/security/SpfAdapter.h" #include <iostream> using proxyplus::security::SpfAdapter; using proxyplus::security::SpfResult; const char *name(SpfResult r) { switch (r) { case SpfResult::None: return "None"; case SpfResult::Neutral: return "Neutral"; case SpfResult::Pass: return "Pass"; case SpfResult::Fail: return "Fail"; case SpfResult::SoftFail: return "SoftFail"; case SpfResult::TempError: return "TempError"; case SpfResult::PermError: return "PermError"; } return "?"; } int main() { SpfAdapter spf; // google.com's record is "v=spf1 include:_spf.google.com ~all" -- an IP with no business // being in any of Google's ranges must fail (through the ~all catch-all), and a domain // with no SPF record at all must be None. std::cout << "google.com vs 203.0.113.1 (documentation range, TEST-NET-3, RFC 5737): " << name(spf.check("google.com", "203.0.113.1")) << " (expect SoftFail)\n"; std::cout << "example.com vs 203.0.113.1 (no SPF record at all): " << name(spf.check("example.com", "203.0.113.1")) << " (expect None)\n"; // A known Google Workspace outbound source range from _spf.google.com's own published // "include:_netblocks.google.com" (ip4:172.217.0.0/16 has stayed stable across Google's // ranges for years, since it's the core Google IPv4 block) -- exercises ip4 CIDR matching // through a real "include:" chain. std::cout << "google.com vs 172.217.1.1 (inside Google's own 172.217.0.0/16): " << name(spf.check("google.com", "172.217.1.1")) << " (expect Pass)\n"; return 0; }
tools/proxyplus-bootstrap.cpp +45 −12 Changes for tools/proxyplus-bootstrap.cpp: 45 added lines, 12 removed lines. Original line number Diff line number Diff line // One-shot ProxyPlus/authdb bootstrap helper. Real, compiled tool -- not a shell script // guessing at JSON payloads -- because authdb's CLI (authdb/cli/authcli.cpp) exposes // addclient/remclient/editclient/gpoexists but has no "add a GPO" subcommand; only // authdb::client::Client::GPOadd() (a C++ library call) can create one. See // docs/ROLES.md's "Bekannte Luecke" for the matching authdb-side gap (no addGroup either -- // the proxyplus.* groups this tool references as GPO members must already exist, created // by an authdb administrator through whatever means authdb currently offers for that). // addclient/remclient/editclient/gpoexists but has no "add a GPO"/"add a group" subcommand; // only authdb::client::Client::GPOadd()/addGroup() (C++ library calls) can create those. // See docs/ROLES.md's "Gruppenanlage" section for how group/GPO ids stay static across // reruns (idempotent id-match fast-path) instead of colliding on name. // // What this tool does, in order: // 1. Logs into authdb as an existing admin user (username/password given on argv). // 2. Creates ProxyPlus's own service Client-Credential (authdb's API-key equivalent, // see docs/ARCHITECTURE.md §2) and writes "<data-dir>/service-credential". // 3. Creates one GPO per permission in docs/ROLES.md's list, with the standard // proxyplus.* groups as allowed members, and records permission->GPO-id into // 3. Creates the standard-role proxyplus.* groups (docs/ROLES.md's role table). // 4. Creates one GPO per permission in docs/ROLES.md's list, with those groups // resolved to their real ids as allowed members, and records permission->GPO-id into // ProxyPlus's own binary ConfigStore at <data-dir> (never into authdb itself). // // Usage: Loading @@ -30,6 +30,27 @@ namespace { struct GroupSpec { std::string name; // Static id, same reasoning as PermissionSpec::gpoId below. std::string groupId; }; // The standard-role groups from docs/ROLES.md's "Rolle -> authdb-Gruppe" table. Service // Account has no group (client-credential only, see docs/ARCHITECTURE.md Sz2). const std::vector<GroupSpec> kGroups = { {"proxyplus.super_admin", "9e971dbf-4194-4633-a4b2-62aa1440bead"}, {"proxyplus.cluster_admin", "9f8a9656-cd77-449e-b7a7-4dbe0e183d2d"}, {"proxyplus.http_admin", "9a55e711-6b13-4802-b25b-7fca12407489"}, {"proxyplus.smtp_admin", "b4fceb46-3248-41cc-adc6-1d8fa1d38bcb"}, {"proxyplus.security_admin", "50f5a682-1c86-48fd-8c1b-c33b219ae5b6"}, {"proxyplus.user_admin", "77cab5f0-b3a5-4285-9843-051e903e504a"}, {"proxyplus.auditor", "1f0cc72c-2146-450f-af6a-ba094e9c4f5b"}, {"proxyplus.readonly_operator", "b5546181-8b5e-48ac-b6e7-3f79fc566e9e"}, {"proxyplus.http_proxy_user", "b11222ab-7ab8-46fe-920c-9dc8485af97f"}, {"proxyplus.smtp_relay_user", "1395d6f6-5cf3-4025-bee5-5aeca660f9c4"}, }; struct PermissionSpec { std::string permission; // Static per-permission GPO id, in the same spirit as authdb's own src/gpo_default.h: Loading Loading @@ -121,10 +142,23 @@ int main(int argc, char **argv) { std::cerr << "created service client '" << serviceClientName << "' (id=" << clientId << ")" << std::endl; // Create the standard-role groups (idempotent: addGroup's id-match fast-path in // authdb's api.cpp returns success if a group with this id already exists). for (const auto &g : kGroups) { uuid::uuid groupId(g.groupId.c_str()); try { admin.addGroup(groupId, g.name); } catch (const std::exception &e) { std::cerr << "addGroup(" << g.name << ") failed: " << e.what() << std::endl; continue; } std::cerr << "ensured group '" << g.name << "' (id=" << g.groupId << ")" << std::endl; } // authdb's wire format takes GPO members as group UUIDs, not names (api.cpp parses each // gpomembers entry as uuid::uuid(str)) -- resolve docs/ROLES.md's proxyplus.* group names // to their real ids here. Groups not yet created by an authdb admin (see "Bekannte Luecke" // above) are dropped from that GPO's members with a warning rather than sent as garbage. // to their real ids here. Any group that still failed to create above is dropped from // that GPO's members with a warning rather than sent as garbage. std::map<std::string, std::string> groupNameToId; for (const auto &g : admin.listGroups()) { groupNameToId[g.name] = g.gid; Loading @@ -136,9 +170,8 @@ int main(int argc, char **argv) { for (const auto &groupName : spec.allowedGroups) { auto it = groupNameToId.find(groupName); if (it == groupNameToId.end()) { std::cerr << "warning: group '" << groupName << "' does not exist yet in authdb" << " -- omitted from " << spec.permission << "'s GPO members until an authdb admin creates it" << std::endl; std::cerr << "warning: group '" << groupName << "' does not exist in authdb" << " -- omitted from " << spec.permission << "'s GPO members" << std::endl; continue; } memberIds.push_back(it->second); Loading