P0 security batch: true global admin, quota gates on all spending paths, default quota for quota-less users, fix-review author trust + docker/owner attribution, GitHub App setup issuer binding, real cancel kill, wider shell env exclusion
- CODEX_GLOBAL_ADMINS replaces maintainer-derived global admin; admin endpoints (quotas, sweep, backup, push dispatch) fail-closed 403 when unset, with audit on allow and deny. - continue/followups/messages/retry check both caller and original creator quotas (USD + unpriced token cap), 402 on either overrun. - Quota-less users now get the #86 default quota instead of unlimited; init_db backfills and OAuth create/bind/existing-login branches INSERT OR IGNORE a default row; explicit NULL row stays unlimited. user_quotas added to migration tables. - /fix-review: untrusted authors rejected before dedup claim, no task; trusted tasks force executor=docker, created_by=project owner, owner quota gate, prompt is a fixed template plus a 2000-char excerpt. - GitHub App setup state only issued by global admins, state rows bind issued_by (ALTER-compatible for old DBs), callback rejects issuers no longer in the admin list without consuming the state. - Cancel: popen poll checks DB status every second, Docker path runs a cancel watchdog that kills by container name, write-back is CAS-guarded so late results never overwrite a new status. - SHELL_ENV_EXCLUDE adds *_TOKEN, *_AUTH*, *_CREDENTIAL*, *API_KEY*; comment states the residual risk: same-UID /proc environ remains readable, true isolation needs UID separation or hidepid. - Tests: new cloud/tests/test_p0_security.py (17 cases); old tests updated to the new semantics (admin env, default quota, trusted fix-review author) without weakening any gate.
This commit is contained in:
1 parent
d0ca1f7f60
commit
7091a2d0d2
13 files changed
+1017
-146
No files matched your search
@@ -90,6 +90,10 @@ class ServerQuotasTestCase(unittest.TestCase):
|
||||
self.auth = AuthStore(self.db_path)
|
||||
self.owner_id, self.owner_key = self.auth.create_user("owner")
|
||||
self.other_id, self.other_key = self.auth.create_user("other")
|
||||
# P0 安全批后:admin 端点只认 CODEX_GLOBAL_ADMINS,
|
||||
# 本文件里 owner 即管理员,测前写 env、测后还原。
|
||||
self._old_admins = os.environ.get("CODEX_GLOBAL_ADMINS")
|
||||
os.environ["CODEX_GLOBAL_ADMINS"] = self.owner_id
|
||||
self.server = create_server(self.db_path, "127.0.0.1", 0)
|
||||
self.port = self.server.server_address[1]
|
||||
self.thread = threading.Thread(
|
||||
@@ -104,6 +108,10 @@ class ServerQuotasTestCase(unittest.TestCase):
|
||||
self.server.shutdown()
|
||||
self.server.server_close()
|
||||
self.tmp.cleanup()
|
||||
if self._old_admins is None:
|
||||
os.environ.pop("CODEX_GLOBAL_ADMINS", None)
|
||||
else:
|
||||
os.environ["CODEX_GLOBAL_ADMINS"] = self._old_admins
|
||||
|
||||
def _create_task(self, key=None):
|
||||
return _req(self.port, "POST", "/tasks",
|
||||
@@ -116,13 +124,14 @@ class ServerQuotasTestCase(unittest.TestCase):
|
||||
{"user_id": target, "quota_usd_micros": quota},
|
||||
key if key is not None else self.owner_key)
|
||||
|
||||
def test_self_quota_default_unlimited(self):
|
||||
def test_self_quota_default_is_default_quota_not_unlimited(self):
|
||||
# P0 安全批后:无配额行按默认额度处理,不再是 None。
|
||||
s, q = _req(self.port, "GET", "/users/me/quota",
|
||||
key=self.owner_key)
|
||||
self.assertEqual(s, 200, q)
|
||||
self.assertIsNone(q["quota_usd_micros"])
|
||||
self.assertEqual(q["quota_usd_micros"], 10000000)
|
||||
self.assertEqual(q["used_usd_micros"], 0)
|
||||
self.assertIsNone(q["remaining_usd_micros"])
|
||||
self.assertEqual(q["remaining_usd_micros"], 10000000)
|
||||
self.assertEqual(q["month"],
|
||||
datetime.now(timezone.utc).strftime("%Y-%m"))
|
||||
|
||||
@@ -234,9 +243,11 @@ class ServerQuotasTestCase(unittest.TestCase):
|
||||
self.assertEqual(s, 200, retried)
|
||||
self.assertEqual(retried["status"], "queued")
|
||||
|
||||
def test_retry_uses_creator_quota_not_operator(self):
|
||||
# 重试按任务创建者查配额:费用记创建者名下,操作人
|
||||
# 是谁不应影响准入。other 须是 maintainer 才能过
|
||||
def test_retry_blocked_when_operator_or_creator_over_quota(self):
|
||||
# P0 安全批后:重试按双人门查配额(触发人 + 创建人)。
|
||||
# #90 的“只查创建者、操作人超限不拦”已被 P0 有意收紧:
|
||||
# 操作人用自己的额度过门、费用记创建者名下,仍是绕限,
|
||||
# 所以操作人超限也必须拦。other 须是 maintainer 才能过
|
||||
# approve_merge 权限门,否则 403 会掩盖配额判断。
|
||||
self.auth.set_member(self.project_id, self.owner_id,
|
||||
self.other_id, "maintainer")
|
||||
@@ -252,12 +263,22 @@ class ServerQuotasTestCase(unittest.TestCase):
|
||||
"/tasks/%s/retry" % task["id"], {},
|
||||
self.other_key)
|
||||
self.assertEqual(s, 402, body)
|
||||
# 创建者不限额、操作人额度为 0:按旧口径(查操作人)
|
||||
# 会拦,按创建者口径应放行。
|
||||
# 创建者不限额、操作人额度为 0:P0 双人门应拦(#90
|
||||
# 只查创建者的旧口径会放行,那正是 P0 要堵的绕限)。
|
||||
s, _ = self._set_quota(self.owner_id, None)
|
||||
self.assertEqual(s, 200)
|
||||
s, _ = self._set_quota(self.other_id, 0)
|
||||
self.assertEqual(s, 200)
|
||||
s, body = _req(self.port, "POST",
|
||||
"/tasks/%s/retry" % task["id"], {},
|
||||
self.other_key)
|
||||
self.assertEqual(s, 402, body)
|
||||
s, row = _req(self.port, "GET", "/tasks/" + task["id"],
|
||||
key=self.owner_key)
|
||||
self.assertEqual(row["status"], "failed")
|
||||
# 两人都不超限才放行。
|
||||
s, _ = self._set_quota(self.other_id, None)
|
||||
self.assertEqual(s, 200)
|
||||
s, retried = _req(self.port, "POST",
|
||||
"/tasks/%s/retry" % task["id"], {},
|
||||
self.other_key)
|
||||
@@ -289,7 +310,6 @@ class ServerQuotasTestCase(unittest.TestCase):
|
||||
{"message": "again"}, self.owner_key)
|
||||
self.assertEqual(s, 200, continued)
|
||||
self.assertEqual(continued["status"], "queued")
|
||||
# 续跑同样按创建者查:操作人额度为 0、创建者不限额时放行。
|
||||
self.auth.set_member(self.project_id, self.owner_id,
|
||||
self.other_id, "maintainer")
|
||||
s, task2 = self._create_task()
|
||||
@@ -297,9 +317,21 @@ class ServerQuotasTestCase(unittest.TestCase):
|
||||
_set_task_status(self.db_path, task2["id"], "completed")
|
||||
s, _ = self._set_quota(self.other_id, 0)
|
||||
self.assertEqual(s, 200)
|
||||
# P0 安全批后:续跑按双人门查,操作人额度为 0、
|
||||
# 创建者不限额时也应拦(#90 只查创建者的旧口径会
|
||||
# 放行,那正是 P0 要堵的绕限);两人都不超限才放行。
|
||||
s, body = _req(self.port, "POST",
|
||||
"/tasks/%s/continue" % task2["id"],
|
||||
{"message": "again"}, self.other_key)
|
||||
self.assertEqual(s, 402, body)
|
||||
s, row = _req(self.port, "GET", "/tasks/" + task2["id"],
|
||||
key=self.owner_key)
|
||||
self.assertEqual(row["status"], "completed")
|
||||
s, _ = self._set_quota(self.other_id, None)
|
||||
self.assertEqual(s, 200)
|
||||
s, continued2 = _req(self.port, "POST",
|
||||
"/tasks/%s/continue" % task2["id"],
|
||||
{"message": "again"}, self.other_key)
|
||||
"/tasks/%s/continue" % task2["id"],
|
||||
{"message": "again"}, self.other_key)
|
||||
self.assertEqual(s, 200, continued2)
|
||||
self.assertEqual(continued2["status"], "queued")
|
||||
|
||||
|
||||
Reference in new issue
Block a user