mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-01 12:56:34 +08:00
refactor(startup): isolate security composition
This commit is contained in:
59
app/startup/composition/security.py
Normal file
59
app/startup/composition/security.py
Normal 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)
|
||||
@@ -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()
|
||||
# 站点管理
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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 并
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user