"""admin 应用的依赖装配:鉴权 + session + 各域 service。 **不 import ``apps.api`` 的任何东西**(import-linter:apps 之间互不依赖)—— session 依赖在这里按同样语义自建一份;``ok``/``AppError`` 等统一信封件来自 ``packages.shared``,是允许的共享层。 """ from __future__ import annotations import hmac from collections.abc import AsyncIterator from typing import Annotated from fastapi import Depends, Request from sqlalchemy.ext.asyncio import AsyncSession from packages.kb.service import KbAssetService, KbService from packages.rules.service import RuleService from packages.shared.config import Settings from packages.shared.db import get_session_factory from packages.shared.errors import AppError, ErrorCode #: 鉴权请求头。共享令牌方案(见包 docstring 的 V1 边界说明)。 ADMIN_TOKEN_HEADER = "X-Admin-Token" def admin_settings(request: Request) -> Settings: """取应用工厂装配时挂上的 settings。 鉴权口径必须与应用装配口径同源:工厂收到的 settings 决定路由是否注册, 也必须决定令牌是否匹配——两处各读一遍 ``get_settings()`` 会给「换令牌 重启了一半」的窗口留缝。 """ settings: Settings = request.app.state.admin_settings return settings async def admin_auth(request: Request) -> None: """``X-Admin-Token`` 精确匹配校验,失败 → 40100(UNAUTHENTICATED)。 用 ``hmac.compare_digest`` 而非 ``==``:令牌比较不该泄露「对到了第几位」。 先 encode 成 bytes 再比较——str 版本的 compare_digest 遇到非 ASCII 头会 直接 TypeError 变 500,那等于给攻击者一个免费的服务端异常。 """ settings = admin_settings(request) expected = settings.admin_token.encode() if not expected: # 正常到不了这里(空令牌时路由根本没注册);留作纵深防御: # 万一未来有人在别的 app 里复用这个依赖,也不许空配置放行。 raise AppError(ErrorCode.UNAUTHENTICATED, message="后台未配置访问令牌") provided = request.headers.get(ADMIN_TOKEN_HEADER, "").encode() if not hmac.compare_digest(provided, expected): raise AppError( ErrorCode.UNAUTHENTICATED, message="管理令牌缺失或不正确", hint=f"请在请求头携带 {ADMIN_TOKEN_HEADER}", ) async def session_dep() -> AsyncIterator[AsyncSession]: """请求级事务作用域:成功统一 commit、异常统一 rollback。 与 ``apps.api`` 的同名依赖语义一致(包内自建,不跨 app import)。 """ factory = get_session_factory() async with factory() as session: try: yield session await session.commit() except Exception: await session.rollback() raise def kb_service_dep(session: SessionDep) -> KbService: return KbService(session) def kb_asset_service_dep(session: SessionDep) -> KbAssetService: return KbAssetService(session) def rule_service_dep(session: SessionDep) -> RuleService: return RuleService(session) SessionDep = Annotated[AsyncSession, Depends(session_dep)] KbServiceDep = Annotated[KbService, Depends(kb_service_dep)] KbAssetServiceDep = Annotated[KbAssetService, Depends(kb_asset_service_dep)] RuleServiceDep = Annotated[RuleService, Depends(rule_service_dep)]