ForcePilot/backend/server/routers/external_systems/access_rule_router.py
Kris aabde54f2e refactor(routers): 统一完善所有外部系统接口的参数校验和类型约束
1. 为所有查询参数添加max_length长度限制,规范参数输入范围
2. 使用Literal类型替换普通字符串参数,限定合法取值范围
3. 为路径参数添加Path校验,确保ID参数合法有效
4. 优化请求体参数的声明,补充缺失的Body注解和校验规则
5. 统一分页参数的offset/limit使用方式,替换旧的page/page_size模式
2026-07-04 00:16:45 +08:00

373 lines
13 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.

"""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()}