```
feat: 添加审批系统和遗留查询功能支持 - 添加审批系统,包括审批请求模型、服务和路由,支持创建、批准和拒绝操作 - 实现审批API密钥验证机制,区分普通API和审批API访问权限 - 添加Alembic数据库迁移支持,更新初始schema版本并添加降级保护 - 配置遗留MySQL查询白名单机制,支持命名查询和参数化查询 - 更新业务服务以集成审批流程,高风险操作需要审批票证 - 调整安全认证使用常量定义的HTTP头,增强安全性比较 - 优化.gitignore配置,添加日志目录排除和文档文件包含规则 - 更新Dockerfile添加alembic依赖包,修复OpenClaw适配器错误处理 ```
This commit is contained in:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user