269 lines
8.1 KiB
Python
269 lines
8.1 KiB
Python
|
|
# ============================================================
|
|||
|
|
# 治理 API(moduleId: gateway-governance-api, 可重生 ✅)
|
|||
|
|
# R71.6:约束/自动化规则 CRUD + 启停 + 规则审计轨迹(/api/governance)
|
|||
|
|
# 与审计统计聚合(/api/gov/audit/stats)。
|
|||
|
|
# 前缀建议:/api/governance(规则管理)与 /api/gov(审计统计)。
|
|||
|
|
# ============================================================
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
from typing import Annotated, Any
|
|||
|
|
|
|||
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
|||
|
|
from pydantic import BaseModel, ConfigDict, Field
|
|||
|
|
|
|||
|
|
from server.aps_domain.governance_rules import (
|
|||
|
|
RULE_ID_PATTERN,
|
|||
|
|
GovernanceRuleError,
|
|||
|
|
GovernanceRuleService,
|
|||
|
|
)
|
|||
|
|
from server.auth.context import get_identity
|
|||
|
|
|
|||
|
|
router = APIRouter(prefix="/api/governance", tags=["governance"])
|
|||
|
|
gov_router = APIRouter(prefix="/api/gov", tags=["governance-audit"])
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleCreateRequest(BaseModel):
|
|||
|
|
model_config = ConfigDict(extra="forbid")
|
|||
|
|
|
|||
|
|
ruleId: str | None = Field(default=None, pattern=RULE_ID_PATTERN.pattern)
|
|||
|
|
roomId: str = Field(min_length=1, max_length=128)
|
|||
|
|
trigger: dict[str, Any]
|
|||
|
|
gear: str = Field(default="G1", pattern="^G[0-4]$")
|
|||
|
|
action: str = "notify"
|
|||
|
|
guardrails: list[str] = Field(default_factory=list)
|
|||
|
|
enabled: bool = True
|
|||
|
|
description: str = ""
|
|||
|
|
params: dict[str, Any] = Field(default_factory=dict)
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleUpdateRequest(BaseModel):
|
|||
|
|
model_config = ConfigDict(extra="forbid")
|
|||
|
|
|
|||
|
|
ruleId: str | None = Field(default=None, pattern=RULE_ID_PATTERN.pattern)
|
|||
|
|
roomId: str | None = Field(default=None, min_length=1, max_length=128)
|
|||
|
|
trigger: dict[str, Any] | None = None
|
|||
|
|
gear: str | None = Field(default=None, pattern="^G[0-4]$")
|
|||
|
|
action: str | None = None
|
|||
|
|
guardrails: list[str] | None = None
|
|||
|
|
enabled: bool | None = None
|
|||
|
|
description: str | None = None
|
|||
|
|
params: dict[str, Any] | None = None
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleView(BaseModel):
|
|||
|
|
ruleId: str
|
|||
|
|
roomId: str
|
|||
|
|
trigger: dict[str, Any]
|
|||
|
|
gear: str
|
|||
|
|
action: str
|
|||
|
|
guardrails: list[str]
|
|||
|
|
enabled: bool
|
|||
|
|
paused: bool = False
|
|||
|
|
description: str
|
|||
|
|
params: dict[str, Any]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleEnvelope(BaseModel):
|
|||
|
|
rule: RuleView
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleListEnvelope(BaseModel):
|
|||
|
|
rules: list[RuleView]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleDeleteEnvelope(BaseModel):
|
|||
|
|
deleted: bool
|
|||
|
|
ruleId: str
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleStageEnvelope(BaseModel):
|
|||
|
|
message: str
|
|||
|
|
block: dict[str, Any]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class RuleAuditEnvelope(BaseModel):
|
|||
|
|
ruleId: str
|
|||
|
|
total: int
|
|||
|
|
events: list[dict[str, Any]]
|
|||
|
|
|
|||
|
|
|
|||
|
|
class AuditStatsEnvelope(BaseModel):
|
|||
|
|
ok: bool
|
|||
|
|
source: str
|
|||
|
|
total: int
|
|||
|
|
filtered: int
|
|||
|
|
window: dict[str, Any]
|
|||
|
|
byModule: list[dict[str, Any]]
|
|||
|
|
byAction: list[dict[str, Any]]
|
|||
|
|
byResult: list[dict[str, Any]]
|
|||
|
|
trend: list[dict[str, Any]]
|
|||
|
|
rows: list[dict[str, Any]]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def get_governance_service() -> GovernanceRuleService:
|
|||
|
|
"""默认服务接入网关既有 AutomationRuntime 单例与当前 store。"""
|
|||
|
|
return GovernanceRuleService()
|
|||
|
|
|
|||
|
|
|
|||
|
|
ServiceDep = Annotated[GovernanceRuleService, Depends(get_governance_service)]
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _raise_governance_error(exc: GovernanceRuleError) -> None:
|
|||
|
|
if exc.code == "RULE_NOT_FOUND":
|
|||
|
|
http_code = status.HTTP_404_NOT_FOUND
|
|||
|
|
elif exc.code in ("RULE_EXISTS", "RULE_ID_MISMATCH", "RULE_CONFLICT"):
|
|||
|
|
http_code = status.HTTP_409_CONFLICT
|
|||
|
|
elif exc.code in ("INVALID_GRANULARITY", "INVALID_TIME", "INVALID_TIME_WINDOW"):
|
|||
|
|
http_code = status.HTTP_400_BAD_REQUEST
|
|||
|
|
elif exc.code == "PERSISTENCE_FAILED":
|
|||
|
|
http_code = status.HTTP_503_SERVICE_UNAVAILABLE
|
|||
|
|
else:
|
|||
|
|
http_code = 422
|
|||
|
|
raise HTTPException(
|
|||
|
|
status_code=http_code,
|
|||
|
|
detail={"code": exc.code, "message": str(exc)},
|
|||
|
|
) from exc
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _stage_governance_change(
|
|||
|
|
service: GovernanceRuleService,
|
|||
|
|
action: str,
|
|||
|
|
payload: dict[str, Any],
|
|||
|
|
) -> RuleStageEnvelope:
|
|||
|
|
identity = get_identity(required=True)
|
|||
|
|
try:
|
|||
|
|
block = service.stage_change(
|
|||
|
|
action,
|
|||
|
|
payload,
|
|||
|
|
session_id=f"governance:{identity.user_id}",
|
|||
|
|
)
|
|||
|
|
except GovernanceRuleError as exc:
|
|||
|
|
_raise_governance_error(exc)
|
|||
|
|
except PermissionError as exc:
|
|||
|
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=str(exc)) from exc
|
|||
|
|
return RuleStageEnvelope(
|
|||
|
|
message="治理规则变更已进入 P2 确认队列,确认前不会写入规则状态。",
|
|||
|
|
block=block.model_dump(),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/rules", response_model=RuleListEnvelope)
|
|||
|
|
def list_governance_rules(
|
|||
|
|
service: ServiceDep,
|
|||
|
|
enabled: bool | None = None,
|
|||
|
|
) -> RuleListEnvelope:
|
|||
|
|
"""规则清单(可按启用状态过滤)。"""
|
|||
|
|
return RuleListEnvelope(rules=service.list_rules(enabled=enabled))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/rules/{rule_id}", response_model=RuleEnvelope)
|
|||
|
|
def get_governance_rule(
|
|||
|
|
rule_id: str,
|
|||
|
|
service: ServiceDep,
|
|||
|
|
) -> RuleEnvelope:
|
|||
|
|
"""单条规则详情。"""
|
|||
|
|
try:
|
|||
|
|
return RuleEnvelope(rule=service.get_rule(rule_id))
|
|||
|
|
except GovernanceRuleError as exc:
|
|||
|
|
_raise_governance_error(exc)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/rules", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
|
|||
|
|
def create_governance_rule(
|
|||
|
|
request: RuleCreateRequest,
|
|||
|
|
service: ServiceDep,
|
|||
|
|
) -> RuleStageEnvelope:
|
|||
|
|
"""创建规则 P2 出卡;确认前不写规则注册表。"""
|
|||
|
|
return _stage_governance_change(
|
|||
|
|
service,
|
|||
|
|
"governance.rule.create",
|
|||
|
|
request.model_dump(),
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.put("/rules/{rule_id}", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
|
|||
|
|
def update_governance_rule(
|
|||
|
|
rule_id: str,
|
|||
|
|
request: RuleUpdateRequest,
|
|||
|
|
service: ServiceDep,
|
|||
|
|
) -> RuleStageEnvelope:
|
|||
|
|
"""更新规则 P2 出卡;确认前不替换规则定义。"""
|
|||
|
|
return _stage_governance_change(
|
|||
|
|
service,
|
|||
|
|
"governance.rule.update",
|
|||
|
|
{"ruleId": rule_id, "patch": request.model_dump(exclude_none=True)},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.delete("/rules/{rule_id}", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
|
|||
|
|
def delete_governance_rule(
|
|||
|
|
rule_id: str,
|
|||
|
|
service: ServiceDep,
|
|||
|
|
) -> RuleStageEnvelope:
|
|||
|
|
"""删除规则 P2 出卡;确认前不移除规则。"""
|
|||
|
|
return _stage_governance_change(
|
|||
|
|
service,
|
|||
|
|
"governance.rule.delete",
|
|||
|
|
{"ruleId": rule_id},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/rules/{rule_id}/enable", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
|
|||
|
|
def enable_governance_rule(
|
|||
|
|
rule_id: str,
|
|||
|
|
service: ServiceDep,
|
|||
|
|
) -> RuleStageEnvelope:
|
|||
|
|
"""启用规则 P2 出卡;确认前不参与 tick。"""
|
|||
|
|
return _stage_governance_change(
|
|||
|
|
service,
|
|||
|
|
"governance.rule.enable",
|
|||
|
|
{"ruleId": rule_id},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.post("/rules/{rule_id}/disable", response_model=RuleStageEnvelope, status_code=status.HTTP_202_ACCEPTED)
|
|||
|
|
def disable_governance_rule(
|
|||
|
|
rule_id: str,
|
|||
|
|
service: ServiceDep,
|
|||
|
|
) -> RuleStageEnvelope:
|
|||
|
|
"""停用规则 P2 出卡;确认前保持原启用状态。"""
|
|||
|
|
return _stage_governance_change(
|
|||
|
|
service,
|
|||
|
|
"governance.rule.disable",
|
|||
|
|
{"ruleId": rule_id},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
|
|||
|
|
@router.get("/rules/{rule_id}/audit", response_model=RuleAuditEnvelope)
|
|||
|
|
def governance_rule_audit(
|
|||
|
|
rule_id: str,
|
|||
|
|
service: ServiceDep,
|
|||
|
|
limit: int = Query(default=50, ge=1, le=1000),
|
|||
|
|
) -> RuleAuditEnvelope:
|
|||
|
|
"""规则审计轨迹:target 命中该规则的全部审计事件。"""
|
|||
|
|
return RuleAuditEnvelope(**service.rule_audit(rule_id, limit=limit))
|
|||
|
|
|
|||
|
|
|
|||
|
|
@gov_router.get("/audit/stats", response_model=AuditStatsEnvelope)
|
|||
|
|
def governance_audit_stats(
|
|||
|
|
service: ServiceDep,
|
|||
|
|
startTime: str | None = None,
|
|||
|
|
endTime: str | None = None,
|
|||
|
|
module: str | None = None,
|
|||
|
|
action: str | None = None,
|
|||
|
|
result: str | None = None,
|
|||
|
|
granularity: str = Query(default="day", pattern="^(day|hour)$"),
|
|||
|
|
) -> AuditStatsEnvelope:
|
|||
|
|
"""审计量聚合:按模块/动作/结果/时间窗口统计(数据源复用 audit ledger)。"""
|
|||
|
|
try:
|
|||
|
|
return AuditStatsEnvelope(**service.audit_stats(
|
|||
|
|
start_time=startTime,
|
|||
|
|
end_time=endTime,
|
|||
|
|
module=module,
|
|||
|
|
action=action,
|
|||
|
|
result=result,
|
|||
|
|
granularity=granularity,
|
|||
|
|
))
|
|||
|
|
except GovernanceRuleError as exc:
|
|||
|
|
_raise_governance_error(exc)
|