"""对齐 Muse P5-B 测试:五档严格能力 + 默认策略三档。 只用标准库,绝不联网、不含真实 token(测试值全是假值)。覆盖: - 五档签发与匹配:one-shot/session/task/timed/permanent; - 会话/任务绑定(换会话/换任务即不匹配); - timed 过期、one-shot 用完即废; - 超范围调用被拒(换主机/换用例/换方法/换路径各一例); - 撤销后被拒、permanent 默认拒绝签发 + 显式开启后签发成功; - 签发校验失败(未知档位、空用例、缺绑定 id、timed 缺 ttl); - 默认策略三档:default 放行只读已允许、cautious 收窄、 permissive 仍挡写操作与敏感路径; - 规则变化有审计;审计记录只有事件名/能力 id/原因,无值。 """ import json import sys import unittest from pathlib import Path from types import SimpleNamespace try: from cloud.chv.capabilities import ( TIERS, CapabilityIssuer, ScopedCapability, ) from cloud.chv.policy import ( CAUTION_LEVELS, READ_METHODS, WRITE_METHODS, DefaultPolicy, ) from cloud.chv.sentinel import EgressRequest except ImportError: sys.path.insert(0, str(Path(__file__).resolve().parents[2])) from cloud.chv.capabilities import ( TIERS, CapabilityIssuer, ScopedCapability, ) from cloud.chv.policy import ( CAUTION_LEVELS, READ_METHODS, WRITE_METHODS, DefaultPolicy, ) from cloud.chv.sentinel import EgressRequest HOST = "gitea.tose.top" RULES = (("GET", "prefix", "/api/v1/"), ("POST", "prefix", "/api/v1/")) NOW = 1000.0 def _req(host=HOST, method="GET", path="/api/v1/version"): return EgressRequest("gitea", method, "https", host, 443, path) def _scope(use_case="gitea-sync", session_id=None, task_id=None): scope = {"use_case": use_case} if session_id is not None: scope["session_id"] = session_id if task_id is not None: scope["task_id"] = task_id return scope def _events(audit, event): return [r for r in audit if r["event"] == event] class CapabilitiesTest(unittest.TestCase): def setUp(self): self.audit = [] self.issuer = CapabilityIssuer(audit=self.audit) def _issue(self, tier="one-shot", **kw): kw.setdefault("connector", "gitea") kw.setdefault("host", HOST) kw.setdefault("methods", {"GET"}) kw.setdefault("path_rules", RULES) kw.setdefault("use_case", "gitea-sync") kw.setdefault("now", NOW) return self.issuer.issue(tier, **kw) def test_tiers_listed(self): self.assertEqual( TIERS, ("one-shot", "session", "task", "timed", "permanent")) def test_one_shot_defaults(self): cap = self._issue() self.assertIsInstance(cap, ScopedCapability) self.assertEqual(cap.tier, "one-shot") self.assertEqual(cap.max_uses, 1) self.assertEqual(cap.uses_left, 1) self.assertEqual(cap.expires_at, NOW + 300) self.assertEqual(cap.use_case, "gitea-sync") self.assertFalse(cap.revoked) self.assertTrue(cap.matches(_req(), scope=_scope(), now=NOW)) issued = _events(self.audit, "sentinel_capability_issued") self.assertEqual(len(issued), 1) self.assertEqual(issued[0]["capability_id"], cap.id) self.assertEqual(issued[0]["reason"], "issued") def test_one_shot_use_once_then_exhausted(self): cap = self._issue() self.assertTrue(cap.use(_req(), scope=_scope(), now=NOW)) self.assertEqual(cap.uses_left, 0) self.assertFalse(cap.matches(_req(), scope=_scope(), now=NOW)) self.assertFalse(cap.use(_req(), scope=_scope(), now=NOW)) denied = _events(self.audit, "sentinel_capability_denied") reasons = [r["reason"] for r in denied] self.assertIn("uses_exhausted", reasons) self.assertEqual( len(_events(self.audit, "sentinel_capability_used")), 1) def test_session_bound_and_multi_use(self): cap = self._issue("session", session_id="s1") self.assertEqual(cap.max_uses, None) self.assertEqual(cap.expires_at, NOW + 3600) good = _scope(session_id="s1") self.assertTrue(cap.matches(_req(), scope=good, now=NOW)) for _ in range(3): self.assertTrue(cap.use(_req(), scope=good, now=NOW)) self.assertIsNone(cap.uses_left) self.assertFalse(cap.matches( _req(), scope=_scope(session_id="s2"), now=NOW)) self.assertFalse(cap.matches(_req(), scope=_scope(), now=NOW)) denied = _events(self.audit, "sentinel_capability_denied") reasons = [r["reason"] for r in denied] self.assertIn("session_mismatch", reasons) # scope 也可是带属性的对象,不只 dict。 obj = SimpleNamespace(use_case="gitea-sync", session_id="s1") self.assertTrue(cap.matches(_req(), scope=obj, now=NOW)) def test_task_bound(self): cap = self._issue("task", task_id="t1") self.assertTrue(cap.matches( _req(), scope=_scope(task_id="t1"), now=NOW)) self.assertTrue(cap.use( _req(), scope=_scope(task_id="t1"), now=NOW)) self.assertTrue(cap.use( _req(), scope=_scope(task_id="t1"), now=NOW)) self.assertFalse(cap.matches( _req(), scope=_scope(task_id="t2"), now=NOW)) self.assertFalse(cap.matches(_req(), scope=_scope(), now=NOW)) def test_timed_expiry(self): cap = self._issue("timed", ttl_seconds=60) self.assertEqual(cap.expires_at, NOW + 60) self.assertTrue(cap.matches(_req(), scope=_scope(), now=NOW + 59)) self.assertFalse(cap.matches(_req(), scope=_scope(), now=NOW + 60)) self.assertFalse(cap.use(_req(), scope=_scope(), now=NOW + 60)) expired = _events(self.audit, "sentinel_capability_expired") self.assertTrue(expired) self.assertTrue(all(r["reason"] == "expired" for r in expired)) def test_out_of_scope_denied(self): cap = self._issue() cases = [ (_req(host="other.example"), _scope(), "host_mismatch"), (_req(), _scope(use_case="other-use"), "use_case_mismatch"), (_req(method="POST"), _scope(), "method_not_allowed"), (_req(path="/login"), _scope(), "path_not_allowed"), (_req(), None, "use_case_mismatch"), ] for req, scope, reason in cases: self.assertFalse(cap.matches(req, scope=scope, now=NOW), reason) denied = _events(self.audit, "sentinel_capability_denied") reasons = [r["reason"] for r in denied] for _, _, reason in cases: self.assertIn(reason, reasons) def test_revoke(self): cap = self._issue() self.assertTrue(cap.revoke()) self.assertTrue(cap.revoked) self.assertFalse(cap.matches(_req(), scope=_scope(), now=NOW)) self.assertFalse(cap.use(_req(), scope=_scope(), now=NOW)) self.assertFalse(cap.revoke()) revoked = _events(self.audit, "sentinel_capability_revoked") self.assertEqual(len(revoked), 1) denied = _events(self.audit, "sentinel_capability_denied") self.assertIn("revoked", [r["reason"] for r in denied]) def test_permanent_default_refused(self): with self.assertRaises(ValueError) as ctx: self._issue("permanent") self.assertIn("permanent_requires_explicit_opt_in", str(ctx.exception)) denied = _events(self.audit, "sentinel_capability_denied") self.assertTrue(any( r["reason"] == "permanent_requires_explicit_opt_in" for r in denied)) def test_permanent_explicit_issue(self): cap = self._issue("permanent", allow_permanent=True) self.assertEqual(cap.tier, "permanent") self.assertIsNone(cap.expires_at) self.assertIsNone(cap.max_uses) self.assertTrue(cap.matches(_req(), scope=_scope(), now=NOW + 10 ** 9)) for _ in range(5): self.assertTrue(cap.use(_req(), scope=_scope(), now=NOW + 10 ** 9)) def test_issue_validation_errors(self): with self.assertRaises(ValueError): self._issue("nope") with self.assertRaises(ValueError): self._issue(use_case="") with self.assertRaises(ValueError): self._issue("session") with self.assertRaises(ValueError): self._issue("task") with self.assertRaises(ValueError): self._issue("timed") def test_audit_records_shape_and_no_values(self): cap = self._issue() cap.use(_req(), scope=_scope(), now=NOW) cap.revoke() for record in self.audit: self.assertIn("event", record) self.assertIn("reason", record) self.assertEqual(record["capability_id"], cap.id) self.assertEqual(record["tier"], "one-shot") self.assertEqual(record["connector"], "gitea") self.assertEqual(record["host"], HOST) self.assertEqual(record["use_case"], "gitea-sync") text = json.dumps(self.audit) self.assertNotIn("fake-secret", text) self.assertEqual( [r["event"] for r in self.audit], ["sentinel_capability_issued", "sentinel_capability_used", "sentinel_capability_revoked"]) class PolicyTest(unittest.TestCase): def setUp(self): self.audit = [] self.policy = DefaultPolicy( allow_entries=[("gitea", HOST, "/api/v1/")], audit=self.audit) def test_caution_levels_and_method_sets(self): self.assertEqual(CAUTION_LEVELS, ("default", "cautious", "permissive")) self.assertEqual(READ_METHODS, {"GET", "HEAD"}) self.assertEqual(WRITE_METHODS, {"POST", "PUT", "DELETE", "PATCH"}) self.assertEqual(self.policy.caution, "default") def test_default_allows_read_allowlisted(self): self.assertTrue(self.policy.auto_allow_eligible( _req(), tainted=False)) self.assertTrue(self.policy.auto_allow_eligible( _req(path="/api/v1/repos"), tainted=False)) self.assertTrue(self.policy.auto_allow_eligible( _req(method="HEAD"), tainted=False)) def test_default_denies_unlisted_host_and_write(self): self.assertFalse(self.policy.auto_allow_eligible( _req(host="other.example"), tainted=False)) for method in ("POST", "PUT", "DELETE", "PATCH"): self.assertFalse(self.policy.auto_allow_eligible( _req(method=method), tainted=False), method) def test_default_denies_sensitive_paths(self): for path in ("/api/v1/admin/users", "/api/v1/settings", "/api/v1/login", "/api/v1/tokens", "/api/v1/user/token"): self.assertFalse(self.policy.auto_allow_eligible( _req(path=path), tainted=False), path) def test_tainted_never_auto_allow(self): self.assertFalse(self.policy.auto_allow_eligible( _req(), tainted=True)) self.policy.set_caution("permissive") self.assertFalse(self.policy.auto_allow_eligible( _req(), tainted=True)) def test_cautious_narrows_to_exact_path(self): req = _req() self.assertTrue(self.policy.auto_allow_eligible( req, tainted=False)) self.assertTrue(self.policy.set_caution("cautious")) self.assertFalse(self.policy.auto_allow_eligible( req, tainted=False)) self.assertTrue(self.policy.auto_allow_eligible( _req(path="/api/v1/"), tainted=False)) self.assertTrue(self.policy.add_rule( "gitea", HOST, "/api/v1/version")) self.assertTrue(self.policy.auto_allow_eligible( req, tainted=False)) def test_permissive_widens_but_blocks_write_and_sensitive(self): self.assertTrue(self.policy.set_caution("permissive")) self.assertTrue(self.policy.auto_allow_eligible( _req(host="other.example"), tainted=False)) self.assertTrue(self.policy.auto_allow_eligible( _req(host="other.example", path="/x"), tainted=False)) self.assertFalse(self.policy.auto_allow_eligible( _req(host="other.example", method="POST"), tainted=False)) for path in ("/admin/x", "/settings", "/login", "/api/v1/tokens"): self.assertFalse(self.policy.auto_allow_eligible( _req(host="other.example", path=path), tainted=False), path) def test_set_caution_audits_and_validates(self): self.assertTrue(self.policy.set_caution("cautious")) self.assertEqual(self.policy.caution, "cautious") changed = _events(self.audit, "sentinel_policy_changed") self.assertEqual(len(changed), 1) self.assertEqual(changed[0]["change"], "set_caution") self.assertEqual(changed[0]["old_value"], "default") self.assertEqual(changed[0]["new_value"], "cautious") self.assertFalse(self.policy.set_caution("cautious")) self.assertEqual( len(_events(self.audit, "sentinel_policy_changed")), 1) with self.assertRaises(ValueError): self.policy.set_caution("reckless") with self.assertRaises(ValueError): DefaultPolicy(caution="reckless") def test_add_remove_rule_audits(self): self.assertTrue(self.policy.add_rule( "gitea", "other.example", "/api/v1/")) changed = _events(self.audit, "sentinel_policy_changed") self.assertEqual(len(changed), 1) self.assertEqual(changed[0]["change"], "add_rule") self.assertIsNone(changed[0]["old_value"]) self.assertEqual(changed[0]["new_value"]["host"], "other.example") self.assertFalse(self.policy.add_rule( "gitea", "other.example", "/api/v1/")) self.assertEqual( len(_events(self.audit, "sentinel_policy_changed")), 1) self.assertTrue(self.policy.remove_rule( "gitea", "other.example", "/api/v1/")) changed = _events(self.audit, "sentinel_policy_changed") self.assertEqual(changed[-1]["change"], "remove_rule") self.assertEqual(changed[-1]["new_value"], None) self.assertFalse(self.policy.remove_rule( "gitea", "other.example", "/api/v1/")) self.assertEqual(len(self.policy.list_rules()), 1) self.assertEqual(self.policy.list_rules()[0]["host"], HOST) if __name__ == "__main__": unittest.main()