- app_state.py: host_state_db_path() (host-level DB, distinct from per-VM state_db_path); kv_get/kv_put/kv_delete on meta (namespaced keys; kv_put skips the no-secrets guard by design — registry docs hold user prompts, documented); audit_trail table (P12 record shape + hash chain, seq explicit); counts()/backup() gain audit_trail. - swarm.py / presentation.py / browser_tasks.py: JSON-file backends replaced by KV rows (p8-swarm / p8-ledger / p8-browser-tasks); legacy JSON imported once on first load (renamed .migrated); corrupt row / unreadable store returns empty (fail-safe, same semantics as before; next successful save self-heals). - audit_trail.py: StoreAuditTrail (same record shape, hash chain, filters, export as JSONL version, backed by audit_trail table); migrate_jsonl_to_store() moves a chain verbatim (broken chain refused, divergent seq refused, re-run idempotent). JSONL class kept for reading old files. - P12 dict contracts now run against real P3/P5 code (ApprovalQueue.record -> build_approval_card -> card_matches_channel; CapabilityIssuer.issue -> describe -> PermissionRegistry.record_grant). - P6->P3 local protocol test: real PrivsepClient + real worker server + real git status; worker git argv carries the real P3 helper + useHttpPath; no token in argv/env/audit. runuser + HTTP pull stay real-machine-only (see gap docs). - Gap docs updated: P8 migration done, guest-side browser follow-up explicitly left for the real-machine track (no task_id in the browser protocol yet; lifecycle needs a real VM to verify); P6 wiring + P12 migration marked done; production-readiness.md gains §20 (round-12 integration). - Tests: new cloud/tests/test_chv_muse_round12.py, 24/24 OK; test_chv_muse_p8.py ledger corrupt test rewritten for the KV backend (+ legacy-import test). P7+P8: 95+1 tests OK.
529 lines
22 KiB
Python
529 lines
22 KiB
Python
"""对齐 Muse round-12 集成测试(P4~P12 合入后的跨轨接线)。
|
||
|
||
只测本轨新增的接线,不复述各轨自有测试:
|
||
- AppStateStore KV API + audit_trail 表(迁移地基)。
|
||
- P8 三处登记 + P12 审计 JSONL 到 AppStateStore 的迁移
|
||
(语义不变:去重、哈希链、project_id 隔离;旧文件一次
|
||
性导入;损坏回空 fail-safe)。
|
||
- P6 worker → P3 helper/authd → git 的本地协议链(真
|
||
PrivsepClient + 真 worker server + 真 git status;runuser
|
||
与 HTTP 拉取需真机,见测试 docstring)。
|
||
- P12 审批卡 / 权限登记对真实 P3/P5 代码的 dict 契约
|
||
(不再用替身)。
|
||
"""
|
||
|
||
import importlib.util
|
||
import json
|
||
import os
|
||
import socket
|
||
import sqlite3
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
import threading
|
||
import time
|
||
import unittest
|
||
from pathlib import Path
|
||
|
||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||
sys.path.insert(0, str(REPO_ROOT))
|
||
|
||
from cloud.chv.app_state import AppStateStore, host_state_db_path
|
||
from cloud.chv.audit_trail import (
|
||
AuditTrail,
|
||
StoreAuditTrail,
|
||
migrate_jsonl_to_store,
|
||
)
|
||
from cloud.chv.swarm import (
|
||
create_swarm,
|
||
get_swarm,
|
||
)
|
||
from cloud.chv.presentation import NotificationLedger
|
||
from cloud.chv.browser_tasks import (
|
||
BrowserTransport,
|
||
create_instance,
|
||
get_instance,
|
||
)
|
||
|
||
TOKEN = "fake-token-123"
|
||
T0 = 1700000000.0
|
||
|
||
|
||
def _load_guest_module(filename, module_name):
|
||
"""按文件加载 chv-guest 下的模块(不走包名,与 P3 测试同款)。"""
|
||
path = REPO_ROOT / "chv-guest" / filename
|
||
spec = importlib.util.spec_from_file_location(module_name, path)
|
||
mod = importlib.util.module_from_spec(spec)
|
||
spec.loader.exec_module(mod)
|
||
return mod
|
||
|
||
|
||
class KvApiTest(unittest.TestCase):
|
||
"""AppStateStore KV API(P8 迁移的后端)。"""
|
||
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.dir = self.tmp.name
|
||
self.store = AppStateStore(
|
||
os.path.join(self.dir, "app-state.sqlite"))
|
||
self.store.open()
|
||
|
||
def tearDown(self):
|
||
self.store.close()
|
||
self.tmp.cleanup()
|
||
|
||
def test_put_get_delete_roundtrip(self):
|
||
self.assertIsNone(self.store.kv_get("p8-swarm", "registry"))
|
||
self.store.kv_put("p8-swarm", "registry", '{"a": 1}')
|
||
self.assertEqual(self.store.kv_get("p8-swarm", "registry"),
|
||
'{"a": 1}')
|
||
self.store.kv_put("p8-swarm", "registry", '{"a": 2}')
|
||
self.assertEqual(self.store.kv_get("p8-swarm", "registry"),
|
||
'{"a": 2}')
|
||
self.store.kv_delete("p8-swarm", "registry")
|
||
self.assertIsNone(self.store.kv_get("p8-swarm", "registry"))
|
||
self.store.kv_delete("p8-swarm", "registry")
|
||
|
||
def test_namespaces_isolated(self):
|
||
self.store.kv_put("p8-swarm", "registry", "swarm-doc")
|
||
self.store.kv_put("p8-ledger", "registry", "ledger-doc")
|
||
self.assertEqual(self.store.kv_get("p8-swarm", "registry"),
|
||
"swarm-doc")
|
||
self.assertEqual(self.store.kv_get("p8-ledger", "registry"),
|
||
"ledger-doc")
|
||
|
||
def test_bad_namespace_or_key_rejected(self):
|
||
for ns, key in (("", "k"), ("a:b", "k"), ("ns", ""),
|
||
("ns", "a:b")):
|
||
with self.assertRaises(ValueError):
|
||
self.store.kv_put(ns, key, "v")
|
||
with self.assertRaises(ValueError):
|
||
self.store.kv_put("ns", "k", 123)
|
||
|
||
def test_host_path_differs_from_vm_path(self):
|
||
from cloud.chv.app_state import state_db_path
|
||
host = host_state_db_path(self.dir)
|
||
vm = state_db_path(self.dir, "chv-abc123")
|
||
self.assertNotEqual(host, vm)
|
||
self.assertTrue(host.endswith("app-state.sqlite"))
|
||
self.assertEqual(os.path.dirname(host), self.dir)
|
||
|
||
def test_counts_includes_audit_trail(self):
|
||
counts = self.store.counts()
|
||
self.assertIn("audit_trail", counts)
|
||
self.assertEqual(counts["audit_trail"], 0)
|
||
|
||
|
||
class StoreAuditTrailTest(unittest.TestCase):
|
||
"""StoreAuditTrail:与 JSONL 版同款链与隔离,走 SQLite。"""
|
||
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.dir = self.tmp.name
|
||
|
||
def tearDown(self):
|
||
self.tmp.cleanup()
|
||
|
||
def test_append_chain_resumes_across_instances(self):
|
||
first = StoreAuditTrail(self.dir)
|
||
r1 = first.append("p1", "worker", "task_dispatch", "run",
|
||
object_id="t1", ts=T0)
|
||
r2 = first.append("p1", "worker", "vm_lifecycle", "wake",
|
||
object_id="vm1", ts=T0 + 1)
|
||
self.assertEqual(r1["seq"], 1)
|
||
self.assertEqual(r2["seq"], 2)
|
||
self.assertEqual(r2["prev_hash"], r1["hash"])
|
||
second = StoreAuditTrail(self.dir)
|
||
r3 = second.append("p1", "worker", "task_dispatch", "done",
|
||
object_id="t1", ts=T0 + 2)
|
||
self.assertEqual(r3["seq"], 3)
|
||
self.assertEqual(r3["prev_hash"], r2["hash"])
|
||
ok, bad = second.verify()
|
||
self.assertTrue(ok)
|
||
self.assertEqual(bad, -1)
|
||
|
||
def test_project_isolation_and_filters(self):
|
||
trail = StoreAuditTrail(self.dir)
|
||
trail.append("p1", "worker", "task_dispatch", "run",
|
||
object_id="t1", ts=T0)
|
||
trail.append("p2", "worker", "task_dispatch", "run",
|
||
object_id="t2", ts=T0)
|
||
trail.append("p1", "worker", "quota", "denied",
|
||
object_id="t1", ts=T0 + 1)
|
||
self.assertEqual(len(trail.list_records("p1", limit=0)), 2)
|
||
self.assertEqual(len(trail.list_records("p2", limit=0)), 1)
|
||
self.assertEqual(trail.list_records("p3", limit=0), [])
|
||
self.assertEqual(trail.list_records("", limit=0), [])
|
||
self.assertEqual(
|
||
len(trail.list_records("p1", category="quota",
|
||
limit=0)), 1)
|
||
rows = trail.list_records("p1", limit=0)
|
||
self.assertGreater(rows[0]["ts"], rows[1]["ts"])
|
||
|
||
def test_tamper_detected_with_row_index(self):
|
||
trail = StoreAuditTrail(self.dir)
|
||
trail.append("p1", "worker", "task_dispatch", "run",
|
||
object_id="t1", ts=T0)
|
||
trail.append("p1", "worker", "task_dispatch", "done",
|
||
object_id="t1", ts=T0 + 1)
|
||
db = host_state_db_path(self.dir)
|
||
conn = sqlite3.connect(db)
|
||
conn.execute("UPDATE audit_trail SET action = ? WHERE seq = 2",
|
||
("forged",))
|
||
conn.commit()
|
||
conn.close()
|
||
ok, bad = StoreAuditTrail(self.dir).verify()
|
||
self.assertFalse(ok)
|
||
self.assertEqual(bad, 1)
|
||
|
||
def test_export_json(self):
|
||
trail = StoreAuditTrail(self.dir)
|
||
trail.append("p1", "worker", "task_dispatch", "run",
|
||
object_id="t1", ts=T0)
|
||
trail.append("p1", "worker", "task_dispatch", "done",
|
||
object_id="t1", ts=T0 + 1)
|
||
dest = os.path.join(self.dir, "export.json")
|
||
self.assertEqual(trail.export_json("p1", dest), 2)
|
||
self.assertEqual(os.stat(dest).st_mode & 0o777, 0o600)
|
||
with open(dest) as fh:
|
||
rows = json.load(fh)
|
||
self.assertEqual([r["seq"] for r in rows], [1, 2])
|
||
|
||
def test_secret_detail_scrubbed(self):
|
||
trail = StoreAuditTrail(self.dir)
|
||
rec = trail.append("p1", "worker", "task_dispatch", "run",
|
||
detail={"token": TOKEN, "note": "ok"},
|
||
ts=T0)
|
||
self.assertNotIn("token", rec["detail"])
|
||
self.assertEqual(rec["detail"]["note"], "ok")
|
||
|
||
|
||
class MigrateJsonlTest(unittest.TestCase):
|
||
"""migrate_jsonl_to_store:原样搬链。"""
|
||
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.dir = self.tmp.name
|
||
self.jsonl = os.path.join(self.dir, "audit.jsonl")
|
||
|
||
def tearDown(self):
|
||
self.tmp.cleanup()
|
||
|
||
def _seed_jsonl(self):
|
||
trail = AuditTrail(self.jsonl)
|
||
trail.append("p1", "worker", "task_dispatch", "run",
|
||
object_id="t1", ts=T0)
|
||
trail.append("p2", "worker", "quota", "denied",
|
||
object_id="t2", ts=T0 + 1)
|
||
trail.append("p1", "worker", "task_dispatch", "done",
|
||
object_id="t1", ts=T0 + 2)
|
||
return trail
|
||
|
||
def test_migrate_then_verify_and_list(self):
|
||
self._seed_jsonl()
|
||
self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir),
|
||
3)
|
||
store_trail = StoreAuditTrail(self.dir)
|
||
ok, bad = store_trail.verify()
|
||
self.assertTrue(ok)
|
||
self.assertEqual(bad, -1)
|
||
self.assertEqual(len(store_trail.list_records("p1",
|
||
limit=0)), 2)
|
||
self.assertEqual(len(store_trail.list_records("p2",
|
||
limit=0)), 1)
|
||
rows = store_trail.list_records("p1", limit=0)
|
||
self.assertEqual([r["seq"] for r in rows], [3, 1])
|
||
|
||
def test_migrate_is_idempotent(self):
|
||
self._seed_jsonl()
|
||
self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir),
|
||
3)
|
||
self.assertEqual(migrate_jsonl_to_store(self.jsonl, self.dir),
|
||
0)
|
||
ok, _ = StoreAuditTrail(self.dir).verify()
|
||
self.assertTrue(ok)
|
||
|
||
def test_missing_file_returns_zero(self):
|
||
self.assertEqual(
|
||
migrate_jsonl_to_store(os.path.join(self.dir, "nope"),
|
||
self.dir), 0)
|
||
|
||
def test_broken_chain_refused(self):
|
||
self._seed_jsonl()
|
||
with open(self.jsonl, "a") as fh:
|
||
fh.write("{broken json\n")
|
||
with self.assertRaises(ValueError):
|
||
migrate_jsonl_to_store(self.jsonl, self.dir)
|
||
self.assertEqual(
|
||
len(StoreAuditTrail(self.dir).list_records("p1",
|
||
limit=0)), 0)
|
||
|
||
def test_divergent_seq_refused(self):
|
||
self._seed_jsonl()
|
||
migrate_jsonl_to_store(self.jsonl, self.dir)
|
||
other = os.path.join(self.dir, "other.jsonl")
|
||
trail = AuditTrail(other)
|
||
trail.append("p1", "worker", "task_dispatch", "run",
|
||
object_id="tX", ts=T0)
|
||
with self.assertRaises(ValueError):
|
||
migrate_jsonl_to_store(other, self.dir)
|
||
|
||
|
||
class P8MigrationTest(unittest.TestCase):
|
||
"""P8 三处登记走 KV:无 JSON 文件、旧文件导入、损坏恢复。"""
|
||
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.dir = self.tmp.name
|
||
|
||
def tearDown(self):
|
||
self.tmp.cleanup()
|
||
|
||
def test_swarm_uses_kv_no_json_file(self):
|
||
swarm = create_swarm(self.dir, "p1", "parent1", now=T0)
|
||
self.assertFalse(os.path.exists(
|
||
os.path.join(self.dir, "swarms.json")))
|
||
self.assertTrue(os.path.exists(host_state_db_path(self.dir)))
|
||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||
raw = store.kv_get("p8-swarm", "registry")
|
||
self.assertIsNotNone(raw)
|
||
self.assertIn(swarm["id"], json.loads(raw)["swarms"])
|
||
self.assertEqual(get_swarm(self.dir, swarm["id"])["id"],
|
||
swarm["id"])
|
||
|
||
def test_swarm_legacy_json_imported_once(self):
|
||
legacy = {"swarms": {"s1": {"id": "s1", "project_id": "p1",
|
||
"parent_task_id": "parent1",
|
||
"agents": {}, "status": "active",
|
||
"created_at": T0}},
|
||
"tools": {}, "invocations": [], "audit": []}
|
||
with open(os.path.join(self.dir, "swarms.json"), "w") as fh:
|
||
json.dump(legacy, fh)
|
||
self.assertEqual(get_swarm(self.dir, "s1")["id"], "s1")
|
||
self.assertTrue(os.path.exists(
|
||
os.path.join(self.dir, "swarms.json.migrated")))
|
||
self.assertFalse(os.path.exists(
|
||
os.path.join(self.dir, "swarms.json")))
|
||
|
||
def test_swarm_corrupt_row_recovers(self):
|
||
create_swarm(self.dir, "p1", "parent1", now=T0)
|
||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||
store.kv_put("p8-swarm", "registry", "{not json")
|
||
swarm = create_swarm(self.dir, "p1", "parent2", now=T0)
|
||
self.assertEqual(swarm["parent_task_id"], "parent2")
|
||
|
||
def test_ledger_uses_kv_no_json_file(self):
|
||
ledger = NotificationLedger(self.dir)
|
||
self.assertTrue(ledger.should_notify("t1", "failed", now=T0))
|
||
self.assertFalse(os.path.exists(os.path.join(
|
||
self.dir, "notification-ledger.json")))
|
||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||
raw = store.kv_get("p8-ledger", "registry")
|
||
self.assertIn("t1:failed", json.loads(raw)["notified"])
|
||
|
||
def test_browser_tasks_legacy_import_and_corrupt_row(self):
|
||
legacy = {"instances": {
|
||
"old-task": {"instance_id": "i1", "task_id": "old-task",
|
||
"status": "destroyed",
|
||
"shots_dir": "browser-tasks/i1/shots",
|
||
"created_at": T0}}}
|
||
with open(os.path.join(self.dir, "browser-tasks.json"),
|
||
"w") as fh:
|
||
json.dump(legacy, fh)
|
||
self.assertEqual(
|
||
get_instance(self.dir, "old-task")["instance_id"], "i1")
|
||
self.assertTrue(os.path.exists(os.path.join(
|
||
self.dir, "browser-tasks.json.migrated")))
|
||
|
||
class _FakeTransport(BrowserTransport):
|
||
def spawn(self, instance):
|
||
return None
|
||
|
||
def close(self, instance):
|
||
return None
|
||
|
||
def ping(self, instance):
|
||
return True
|
||
|
||
with AppStateStore(host_state_db_path(self.dir)) as store:
|
||
store.kv_put("p8-browser-tasks", "registry", "{not json")
|
||
inst = create_instance(self.dir, "new-task",
|
||
_FakeTransport(), now=T0)
|
||
self.assertEqual(inst["status"], "active")
|
||
self.assertFalse(os.path.exists(os.path.join(
|
||
self.dir, "browser-tasks.json")))
|
||
|
||
|
||
class WorkerProtocolTest(unittest.TestCase):
|
||
"""P6 worker → P3 helper/authd → git 本地协议链。
|
||
|
||
真 PrivsepClient + 真 chv-gitea-worker server(Unix socket
|
||
+ SO_PEERCRED)+ 真 git status;run_git 探针调 worker 自身
|
||
的 ``_git_base_argv()``(含 P3 helper 与 useHttpPath)组
|
||
argv,只去掉本机不存在的 ``runuser -u chv`` 包装——runuser
|
||
与 HTTP 拉取(需真实 gitea.tose.top + Sentinel 放行)留
|
||
给真机轨,本测试不假装已覆盖。
|
||
"""
|
||
|
||
HELPER = REPO_ROOT / "chv-guest" / "git-credential-chv-authd"
|
||
|
||
def setUp(self):
|
||
self.tmp = tempfile.TemporaryDirectory()
|
||
self.dir = self.tmp.name
|
||
self.workspace = os.path.join(self.dir, "workspace")
|
||
os.makedirs(self.workspace)
|
||
self.repo = os.path.join(self.workspace, "testrepo")
|
||
subprocess.run(["git", "init", "-q", "-b", "main", self.repo],
|
||
check=True)
|
||
subprocess.run(["git", "-C", self.repo, "config",
|
||
"user.email", "t@example.invalid"], check=True)
|
||
subprocess.run(["git", "-C", self.repo, "config",
|
||
"user.name", "Test"], check=True)
|
||
with open(os.path.join(self.repo, "README.md"), "w") as fh:
|
||
fh.write("hello\n")
|
||
subprocess.run(["git", "-C", self.repo, "add", "."],
|
||
check=True)
|
||
subprocess.run(["git", "-C", self.repo, "commit", "-q",
|
||
"-m", "init"], check=True)
|
||
self.socket_path = os.path.join(self.dir, "worker.sock")
|
||
self.log_path = os.path.join(self.dir, "worker.log")
|
||
self.mod = _load_guest_module("chv-gitea-worker.py",
|
||
"chv_gitea_worker_round12")
|
||
self.mod.HELPER_PATH = str(self.HELPER)
|
||
self.argv_seen = []
|
||
self.env_seen = []
|
||
|
||
def run_git(action, repo):
|
||
argv = ["runuser", "-u", self.mod.SYNC_USER, "--",
|
||
"env", "HOME=/home/chv"] + \
|
||
self.mod._git_base_argv()
|
||
if action == "status":
|
||
argv += ["-C", repo, "status", "-sb"]
|
||
elif action == "pull":
|
||
argv += ["-C", repo, "pull", "--ff-only"]
|
||
elif action == "push":
|
||
argv += ["-C", repo, "push"]
|
||
else:
|
||
return 2, "", "未知 git 操作:%s" % action
|
||
self.argv_seen.append(list(argv))
|
||
env = {"PATH": os.environ.get("PATH", ""),
|
||
"HOME": self.dir,
|
||
"CHV_AUTHD_SOCKET": ""}
|
||
self.env_seen.append(dict(env))
|
||
runnable = [a for a in argv
|
||
if a not in ("runuser", "-u", "chv", "--")]
|
||
runnable[1] = "HOME=%s" % self.dir
|
||
proc = subprocess.run(runnable, capture_output=True,
|
||
text=True, timeout=60,
|
||
env={**env, "HOME": self.dir})
|
||
return proc.returncode, proc.stdout, proc.stderr
|
||
|
||
self.server = self.mod.WorkerServer(
|
||
self.socket_path, self.workspace, self.log_path,
|
||
allowed_uids=(os.getuid(),), run_git=run_git,
|
||
helper_path=str(self.HELPER))
|
||
self.thread = threading.Thread(
|
||
target=self.server.serve_forever, daemon=True)
|
||
self.thread.start()
|
||
|
||
def tearDown(self):
|
||
self.server.shutdown()
|
||
self.server.server_close()
|
||
self.tmp.cleanup()
|
||
|
||
def test_status_through_real_client_and_server(self):
|
||
from cloud.chv.privsep import PrivsepClient, WorkerRequest
|
||
client = PrivsepClient(socket_path=self.socket_path,
|
||
connector="gitea",
|
||
workspace=self.workspace)
|
||
ping = client.ping()
|
||
self.assertTrue(ping.ok)
|
||
resp = client.call(WorkerRequest(
|
||
"status", "gitea", self.repo, "req-1"))
|
||
self.assertTrue(resp.ok, resp.stderr)
|
||
self.assertEqual(resp.returncode, 0)
|
||
self.assertIn("## main", resp.stdout)
|
||
self.assertEqual(resp.request_id, "req-1")
|
||
# worker 的 git argv 走 P3 helper + useHttpPath,不含 token:
|
||
self.assertEqual(len(self.argv_seen), 1)
|
||
argv = self.argv_seen[0]
|
||
self.assertIn("credential.helper=%s" % self.HELPER, argv)
|
||
self.assertIn("credential.useHttpPath=true", argv)
|
||
self.assertNotIn(TOKEN, " ".join(argv))
|
||
self.assertNotIn(TOKEN, json.dumps(self.env_seen))
|
||
# worker 用的 helper 就是 P3 的 helper 文件本身:
|
||
self.assertTrue(os.path.samefile(
|
||
self.server.helper_path, str(self.HELPER)))
|
||
# 审计日志不含 token(本测试的 token 只存在于断言字面量,
|
||
# 不进任何被测输入——worker 从未见过它):
|
||
with open(self.log_path) as fh:
|
||
self.assertNotIn(TOKEN, fh.read())
|
||
|
||
def test_helper_file_is_the_p3_helper(self):
|
||
# worker 硬编码的 helper 路径与 P3 测试用的 helper 是同一
|
||
# 个文件(接线不是替身)。
|
||
from cloud.tests.test_chv_muse_p3 import HelperE2ETest
|
||
self.assertTrue(os.path.samefile(
|
||
str(self.HELPER), str(HelperE2ETest.HELPER)))
|
||
|
||
|
||
class P12DictContractTest(unittest.TestCase):
|
||
"""P12 接线用真实 P3/P5 代码,不用替身。"""
|
||
|
||
def test_approval_card_from_real_p3_queue(self):
|
||
from cloud.chv.sentinel import ApprovalQueue, EgressRequest
|
||
from cloud.chv.approval_card import (
|
||
build_approval_card,
|
||
card_matches_channel,
|
||
)
|
||
from cloud.chv.approval_channel import request_approval
|
||
queue = ApprovalQueue(clock=lambda: T0)
|
||
req = EgressRequest("gitea", "POST", "https",
|
||
"gitea.tose.top", 443,
|
||
"/api/v1/repos/admin/x/issues")
|
||
pending_id = queue.record(req, ["needs_approval"])
|
||
record = queue.list_pending()[0]
|
||
self.assertEqual(record["pending_id"], pending_id)
|
||
card = build_approval_card(record)
|
||
self.assertEqual(card["pending_id"], pending_id)
|
||
self.assertEqual(card["target"]["host"], "gitea.tose.top")
|
||
self.assertEqual(card["target"]["method"], "POST")
|
||
self.assertEqual(card["risk"], "medium")
|
||
self.assertIn("needs_approval", card["reasons"])
|
||
channel = request_approval(record, secret="test-secret",
|
||
now=T0)
|
||
self.assertTrue(
|
||
card_matches_channel(card, channel.payload()))
|
||
|
||
def test_permission_registry_from_real_p5_describe(self):
|
||
from cloud.chv.capabilities import CapabilityIssuer
|
||
from cloud.chv.activity import PermissionRegistry
|
||
issuer = CapabilityIssuer(clock=lambda: T0)
|
||
cap = issuer.issue(
|
||
"one-shot", "gitea", "gitea.tose.top", ["GET"],
|
||
[("GET", "/api/v1/*", "allow")], "sync", now=T0)
|
||
described = cap.describe()
|
||
reg_path = os.path.join(
|
||
tempfile.mkdtemp(), "permissions.json")
|
||
reg = PermissionRegistry(reg_path)
|
||
grant = reg.record_grant("p1", described, ts=T0)
|
||
self.assertEqual(grant["capability_id"],
|
||
described["capability_id"])
|
||
self.assertEqual(grant["connector"], "gitea")
|
||
self.assertEqual(grant["host"], "gitea.tose.top")
|
||
self.assertEqual(grant["use_case"], "sync")
|
||
self.assertEqual(grant["tier"], "one-shot")
|
||
active = reg.list_active("p1", now=T0 + 10)
|
||
self.assertEqual(len(active), 1)
|
||
self.assertEqual(active[0]["capability_id"],
|
||
described["capability_id"])
|
||
self.assertTrue(reg.note_use("p1",
|
||
described["capability_id"]))
|
||
self.assertTrue(reg.revoke("p1",
|
||
described["capability_id"]))
|
||
self.assertEqual(reg.list_active("p1", now=T0 + 10), [])
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|