Skip to content

fix(security): #53 API_TOKEN/API_KEY 经 URL 不再等价超管 [P0安全·BREAKING·优先级2/3] - #90

Open
Aqr-K wants to merge 236 commits into
v3-pythonfrom
fix/api-token-hardening
Open

fix(security): #53 API_TOKEN/API_KEY 经 URL 不再等价超管 [P0安全·BREAKING·优先级2/3]#90
Aqr-K wants to merge 236 commits into
v3-pythonfrom
fix/api-token-hardening

Conversation

@Aqr-K

@Aqr-K Aqr-K commented Jun 28, 2026

Copy link
Copy Markdown
Owner

架构审计 3.A · #53(P0 安全,⚠️ BREAKING,优先级 2/3)

问题:静态密钥 settings.API_TOKEN 经 URL query(?token=?apikey=)即等价超级管理员——凭据随 URL 进 access-log/浏览器历史/Referer(CWE-598),且任何持有者直达全部超管端点。tokenapikey 校验的是同一个 settings.API_TOKEN

修复

  1. 首选请求头:新增 X-API-TOKEN / 复用 X-API-KEY__get_api_token/__get_api_key 返回 header or query(保留 query 向后兼容但优先 header)。
  2. 静态密钥一致降权verify_tokenapi_keyapi_token 两分支均铸非超管服务 payload(super_user=False);删除 __create_superuser_token_payload,杜绝 ?apikey=<API_TOKEN> 旁路(首版只降 token 分支会被此旁路绕过——对抗式复核已捕获并修正)。
  3. 超管守卫加固get_current_active_superuser/_async 额外要求 token_data.super_user 为真(防降权后凭 sub 解析到超管用户而绕过)。

⚠️ BREAKING 影响:经 API_TOKEN/API_KEY(URL 或 Header)访问的约 94 个超管端点将返回 401;超管能力此后仅由真实用户名口令登录签发的 JWT 承载(前端登录不受影响)。受影响端点(13 文件):
dashboard(9)·history(4)·llm(5)·media(1)·message(1)·notification(5)·plugin(21)·site(17)·storage(10)·system(8 含 restart/upgrade/env/setting)·torrent(5)·transfer(3)·user(5)

依赖 API_TOKEN 做管理自动化的集成需改用具备超管身份的 JWT,或评估是否接受此降权——这是本组唯一 breaking 项,建议单独评审/合并。

测试tests/test_api_token_hardening.py(含 apikey 旁路回归)+ tests/test_api_authorization.py31 passed

对抗式安全复核:PASS(apikey 旁路确认闭合、无第四条静态密钥路径、删函数无残留、JWT 超管路径不受影响)。非阻塞建议:补 async 超管守卫正向用例 + 一条 TestClient 集成测试。基于 origin/v3-python。

Aqr-K and others added 30 commits June 17, 2026 03:57
WordsMatcher / CustomizationMatcher / ReleaseGroupsMatcher 此前在 __init__ 直接
实例化 SystemConfigOper 并在运行时读取用户自定义识别配置,使 core/meta 顶层硬依赖
app.db,导致元数据解析算法无法脱离数据库复用与测试(架构审计 S1)。

改动:
- 新增 app/core/meta/config_source.py:可注入的 get_meta_config / set_meta_config_provider
  seam,仅依赖 app.schemas.types,零 app.db 依赖;未注册 provider 时返回 None
  (语义为“无自定义、无需数据库”)。
- 三个 matcher 改用 get_meta_config 读取自定义配置,移除 SystemConfigOper 导入与
  self.systemconfig 句柄。
- 在 app/startup/lifecycle.py(composition root)启动时注册 DB-backed provider
  (lambda key: SystemConfigOper().get(key)),保持线上行为逐字节不变。
- 现有测试改用注入 provider;新增 test_meta_config_source.py 验证无 DB 可用 + 注入生效。

效果:消除 words/customization/releasegroup 三条 top-level core→db 边;core/meta
元数据算法现可在无数据库环境下独立复用与单测。
…lper 反向依赖

