refactor(startup): isolate security composition

This commit is contained in:
jxxghp
2026-08-29 17:03:25 +08:00
parent d2a860d89a
commit da67608339
6 changed files with 77 additions and 43 deletions

View File

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

View File

@@ -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()
# 站点管理

View File

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

View File

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

View File

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

View File

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