Files
meida_backend/apps/admin/deps.py
T
xsl 3961b6fbfa feat(admin): minimal console for KB content operations
apps/admin 独立应用:X-Admin-Token 鉴权(空=全部路由不注册=404)、实体/规则
维护与状态机流转、发布编排(publish/archive → 规则缓存 invalidate +
retrieval invalidate_cache 双失效)、素材登记与审核(tryon_ready 门禁透传)、
缺口看板(T-P2-09,FR-ADM-01~05)。V1 共享令牌鉴权,无角色无审计字段,
操作留证靠 structlog request_id。

测试:单测 tests/unit/test_admin_app.py(10 例:空令牌 404、错/缺/非 ASCII
令牌 40100、正确令牌 200 假服务、发布编排接线、幂等重放无副作用);
集成 tests/integration/test_admin_console.py(5 例:顾问全流程不碰客户端
API、双失效副作用可观察、非法跃迁 40900、素材审核门禁、缺口看板排序、
令牌 401)。483 单测 + 43 集成全绿;ruff/mypy/lint-imports 全绿。

偏差说明:
1. packages/retrieval.repo.list_gaps 有缺陷:对 select(KbGap) 的结果调
   .mappings(),行键是实体名而非列名,GapSummary(row) 取 row.gap_id 必然
   AttributeError(该路径无既有测试覆盖,接真库即暴露)。packages/** 本
   任务禁改,故缺口查询在 apps/admin/routers/gaps.py 以等价 SQL 自实现
   (hit_count 降序、同频次按 last_seen_at 降序、limit/offset 语义一致);
   建议合并阶段修复上游后切回 retrieval.list_gaps。
2. 实体 transition 的 target 收全量四态(任务文档示例仅列 reviewing/
   published/archived):按任务测试要求 published→draft 必须返回 40900
   (状态机语义冲突)而非 schema 422;draft 作为目标由服务层跃迁矩阵恒拒。
3. 规则路由以 rule_key 为路径参数(任务文档写 /rules/{id},但 RuleService
   全程以 rule_key 寻址,无按数字 id 的接口,以代码为准)。
4. 发布编排触发面含 archived(撤稿):依据 retrieval.invalidate_cache 的
   自身契约「发布/归档知识后调用」,比文档字面的「发布成功后」多了撤稿
   方向;reviewing 等其余状态不触发。
5. updated_by/author 记 "admin-console":V1 共享令牌无操作者身份,字段
   留渠道来源;运营账号体系属 V2(见包 docstring 的升级路径)。
2026-08-14 21:19:31 +08:00

91 lines
3.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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)]