feat: 添加审批系统和遗留查询功能支持

- 添加审批系统,包括审批请求模型、服务和路由,支持创建、批准和拒绝操作
- 实现审批API密钥验证机制,区分普通API和审批API访问权限
- 添加Alembic数据库迁移支持,更新初始schema版本并添加降级保护
- 配置遗留MySQL查询白名单机制,支持命名查询和参数化查询
- 更新业务服务以集成审批流程,高风险操作需要审批票证
- 调整安全认证使用常量定义的HTTP头,增强安全性比较
- 优化.gitignore配置,添加日志目录排除和文档文件包含规则
- 更新Dockerfile添加alembic依赖包,修复OpenClaw适配器错误处理
```
This commit is contained in:
2026-07-06 09:39:25 +08:00
parent ae5990eaef
commit e3a4a6d426
28 changed files with 536 additions and 86 deletions

View File

@@ -48,14 +48,17 @@ def _coerce_column_value(column: Column, value: Any) -> Any:
def _model_payload(model: Any, data: dict[str, Any]) -> dict[str, Any]:
"""Filter unknown keys and coerce values according to model column types."""
"""Validate keys and coerce values according to model column types."""
columns = {column.name: column for column in model.__table__.columns if column.name != "id"}
payload: dict[str, Any] = {}
for key, value in data.items():
column = columns.get(key)
if column is None:
continue
raise HTTPException(
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
detail=f"Unknown field '{key}'",
)
try:
payload[key] = _coerce_column_value(column, value)
except (ValueError, TypeError, InvalidOperation) as exc:
@@ -104,12 +107,20 @@ class BusinessService:
actor: str = ActorValue.API,
approval_ticket_id: str | None = None,
) -> dict[str, Any]:
if domain in HIGH_RISK_DOMAINS:
self._ensure_approved(approval_ticket_id, domain, None, f"create:{domain}")
model = get_domain_model(domain)
payload = _model_payload(model, data)
record = model(**payload)
self.db.add(record)
if domain in HIGH_RISK_DOMAINS:
self.db.flush()
self._consume_approval(
approval_ticket_id,
domain,
record.id,
f"create:{domain}",
data,
actor,
)
self.db.commit()
self.db.refresh(record)
result = serialize_model(record)
@@ -137,8 +148,6 @@ class BusinessService:
actor: str = ActorValue.API,
approval_ticket_id: str | None = None,
) -> dict[str, Any]:
if domain in HIGH_RISK_DOMAINS:
self._ensure_approved(approval_ticket_id, domain, record_id, f"update:{domain}")
model = get_domain_model(domain)
record = self.db.get(model, record_id)
if record is None:
@@ -146,7 +155,17 @@ class BusinessService:
status_code=status.HTTP_404_NOT_FOUND,
detail="Record not found",
)
for key, value in _model_payload(model, data).items():
payload = _model_payload(model, data)
if domain in HIGH_RISK_DOMAINS:
self._consume_approval(
approval_ticket_id,
domain,
record_id,
f"update:{domain}",
data,
actor,
)
for key, value in payload.items():
setattr(record, key, value)
self.db.commit()
self.db.refresh(record)
@@ -167,25 +186,25 @@ class BusinessService:
)
return result
def _ensure_approved(
def _consume_approval(
self,
approval_ticket_id: str | None,
domain: str,
record_id: str | int | None,
action: str,
payload: dict[str, Any],
actor: str,
) -> None:
if not approval_ticket_id:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail="High-risk domain change requires approval_ticket_id",
)
if not ApprovalService(self.db).is_approved_for(
ApprovalService(self.db).consume_for(
approval_ticket_id,
domain,
record_id,
action,
):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Approval ticket is not approved for this change",
)
payload,
actor,
)