ThreadHelper 是依赖 settings 的线程池基础设施,却位于 app/helper/thread.py,导致
core/event.py 顶层反向依赖 helper 层(架构审计 P1:core 应为零 helper 依赖的稳定底座)。
注意 ThreadHelper 依赖 settings,故不可下沉到 utils(会形成 utils→core 反向边),其正确
归属是 core 自身。

改动:
- 新增 app/core/thread.py:ThreadHelper 的权威新家(内容不变;仅依赖 core.config 与
  utils.singleton,方向正确)。
- app/helper/thread.py 改为 re-export 兼容垫片,保留历史导入路径(无插件依赖该模块,
  纯防御性兼容)。
- 5 处内部 importer 改为 from app.core.thread import ThreadHelper:
  core/event、command、chain/download、modules/telegram、startup/modules_initializer。
- 新增 test_core_thread_relocation.py:垫片同类 / submit 可用 / core/event 不再引用
  helper.thread / core.thread 无 helper 依赖。

效果:消除 core/event→helper.thread top-level 反向边;ThreadHelper 行为零变化。
(event.py:706 的 lazy helper.message 属另一接缝,且 MessageHelper 是插件注入 API,本 PR 不动。)
…lper 反向依赖

RedisHelper/AsyncRedisHelper 是依赖 settings 的缓存后端基础设施,却位于 app/helper/redis.py,
导致 core/cache.py 顶层反向依赖 helper 层(架构审计 P1:core 应为零 helper 依赖的稳定底座)。

改动:
- git mv app/helper/redis.py -> app/core/redis.py(内容不变;仅依赖 core.config 与 utils,
  方向正确;ConfigReloadMixin 的 config.updated 订阅在新 qualname 下经验证正常工作)。
- app/helper/redis.py 改为 re-export 兼容垫片(RedisHelper / AsyncRedisHelper / serialize /
  deserialize),保留历史导入路径——含社区插件 p115strmhelper(utils/tree、core/cache),
  插件无需任何改动即可继续工作。
- 3 处内部 importer 改为 from app.core.redis:core/cache、startup/modules_initializer、
  modules/redis。
- 新增 test_core_redis_relocation.py:垫片同类(含插件路径)/ serialize 垫片可用 /
  core/cache 不再引用 helper.redis / core.redis 无 helper 依赖。

效果:消除 core/cache→helper.redis top-level 反向边;行为零变化;插件契约经垫片完整保留。
…per.module 反向依赖

ModuleHelper 是仅依赖标准库与 app.log 的动态模块加载工具,原置于 helper 层,造成
core.module -> helper.module 的反向依赖。现迁移到正确的 core 层(内容不变),
app/helper/module.py 改为 re-export 垫片,使既有导入路径(含插件 autosignin)零改动可用。

- git mv app/helper/module.py -> app/core/module_loader.py(内容不变)
- 更新 4 个内部导入方:core/module、workflow、modules/indexer、modules/filemanager
- 插件 app.plugins.autosignin 经垫片继续工作,无需改动(已验证解析到同一类)
- 新增 tests/test_core_module_relocation.py:垫片同类(含插件路径)/ load 功能冒烟 /
  core.module 不再引用 helper.module / module_loader 无 helper 依赖
…th_bridge)

core/security 与 core/auth_bridge 原先直接 import SitesHelper(资源包拉取的编译 .so,
属 helper 层)仅为读取 auth_level,形成 core->helper 反向依赖。引入注入式 seam
app/core/auth_level.py(零外部依赖),由组合根在 lifespan 注入
lambda: SitesHelper().auth_level;未注册时返回未认证默认等级 1(与全新 SitesHelper 一致)。

