fix(auth): normalize site authentication parameters

This commit is contained in:
jxxghp
2026-09-14 13:24:05 +08:00
parent 6d82f44fc8
commit 1cb1f02f19
6 changed files with 275 additions and 9 deletions

View File

@@ -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()

View 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

View File

@@ -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} 用户认证成功")

View File

@@ -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:

View File

@@ -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
View 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()