test_policy_manager.cpp 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342
  1. // v2.8.0 — access policy engine tests.
  2. //
  3. // The properties that matter most here are the safety ones, because they are
  4. // what make the feature usable on a live system rather than a footgun:
  5. //
  6. // * security OFF is the default and allows everything, so upgrading changes
  7. // nothing for any existing deployment;
  8. // * enabling is REFUSED without an admin policy, which makes lockout
  9. // structurally impossible rather than a matter of operator care;
  10. // * removing the last admin of a secured project is refused for the same
  11. // reason;
  12. // * audit mode evaluates honestly and logs, but allows - the migration path
  13. // for switching a live project on;
  14. // * once enforcing, an unlisted principal gets nothing (deny-by-default), and
  15. // system collections are admin-only, since read access to `_policies` would
  16. // expose the whole access model and write access would be escalation.
  17. #include <atomic>
  18. #include <filesystem>
  19. #include <iostream>
  20. #include <string>
  21. #include <unistd.h>
  22. #include <nlohmann/json.hpp>
  23. #include "memory_store.hpp"
  24. #include "security/policy_manager.hpp"
  25. // setSecurity(enabled=true) is refused while kEnforcementCoverageComplete is
  26. // false - see policy_manager.hpp. The fixture calls allowEnableForTests() so
  27. // these tests can still exercise enforcing behaviour; a separate test below
  28. // asserts the guard itself.
  29. namespace fs = std::filesystem;
  30. using namespace smartbotic::database;
  31. namespace {
  32. int g_pass = 0;
  33. int g_fail = 0;
  34. void check(bool cond, const char* msg) {
  35. if (cond) {
  36. ++g_pass;
  37. } else {
  38. ++g_fail;
  39. std::cerr << "FAIL: " << msg << "\n";
  40. }
  41. }
  42. struct Fixture {
  43. MemoryStore store;
  44. PolicyManager pm;
  45. Fixture() : store(MemoryStore::Config{}), pm(store) {
  46. store.start();
  47. pm.loadFromStore();
  48. // These tests are the engine's own; they must be able to reach
  49. // enforcing behaviour even though the deployment-level guard is armed.
  50. pm.allowEnableForTests();
  51. }
  52. ~Fixture() { store.stop(); }
  53. };
  54. Policy mkPolicy(const std::string& principal, bool admin = false) {
  55. Policy p;
  56. p.principal = principal;
  57. p.admin = admin;
  58. return p;
  59. }
  60. PolicyRule rw(bool r, bool w) {
  61. PolicyRule x;
  62. x.read = r;
  63. x.write = w;
  64. return x;
  65. }
  66. // -------------------------------------------------------------------------
  67. void test_security_off_allows_everything() {
  68. Fixture f;
  69. // No policies, no security record: the pre-2.8.0 world.
  70. auto d = f.pm.authorize("acme", "nobody", "users", Access::Read);
  71. check(d.allowed, "security off allows an unknown principal to read");
  72. check(f.pm.authorize("acme", "nobody", "users", Access::Write).allowed,
  73. "security off allows writes too");
  74. check(f.pm.authorize("acme", "nobody", "_policies", Access::Read).allowed,
  75. "security off does not even guard system collections");
  76. check(!f.pm.anyProjectSecured(), "no project is secured by default");
  77. check(f.pm.mayAdminister("acme", "anyone"),
  78. "with security off anyone may create the first policy");
  79. }
  80. void test_enabling_without_admin_is_refused() {
  81. Fixture f;
  82. std::string err;
  83. ProjectSecurity sec;
  84. sec.enabled = true;
  85. check(!f.pm.setSecurity("acme", sec, err),
  86. "enabling security with no admin policy must be refused");
  87. check(err.find("admin") != std::string::npos,
  88. "and the error must say why - no admin policy");
  89. check(!f.pm.securityOf("acme").enabled, "security stays off after refusal");
  90. // Non-admin policies are not enough.
  91. auto p = mkPolicy("shadowman");
  92. p.collections["users"] = rw(true, false);
  93. check(f.pm.setPolicy("acme", p, err), "a non-admin policy can be created");
  94. check(!f.pm.setSecurity("acme", sec, err),
  95. "a non-admin policy does not satisfy the admin requirement");
  96. check(f.pm.setPolicy("acme", mkPolicy("ops", /*admin=*/true), err),
  97. "an admin policy can be created");
  98. check(f.pm.setSecurity("acme", sec, err),
  99. "with an admin present, enabling succeeds");
  100. check(f.pm.securityOf("acme").enabled, "and security is now on");
  101. check(f.pm.anyProjectSecured(), "anyProjectSecured reflects it");
  102. }
  103. void test_deny_by_default_once_enforcing() {
  104. Fixture f;
  105. std::string err;
  106. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  107. auto reader = mkPolicy("reader");
  108. reader.collections["users"] = rw(true, false);
  109. f.pm.setPolicy("acme", reader, err);
  110. ProjectSecurity sec; sec.enabled = true;
  111. f.pm.setSecurity("acme", sec, err);
  112. check(!f.pm.authorize("acme", "stranger", "users", Access::Read).allowed,
  113. "a principal with no policy gets nothing");
  114. check(f.pm.authorize("acme", "reader", "users", Access::Read).allowed,
  115. "a granted read is allowed");
  116. check(!f.pm.authorize("acme", "reader", "users", Access::Write).allowed,
  117. "read does not imply write");
  118. check(!f.pm.authorize("acme", "reader", "sessions", Access::Read).allowed,
  119. "a collection with no rule is denied even for a known principal");
  120. check(f.pm.authorize("acme", "ops", "anything", Access::Write).allowed,
  121. "admin may write anything in the project");
  122. // Another project is untouched.
  123. check(f.pm.authorize("other", "stranger", "users", Access::Read).allowed,
  124. "enabling one project must not affect another");
  125. }
  126. void test_wildcard_rule_and_exact_match_precedence() {
  127. Fixture f;
  128. std::string err;
  129. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  130. auto p = mkPolicy("svc");
  131. p.collections["*"] = rw(true, false);
  132. p.collections["secrets"] = rw(false, false);
  133. f.pm.setPolicy("acme", p, err);
  134. ProjectSecurity sec; sec.enabled = true;
  135. f.pm.setSecurity("acme", sec, err);
  136. check(f.pm.authorize("acme", "svc", "anything", Access::Read).allowed,
  137. "the * fallback grants read on an unlisted collection");
  138. check(!f.pm.authorize("acme", "svc", "secrets", Access::Read).allowed,
  139. "an exact rule overrides the * fallback, even to deny");
  140. }
  141. void test_system_collections_are_admin_only() {
  142. Fixture f;
  143. std::string err;
  144. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  145. auto p = mkPolicy("svc");
  146. p.collections["*"] = rw(true, true);
  147. f.pm.setPolicy("acme", p, err);
  148. ProjectSecurity sec; sec.enabled = true;
  149. f.pm.setSecurity("acme", sec, err);
  150. check(!f.pm.authorize("acme", "svc", "_policies", Access::Read).allowed,
  151. "a * grant must NOT reach _policies - that would expose the access model");
  152. check(!f.pm.authorize("acme", "svc", "_policies", Access::Write).allowed,
  153. "nor allow writing it - that would be privilege escalation");
  154. check(!f.pm.authorize("acme", "svc", "_views", Access::Read).allowed,
  155. "no system collection is reachable via *");
  156. check(f.pm.authorize("acme", "ops", "_policies", Access::Write).allowed,
  157. "an admin may still manage system collections");
  158. check(!f.pm.mayAdminister("acme", "svc"), "a non-admin may not administer policy");
  159. check(f.pm.mayAdminister("acme", "ops"), "an admin may");
  160. }
  161. void test_mask_and_row_predicate_are_returned() {
  162. Fixture f;
  163. std::string err;
  164. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  165. auto p = mkPolicy("svc");
  166. PolicyRule r = rw(true, false);
  167. r.mask = {"ssn", "profile.dob"};
  168. Filter tenant;
  169. tenant.field = "tenant";
  170. tenant.op = FilterOp::EQ;
  171. tenant.value = "acme";
  172. r.row = {tenant};
  173. p.collections["users"] = r;
  174. f.pm.setPolicy("acme", p, err);
  175. ProjectSecurity sec; sec.enabled = true;
  176. f.pm.setSecurity("acme", sec, err);
  177. auto d = f.pm.authorize("acme", "svc", "users", Access::Read);
  178. check(d.allowed, "granted");
  179. check(d.mask.size() == 2, "the column mask comes back with the decision");
  180. check(d.mask[0] == "ssn", "mask paths preserved");
  181. check(d.row.size() == 1 && d.row[0].field == "tenant",
  182. "the row predicate comes back so the handler can AND-merge it");
  183. }
  184. void test_audit_mode_logs_but_allows() {
  185. Fixture f;
  186. std::string err;
  187. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  188. ProjectSecurity sec;
  189. sec.enabled = true;
  190. sec.mode = SecurityMode::Audit;
  191. check(f.pm.setSecurity("acme", sec, err), "audit mode can be enabled");
  192. auto d = f.pm.authorize("acme", "stranger", "users", Access::Read);
  193. check(d.allowed, "audit mode ALLOWS what enforce mode would deny");
  194. check(d.audited_denial, "but records that it was really a denial");
  195. check(!d.reason.empty(), "and keeps the reason for the operator log");
  196. // Flip to enforce and the same request is refused.
  197. sec.mode = SecurityMode::Enforce;
  198. f.pm.setSecurity("acme", sec, err);
  199. auto d2 = f.pm.authorize("acme", "stranger", "users", Access::Read);
  200. check(!d2.allowed, "enforce mode denies the same request");
  201. check(!d2.audited_denial, "and does not mark it as merely audited");
  202. }
  203. void test_cannot_remove_last_admin_of_secured_project() {
  204. Fixture f;
  205. std::string err;
  206. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  207. ProjectSecurity sec; sec.enabled = true;
  208. f.pm.setSecurity("acme", sec, err);
  209. check(!f.pm.removePolicy("acme", "ops", err),
  210. "removing the last admin of a secured project must be refused");
  211. check(err.find("last admin") != std::string::npos, "with a clear reason");
  212. check(f.pm.setPolicy("acme", mkPolicy("ops2", true), err), "add a second admin");
  213. check(f.pm.removePolicy("acme", "ops", err),
  214. "now the first admin can be removed");
  215. check(!f.pm.removePolicy("acme", "ops2", err),
  216. "but not the remaining last one");
  217. }
  218. void test_policies_survive_reload() {
  219. Fixture f;
  220. std::string err;
  221. auto p = mkPolicy("svc");
  222. PolicyRule r = rw(true, false);
  223. r.mask = {"ssn"};
  224. p.collections["users"] = r;
  225. f.pm.setPolicy("acme", p, err);
  226. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  227. ProjectSecurity sec; sec.enabled = true; sec.mode = SecurityMode::Audit;
  228. f.pm.setSecurity("acme", sec, err);
  229. // Reload from the same store, as a restart would.
  230. f.pm.loadFromStore();
  231. check(f.pm.securityOf("acme").enabled, "enable flag survives reload");
  232. check(f.pm.securityOf("acme").mode == SecurityMode::Audit,
  233. "mode survives reload");
  234. auto got = f.pm.getPolicy("acme", "svc");
  235. check(got.has_value(), "policy survives reload");
  236. check(got && got->collections.count("users") == 1, "rules survive reload");
  237. check(got && got->collections["users"].mask.size() == 1,
  238. "the column mask survives reload");
  239. check(f.pm.listPolicies("acme").size() == 2, "both policies listed");
  240. check(f.pm.listPolicies("other").empty(),
  241. "listing is scoped to the project");
  242. }
  243. void test_file_rules_are_separate_from_collections() {
  244. Fixture f;
  245. std::string err;
  246. f.pm.setPolicy("acme", mkPolicy("ops", true), err);
  247. auto p = mkPolicy("svc");
  248. p.collections["*"] = rw(true, true);
  249. p.files["plugin"] = rw(true, false);
  250. f.pm.setPolicy("acme", p, err);
  251. ProjectSecurity sec; sec.enabled = true;
  252. f.pm.setSecurity("acme", sec, err);
  253. check(f.pm.authorizeFile("acme", "svc", "plugin", Access::Read).allowed,
  254. "a granted file type reads");
  255. check(!f.pm.authorizeFile("acme", "svc", "plugin", Access::Write).allowed,
  256. "file write is separately gated");
  257. check(!f.pm.authorizeFile("acme", "svc", "document", Access::Read).allowed,
  258. "an unlisted file type is denied - a collection * does not cover files");
  259. }
  260. // The deployment guard itself: without the test seam, arming security must be
  261. // refused while enforcement is not wired into every handler. A project
  262. // protected on some paths and open on others reports safety it does not have.
  263. void test_enable_is_refused_while_coverage_incomplete() {
  264. MemoryStore store(MemoryStore::Config{});
  265. store.start();
  266. PolicyManager pm(store); // note: NO allowEnableForTests()
  267. pm.loadFromStore();
  268. std::string err;
  269. pm.setPolicy("acme", mkPolicy("ops", true), err);
  270. ProjectSecurity sec; sec.enabled = true;
  271. if (kEnforcementCoverageComplete) {
  272. check(pm.setSecurity("acme", sec, err),
  273. "coverage complete: enabling is permitted");
  274. } else {
  275. check(!pm.setSecurity("acme", sec, err),
  276. "coverage incomplete: enabling must be refused even with an admin");
  277. check(err.find("not yet wired") != std::string::npos,
  278. "and the refusal must say enforcement is incomplete");
  279. check(!pm.securityOf("acme").enabled, "security stays off");
  280. }
  281. store.stop();
  282. }
  283. } // namespace
  284. int main() {
  285. std::cout << "=== test_policy_manager ===\n";
  286. test_security_off_allows_everything();
  287. test_enabling_without_admin_is_refused();
  288. test_deny_by_default_once_enforcing();
  289. test_wildcard_rule_and_exact_match_precedence();
  290. test_system_collections_are_admin_only();
  291. test_mask_and_row_predicate_are_returned();
  292. test_audit_mode_logs_but_allows();
  293. test_cannot_remove_last_admin_of_secured_project();
  294. test_policies_survive_reload();
  295. test_file_rules_are_separate_from_collections();
  296. test_enable_is_refused_while_coverage_incomplete();
  297. std::cout << "passed: " << g_pass << ", failed: " << g_fail << "\n";
  298. return g_fail == 0 ? 0 : 1;
  299. }