- 新增 app/core/auth_level.py:get_auth_level / set_auth_level_provider / DEFAULT_AUTH_LEVEL=1
- core/security.py:删除惰性 import SitesHelper,改用 get_auth_level()
- core/auth_bridge.py:删除顶层 import SitesHelper,改用 get_auth_level()
- startup/lifecycle.py:组合根注入 provider(唯一 import SitesHelper 处)
- 与 S1(core/meta/config_source)同属注入式 seam 技术
- plugin.py 仍引用 SitesHelper(完整对象,S5 god-object,另行处理)
- 新增 tests/test_core_auth_level.py(默认值/注入生效/security 与 auth_bridge 无 helper.sites/seam 无 helper 依赖)
复核发现 core/plugin.py 的 SitesHelper 用法(__verify_plugin 中 siteshelper.auth_level)
同样仅读取 auth_level,与 security/auth_bridge 属同一接缝。改用 get_auth_level() 后,
core 层三个文件全部不再 import app.helper.sites,该反向边彻底消除。

- core/plugin.py:删除 import SitesHelper,siteshelper.auth_level(两处)改为 get_auth_level()
- plugin.py 仍保留 helper.server / helper.plugin(各自独立接缝,另行处理)
- 行为不变:provider 在 init_plugins 之前于 lifespan 注入;get_auth_level() 解析到同一 SitesHelper 单例
- test_core_auth_level.py 增加 plugin.py 无 helper.sites 断言
core/plugin 在插件安装成功后调用 MoviePilotServerHelper.install_plugin_reg 上报统计
(helper 层,依赖 db/http/外部 MP 服务器),形成 core->helper 反向依赖。该上报为
fire-and-forget 且受 settings.PLUGIN_STATISTIC_SHARE 开关控制,调用方忽略返回值,
故采用注入式 seam 而非新增事件(现无插件安装事件,新增 EventType+订阅者不成比例)。

- 新增 app/core/plugin_reporter.py(零依赖):report_plugin_install / set_plugin_install_reporter
- core/plugin.py:删除 import MoviePilotServerHelper,install_plugin_reg(...) 改为 report_plugin_install(...)
- startup/lifecycle.py:组合根注入 set_plugin_install_reporter(MoviePilotServerHelper.install_plugin_reg)
- 行为不变:provider 在 init_plugins 之前于 lifespan 注入;未注册时 no-op(与 PLUGIN_STATISTIC_SHARE=False 跳过上报一致)
- plugin.py 仅剩 helper.plugin 一条 core->helper 边(PluginHelper,~14 处,S5 硬核,另行处理)
- 新增 tests/test_core_plugin_reporter.py(no-op 默认/注入透传 kwargs/plugin.py 无 helper.server/seam 无 helper 依赖)
…_bridge/plugin(inject provider)

# Conflicts:
#	app/startup/lifecycle.py
…ct provider)

# Conflicts:
#	app/startup/lifecycle.py
S5(core/plugin 去 helper.plugin)第一刀:剥离无状态的 URL 工具。
is_local_repo_url / make_local_repo_url 是纯字符串/URL 处理,原以 @staticmethod
挂在 helper 层 PluginHelper 上,core/plugin 为调用它们而 import 整个 PluginHelper。

- 新增 app/utils/plugin_repo.py(纯工具,零 app 依赖):LOCAL_REPO_PREFIX / is_local_repo_url / make_local_repo_url
- core/plugin.py:6 处调用改用 utils(is_local ×5、make_local ×1)
- helper/plugin.py:同名静态方法委托至 utils,LOCAL_REPO_PREFIX 统一来源(DRY;对插件/agent 调用方零改动)
- 注:core/plugin 仍 import PluginHelper(尚有 8 处有状态调用:仓库发现/市场/安装/annotate),由 S5b 用 provider seam 收尾后彻底移除该边
- 新增 tests/test_plugin_repo_url.py(纯函数行为/PluginHelper 委托一致/core 改用 utils/util 无 app 依赖)
…顶层依赖(S5b)

S5 收尾刀(承接 S5a 抽取 URL 工具)。PluginHelper 是 2488 行 helper 重类,
core/plugin 原在顶层 import 并在 10 处调用 9 个方法(仓库发现/候选/市场/安装/依赖/annotate),
构成 core->helper 顶层反向依赖。

