| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342 |
- // v2.8.0 — access policy engine tests.
- //
- // The properties that matter most here are the safety ones, because they are
- // what make the feature usable on a live system rather than a footgun:
- //
- // * security OFF is the default and allows everything, so upgrading changes
- // nothing for any existing deployment;
- // * enabling is REFUSED without an admin policy, which makes lockout
- // structurally impossible rather than a matter of operator care;
- // * removing the last admin of a secured project is refused for the same
- // reason;
- // * audit mode evaluates honestly and logs, but allows - the migration path
- // for switching a live project on;
- // * once enforcing, an unlisted principal gets nothing (deny-by-default), and
- // system collections are admin-only, since read access to `_policies` would
- // expose the whole access model and write access would be escalation.
- #include <atomic>
- #include <filesystem>
- #include <iostream>
- #include <string>
- #include <unistd.h>
- #include <nlohmann/json.hpp>
- #include "memory_store.hpp"
- #include "security/policy_manager.hpp"
- // setSecurity(enabled=true) is refused while kEnforcementCoverageComplete is
- // false - see policy_manager.hpp. The fixture calls allowEnableForTests() so
- // these tests can still exercise enforcing behaviour; a separate test below
- // asserts the guard itself.
- namespace fs = std::filesystem;
- using namespace smartbotic::database;
- namespace {
- int g_pass = 0;
- int g_fail = 0;
- void check(bool cond, const char* msg) {
- if (cond) {
- ++g_pass;
- } else {
- ++g_fail;
- std::cerr << "FAIL: " << msg << "\n";
- }
- }
- struct Fixture {
- MemoryStore store;
- PolicyManager pm;
- Fixture() : store(MemoryStore::Config{}), pm(store) {
- store.start();
- pm.loadFromStore();
- // These tests are the engine's own; they must be able to reach
- // enforcing behaviour even though the deployment-level guard is armed.
- pm.allowEnableForTests();
- }
- ~Fixture() { store.stop(); }
- };
- Policy mkPolicy(const std::string& principal, bool admin = false) {
- Policy p;
- p.principal = principal;
- p.admin = admin;
- return p;
- }
- PolicyRule rw(bool r, bool w) {
- PolicyRule x;
- x.read = r;
- x.write = w;
- return x;
- }
- // -------------------------------------------------------------------------
- void test_security_off_allows_everything() {
- Fixture f;
- // No policies, no security record: the pre-2.8.0 world.
- auto d = f.pm.authorize("acme", "nobody", "users", Access::Read);
- check(d.allowed, "security off allows an unknown principal to read");
- check(f.pm.authorize("acme", "nobody", "users", Access::Write).allowed,
- "security off allows writes too");
- check(f.pm.authorize("acme", "nobody", "_policies", Access::Read).allowed,
- "security off does not even guard system collections");
- check(!f.pm.anyProjectSecured(), "no project is secured by default");
- check(f.pm.mayAdminister("acme", "anyone"),
- "with security off anyone may create the first policy");
- }
- void test_enabling_without_admin_is_refused() {
- Fixture f;
- std::string err;
- ProjectSecurity sec;
- sec.enabled = true;
- check(!f.pm.setSecurity("acme", sec, err),
- "enabling security with no admin policy must be refused");
- check(err.find("admin") != std::string::npos,
- "and the error must say why - no admin policy");
- check(!f.pm.securityOf("acme").enabled, "security stays off after refusal");
- // Non-admin policies are not enough.
- auto p = mkPolicy("shadowman");
- p.collections["users"] = rw(true, false);
- check(f.pm.setPolicy("acme", p, err), "a non-admin policy can be created");
- check(!f.pm.setSecurity("acme", sec, err),
- "a non-admin policy does not satisfy the admin requirement");
- check(f.pm.setPolicy("acme", mkPolicy("ops", /*admin=*/true), err),
- "an admin policy can be created");
- check(f.pm.setSecurity("acme", sec, err),
- "with an admin present, enabling succeeds");
- check(f.pm.securityOf("acme").enabled, "and security is now on");
- check(f.pm.anyProjectSecured(), "anyProjectSecured reflects it");
- }
- void test_deny_by_default_once_enforcing() {
- Fixture f;
- std::string err;
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- auto reader = mkPolicy("reader");
- reader.collections["users"] = rw(true, false);
- f.pm.setPolicy("acme", reader, err);
- ProjectSecurity sec; sec.enabled = true;
- f.pm.setSecurity("acme", sec, err);
- check(!f.pm.authorize("acme", "stranger", "users", Access::Read).allowed,
- "a principal with no policy gets nothing");
- check(f.pm.authorize("acme", "reader", "users", Access::Read).allowed,
- "a granted read is allowed");
- check(!f.pm.authorize("acme", "reader", "users", Access::Write).allowed,
- "read does not imply write");
- check(!f.pm.authorize("acme", "reader", "sessions", Access::Read).allowed,
- "a collection with no rule is denied even for a known principal");
- check(f.pm.authorize("acme", "ops", "anything", Access::Write).allowed,
- "admin may write anything in the project");
- // Another project is untouched.
- check(f.pm.authorize("other", "stranger", "users", Access::Read).allowed,
- "enabling one project must not affect another");
- }
- void test_wildcard_rule_and_exact_match_precedence() {
- Fixture f;
- std::string err;
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- auto p = mkPolicy("svc");
- p.collections["*"] = rw(true, false);
- p.collections["secrets"] = rw(false, false);
- f.pm.setPolicy("acme", p, err);
- ProjectSecurity sec; sec.enabled = true;
- f.pm.setSecurity("acme", sec, err);
- check(f.pm.authorize("acme", "svc", "anything", Access::Read).allowed,
- "the * fallback grants read on an unlisted collection");
- check(!f.pm.authorize("acme", "svc", "secrets", Access::Read).allowed,
- "an exact rule overrides the * fallback, even to deny");
- }
- void test_system_collections_are_admin_only() {
- Fixture f;
- std::string err;
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- auto p = mkPolicy("svc");
- p.collections["*"] = rw(true, true);
- f.pm.setPolicy("acme", p, err);
- ProjectSecurity sec; sec.enabled = true;
- f.pm.setSecurity("acme", sec, err);
- check(!f.pm.authorize("acme", "svc", "_policies", Access::Read).allowed,
- "a * grant must NOT reach _policies - that would expose the access model");
- check(!f.pm.authorize("acme", "svc", "_policies", Access::Write).allowed,
- "nor allow writing it - that would be privilege escalation");
- check(!f.pm.authorize("acme", "svc", "_views", Access::Read).allowed,
- "no system collection is reachable via *");
- check(f.pm.authorize("acme", "ops", "_policies", Access::Write).allowed,
- "an admin may still manage system collections");
- check(!f.pm.mayAdminister("acme", "svc"), "a non-admin may not administer policy");
- check(f.pm.mayAdminister("acme", "ops"), "an admin may");
- }
- void test_mask_and_row_predicate_are_returned() {
- Fixture f;
- std::string err;
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- auto p = mkPolicy("svc");
- PolicyRule r = rw(true, false);
- r.mask = {"ssn", "profile.dob"};
- Filter tenant;
- tenant.field = "tenant";
- tenant.op = FilterOp::EQ;
- tenant.value = "acme";
- r.row = {tenant};
- p.collections["users"] = r;
- f.pm.setPolicy("acme", p, err);
- ProjectSecurity sec; sec.enabled = true;
- f.pm.setSecurity("acme", sec, err);
- auto d = f.pm.authorize("acme", "svc", "users", Access::Read);
- check(d.allowed, "granted");
- check(d.mask.size() == 2, "the column mask comes back with the decision");
- check(d.mask[0] == "ssn", "mask paths preserved");
- check(d.row.size() == 1 && d.row[0].field == "tenant",
- "the row predicate comes back so the handler can AND-merge it");
- }
- void test_audit_mode_logs_but_allows() {
- Fixture f;
- std::string err;
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- ProjectSecurity sec;
- sec.enabled = true;
- sec.mode = SecurityMode::Audit;
- check(f.pm.setSecurity("acme", sec, err), "audit mode can be enabled");
- auto d = f.pm.authorize("acme", "stranger", "users", Access::Read);
- check(d.allowed, "audit mode ALLOWS what enforce mode would deny");
- check(d.audited_denial, "but records that it was really a denial");
- check(!d.reason.empty(), "and keeps the reason for the operator log");
- // Flip to enforce and the same request is refused.
- sec.mode = SecurityMode::Enforce;
- f.pm.setSecurity("acme", sec, err);
- auto d2 = f.pm.authorize("acme", "stranger", "users", Access::Read);
- check(!d2.allowed, "enforce mode denies the same request");
- check(!d2.audited_denial, "and does not mark it as merely audited");
- }
- void test_cannot_remove_last_admin_of_secured_project() {
- Fixture f;
- std::string err;
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- ProjectSecurity sec; sec.enabled = true;
- f.pm.setSecurity("acme", sec, err);
- check(!f.pm.removePolicy("acme", "ops", err),
- "removing the last admin of a secured project must be refused");
- check(err.find("last admin") != std::string::npos, "with a clear reason");
- check(f.pm.setPolicy("acme", mkPolicy("ops2", true), err), "add a second admin");
- check(f.pm.removePolicy("acme", "ops", err),
- "now the first admin can be removed");
- check(!f.pm.removePolicy("acme", "ops2", err),
- "but not the remaining last one");
- }
- void test_policies_survive_reload() {
- Fixture f;
- std::string err;
- auto p = mkPolicy("svc");
- PolicyRule r = rw(true, false);
- r.mask = {"ssn"};
- p.collections["users"] = r;
- f.pm.setPolicy("acme", p, err);
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- ProjectSecurity sec; sec.enabled = true; sec.mode = SecurityMode::Audit;
- f.pm.setSecurity("acme", sec, err);
- // Reload from the same store, as a restart would.
- f.pm.loadFromStore();
- check(f.pm.securityOf("acme").enabled, "enable flag survives reload");
- check(f.pm.securityOf("acme").mode == SecurityMode::Audit,
- "mode survives reload");
- auto got = f.pm.getPolicy("acme", "svc");
- check(got.has_value(), "policy survives reload");
- check(got && got->collections.count("users") == 1, "rules survive reload");
- check(got && got->collections["users"].mask.size() == 1,
- "the column mask survives reload");
- check(f.pm.listPolicies("acme").size() == 2, "both policies listed");
- check(f.pm.listPolicies("other").empty(),
- "listing is scoped to the project");
- }
- void test_file_rules_are_separate_from_collections() {
- Fixture f;
- std::string err;
- f.pm.setPolicy("acme", mkPolicy("ops", true), err);
- auto p = mkPolicy("svc");
- p.collections["*"] = rw(true, true);
- p.files["plugin"] = rw(true, false);
- f.pm.setPolicy("acme", p, err);
- ProjectSecurity sec; sec.enabled = true;
- f.pm.setSecurity("acme", sec, err);
- check(f.pm.authorizeFile("acme", "svc", "plugin", Access::Read).allowed,
- "a granted file type reads");
- check(!f.pm.authorizeFile("acme", "svc", "plugin", Access::Write).allowed,
- "file write is separately gated");
- check(!f.pm.authorizeFile("acme", "svc", "document", Access::Read).allowed,
- "an unlisted file type is denied - a collection * does not cover files");
- }
- // The deployment guard itself: without the test seam, arming security must be
- // refused while enforcement is not wired into every handler. A project
- // protected on some paths and open on others reports safety it does not have.
- void test_enable_is_refused_while_coverage_incomplete() {
- MemoryStore store(MemoryStore::Config{});
- store.start();
- PolicyManager pm(store); // note: NO allowEnableForTests()
- pm.loadFromStore();
- std::string err;
- pm.setPolicy("acme", mkPolicy("ops", true), err);
- ProjectSecurity sec; sec.enabled = true;
- if (kEnforcementCoverageComplete) {
- check(pm.setSecurity("acme", sec, err),
- "coverage complete: enabling is permitted");
- } else {
- check(!pm.setSecurity("acme", sec, err),
- "coverage incomplete: enabling must be refused even with an admin");
- check(err.find("not yet wired") != std::string::npos,
- "and the refusal must say enforcement is incomplete");
- check(!pm.securityOf("acme").enabled, "security stays off");
- }
- store.stop();
- }
- } // namespace
- int main() {
- std::cout << "=== test_policy_manager ===\n";
- test_security_off_allows_everything();
- test_enabling_without_admin_is_refused();
- test_deny_by_default_once_enforcing();
- test_wildcard_rule_and_exact_match_precedence();
- test_system_collections_are_admin_only();
- test_mask_and_row_predicate_are_returned();
- test_audit_mode_logs_but_allows();
- test_cannot_remove_last_admin_of_secured_project();
- test_policies_survive_reload();
- test_file_rules_are_separate_from_collections();
- test_enable_is_refused_while_coverage_incomplete();
- std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
- return g_fail == 0 ? 0 : 1;
- }
|