"""AccessRule 子域 Router。 外部系统限界上下文的工具访问控制规则管理 API,覆盖访问规则 CRUD。 所有端点通过 ``create_use_cases_from_db`` 装配 use_cases,经 ``access_rule_service`` 端口调用用例。 Request Schema 与 Input DTO 不共享类,Router 内显式构造 DTO,操作人字段 (``created_by`` / ``updated_by``)由 ``current_user.uid`` 填充。 """ from __future__ import annotations from datetime import datetime from typing import Any, Literal from fastapi import APIRouter, Depends, Path, Query from pydantic import BaseModel, ConfigDict, Field from sqlalchemy.ext.asyncio import AsyncSession from yuxi.external_systems.infrastructure.container import create_use_cases_from_db from yuxi.external_systems.use_cases.dto.access_rule import ( BatchCloneAccessRulesInput, BatchToggleAccessRulesInput, CreateAccessRuleInput, DeleteAccessRuleInput, EvaluateAccessInput, GetAccessRuleInput, GetConditionsInput, ListAccessRulesInput, ReorderAccessRulesInput, SimulateAccessInput, UpdateAccessRuleInput, UpdateConditionsInput, ) from yuxi.storage.postgres.models_business import User from server.utils.auth_middleware import get_admin_user, get_db, get_required_user access_rule_router = APIRouter(prefix="/access-rules", tags=["external-systems-access-rule"]) # ---------------- Request Schemas ---------------- class CreateAccessRuleRequest(BaseModel): """创建访问规则请求体。字段对齐 ``CreateAccessRuleInput``(不含 created_by)。 ``created_by`` 由 Router 从 ``current_user.uid`` 注入。字段长度约束对齐 ``ExternalToolAccessRule`` ORM 列定义,在边界层拦截非法输入。 """ model_config = ConfigDict(frozen=True) principal_type: str = Field(..., min_length=1, max_length=32) principal_id: str = Field(..., min_length=1, max_length=64) tool_slug: str | None = Field(default=None, max_length=128) system_id: int | None = None principal_name: str | None = Field(default=None, max_length=128) env_key: str | None = Field(default=None, max_length=32) effect: Literal["allow", "deny"] = "allow" priority: int = 0 conditions: dict[str, Any] = Field(default_factory=dict) expires_at: datetime | None = None enabled: bool = True description: str | None = None class UpdateAccessRuleRequest(BaseModel): """更新访问规则请求体。字段对齐 ``UpdateAccessRuleInput``(不含 id 与 updated_by)。 ``id`` 由路径参数提供,``updated_by`` 由 Router 从 ``current_user.uid`` 注入。 所有字段均为可选,仅透传客户端显式设置的字段。字符串字段长度约束对齐 ``ExternalToolAccessRule`` ORM 列定义。 """ model_config = ConfigDict(frozen=True) principal_name: str | None = Field(default=None, max_length=128) effect: Literal["allow", "deny"] | None = None priority: int | None = None conditions: dict[str, Any] | None = None expires_at: datetime | None = None enabled: bool | None = None description: str | None = None # ---------------- 扩展 Request Schemas ---------------- class EvaluateAccessRequest(BaseModel): """评估访问规则请求体。字段对齐 EvaluateAccessInput(不含操作人字段)。""" model_config = ConfigDict(frozen=True) principal_type: str = Field(..., min_length=1, max_length=32) principal_id: str = Field(..., min_length=1, max_length=64) tool_slug: str | None = Field(default=None, max_length=128) system_id: int | None = None env_key: str | None = Field(default=None, max_length=32) class SimulateAccessRequest(BaseModel): """模拟规则执行请求体。字段对齐 SimulateAccessInput(不含操作人字段)。""" model_config = ConfigDict(frozen=True) principal_type: str = Field(..., min_length=1, max_length=32) principal_id: str = Field(..., min_length=1, max_length=64) tool_slug: str | None = Field(default=None, max_length=128) system_id: int | None = None env_key: str | None = Field(default=None, max_length=32) class BatchToggleAccessRulesRequest(BaseModel): """批量启停访问规则请求体。""" model_config = ConfigDict(frozen=True) rule_ids: list[int] = Field(..., min_length=1, max_length=100) enabled: bool class ReorderItem(BaseModel): """单条规则优先级调整项。""" model_config = ConfigDict(frozen=True) rule_id: int priority: int class ReorderAccessRulesRequest(BaseModel): """批量调整规则优先级请求体。""" model_config = ConfigDict(frozen=True) orders: list[ReorderItem] = Field(..., min_length=1, max_length=100) class UpdateConditionsRequest(BaseModel): """更新规则条件请求体。""" model_config = ConfigDict(frozen=True) conditions: dict[str, Any] class BatchCloneAccessRulesRequest(BaseModel): """批量克隆访问规则请求体。""" model_config = ConfigDict(frozen=True) source_rule_ids: list[int] = Field(..., min_length=1, max_length=100) override_config: dict[str, Any] = Field(default_factory=dict) # ---------------- Endpoints ---------------- @access_rule_router.get("", response_model=dict) async def list_access_rules( limit: int = Query(100, ge=1, le=500), offset: int = Query(0, ge=0), system_id: int | None = Query(None, ge=1), tool_slug: str | None = Query(None, max_length=128), principal_type: str | None = Query(None, max_length=32), principal_id: str | None = Query(None, max_length=64), env_key: str | None = Query(None, max_length=32), effect: Literal["allow", "deny"] | None = Query(None, description="规则效果:allow/deny"), enabled: bool | None = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """分页列出工具访问控制规则。""" use_cases = create_use_cases_from_db(db) input_dto = ListAccessRulesInput( page=offset // limit + 1, page_size=limit, system_id=system_id, tool_slug=tool_slug, principal_type=principal_type, principal_id=principal_id, env_key=env_key, effect=effect, enabled=enabled, ) output = await use_cases.access_rule_service.list_access_rules(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.post("", response_model=dict) async def create_access_rule( payload: CreateAccessRuleRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """创建工具访问控制规则。``created_by`` 由当前管理员填充。""" use_cases = create_use_cases_from_db(db) input_dto = CreateAccessRuleInput( **payload.model_dump(), created_by=current_user.uid, ) output = await use_cases.access_rule_service.create_access_rule(input_dto) return {"success": True, "data": output.model_dump()} # ---------------- 静态路径扩展端点(必须在 /{rule_id} 之前声明)---------------- @access_rule_router.post("/evaluate", response_model=dict) async def evaluate_access( payload: EvaluateAccessRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """评估访问规则(只读查询,不抛异常)。""" use_cases = create_use_cases_from_db(db) input_dto = EvaluateAccessInput( principal_type=payload.principal_type, principal_id=payload.principal_id, tool_slug=payload.tool_slug, system_id=payload.system_id, env_key=payload.env_key, ) output = await use_cases.access_rule_service.evaluate_access(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.post("/simulate", response_model=dict) async def simulate_access( payload: SimulateAccessRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """模拟规则执行(只读查询,返回每条规则的评估详情)。""" use_cases = create_use_cases_from_db(db) input_dto = SimulateAccessInput( principal_type=payload.principal_type, principal_id=payload.principal_id, tool_slug=payload.tool_slug, system_id=payload.system_id, env_key=payload.env_key, ) output = await use_cases.access_rule_service.simulate_access(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.post("/batch-toggle", response_model=dict) async def batch_toggle_access_rules( payload: BatchToggleAccessRulesRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """批量启停访问规则。``updated_by`` 由当前管理员填充。""" use_cases = create_use_cases_from_db(db) input_dto = BatchToggleAccessRulesInput( rule_ids=payload.rule_ids, enabled=payload.enabled, updated_by=current_user.uid, ) output = await use_cases.access_rule_service.batch_toggle_access_rules(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.put("/reorder", response_model=dict) async def reorder_access_rules( payload: ReorderAccessRulesRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """批量调整规则优先级。``updated_by`` 由当前管理员填充。""" use_cases = create_use_cases_from_db(db) input_dto = ReorderAccessRulesInput( orders=[item.model_dump() for item in payload.orders], updated_by=current_user.uid, ) output = await use_cases.access_rule_service.reorder_access_rules(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.post("/batch-clone", response_model=dict) async def batch_clone_access_rules( payload: BatchCloneAccessRulesRequest, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """批量克隆访问规则。``created_by`` 由当前管理员填充。""" use_cases = create_use_cases_from_db(db) input_dto = BatchCloneAccessRulesInput( source_rule_ids=payload.source_rule_ids, override_config=payload.override_config, created_by=current_user.uid, ) output = await use_cases.access_rule_service.batch_clone_access_rules(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.get("/{rule_id}", response_model=dict) async def get_access_rule( rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """获取访问规则详情。""" use_cases = create_use_cases_from_db(db) input_dto = GetAccessRuleInput(id=rule_id) output = await use_cases.access_rule_service.get_access_rule(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.put("/{rule_id}", response_model=dict) async def update_access_rule( payload: UpdateAccessRuleRequest, rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """更新工具访问控制规则。``updated_by`` 由当前管理员填充。仅透传显式设置的字段。""" use_cases = create_use_cases_from_db(db) input_dto = UpdateAccessRuleInput( id=rule_id, updated_by=current_user.uid, **payload.model_dump(exclude_unset=True), ) output = await use_cases.access_rule_service.update_access_rule(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.delete("/{rule_id}", response_model=dict) async def delete_access_rule( rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """删除工具访问控制规则。""" use_cases = create_use_cases_from_db(db) input_dto = DeleteAccessRuleInput(id=rule_id) output = await use_cases.access_rule_service.delete_access_rule(input_dto) return {"success": True, "data": output.model_dump()} # ---------------- 动态路径扩展端点(/{rule_id}/conditions)---------------- @access_rule_router.get("/{rule_id}/conditions", response_model=dict) async def get_conditions( rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_required_user), ) -> dict[str, Any]: """查询访问规则的附加条件。""" use_cases = create_use_cases_from_db(db) input_dto = GetConditionsInput(rule_id=rule_id) output = await use_cases.access_rule_service.get_conditions(input_dto) return {"success": True, "data": output.model_dump()} @access_rule_router.put("/{rule_id}/conditions", response_model=dict) async def update_conditions( payload: UpdateConditionsRequest, rule_id: int = Path(ge=1), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """更新访问规则的附加条件。``updated_by`` 由当前管理员填充。""" use_cases = create_use_cases_from_db(db) input_dto = UpdateConditionsInput( rule_id=rule_id, conditions=payload.conditions, updated_by=current_user.uid, ) output = await use_cases.access_rule_service.update_conditions(input_dto) return {"success": True, "data": output.model_dump()}