P1 security batch: quota execution checks, cost accounting, goal/coordinator semantics, PR channel, orphan signature, control-plane and console hardening
- Docker quota watchdog + fail-closed quota checks, system tasks need real owner - max_cost_usd actually accumulated, cancel records cost, estimated rows excluded from unpriced cap, cache_write declared non-billed - goal-resume keeps original verify on breakpoint resume, goal retry falls back to resume, goal_max_rounds capped at 100 at create and execution - coordinator children inherit parent model/effort, orphans marked interrupted with audit - PR jobs on docker workers return explicit needs_local_worker instead of silent empty - worker code signature covers 6 files and is checked before claim, fail-closed - sub2api lazy provisioning with rotation, backup whitelist + 0600, push SSRF block, repos/workers desensitized, key/project audits and limits, console buttons/quota/status wording, coordinator/automation quota reserve
This commit is contained in:
1 parent
d254240d84
commit
1402dc67fd
20 files changed
+2616
-179
No files matched your search
+101
-8
@@ -441,8 +441,28 @@ def list_gateway_models(origin, api_key, request=None):
|
||||
return out
|
||||
|
||||
|
||||
def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
|
||||
"""幂等开通:已有行直接返回 key;否则建号->登录->发 key->授余额->入库。
|
||||
def gateway_key_health(origin, api_key, request=None):
|
||||
"""真健康检查:用已有 key 调一次 /v1/models。
|
||||
|
||||
返回 True(200,可用)/ False(401/403:key 已失效,或余额
|
||||
为 0——网关在余额为 0 时鉴权层直接 403,两者都算死 key)/
|
||||
"unknown"(其它状态或网络异常:网关本身不可用时不据此轮换,
|
||||
避免把一次抖动当成 key 死亡、反复删号重建)。
|
||||
"""
|
||||
req = request or _default_request
|
||||
try:
|
||||
st, _data = req("GET", origin + "/v1/models", token=api_key)
|
||||
except Exception:
|
||||
return "unknown"
|
||||
if st == 200:
|
||||
return True
|
||||
if st in (401, 403):
|
||||
return False
|
||||
return "unknown"
|
||||
|
||||
|
||||
def _create_fresh_gateway_key(conn, user_id, admin=None, request=None):
|
||||
"""建号->登录->发 key->授余额->入库,返回 (key, sub_id, key_id)。
|
||||
|
||||
入库失败时尽力清理刚建的 key 与账号(避免孤儿 key),再抛原错;
|
||||
授余额失败同样走这条清理(余额为 0 的 key 在网关侧直接 403,
|
||||
@@ -451,11 +471,9 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
|
||||
(同一进程内第二次起免管理员登录,见 shared_admin_session)。
|
||||
调用方负责 conn 的提交语义:本函数在入库后
|
||||
commit(开通是独立事务,不跟调用方的业务事务绑在一起)。
|
||||
调用方须保证调用前本用户无有效行(ensure/rotate 先处理
|
||||
旧行),否则会主键冲突(并发撞号除外,走轮询复用)。
|
||||
"""
|
||||
ensure_gateway_keys_table(conn)
|
||||
row = get_user_gateway_key(conn, user_id)
|
||||
if row is not None:
|
||||
return row["api_key"]
|
||||
origin = gateway_origin()
|
||||
if not origin:
|
||||
raise Sub2apiError("SUB2API_BASE_URL 未配置,无法开通网关账号")
|
||||
@@ -476,9 +494,16 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
|
||||
time.sleep(2)
|
||||
row = get_user_gateway_key(conn, user_id)
|
||||
if row is not None:
|
||||
return row["api_key"]
|
||||
# 合并修复(P1 rebase 到 d254240):main 的 #92 竞态
|
||||
# 分支原在单体 ensure 里只回一个 key 字符串;P1
|
||||
# 抽出 _create_fresh 后契约是 3 元组,4 个调用点
|
||||
# 都按 3 元组解包,裸字符串会被当成字符序列解包
|
||||
# 而 ValueError。这里按契约回 3 元组,key_id 未知
|
||||
# 故为 None(与 validate_or_rotate 的 ok 路一致)。
|
||||
return (row["api_key"], row["sub2api_user_id"], None)
|
||||
raise
|
||||
key_id = None
|
||||
user_token = None
|
||||
try:
|
||||
user_token, refresh = user_login(origin, email, password,
|
||||
request=req)
|
||||
@@ -497,7 +522,7 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
|
||||
(user_id, sub_id, key, refresh, now, now))
|
||||
conn.commit()
|
||||
except Exception:
|
||||
if key_id is not None:
|
||||
if key_id is not None and user_token is not None:
|
||||
try:
|
||||
user_delete_key(origin, user_token, key_id, request=req)
|
||||
except Exception:
|
||||
@@ -507,4 +532,72 @@ def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
|
||||
except Exception:
|
||||
pass
|
||||
raise
|
||||
return key, sub_id, key_id
|
||||
|
||||
|
||||
def ensure_user_gateway_key(conn, user_id, admin=None, request=None):
|
||||
"""幂等开通:已有行直接返回 key;否则走 _create_fresh 开通。
|
||||
|
||||
只保证“行存在”,不保证 key 仍有效:要校验有效性用
|
||||
validate_or_rotate_gateway_key(死 key 会触发轮换重开)。
|
||||
"""
|
||||
ensure_gateway_keys_table(conn)
|
||||
row = get_user_gateway_key(conn, user_id)
|
||||
if row is not None:
|
||||
return row["api_key"]
|
||||
key, _sub_id, _key_id = _create_fresh_gateway_key(
|
||||
conn, user_id, admin=admin, request=request)
|
||||
return key
|
||||
|
||||
|
||||
def validate_or_rotate_gateway_key(conn, user_id, admin=None, request=None):
|
||||
"""懒开通/健康检查合一:返回 (status, key, sub_id, key_id)。
|
||||
|
||||
status:ok(旧 key 经 /v1/models 实测可用)/ provisioned
|
||||
(无行,新开通)/ rotated(旧 key 经实测已死:401/403,
|
||||
含余额为 0 的 403;删旧行后重开,死 key 不再被当成已开通)。
|
||||
网关本身不可用(健康检查 unknown)时抛 Sub2apiError,调用方
|
||||
应回 502、保留旧行不轮换;绝不在此时把“待开通”当成可用。
|
||||
"""
|
||||
ensure_gateway_keys_table(conn)
|
||||
row = get_user_gateway_key(conn, user_id)
|
||||
origin = gateway_origin()
|
||||
if not origin:
|
||||
raise Sub2apiError("SUB2API_BASE_URL 未配置,无法开通网关账号")
|
||||
if row is None or not row.get("api_key"):
|
||||
key, sub_id, key_id = _create_fresh_gateway_key(
|
||||
conn, user_id, admin=admin, request=request)
|
||||
return "provisioned", key, sub_id, key_id
|
||||
health = gateway_key_health(origin, row["api_key"], request=request)
|
||||
if health is True:
|
||||
return "ok", row["api_key"], row.get("sub2api_user_id"), None
|
||||
if health is False:
|
||||
conn.execute(
|
||||
"DELETE FROM user_gateway_keys WHERE user_id = ?",
|
||||
(user_id,))
|
||||
conn.commit()
|
||||
key, sub_id, key_id = _create_fresh_gateway_key(
|
||||
conn, user_id, admin=admin, request=request)
|
||||
return "rotated", key, sub_id, key_id
|
||||
raise Sub2apiError("sub2api 网关健康检查暂不可用,保留现有 key")
|
||||
|
||||
|
||||
def rotate_user_gateway_key(conn, user_id, admin=None, request=None):
|
||||
"""仅限本人调用的重新开通/轮换:删旧行后重开。
|
||||
|
||||
无旧行时直接走开通(与 validate 的 provisioned 等价);返回
|
||||
(status, key, sub_id, key_id),调用方记审计时只可用 sub_id /
|
||||
key_id,绝不记 key 明文。旧 key 即使只是“疑似失效”也会被
|
||||
换掉:轮换是用户显式动作,不以健康检查为前提。
|
||||
"""
|
||||
ensure_gateway_keys_table(conn)
|
||||
row = get_user_gateway_key(conn, user_id)
|
||||
if row is not None:
|
||||
conn.execute(
|
||||
"DELETE FROM user_gateway_keys WHERE user_id = ?",
|
||||
(user_id,))
|
||||
conn.commit()
|
||||
key, sub_id, key_id = _create_fresh_gateway_key(
|
||||
conn, user_id, admin=admin, request=request)
|
||||
return ("rotated" if row is not None else "provisioned",
|
||||
key, sub_id, key_id)
|
||||
Reference in new issue
Block a user