"""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, 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) 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) 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) 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), tool_slug: str | None = Query(None), principal_type: str | None = Query(None), principal_id: str | None = Query(None), env_key: str | None = Query(None), effect: str | None = Query(None), 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()} # ---------------- 静态路径扩展端点(必须在 /{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, 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.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()} @access_rule_router.put("/{rule_id}", response_model=dict) async def update_access_rule( rule_id: int, payload: UpdateAccessRuleRequest, 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, 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, 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( rule_id: int, payload: UpdateConditionsRequest, 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()}