From 75d8b2e4419b5d5efbd1718f34812fe1daf9bfc2 Mon Sep 17 00:00:00 2001 From: LIN student <86354789+MiCat-S@users.noreply.github.com> Date: Mon, 5 Oct 2026 15:02:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(system):=20=E5=85=AC=E5=BC=80=E8=AE=BE?= =?UTF-8?q?=E7=BD=AE=E4=B8=8D=E5=86=8D=E8=BF=94=E5=9B=9E=E5=AD=98=E5=82=A8?= =?UTF-8?q?=E7=9A=84=E8=BF=9E=E6=8E=A5=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit /system/setting/public/{key} 只要求登录用户,白名单中的 Storages 原样返回 整个存储列表,每项的 config 保存着各存储的连接配置和凭据(SMB 用户名密码、 alist 账号、115 token 等)。设置合同已将 Storages 标记为 sensitive, /system/settings 默认会脱敏,只有这个公开接口直接返回原值。 前端 v3 已改用 /storage/options 读取存储名称和类型,公开接口的 Storages 目前只剩 Agent 的 config.public.get 和旧版前端页面会用到,它们只需要 name、type。因此保留该 key,改为与 /storage/options 相同的投影:只返回 name(缺省时取 type)和 type,跳过没有 type 的项。所有调用者返回相同 结果,超级管理员仍可通过 /system/setting/Storages 读取完整配置。 白名单内其他配置(目录、站点范围、集数定位规则、默认订阅规则、Follow 订阅者)不含连接凭据,保持原值返回。同步更新 docs/mcp-api.md 的接口说明。 Co-Authored-By: Claude Opus 5.5 --- app/api/endpoints/system.py | 18 ++++++++-- docs/mcp-api.md | 2 +- tests/test_api_authorization.py | 59 +++++++++++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 3 deletions(-) diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index 864a65547..5db069747 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -110,6 +110,17 @@ _PUBLIC_SYSTEM_CONFIG_KEYS = { _PUBLIC_SETTINGS_KEYS = {"PLUGIN_MARKET"} +def _project_public_storages(value: Any) -> list[dict[str, Any]]: + """存储配置只公开名称和类型,与 /storage/options 一致,config 中的连接配置不返回。""" + if not isinstance(value, list): + return [] + return [ + {"name": item.get("name") or item["type"], "type": item["type"]} + for item in value + if isinstance(item, dict) and item.get("type") + ] + + async def _get_image_proxy_allowed_domains() -> set[str]: """合并站点快照与当前图片代理配置,仍逐请求执行 DNS 安全校验。""" runtime_settings = get_runtime_settings() @@ -660,7 +671,7 @@ async def get_progress( ) async def get_public_setting(key: str, _: ApiPrincipal = Depends(get_current_active_user_async)) -> _SchemaResponse: """ - 查询普通用户可读取的非敏感系统设置 + 查询普通用户可读取的非敏感系统设置,存储配置只返回名称和类型 """ if key in _PUBLIC_SETTINGS_KEYS: return _SchemaResponse( @@ -669,7 +680,10 @@ async def get_public_setting(key: str, _: ApiPrincipal = Depends(get_current_act ) if key not in _PUBLIC_SYSTEM_CONFIG_KEYS: raise HTTPException(status_code=404, detail="配置项不存在") - value = get_configured_system_config().get(_PUBLIC_SYSTEM_CONFIG_KEYS[key]) + config_key = _PUBLIC_SYSTEM_CONFIG_KEYS[key] + value = get_configured_system_config().get(config_key) + if config_key is SystemConfigKey.Storages: + value = _project_public_storages(value) return _SchemaResponse(success=True, data={"value": value}) diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 7629a63b8..46dc1a8d4 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -743,7 +743,7 @@ AMLL 使用无需鉴权的原生搜索与获取接口,先尝试 ISRC,再核 | GET | `/api/v1/dashboard/schedule` | 查询所有后台定时服务,包含当前完成百分比、进度文本和执行状态 | | GET | `/api/v1/dashboard/schedule/{job_id}/progress` | 查询指定后台定时服务的实时进度详情 | | GET | `/api/v1/dashboard/schedule2/{job_id}/progress` | 使用 API_TOKEN 查询指定后台定时服务的实时进度详情 | -| GET | `/api/v1/system/setting/public/{key}` | 登录用户读取白名单内非敏感系统设置,仅支持目录、存储、站点范围、默认订阅规则、Follow 订阅者和插件市场地址等前端必需配置 | +| GET | `/api/v1/system/setting/public/{key}` | 登录用户读取白名单内非敏感系统设置,仅支持目录、存储、站点范围、默认订阅规则、Follow 订阅者和插件市场地址等前端必需配置;存储只返回 `name` 和 `type`,与 `/api/v1/storage/options` 一致,不含连接配置 | | POST | `/api/v1/system/setting/PLUGIN_MARKET/sync-wiki` | 管理员从 MoviePilot Wiki 的插件文档同步公开插件仓库清单,和本地 `PLUGIN_MARKET` 合并去重后写入配置 | | GET | `/api/v1/system/module-catalog` | 查询宿主模块及其服务类型目录,供前端选择器构造选项 | | GET | `/api/v1/system/modulelist` | 查询已启用模块,保留 `name` 原始中文字段,并提供 `name_i18n` 和 `name_key` 给多语言前端展示 | diff --git a/tests/test_api_authorization.py b/tests/test_api_authorization.py index 7f481cd8d..6b35d7d4b 100644 --- a/tests/test_api_authorization.py +++ b/tests/test_api_authorization.py @@ -232,6 +232,65 @@ def test_system_public_setting_allows_only_non_sensitive_keys(monkeypatch): assert exc_info.value.detail == "配置项不存在" +def test_system_public_setting_storages_returns_name_and_type_only(monkeypatch): + """公开设置读取存储时只返回名称和类型,与 /storage/options 一致,不含连接配置。""" + storages = [ + {"type": "local", "name": "本地存储", "config": {}}, + { + "type": "smb", + "name": "NAS", + "config": {"host": "192.168.1.2", "username": "smb-user", "password": "smb-password"}, + }, + {"type": "u115", "config": {"refresh_token": "u115-refresh", "access_token": "u115-access"}}, + {"name": "缺少类型", "config": {"token": "untyped-token"}}, + ] + fake_config = SimpleNamespace(get=lambda key: storages if key == SystemConfigKey.Storages else None) + monkeypatch.setattr(system_endpoint, "get_configured_system_config", lambda: fake_config) + monkeypatch.setattr("app.application.storage.get_configured_system_config", lambda: fake_config) + + response = asyncio.run(system_endpoint.get_public_setting(SystemConfigKey.Storages.value)) + + assert response.success is True + assert response.data == { + "value": [ + {"name": "本地存储", "type": "local"}, + {"name": "NAS", "type": "smb"}, + {"name": "u115", "type": "u115"}, + ] + } + assert response.data["value"] == [option.model_dump() for option in storage_endpoint.storage_options()] + serialized = response.model_dump_json() + for value in ("smb-user", "smb-password", "192.168.1.2", "u115-refresh", "u115-access", "untyped-token"): + assert value not in serialized + + +@pytest.mark.parametrize( + "config_key", + [ + SystemConfigKey.Directories, + SystemConfigKey.IndexerSites, + SystemConfigKey.EpisodeFormatRuleTable, + SystemConfigKey.DefaultMovieSubscribeConfig, + SystemConfigKey.DefaultTvSubscribeConfig, + SystemConfigKey.DefaultMusicSubscribeConfig, + SystemConfigKey.FollowSubscribers, + ], +) +def test_system_public_setting_returns_other_keys_as_stored(monkeypatch, config_key): + """存储以外的公开配置仍按原值返回。""" + stored = [{"name": "配置项", "config": {"nested": True}}] + monkeypatch.setattr( + system_endpoint, + "get_configured_system_config", + lambda: SimpleNamespace(get=lambda key: stored if key == config_key else None), + ) + + response = asyncio.run(system_endpoint.get_public_setting(config_key.value)) + + assert response.success is True + assert response.data == {"value": stored} + + def test_system_ping_returns_success(): """服务存活检测接口返回标准成功响应。""" response = asyncio.run(system_endpoint.ping())