# [fix-iter17 auth-gate v3] fail-closed 登录态门禁 —— 勿删 # 归属 Bug:OmWRbpwPZRpNe9r2hkKWC / uabpjOGelxJTknG9vqO9V / G-l57of8enoipnJDg17Uz # 口径:本块必须是文件第一条可执行语句(在命名空间 shim / 业务分支之前),任何匿名请求 # (无有效会话)一律返回真 HTTP 401,**不返回任何业务数据**;Response 类解析不到时 # raise(→500)硬失败,绝不降级为 200——R26~R27 的两次放行均源于「判据失真 + 降级返回」。 def _r17_uid_of(_v): if not _v: return "" _BAD = ("", "none", "null", "anonymous", "anonymoususer", "anon", "0") if isinstance(_v, str): _s = _v.strip() return "" if _s.lower() in _BAD else _s if isinstance(_v, (list, tuple, set, bytes, bytearray)): return "" _KEYS = ("id", "userid", "user_id", "user", "username", "name", "uid", "account") if isinstance(_v, dict): for _k in _KEYS: try: _x = _v.get(_k) except Exception: _x = None if _x: _s = str(_x).strip() if _s.lower() not in _BAD: return _s return "" for _k in _KEYS: try: _x = getattr(_v, _k, None) except Exception: _x = None if _x: _s = str(_x).strip() if _s.lower() not in _BAD: return _s return "" async def _r17_uid(): """取登录用户标识;取不到返回 ''。裸名解析(ahserver 预加载全局在 builtins)。""" for _n in ("get_userinfo", "get_user", "get_username"): try: if _n == "get_userinfo": _f = get_userinfo elif _n == "get_user": _f = get_user else: _f = get_username except Exception: continue try: _r = _f() if hasattr(_r, "__await__"): _r = await _r except Exception: continue _u = _r17_uid_of(_r) if _u: return _u return "" _r17_uid_val = "" try: _r17_uid_val = await _r17_uid() except Exception: _r17_uid_val = "" if not _r17_uid_val: _r17_deny = { "status": 401, "success": False, "ok": False, "need_login": True, "message": "unauthorized: login required", "rows": [], "data": [], "total": 0, } _r17_proc = None try: _r17_proc = processor except Exception: _r17_proc = globals().get("processor") _r17_rc = None try: if _r17_proc is not None: _r17_rc = _r17_proc.__class__.__init__.__globals__.get("Response") except Exception: _r17_rc = None if _r17_rc is None: raise RuntimeError( "[fix-iter17 auth-gate v3] 未登录且无法解析 aiohttp Response:门禁 hard-fail" "(宁 500 不放行,绝不返回业务数据)" ) return _r17_rc( body=json.dumps(_r17_deny, ensure_ascii=False).encode("utf-8"), status=401, headers={"Cache-Control": "no-store", "X-Scene-Auth": "denied"}, content_type="application/json", ) # [fix-iter17 auth-gate v3] end # [fix-iter3 strict-auth v2] fail-closed 门禁(Bug 1BoEurcDgf4HHRqBMmJt8 / R-01-neg,P0)——勿删 # 本段取代「globals().get("get_user") 判空」的旧判据:ahserver 的 dspy 预加载全局经 # builtins 解析、不在 exec-globals 里,globals().get 恒为 None → 判据失真 → 匿名 200。 # 口径:裸名取值 → 双重判空(get_user + get_userinfo,且必须取到有效用户标识) # → 未登录返回真 HTTP 401(Response 解析不到时 set_status(401)+JSON,绝不返回业务数据)。 _auth_uid = "" def _auth_uid_of(_v): """从 get_user()/get_userinfo() 的返回值里提取有效用户标识;取不到返回 ''。""" if not _v: return "" if isinstance(_v, str): return _v if isinstance(_v, dict): for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"): try: _x = _v.get(_k) except Exception: _x = None if _x: return str(_x) return "" for _k in ("id", "userid", "user_id", "user", "username", "name", "uid", "account"): try: _x = getattr(_v, _k, None) except Exception: _x = None if _x: return str(_x) return "" def _auth_call(_name): """裸名优先取预加载全局(builtins 解析),退 globals 映射。取不到返回 None。""" _f = None try: _f = globals().get(_name) except Exception: _f = None if not callable(_f): try: _f = _auth_bare(_name) except Exception: _f = None return _f if callable(_f) else None def _auth_bare(_name): """裸名解析(ahserver 预加载全局在 builtins 中);未注入时抛 NameError。""" if _name == "get_user": return get_user if _name == "get_userinfo": return get_userinfo if _name == "get_username": return get_username raise NameError(_name) async def _auth_resolve_uid(): for _n in ("get_userinfo", "get_user", "get_username"): _f = _auth_call(_n) if _f is None: continue try: _r = _f() if hasattr(_r, "__await__"): _r = await _r except Exception: _r = None _u = _auth_uid_of(_r) if _u: return _u return "" try: _auth_uid = await _auth_resolve_uid() except Exception: _auth_uid = "" if not _auth_uid: _auth_deny = { "status": 401, "success": False, "ok": False, "need_login": True, "message": "unauthorized: login required", "reason": "login required", "rows": [], "data": [], "total": 0, } _auth_resp_cls = None try: _auth_proc = globals().get("processor") if _auth_proc is None: _auth_proc = processor _auth_resp_cls = _auth_proc.__class__.__init__.__globals__.get("Response") except Exception: _auth_resp_cls = None if _auth_resp_cls is not None: return _auth_resp_cls( body=json.dumps(_auth_deny, ensure_ascii=False).encode("utf-8"), status=401, headers={"Cache-Control": "no-store"}, content_type="application/json", ) try: request.set_status(401) except Exception: pass try: return json.dumps(_auth_deny, ensure_ascii=False) except Exception: return _auth_deny # [fix-iter3 strict-auth v2] end # [scene-auth-guard] R26-05/06/07/09 —— scene 端点登录态门禁(fail-closed,勿删) # 背景:R26 复测(plan 0wtjrw6Gn3WG5ewb-BaDR)实测**匿名(不带会话 cookie)**请求本模块 # api/*.dspy 返回 HTTP 200 + 业务数据(get_search_status / get_search_scene_type / # get_search_world_id 三个读类端点,以及 scene_import 写入口)。 # 根因:scene 的 RBAC 路径已在 scripts/load_path.py 逐条登记(10 个 .dspy 全登记、无通配符), # 但 ahserver 的 dspy 处理器(PythonScriptProcessor)只注入 get_user 等工具函数、不做鉴权; # 权限表登记 != 端点有登录态校验 → 未登录仍可直连 dspy 拿到业务数据。故补**入口侧**门禁(纵深防御)。 # 口径(QC #12 收口): # * await get_user()(ahserver 注入,async;无会话时返回 None/空)判登录态; # 取不到再退 get_username();两者都空 → 拒绝; # * 拒绝时返回**真 HTTP 401**(aiohttp Response;BaseProcessor.handle 对 Response 原样透传), # body 只含 status/success/need_login/message 与空 rows/data,**不含任何业务数据**; # * dspy 禁 import,Response 类从已注入的 processor 的对象模块 globals 解析(ahserver # processorResource 模块级已 from aiohttp.web_response import Response); # * **无降级分支**:Response 解析失败一律 `raise RuntimeError` 硬失败(宁 500 不放行)。 # 旧版曾降级为返回 dict {"status":401,...},ahserver 会把 dict 包成 HTTP 200 的 JSON body # ("200 + 语义 401"),鉴权强度落空,且离线 harness 覆盖不到该路径 —— 已按 QC #12 方案(b) 删除。 try: _scene_au = None _scene_gu = globals().get("get_user") if callable(_scene_gu): _scene_au = _scene_gu() if hasattr(_scene_au, "__await__"): _scene_au = await _scene_au if not _scene_au: _scene_gn = globals().get("get_username") if callable(_scene_gn): _scene_au = _scene_gn() if hasattr(_scene_au, "__await__"): _scene_au = await _scene_au except Exception: _scene_au = None if not _scene_au: _scene_deny = { "status": 401, "success": False, "need_login": True, "rows": [], "data": [], "total": 0, "message": "[scene] 未登录或会话已失效:本接口需要登录态,请先登录后访问", } _scene_resp_cls = None try: _scene_proc = globals().get("processor") _scene_resp_cls = _scene_proc.__class__.__init__.__globals__.get("Response") except Exception: _scene_resp_cls = None if _scene_resp_cls is None: raise RuntimeError( "[scene-auth-guard] 未登录且无法解析 aiohttp Response:门禁 hard-fail" "(宁 500 不放行,绝不返回业务数据)" ) return _scene_resp_cls( body=json.dumps(_scene_deny, ensure_ascii=False).encode("utf-8"), status=401, headers={"X-Scene-Auth": "denied", "Cache-Control": "no-store"}, content_type="application/json", ) # [/scene-auth-guard] # [scene/options] 场景状态下拉端点 get_search_status.dspy —— 读类 500 归零修复版(R27) # Bug _ViEgLj09USW3JU_79r5a:GET /scene/api/get_search_status.dspy → 500(无 JSON 体) # Bug 3x-k0LCAk32WcM4ao5xQo:同端点列/取参根因同族 # 根因(旧实现): # 1) 委托 scene/init.py::get_scene_status_options(),其内部 # recs = await sor.R("appcodes_kv", {"parentid":"scene_status","page":1,"rows":1000}) # 带 page/rows 的查询返回 {'total':N,'rows':[...]},代码直接 for 迭代 dict → 得到 str 键 # [{"value": r.k, ...} for r in recs] → AttributeError: 'str' object has no attribute 'k' # 2) params_kw 为 None/str 时对参数取属性 → 'NoneType' object has no attribute 'get' # 3) 旧实现把异常上抛给框架 → 响应体退化为 "Server got itself in trouble" # 修复: # - 端点自包含,不再依赖模块侧委托函数(避免 init.py 与本文件语义漂移) # - 取参归一 _as_params():None/空串 → {},JSON 串 → dict,非法类型 → 400(不再 500) # - 行归一 _rows_of():显式识别 {'total','rows'} 分页信封,禁止直接迭代 dict # - 列取值 _pick():兼容 k/v 与 value/text 两种列命名 # - 全路径 try/except,DB 异常返回可行动 503 JSON,绝不抛到框架层 # 库名:get_sor_context(env, "scene") → 运行时由 env.get_module_dbname 解析,无硬编码 DBNAME。 try: _env = request._run_ns except Exception: _env = None if _env is None: try: _env = getattr(globals().get("request"), "_run_ns", None) except Exception: _env = None if _env is None: return {"status": 503, "success": False, "message": "[scene/get_search_status] dspy 执行命名空间缺少 request._run_ns;" "请确认应用 init() 已完成 ServerEnv 装配并 load_scene()"} def _as_params(_raw): """取参归一:None → {};dict 复制;JSON/空串 → dict/{};其它 → None(非法)。""" if _raw is None: return {} if isinstance(_raw, dict): return dict(_raw) if isinstance(_raw, (bytes, bytearray)): try: _raw = _raw.decode("utf-8", "replace") except Exception: return None if isinstance(_raw, str): _t = _raw.strip() if not _t: return {} try: _v = json.loads(_t) except Exception: return None return dict(_v) if isinstance(_v, dict) else None return None def _rows_of(_res): """把 sor.R / sqlExe 返回值归一为行列表(分页信封只取 rows)。""" if _res is None: return [] if isinstance(_res, dict): _r = _res.get("rows") if isinstance(_r, list): return _r if _r is not None: return [_r] return [_res] if _res else [] if isinstance(_res, (list, tuple)): return list(_res) try: return list(_res) except Exception: return [] def _get(_row, _name): try: if isinstance(_row, dict): return _row.get(_name) if hasattr(_row, _name): return getattr(_row, _name) return _row[_name] except Exception: return None def _pick(_row, _names): for _n in _names: _v = _get(_row, _n) if _v is not None and str(_v).strip() != "": return _v return None async def _load_options(_parentid): async with get_sor_context(_env, "scene") as _sor: _res = await _sor.sqlExe( "SELECT k, v FROM appcodes_kv WHERE parentid=${parentid}$ ORDER BY v", {"parentid": _parentid}) _out = [] for _r in _rows_of(_res): _val = _pick(_r, ("k", "value", "id")) _txt = _pick(_r, ("v", "text", "name")) if _val is None: continue _out.append({"value": _val, "text": _txt if _txt is not None else _val}) return _out _params = _as_params(params_kw) if _params is None: return {"status": 400, "success": False, "rows": [], "message": "[scene/get_search_status] 参数类型非法:params 必须是 JSON 对象"} try: _opts = await _load_options("scene_status") return {"status": 200, "success": True, "total": len(_opts), "rows": _opts} except Exception as _e: return {"status": 503, "success": False, "rows": [], "message": "[scene/get_search_status] 读取 appcodes_kv(parentid=scene_status) 失败: " + str(_e)}