mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-04 06:15:14 +08:00
fix(auth): normalize site authentication parameters
This commit is contained in:
@@ -30,6 +30,7 @@ from app.application.configuration import get_configured_system_config
|
||||
from app.application.plugin.routes import register_plugin_api
|
||||
from app.application.plugin.runtime import get_plugin_manager
|
||||
from app.application.scheduling import get_scheduler
|
||||
from app.application.site.auth import normalize_site_auth_params
|
||||
from app.application.site.mutation import SiteMutationCommand
|
||||
from app.application.site.query import SiteQueryService
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
|
||||
@@ -790,8 +791,18 @@ def auth_site(
|
||||
"""
|
||||
if not auth_info or not auth_info.site or not auth_info.params:
|
||||
return _SchemaResponse(success=False, message="请输入认证站点和认证参数")
|
||||
status, msg = SitesHelper().check_user(auth_info.site, auth_info.params)
|
||||
get_configured_system_config().set(SystemConfigKey.UserSiteAuthParams, auth_info.model_dump())
|
||||
sites_helper = SitesHelper()
|
||||
auth_params = normalize_site_auth_params(
|
||||
auth_info.site,
|
||||
auth_info.params,
|
||||
sites_helper.get_authsites(),
|
||||
)
|
||||
status, msg = sites_helper.check_user(auth_info.site, auth_params)
|
||||
if status:
|
||||
get_configured_system_config().set(
|
||||
SystemConfigKey.UserSiteAuthParams,
|
||||
{"site": auth_info.site, "params": auth_params},
|
||||
)
|
||||
# 认证成功后,重新初始化插件
|
||||
get_plugin_manager().init_config()
|
||||
get_scheduler().init_plugin_jobs()
|
||||
|
||||
61
app/application/site/auth.py
Normal file
61
app/application/site/auth.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""站点认证参数的宿主边界规范化。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Optional
|
||||
|
||||
from app.schemas.common import JsonData
|
||||
|
||||
|
||||
def normalize_site_auth_params(
|
||||
site: Optional[str],
|
||||
params: Optional[Mapping[str, JsonData]],
|
||||
auth_sites: Mapping[str, JsonData],
|
||||
) -> dict[str, JsonData]:
|
||||
"""把站点字段名规范化为认证资源使用的站点前缀键名。
|
||||
|
||||
认证资源的公共认证入口按 ``SITE_PARAMETER`` 读取参数,但旧版 API、
|
||||
CLI 配置和外部调用方可能提交资源清单中的原始字段名。保留未知字段,
|
||||
只将已知站点字段转换为资源契约键名。
|
||||
"""
|
||||
normalized = dict(params or {})
|
||||
if not site or not params or not isinstance(auth_sites, Mapping):
|
||||
return normalized
|
||||
|
||||
site_config = auth_sites.get(site)
|
||||
if not isinstance(site_config, Mapping):
|
||||
return normalized
|
||||
param_definitions = site_config.get("params")
|
||||
if not isinstance(param_definitions, Mapping):
|
||||
return normalized
|
||||
|
||||
for raw_key in param_definitions:
|
||||
if not isinstance(raw_key, str):
|
||||
continue
|
||||
env_key = f"{site.upper()}_{raw_key.upper()}"
|
||||
if env_key in params:
|
||||
normalized[env_key] = params[env_key]
|
||||
elif raw_key in params:
|
||||
normalized[env_key] = params[raw_key]
|
||||
if raw_key != env_key:
|
||||
normalized.pop(raw_key, None)
|
||||
return normalized
|
||||
|
||||
|
||||
def normalize_site_auth_config(
|
||||
auth_config: Optional[Mapping[str, JsonData]],
|
||||
auth_sites: Mapping[str, JsonData],
|
||||
) -> Optional[dict[str, JsonData]]:
|
||||
"""规范化持久化的站点认证配置,兼容旧字段名。"""
|
||||
if not auth_config:
|
||||
return None
|
||||
|
||||
site = auth_config.get("site")
|
||||
params = auth_config.get("params")
|
||||
if not isinstance(site, str) or not isinstance(params, Mapping):
|
||||
return None
|
||||
|
||||
normalized = dict(auth_config)
|
||||
normalized["params"] = normalize_site_auth_params(site, params, auth_sites)
|
||||
return normalized
|
||||
@@ -19,10 +19,12 @@ from app.application.scheduling import ( # noqa: E402
|
||||
JobRecoveryPolicy,
|
||||
JobSpec,
|
||||
)
|
||||
from app.application.site.auth import normalize_site_auth_config
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
|
||||
from app.application.workflow import WorkflowSnapshot
|
||||
from app.runtime.log import logger, wrap_for_plugin_instance
|
||||
from app.runtime.scheduling import TimerUtils
|
||||
from app.runtime.settings import get_runtime_setting
|
||||
from app.scheduler.contract import _SchedulerOwnerBase
|
||||
from app.schemas.message import Message
|
||||
from app.schemas.types import MessageType, SystemConfigKey
|
||||
@@ -478,10 +480,18 @@ class SchedulerReconcileOwner(_SchedulerOwnerBase):
|
||||
return
|
||||
logger.info("用户未认证,正在尝试认证...")
|
||||
auth_conf = get_configured_system_config().get(SystemConfigKey.UserSiteAuthParams)
|
||||
if auth_conf:
|
||||
status, msg = SitesHelper().check_user(**auth_conf)
|
||||
sites_helper = SitesHelper()
|
||||
if get_runtime_setting("AUTH_SITE"):
|
||||
status, msg = sites_helper.check_user()
|
||||
else:
|
||||
status, msg = SitesHelper().check_user()
|
||||
normalized_auth_conf = normalize_site_auth_config(
|
||||
auth_conf,
|
||||
sites_helper.get_authsites(),
|
||||
)
|
||||
if normalized_auth_conf:
|
||||
status, msg = sites_helper.check_user(**normalized_auth_conf)
|
||||
else:
|
||||
status, msg = sites_helper.check_user()
|
||||
if status:
|
||||
self._auth_count = 0
|
||||
logger.info(f"{msg} 用户认证成功")
|
||||
|
||||
@@ -39,6 +39,7 @@ from app.application.outbox import configure_outbox_dispatcher
|
||||
from app.application.plugin.runtime import get_existing_plugin_manager
|
||||
from app.application.security.url import close_image_proxy_block_log_coalescer
|
||||
from app.application.service import configure_service_directory, reset_service_directory
|
||||
from app.application.site.auth import normalize_site_auth_config
|
||||
from app.command import CommandChain
|
||||
from app.db.session import (
|
||||
close_database,
|
||||
@@ -460,7 +461,18 @@ def user_auth():
|
||||
if sites_helper.auth_level >= 2:
|
||||
return
|
||||
auth_conf = get_configured_system_config().get(SystemConfigKey.UserSiteAuthParams)
|
||||
status, msg = sites_helper.check_user(**auth_conf) if auth_conf else sites_helper.check_user()
|
||||
if get_runtime_setting("AUTH_SITE"):
|
||||
status, msg = sites_helper.check_user()
|
||||
else:
|
||||
normalized_auth_conf = normalize_site_auth_config(
|
||||
auth_conf,
|
||||
sites_helper.get_authsites(),
|
||||
)
|
||||
status, msg = (
|
||||
sites_helper.check_user(**normalized_auth_conf)
|
||||
if normalized_auth_conf
|
||||
else sites_helper.check_user()
|
||||
)
|
||||
if status:
|
||||
logger.info(f"{msg} 用户认证成功")
|
||||
else:
|
||||
|
||||
@@ -2594,13 +2594,19 @@ def _apply_local_system_config_inner(config_payload: dict[str, Any]) -> None:
|
||||
and site_auth_item.get("site")
|
||||
and site_auth_item.get("params")
|
||||
):
|
||||
system_config_service.set(SystemConfigKey.UserSiteAuthParams, site_auth_item)
|
||||
try:
|
||||
from app.application.site.auth import normalize_site_auth_params
|
||||
from app.application.site.sites import SitesHelper # pylint: disable=import-error,no-name-in-module
|
||||
|
||||
status, msg = SitesHelper().check_user(
|
||||
site_auth_item.get("site"), site_auth_item.get("params")
|
||||
sites_helper = SitesHelper()
|
||||
auth_params = normalize_site_auth_params(
|
||||
site_auth_item.get("site"),
|
||||
site_auth_item.get("params"),
|
||||
sites_helper.get_authsites(),
|
||||
)
|
||||
site_auth_item = {**site_auth_item, "params": auth_params}
|
||||
system_config_service.set(SystemConfigKey.UserSiteAuthParams, site_auth_item)
|
||||
status, msg = sites_helper.check_user(site_auth_item.get("site"), auth_params)
|
||||
if status:
|
||||
print_step(f"站点认证校验成功:{msg}")
|
||||
else:
|
||||
|
||||
166
tests/test_site_auth.py
Normal file
166
tests/test_site_auth.py
Normal file
@@ -0,0 +1,166 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from app.api.endpoints import site as site_endpoint
|
||||
from app.application.site.auth import (
|
||||
normalize_site_auth_config,
|
||||
normalize_site_auth_params,
|
||||
)
|
||||
from app.scheduler import reconcile as scheduler_reconcile
|
||||
from app.schemas.site import SiteAuth
|
||||
from app.schemas.types import SystemConfigKey
|
||||
from app.startup.initializers import modules as module_initializer
|
||||
|
||||
HAIDAN_AUTH_SITES = {
|
||||
"haidan": {
|
||||
"params": {
|
||||
"id": {"name": "用户ID"},
|
||||
"passkey": {"name": "密钥"},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def test_normalize_site_auth_params_accepts_resource_field_names() -> None:
|
||||
"""站点原始字段名应转换为认证资源读取的环境变量式键名。"""
|
||||
assert normalize_site_auth_params(
|
||||
"haidan",
|
||||
{"id": 63632, "passkey": "secret"},
|
||||
HAIDAN_AUTH_SITES,
|
||||
) == {"HAIDAN_ID": 63632, "HAIDAN_PASSKEY": "secret"}
|
||||
|
||||
|
||||
def test_normalize_site_auth_config_preserves_unknown_fields() -> None:
|
||||
"""认证配置中的扩展字段不能因参数规范化而丢失。"""
|
||||
assert normalize_site_auth_config(
|
||||
{
|
||||
"site": "haidan",
|
||||
"params": {"id": 63632, "passkey": "secret", "extra": "value"},
|
||||
},
|
||||
HAIDAN_AUTH_SITES,
|
||||
) == {
|
||||
"site": "haidan",
|
||||
"params": {
|
||||
"HAIDAN_ID": 63632,
|
||||
"HAIDAN_PASSKEY": "secret",
|
||||
"extra": "value",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _patch_auth_endpoint(monkeypatch, helper: Mock, config: Mock) -> None:
|
||||
"""替换认证端点的外部运行时依赖。"""
|
||||
monkeypatch.setattr(site_endpoint, "SitesHelper", Mock(return_value=helper))
|
||||
monkeypatch.setattr(site_endpoint, "get_configured_system_config", Mock(return_value=config))
|
||||
monkeypatch.setattr(site_endpoint, "get_plugin_manager", Mock(return_value=Mock()))
|
||||
monkeypatch.setattr(site_endpoint, "get_scheduler", Mock(return_value=Mock()))
|
||||
monkeypatch.setattr(site_endpoint, "init_commands", Mock())
|
||||
monkeypatch.setattr(site_endpoint, "register_plugin_api", Mock())
|
||||
|
||||
|
||||
def test_auth_site_normalizes_parameters_before_check_and_persist(monkeypatch) -> None:
|
||||
"""认证端点应以规范化键名调用资源并只持久化成功配置。"""
|
||||
helper = Mock()
|
||||
helper.get_authsites.return_value = HAIDAN_AUTH_SITES
|
||||
helper.check_user.return_value = (True, "haidan")
|
||||
config = Mock()
|
||||
_patch_auth_endpoint(monkeypatch, helper, config)
|
||||
|
||||
response = site_endpoint.auth_site(
|
||||
SiteAuth(site="haidan", params={"id": 63632, "passkey": "secret"}),
|
||||
SimpleNamespace(),
|
||||
)
|
||||
|
||||
assert response.success is True
|
||||
helper.check_user.assert_called_once_with(
|
||||
"haidan",
|
||||
{"HAIDAN_ID": 63632, "HAIDAN_PASSKEY": "secret"},
|
||||
)
|
||||
config.set.assert_called_once_with(
|
||||
SystemConfigKey.UserSiteAuthParams,
|
||||
{
|
||||
"site": "haidan",
|
||||
"params": {"HAIDAN_ID": 63632, "HAIDAN_PASSKEY": "secret"},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def test_auth_site_does_not_persist_failed_parameters(monkeypatch) -> None:
|
||||
"""失败认证不能覆盖已有配置并屏蔽环境变量认证。"""
|
||||
helper = Mock()
|
||||
helper.get_authsites.return_value = HAIDAN_AUTH_SITES
|
||||
helper.check_user.return_value = (False, "api parameter loss")
|
||||
config = Mock()
|
||||
_patch_auth_endpoint(monkeypatch, helper, config)
|
||||
|
||||
response = site_endpoint.auth_site(
|
||||
SiteAuth(site="haidan", params={"id": 63632, "passkey": "secret"}),
|
||||
SimpleNamespace(),
|
||||
)
|
||||
|
||||
assert response.success is False
|
||||
config.set.assert_not_called()
|
||||
|
||||
|
||||
def test_startup_auth_prefers_environment_configuration(monkeypatch) -> None:
|
||||
"""设置环境变量时启动认证应从环境读取,而不是使用旧持久化配置。"""
|
||||
helper = Mock()
|
||||
helper.auth_level = 1
|
||||
helper.get_authsites.return_value = HAIDAN_AUTH_SITES
|
||||
helper.check_user.return_value = (True, "haidan")
|
||||
config = Mock()
|
||||
config.get.return_value = {
|
||||
"site": "haidan",
|
||||
"params": {"id": 63632, "passkey": "stale"},
|
||||
}
|
||||
|
||||
monkeypatch.setattr(module_initializer, "SitesHelper", Mock(return_value=helper))
|
||||
monkeypatch.setattr(
|
||||
module_initializer,
|
||||
"get_configured_system_config",
|
||||
Mock(return_value=config),
|
||||
)
|
||||
monkeypatch.setattr(module_initializer, "get_runtime_setting", lambda key: "haidan")
|
||||
|
||||
module_initializer.user_auth()
|
||||
|
||||
helper.check_user.assert_called_once_with()
|
||||
|
||||
|
||||
def test_scheduler_auth_prefers_environment_configuration(monkeypatch) -> None:
|
||||
"""定时认证应与启动认证使用相同的环境变量优先级。"""
|
||||
helper = Mock()
|
||||
helper.auth_level = 1
|
||||
helper.get_authsites.return_value = HAIDAN_AUTH_SITES
|
||||
helper.check_user.return_value = (True, "haidan")
|
||||
config = Mock()
|
||||
config.get.return_value = {
|
||||
"site": "haidan",
|
||||
"params": {"id": 63632, "passkey": "stale"},
|
||||
}
|
||||
scheduler_config = SimpleNamespace(site_link="")
|
||||
owner = scheduler_reconcile.SchedulerReconcileOwner()
|
||||
owner._auth_count = 0
|
||||
owner._auth_message = False
|
||||
owner._auth_plugin_routes_pending = False
|
||||
owner._scheduler_services = Mock(return_value=Mock())
|
||||
owner.init_plugin_jobs = Mock()
|
||||
|
||||
monkeypatch.setattr(scheduler_reconcile, "SitesHelper", Mock(return_value=helper))
|
||||
monkeypatch.setattr(
|
||||
scheduler_reconcile,
|
||||
"get_configured_system_config",
|
||||
Mock(return_value=config),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
scheduler_reconcile,
|
||||
"get_scheduler_runtime_config",
|
||||
Mock(return_value=scheduler_config),
|
||||
)
|
||||
monkeypatch.setattr(scheduler_reconcile, "get_runtime_setting", lambda key: "haidan")
|
||||
monkeypatch.setattr(scheduler_reconcile, "get_plugin_manager", Mock(return_value=Mock()))
|
||||
monkeypatch.setattr(scheduler_reconcile, "register_plugin_api", Mock())
|
||||
|
||||
owner.user_auth()
|
||||
|
||||
helper.check_user.assert_called_once_with()
|
||||
Reference in New Issue
Block a user