- 新增 app/core/plugin_source.py:get_plugin_source / set_plugin_source_provider
  - 默认懒加载返回真实 PluginHelper 单例(行为字节一致;helper 引用降为函数内惰性,消除顶层边)
  - set_plugin_source_provider 为未来 Rust/进程外插件宿主预留注入点(默认即生产行为,故组合根不注册)
- core/plugin.py:删除 from app.helper.plugin import PluginHelper;10 处调用改为 get_plugin_source().X(...)
- 至此 grep '^from app.helper' app/core/*.py 为空 —— core 层无任何顶层 helper 反向依赖
  (仅剩 event.py:706 与本 seam 的惰性引用,无 import 期环)
- 新增 tests/test_core_plugin_source.py(默认返回真实 PluginHelper/注入生效/core 无 PluginHelper/seam 顶层无 helper import)
…泄漏(S2)

ChainBase.torrent_files() 原返回 Union[qbittorrentapi.TorrentFilesList, List[transmission_rpc.File]],
把下载器 SDK 类型泄漏到插件可见的契约层。仿照 list_torrents->DownloaderTorrent 的既有先例归一化。

- 新增 schemas.DownloaderFile(name/size/progress/priority/index)
- qbittorrent/transmission 模块 torrent_files() 将 SDK 文件对象转为 List[DownloaderFile](SDK 知识下沉进模块)
- ChainBase.torrent_files() 返回 -> Optional[List[DownloaderFile]];删除 chain/__init__.py 的 SDK import
- chain/transfer.py 删除针对 qB TorrentFilesList.data 的兼容判断(现恒为 list)
- 模块返回注解用前向引用字符串,避免导入期对晚初始化的 schemas.DownloaderFile 求值
- 契约影响:DownloaderFile 保留 .name/.size/.progress/.priority 公共属性名,常规用法不破坏;
  仓库内 0 插件消费、唯一内部消费者(transfer.py)只读 .name
- 新增 tests/test_downloader_file_normalization.py(schema/两模块转换/契约无 SDK 泄漏)
refactor(downloader): torrent_files 归一化为 DownloaderFile,消除契约层下载器 SDK 泄漏(S2)
agent→chain 是健康的编排方向(每个 agent tool 包一个 chain),保留;
chain→agent 的回边才是循环源。本 PR 移除全部 chain→agent 的导入期依赖:

- ReplyMode(纯 str,Enum)从 app.agent 迁移至 app.schemas.types;
  agent/__init__.py 顶层 re-export 作 shim,保 `from app.agent import ReplyMode`
  对 api/endpoints/{agent,history}.py 等外部引用兼容(api→agent 健康,不在环内)。
- 新增 app/core/agent_gateway.py:get_agent_manager / get_prompt_manager /
  get_agent_llm / get_agent_capability,惰性导入真实单例/类(无 provider 时
  = 与直接 import 字节一致),set_*_provider 为未来 Rust/进程外 agent host 预留;
  默认不入 composition root(默认即生产行为)。
- chain/transfer.py(3)、message.py(11)、search.py(3)改经网关调用,
  移除顶层及惰性的 from app.agent。
- helper/skill.py 的 _parse_skill_metadata 改为同名惰性转发包装,断开
  chain.skills → helper.skill → app.agent 这条间接 import-time 回边。

验证:导入全部 21 个 app.chain.* 子模块后 app.agent 不进入 sys.modules(环已断);
chain.transfer/message/search 现可干净导入(连带绕开预存 jieba_next 阻塞),
test_data_cleanup_chain 由此解锁(5 passed);新增 tests/test_agent_gateway.py(11);
downloader/metainfo 回归 44 passed。零行为变更(同一单例,仅访问器间接)。
refactor(chain): 经 agent_gateway 打断 chain↔agent import-time 循环(S4/P2)
helper/image.py 顶层 from app.chain.{mediaserver,tmdb} import 是闭合 chain↔helper 环的唯一 helper→chain 反向边(chain/recommend.py:11 → helper/image.py → app.chain)。两个 Chain 仅在 WallpaperHelper 的 4 个方法内于调用时实例化,故降为函数级惰性导入即可断环,行为字节级不变。

技术同 event.py:706 / search.py:381 / plugin_source.py:40 / skill.py:42。

验证:全新解释器 import app.helper.image 拉入 0 个 app.chain 模块;新增 tests/test_helper_image_chain_cycle.py(3);image 回归 + metainfo 共 34 passed。
refactor(helper): image 惰性导入 chain,打断 chain↔helper import-time 循环
utils 是叶子层却有 4 条 utils→core 顶层反向边(mixins→core.event、security/rust_accel/http→core.config.settings),与 core→utils 正向边(core/config.py:21-22、core/event.py:19-20)闭合成 import-time 环。

均为调用期使用,故降为函数级惰性导入(mixins 在 __init_subclass__ 的 CONFIG_WATCH 分支内;其余在使用处);utils 顶层 app.core 清零=真叶子,行为字节级不变。技术同 event.py:706 / search.py:381 / plugin_source.py:40。

验证:utils 顶层 app.core 边=0;4 模块 fresh import 拉入 0 个 app.core;惰性 settings + ConfigReloadMixin 子类注册功能冒烟通过;新增 tests/test_utils_core_cycle.py(5);mixin 子类回归(redis/thread relocation)+ metainfo 共 52 passed。

里程碑:审计 5 个顶层 import 环全部打断(chain↔agent / agent↔helper / core↔helper / chain↔helper / core↔utils)。
refactor(utils): 4 处反向边惰性化,打断 core↔utils import-time 循环(达成 0 环里程碑)
core/auth_bridge.py 的 2 条直接 core→db 顶层边:User 仅用作 build_token_response 的类型注解 → 移入 TYPE_CHECKING + 引号前向引用;SystemConfigOper 仅调用期使用 → build_token_response 内函数级惰性导入。

auth_bridge.py 顶层 app.db 清零(审计 core→db 直接边 4→2)。剩余 2 条在 plugin.py,由 S9 把 PluginManager 迁出 core 时一并消除。

验证:auth_bridge 顶层 app.db=0;build_token_response 可调用(引号注解不破坏 def);惰性 SystemConfigOper 可解析;票据往返+一次性 行为未变;新增 tests/test_auth_bridge_dedb.py(3);auth_level + cycle 回归 + metainfo 共 35 passed。
refactor(core): auth_bridge 去除直接 core→db 依赖(P1 收尾)
…n_manager(S9)

app/core/plugin.py 是 2026 行 god-object,既不该留在 core(高层插件编排不属下层),又顶层反向依赖 db(PluginDataOper/SystemConfigOper)。git mv 迁至 helper 层(helper→{core,db,utils} 均合法层序);core/plugin.py 留 PEP 562 惰性 __getattr__ 兼容垫片,保留 from app.core.plugin import PluginManager(仓内 0 外部插件引用,纯防御性)。

- 16 个内部导入方改指 app.helper.plugin_manager(core/event.py 的 2 处保持函数级 lazy)。- 不依赖 chain/agent → 迁 helper 不造新环;垫片惰性 → 不引入顶层 core→helper 边。- 副作用:core 顶层 app.db 反向边清零(plugin.py 是最后 2 条所在)。

里程碑守护+收获:core 顶层 helper 边=0(解环里程碑保住)、core 顶层 db 边=0(core→db 直接边彻底清零)。4 个内省 seam 测试(S5a/S5b/S1b/S1c)retarget 至新文件;新增 relocation 测试;plugin/cycle/redis/metainfo 共 121 passed。

注:本 PR 仅 relocate(迁位置),god-object 职责拆分(Lifecycle/Market/Cloner/Metadata)留待后续 decompose。
refactor(plugin): PluginManager god-object 迁出 core → app.helper.plugin_manager(S9)
S8 step-1 安全首切:把 224 行 god-method __handle_transfer 中无早返回、仅经 task 变更/简单返回通信的 3 个子块 verbatim 抽成私有 helper:_resolve_episodes_info(TV 集数据)、_resolve_target_directory(目标目录/存储)、_select_storage_opers(运行标记 + StorageOperSelection 事件 → source/target_oper)。入口 __handle_transfer 保持薄编排 + 同序调用 + 原 try/finally。

P5 契约保留(p115strmhelper monkey-patch):name-mangled 入口符号 _TransferChain__handle_transfer 名称/签名 (self,task,callback=None)→Optional[Tuple[bool,str]]/finally 全部逐字不变;3 helper 单下划线、非 mangled、插件不引用 → 零插件影响。

验证:py_compile;入口符号+签名+helper 探针;transfer 套件 51 passed(=改动前基线,零回归);新增 tests/test_transfer_handle_carve.py(4,契约守护)。

边界(本环境无集成测试):含早返回的块 1/2/3/7(识别失败/队列去重/transfer 失败/callback)+ TransferService 抽取(队列+线程+计数器,p115 深探 jobview/_success_target_files)留待有集成测试的 S8b/step-2。
Aqr-K and others added 26 commits June 26, 2026 03:51
- AuthFlow.__init__ 新增 identity_resolver / trusted_step_ids / max_attempts(向后兼容)
- _accept_satisfied:受信内建步或无 resolver(遗留模式)直接取 user_id;外部断言经 identity 端口;非受信步给 user_id 且已配 resolver → 护栏拒绝
- _actionable 驱动 rem 列表:applies_to=False 步骤不计入可用候选,触发 deadlock→failure
- attempts cap:ctx.attempts > max_attempts → failure "认证尝试次数超限"
- challenge hasattr 守卫:兼容 T8 前 dict/Challenge 过渡
- 新增 4 测试(deadlock、非受信 user_id 护栏、attempts cap、遗留向后兼容);验收门 134 passed(0 regression)
… + redact_reason

- _run_mfa: applies_to 过滤 enrolled 子集,避免全量因子集含不适用步导致 NOf 恒假死锁
- _validate_requirement: 拒绝空真 requirement(AllOf([]) / NOf(0))并下调过大 NOf.n → AnyOf 防绕过
- _after_credential: 新增 challenge 分支,SSO RedirectChallenge 不再被错归为 continue
- run_sync: fixpoint 喂入模式,兼容 /access-token 单趟提交(同一 submission 反复推进至终态)
- redact_reason: 白名单脱敏函数,自由文本错误原因 → auth_failed(防内部细节泄漏)
- _run_mfa trusted_step_ids=frozenset():防 "password" 步 id 泄漏进 MFA 阶段(T9 Minor#2)
- 5 new S1 tests; gate 235 → 240 all green
…ervice/begin_login) + 空code快速失败 + 影子告警
…务器辅助认证(I2) + 黄金矩阵双注册测试 + flow 成功资源 cookie
…rl 语义 + 清理告警/陈旧 docstring/类型注解

- Fix 1: trusted_step_ids 先于过滤器计算,排除集==受信集由构造保证;
  插件凭证步声称内建 id 时 logger.warning 告警(镜像 SSO shadow 逻辑);
  新增 test_impostor_auxiliary_excluded_builtin_wins (RED→GREEN)
- Fix 2: login_url 保留,补注释说明 POST-only 语义(选项 b,最低破坏性)
- Fix 3: builtin_factors.py docstring 移除"由 PR2 黄金矩阵守护"陈旧引用
- Fix 4: FastAPI Response 注入不支持 Optional[Response],维持 Response = None
- Fix 5: conftest _Harness 增 deactivate() 防双重卸载;pytest.ini 增
  telegramify_markdown DeprecationWarning 过滤(上游三方库,无法在本仓修复根因)
- Fix 6: ruff 不在环境中,跳过 lint pass
…auth

冲突解决 app/db/models/ssoidentity.py(rename/modify):取本分支改名结果 externalidentity.py,
丢弃来源端 ssoidentity.py。对方仅润色一行 docstring,语义已被本分支的 ExternalIdentity
重命名重构完整覆盖;运行时表改名由 f421d13 的 alembic 迁移处理,无逻辑丢失。
统一为「功能描述 + 参数 + 返回 + 可选其他」,剥离 迁入/T1–T12 re-export 保留、
(Task N)/(T12c-1 review Minor)/(PR8 收口) 任务标记、遗留/旧 sso/先桥后删/向后兼容(旧…)
等过程措辞;保留安全理由、参数/返回契约、spec 引用与测试场景命名。仅改注释/docstring,
不动任何逻辑、断言、签名、import 或数据字面量。
将 auth/mfa 端点内联的 10 个 Pydantic 请求体迁入 app/schemas/auth.py(3 个)与
app/schemas/mfa.py(7 个),经 __init__ 星号导出;端点签名改用 schemas.X,移除多余
BaseModel/Optional import;测试改从 app.schemas 导入模型(函数仍自端点)。
仅迁请求体,响应模型(schemas.Token/Response)与业务逻辑不变。
新增 PasskeyLoginStep(step_id=system:passkey,opt-in 凭证步,仿 RedirectStep 两分支)与
WebAuthnChallenge,登录经统一 /auth/flow/begin|advance 驱动;抽 app/service/auth/passkey_login.py
作框架无关单一来源(begin/verify + PasskeyChallengeStore,TTL+取即销毁),新步与旧
/mfa/passkey/authenticate/start|finish 端点共用之——挑战态从客户端往返改为服务端落地,补上
重放/CSRF 缺口。system:passkey 纳入 _BUILTIN_CREDENTIAL_IDS/trusted_step_ids(受信直落 user_id、
插件不得冒充),_system_auth_providers 补 flow/login_url;保留 usernameless。新增登录步测试。
认证层重构:单 IAuthStep + 多态 Challenge 可插拔模型;PassKey 登录收进 flow 引擎;请求体模型集中入 app/schemas;注释规范化。
auth:多步登录装配逻辑从端点 app/api/endpoints/auth.py 抽到
app/service/auth/assembler.py(build_flow_service 装配桥),端点改为薄壳注入依赖后委托装配。

渠道能力:删除 provides_channel_capabilities 钩子,渠道模块自带 get_channel_capabilities(),
PluginManager 注册渠道模块后按 get_subtype_id() 盖章登记到 ChannelCapabilityManager
(channel id 单处声明,消除两处对齐);删除 plugin_metadata 对应聚合器;测试改写到新 API。
refactor: auth 装配桥抽离 + 插件渠道能力声明收口
- 首选请求头:新增 X-API-TOKEN / 复用 X-API-KEY,__get_api_token/__get_api_key
  返回 header or query(保留 query 向后兼容但优先 header),降低经 URL/access-log 泄露(CWE-598)
- 静态密钥一致降权:token 与 apikey 校验同一 settings.API_TOKEN,verify_token 的
  api_key 与 api_token 两分支均铸非超管服务 payload(super_user=False);删除
  __create_superuser_token_payload,杜绝"静态密钥铸超管"旁路(?apikey=<API_TOKEN>)
- 超管守卫加固:get_current_active_superuser/_async 额外要求 token_data.super_user
  为真,防降权后凭 sub 解析到超管用户而绕过
- 新增回归测试覆盖 header 优先 / 服务令牌降权 / apikey 旁路 / 超管守卫拒非超管

BREAKING: 经 API_TOKEN 或 API_KEY(URL 或 Header)访问的约 94 个超管端点将返回 401;
超管能力仅由真实用户名口令登录签发的 JWT(super_user=True)承载。详见 PR 描述影响清单。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants