Files
codex-app-server/cloud/tests/test_multiuser_gaps.py
T
host 7091a2d0d2 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.
2026-10-27 15:35:23 +00:00

288 lines
11 KiB
Python

"""多用户生产级缺口批单测:sweep 鉴权、默认执行器、新用户默认配额、
无价模型 token 上限(服务端准入)。
只用本地临时端口 + 临时 SQLite,不跑真实模型,不联网。
环境变量(默认执行器、默认配额额、token 上限)是服务端每次
请求现读的,测试里用 try/finally 改回,不影响其他测试。
"""
import http.client
import json
import os
import sqlite3
import sys
import tempfile
import threading
import time
import unittest
from datetime import datetime, timezone
from pathlib import Path
try:
from cloud.auth import AuthStore
from cloud.cost import CostStore
from cloud.server import _apply_default_quota, _default_quota_usd_micros
from cloud.server import create_server
except ImportError: # discover -s cloud/tests 时把项目根目录补进 sys.path
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
from cloud.auth import AuthStore
from cloud.cost import CostStore
from cloud.server import _apply_default_quota, _default_quota_usd_micros
from cloud.server import create_server
def _req(port, method, path, body=None, key=None):
conn = http.client.HTTPConnection("127.0.0.1", port, timeout=10)
hdrs = {}
payload = None
if body is not None:
payload = json.dumps(body).encode("utf-8")
hdrs["Content-Type"] = "application/json"
if key is not None:
hdrs["X-API-Key"] = key
try:
conn.request(method, path, body=payload, headers=hdrs)
resp = conn.getresponse()
data = resp.read()
finally:
conn.close()
try:
parsed = json.loads(data.decode("utf-8")) if data else None
except ValueError:
parsed = None
return resp.status, parsed
def _utc_today():
return datetime.now(timezone.utc).strftime("%Y-%m-%d")
def _setenv(name, value):
old = os.environ.get(name)
if value is None:
os.environ.pop(name, None)
else:
os.environ[name] = value
return old
class MultiuserGapsTestCase(unittest.TestCase):
def setUp(self):
self.tmp = tempfile.TemporaryDirectory()
self.db_path = os.path.join(self.tmp.name, "control.db")
self.auth = AuthStore(self.db_path)
self.owner_id, self.owner_key = self.auth.create_user("owner")
self._old_admins = os.environ.get("CODEX_GLOBAL_ADMINS")
os.environ["CODEX_GLOBAL_ADMINS"] = self.owner_id
self.viewer_id, self.viewer_key = self.auth.create_user("viewer")
self.outsider_id, self.outsider_key = self.auth.create_user("outsider")
self.server = create_server(self.db_path, "127.0.0.1", 0)
self.port = self.server.server_address[1]
self.thread = threading.Thread(
target=self.server.serve_forever, daemon=True)
self.thread.start()
s, proj = _req(self.port, "POST", "/projects",
{"name": "gproj"}, self.owner_key)
self.assertEqual(s, 201, proj)
self.project_id = proj["id"]
self.auth.set_member(
self.project_id, self.owner_id, self.viewer_id, "viewer")
def tearDown(self):
self.server.shutdown()
self.server.server_close()
self.thread.join(timeout=5)
try:
self.auth.conn.close()
except Exception:
pass
self.tmp.cleanup()
if getattr(self, "_old_admins", None) is None:
os.environ.pop("CODEX_GLOBAL_ADMINS", None)
else:
os.environ["CODEX_GLOBAL_ADMINS"] = self._old_admins
# -- helpers -------------------------------------------------------
def _create_task(self, key=None, body_extra=None):
body = {"project_id": self.project_id, "prompt": "gap probe"}
if body_extra:
body.update(body_extra)
return _req(self.port, "POST", "/tasks", body,
key if key is not None else self.owner_key)
def _insert_quota(self, user_id, micros):
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"CREATE TABLE IF NOT EXISTS user_quotas("
"user_id TEXT PRIMARY KEY, quota_usd_micros BIGINT,"
" updated_at DOUBLE PRECISION)")
conn.execute(
"INSERT OR REPLACE INTO user_quotas"
"(user_id, quota_usd_micros, updated_at) VALUES(?, ?, ?)",
(user_id, micros, time.time()))
conn.commit()
finally:
conn.close()
def _insert_unpriced_cost(self, task_id, input_tokens, output_tokens):
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"INSERT INTO tasks(id, project_id, prompt, status,"
" idempotency_key, depends_on, priority, created_by,"
" created_at, updated_at, last_heartbeat)"
" VALUES(?, ?, ?, 'completed', NULL, '[]', 0, ?, ?, ?, NULL)",
(task_id, self.project_id, "donor", self.owner_id,
time.time(), time.time()))
conn.commit()
finally:
conn.close()
# CostStore.record 建表并按 UTC 今天记 day,与服务端查询口径一致。
store = CostStore(self.db_path)
try:
store.record(task_id, self.project_id, "gpt-5.6-terra",
input_tokens=input_tokens,
output_tokens=output_tokens,
usd_micros=None, source="actual")
finally:
try:
store._conn.close()
except Exception:
pass
def _make_running_task(self):
s, task = self._create_task()
self.assertEqual(s, 201, task)
s, _ = _req(self.port, "POST",
"/tasks/%s/heartbeat" % task["id"], {},
self.owner_key)
self.assertEqual(s, 200)
return task["id"]
# -- sweep 鉴权 -----------------------------------------------------
def test_sweep_viewer_and_outsider_forbidden(self):
for key in (self.viewer_key, self.outsider_key):
s, body = _req(self.port, "POST", "/admin/sweep", {},
key)
self.assertEqual(s, 403, body)
def test_sweep_owner_maintainer_ok_and_sweeps_stale(self):
tid = self._make_running_task()
conn = sqlite3.connect(self.db_path)
try:
conn.execute(
"UPDATE tasks SET last_heartbeat = ? WHERE id = ?",
(time.time() - 3600, tid))
conn.commit()
finally:
conn.close()
s, body = _req(self.port, "POST", "/admin/sweep",
{"threshold_seconds": 300}, self.owner_key)
self.assertEqual(s, 200, body)
self.assertIn(tid, body["swept"])
def test_sweep_threshold_clamped_to_60(self):
tid = self._make_running_task()
s, body = _req(self.port, "POST", "/admin/sweep",
{"threshold_seconds": 0}, self.owner_key)
self.assertEqual(s, 200, body)
self.assertNotIn(tid, body["swept"])
# -- 默认执行器 ------------------------------------------------------
def test_default_executor_is_docker(self):
s, task = self._create_task()
self.assertEqual(s, 201, task)
self.assertEqual(task["executor"], "docker")
def test_explicit_executor_kept(self):
s, task = self._create_task(body_extra={"executor": "local"})
self.assertEqual(s, 201, task)
self.assertEqual(task["executor"], "local")
def test_default_executor_env_override(self):
old = _setenv("CLOUD_DEFAULT_EXECUTOR", "local")
try:
s, task = self._create_task()
self.assertEqual(s, 201, task)
self.assertEqual(task["executor"], "local")
finally:
_setenv("CLOUD_DEFAULT_EXECUTOR", old)
old = _setenv("CLOUD_DEFAULT_EXECUTOR", "")
try:
s, task = self._create_task()
self.assertEqual(s, 201, task)
self.assertIsNone(task["executor"])
finally:
_setenv("CLOUD_DEFAULT_EXECUTOR", old)
# -- 新用户默认配额 ---------------------------------------------------
def test_apply_default_quota_idempotent(self):
conn = sqlite3.connect(self.db_path)
try:
self.assertTrue(_apply_default_quota(conn, "newbie"))
row = conn.execute(
"SELECT quota_usd_micros FROM user_quotas"
" WHERE user_id = 'newbie'").fetchone()
self.assertEqual(row[0], _default_quota_usd_micros())
# 第二次不再写,不覆盖管理员后来改过的额度。
conn.execute(
"UPDATE user_quotas SET quota_usd_micros = 12345"
" WHERE user_id = 'newbie'")
conn.commit()
self.assertFalse(_apply_default_quota(conn, "newbie"))
row = conn.execute(
"SELECT quota_usd_micros FROM user_quotas"
" WHERE user_id = 'newbie'").fetchone()
self.assertEqual(row[0], 12345)
finally:
conn.close()
def test_default_quota_env_override(self):
old = _setenv("CLOUD_DEFAULT_QUOTA_USD_MICROS", "5000")
try:
self.assertEqual(_default_quota_usd_micros(), 5000)
finally:
_setenv("CLOUD_DEFAULT_QUOTA_USD_MICROS", old)
# -- 无价 token 上限(服务端准入)--------------------------------------
def test_unpriced_token_cap_blocks_create(self):
self._insert_quota(self.owner_id, 10 ** 12) # USD 很大,不挡
for i in range(7): # 7 * 150 = 1050 token
self._insert_unpriced_cost("donor-%d" % i, 100, 50)
old = _setenv("CLOUD_QUOTA_UNPRICED_TOKEN_CAP", "1000")
try:
s, body = self._create_task()
self.assertEqual(s, 402, body)
self.assertEqual(body["error"], "quota_exceeded")
self.assertEqual(body["unpriced_tokens_used"], 1050)
finally:
_setenv("CLOUD_QUOTA_UNPRICED_TOKEN_CAP", old)
def test_unpriced_token_cap_under_cap_passes(self):
self._insert_quota(self.owner_id, 10 ** 12)
for i in range(6): # 6 * 150 = 900 < 1000
self._insert_unpriced_cost("donor-%d" % i, 100, 50)
old = _setenv("CLOUD_QUOTA_UNPRICED_TOKEN_CAP", "1000")
try:
s, task = self._create_task()
self.assertEqual(s, 201, task)
finally:
_setenv("CLOUD_QUOTA_UNPRICED_TOKEN_CAP", old)
def test_unpriced_token_cap_blocks_without_quota_row(self):
# P0 安全批后:无配额行不再等于不限额,无价 cap 照样挡。
for i in range(7):
self._insert_unpriced_cost("donor-%d" % i, 100, 50)
old = _setenv("CLOUD_QUOTA_UNPRICED_TOKEN_CAP", "1000")
try:
s, body = self._create_task()
self.assertEqual(s, 402, body)
self.assertEqual(body["error"], "quota_exceeded")
finally:
_setenv("CLOUD_QUOTA_UNPRICED_TOKEN_CAP", old)
if __name__ == "__main__":
unittest.main()