From da6760833943a57aa01e8ffcbb2942ec4100e008 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Sat, 29 Aug 2026 17:03:25 +0800 Subject: [PATCH] refactor(startup): isolate security composition --- app/startup/composition/security.py | 59 +++++++++++++++++++++++++ app/startup/initializers/modules.py | 48 ++++---------------- docs/architecture-overview.md | 4 +- docs/architecture-refactor-roadmap.md | 2 + docs/rules/05-architecture.md | 5 ++- tests/test_architecture_dependencies.py | 2 +- 6 files changed, 77 insertions(+), 43 deletions(-) create mode 100644 app/startup/composition/security.py diff --git a/app/startup/composition/security.py b/app/startup/composition/security.py new file mode 100644 index 000000000..a7c388ff9 --- /dev/null +++ b/app/startup/composition/security.py @@ -0,0 +1,59 @@ +"""认证、用户与 PassKey 服务的启动组合。""" + +from app.adapters.web.security.access import set_superuser_token_payload_provider +from app.application.configuration import get_configured_system_config +from app.application.security.auth import ( + AuthService, + build_superuser_token_payload, + configure_auth_service, +) +from app.application.security.passkey import ( + PASSKEY_CHALLENGE_TTL_SECONDS, + PasskeyService, + configure_passkey_challenge_cache, + configure_passkey_service, +) +from app.application.security.user import configure_user_lookups +from app.db.adapters.user import SqlAlchemyUserRepository +from app.db.oper.passkey import PassKeyOper +from app.db.oper.systemconfig import SystemConfigOper +from app.runtime.cache import TTLCache +from app.startup.composition.context import AuthenticationRuntime +from app.startup.composition.database import build_transactional_user_repository + + +def configure_security_services() -> AuthenticationRuntime: + """构造并登记认证、用户查询和 PassKey 服务。""" + configure_user_lookups( + by_id=lambda user_id: build_transactional_user_repository().get_by_id(user_id), + by_name=lambda username: build_transactional_user_repository().get_by_name(username), + by_channel=lambda **bindings: build_transactional_user_repository().find_name_by_bindings( + bindings + ), + ) + configure_auth_service( + AuthService( + users=build_transactional_user_repository(), + config=get_configured_system_config(), + passkeys=PassKeyOper(), + ) + ) + configure_passkey_challenge_cache( + TTLCache( + region="passkey_challenge", + maxsize=4096, + ttl=PASSKEY_CHALLENGE_TTL_SECONDS, + ) + ) + configure_passkey_service(PasskeyService(repository=PassKeyOper())) + return AuthenticationRuntime( + user_repository=SqlAlchemyUserRepository, + standalone_user=build_transactional_user_repository, + system_config=SystemConfigOper, + passkey=PassKeyOper, + ) + + +def configure_security_access() -> None: + """登记 Web 认证边界所需的超级用户令牌载荷提供器。""" + set_superuser_token_payload_provider(build_superuser_token_payload) diff --git a/app/startup/initializers/modules.py b/app/startup/initializers/modules.py index 85217f150..f23bab1b5 100644 --- a/app/startup/initializers/modules.py +++ b/app/startup/initializers/modules.py @@ -30,7 +30,6 @@ from app.adapters.system.resource import ( ResourceHelper, configure_resource_version_provider, ) -from app.adapters.web.security.access import set_superuser_token_payload_provider from app.application.agent import AgentDataContext from app.application.agenttask import ( AgentTaskExecutionService, @@ -93,15 +92,7 @@ from app.application.outbox import ( durable_event_topic, validate_durable_event_handlers, ) -from app.application.security.auth import AuthService, build_superuser_token_payload, configure_auth_service -from app.application.security.passkey import ( - PASSKEY_CHALLENGE_TTL_SECONDS, - PasskeyService, - configure_passkey_challenge_cache, - configure_passkey_service, -) from app.application.security.url import close_image_proxy_block_log_coalescer -from app.application.security.user import configure_user_lookups from app.application.server.report import ServerReportService from app.application.server.share import ServerSharingService from app.application.service import configure_service_directory @@ -145,14 +136,12 @@ from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRep from app.db.adapters.transfer.execution import ( TransactionalTransferExecutionRepository, ) -from app.db.adapters.user import SqlAlchemyUserRepository from app.db.adapters.workflow import ( TransactionalWorkflowExecutionService, ) from app.db.oper.agentchat import AgentChatOper from app.db.oper.mediaserver import MediaServerOper from app.db.oper.message import MessageOper -from app.db.oper.passkey import PassKeyOper from app.db.oper.systemconfig import SystemConfigOper from app.db.oper.workflow import WorkflowOper from app.db.session import ( @@ -166,7 +155,7 @@ from app.db.uow import ( SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork, ) -from app.runtime.cache import AsyncFileCache, FileCache, TTLCache +from app.runtime.cache import AsyncFileCache, FileCache from app.runtime.config import settings as legacy_settings from app.runtime.events import EventHandlerBinding, EventManager from app.runtime.execution import run_in_threadpool_to_completion @@ -196,7 +185,6 @@ from app.startup.composition.configuration import ( ) from app.startup.composition.context import ( AgentChatRuntime, - AuthenticationRuntime, HistoryRuntime, HostRuntime, MessagingRuntime, @@ -215,6 +203,10 @@ from app.startup.composition.database import ( start_database_runtime, stop_database_runtime, ) +from app.startup.composition.security import ( + configure_security_access, + configure_security_services, +) from app.startup.composition.subscription import ( async_rule_group_mutation_scope, build_subscription_batch_writer, @@ -953,6 +945,7 @@ async def _initialize_modules() -> HostRuntime: download_history=download_history_repository, plugin_data=plugin_data_repository, ) + authentication = configure_security_services() host_runtime = HostRuntime( agent_chat=AgentChatRuntime( async_session=get_async_db, @@ -967,12 +960,7 @@ async def _initialize_modules() -> HostRuntime: sync_transaction=SqlAlchemyUnitOfWork, async_transaction=SqlAlchemyAsyncUnitOfWork, ), - authentication=AuthenticationRuntime( - user_repository=SqlAlchemyUserRepository, - standalone_user=build_transactional_user_repository, - system_config=SystemConfigOper, - passkey=PassKeyOper, - ), + authentication=authentication, messaging=MessagingRuntime( repository=MessageOper, helper=message_helper, @@ -1032,26 +1020,6 @@ async def _initialize_modules() -> HostRuntime: configure_database() configure_agent_chat_service(agent_chat_service) configure_agent_chat_persistence(agent_chat_persistence) - configure_user_lookups( - by_id=lambda user_id: build_transactional_user_repository().get_by_id(user_id), - by_name=lambda username: build_transactional_user_repository().get_by_name(username), - by_channel=lambda **bindings: build_transactional_user_repository().find_name_by_bindings(bindings), - ) - configure_auth_service( - AuthService( - users=build_transactional_user_repository(), - config=get_configured_system_config(), - passkeys=PassKeyOper(), - ) - ) - configure_passkey_challenge_cache( - TTLCache( - region="passkey_challenge", - maxsize=4096, - ttl=PASSKEY_CHALLENGE_TTL_SECONDS, - ) - ) - configure_passkey_service(PasskeyService(repository=PassKeyOper())) configure_transfer_history_repository(lambda: transfer_history_repository) configure_site_query_service(SiteQueryService(repository=site_repository)) configure_site_health_service(SiteHealthService(repository=site_repository)) @@ -1084,7 +1052,7 @@ async def _initialize_modules() -> HostRuntime: ) ) # 认证访问层不反向依赖数据库实现,由启动组合层注入载荷提供器。 - set_superuser_token_payload_provider(build_superuser_token_payload) + configure_security_access() # DoH DohHelper() # 站点管理 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 431138905..1fa240b64 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -285,7 +285,9 @@ sequenceDiagram `HostRuntime.settings` 的窄服务读写可变部署设置,业务域不接触 Settings 实例;生产与测试组合根统一 复用 `startup/composition/configuration.py` 的映射、快照加载与发布。`startup/composition/database.py` 持有数据库 worker、兼容事务 runner、查询/插件持久化和旧 `ApiDataPorts` 的具体装配;initializer - 只保留顺序调用。`ApiDataPorts` 仅保留旧导入 ABI,不参与正式请求链路。 + 只保留顺序调用。`startup/composition/security.py` 统一装配认证、用户查询、PassKey 与 Web 访问 + provider,并将同一 `AuthenticationRuntime` 交给 HostRuntime。`ApiDataPorts` 仅保留旧导入 ABI, + 不参与正式请求链路。 - **安全模式**:`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。 - **Scheduler 同名职责包**:旧 `app/scheduler.py` 单体已退役;`catalog.py` 负责作业目录和计划投影, `execution.py`、`bridge.py`、`progress.py` 分别负责执行、跨循环句柄和进度终态,`registry.py` diff --git a/docs/architecture-refactor-roadmap.md b/docs/architecture-refactor-roadmap.md index 928ffbfbd..0460e9df4 100644 --- a/docs/architecture-refactor-roadmap.md +++ b/docs/architecture-refactor-roadmap.md @@ -342,6 +342,8 @@ TMDB、豆瓣、Bangumi、AniList 详情到统一字段的规则一次迁入 `ap - `startup/composition/database.py` 唯一持有 `DatabaseWorker`、兼容事务 runner、查询服务、 插件持久化、工作流查询和旧 `ApiDataPorts` 的具体数据库装配;runtime 拒绝重入,Worker 成功后 才发布事务入口,关闭失败时保留 owner 与 provider,成功关闭后对称撤销。 +- `startup/composition/security.py` 唯一构造并登记认证、用户查询、PassKey 与 Web 认证入口, + initializer 只消费其返回的 `AuthenticationRuntime`,不再保留第二份安全服务装配。 - `startup/initializers/modules.py` 只按原顺序调用 composition API,并继续构造尚未迁移的领域运行时; 本批未改变 HostRuntime、插件 ABI、SDK/Compat 或生命周期 manifest,`app/plugins/**` 未改动。 - 模块 owner 完整收敛后撤销 `app.state.host_runtime`;启动失败直接关闭 Worker、撤销本批 provider 并 diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index a8da579b9..2464a54e9 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -140,7 +140,10 @@ not startup. Lower-level runtime modules must not import startup. runtime/settings construction and publication. `startup/composition/database.py` owns the process database worker, compatibility transaction runner, query services, plugin persistence and legacy API data-port wiring. Initializers call these owners in -startup order and must not recreate their concrete construction. Database runtime +startup order and must not recreate their concrete construction. +`startup/composition/security.py` owns authentication, user lookup, PassKey and Web +access provider assembly; `initializers/modules.py` only invokes that owner and places +its returned `AuthenticationRuntime` into the host runtime. Database runtime ownership rejects overlapping lifespans; after a successful worker shutdown, the configuration/database composition owners must revoke every provider they published before releasing database connections. Other domain providers remain owned by their diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index 028ceb11e..c328d876b 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -1486,7 +1486,7 @@ def test_passkey_application_does_not_select_cache_backend(): def test_startup_explicitly_configures_passkey_challenge_cache(): """PassKey challenge 缓存必须由启动组合根显式装配。""" - path = APP_ROOT / "startup" / "initializers" / "modules.py" + path = APP_ROOT / "startup" / "composition" / "security.py" tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) configured = any( isinstance(node, ast.Call)