0.11.7: 代码审查查缺补漏(安全/并发/前端三轮审查修复)
安全: - 渠道 webhook 入站强制令牌鉴权(恒定时间比较+统一文案),企微签名官方算法; - 报表/概览/systemInfo 端点按 usage:read/audit:read/system:manage 授权; - sso_error 固定错误码;个人渠道令牌仅请求头;工具出站 Dialer.Control 消除 DNS rebinding TOCTOU;新增 channel:read/manage 权限;限流倍数上限 10。 并发/一致性: - 任务上报单条条件 UPDATE 防重放双提交;认领回收过期 claimed 任务; - 审批改先开通后落记录(幂等,无嵌套事务);聊天消息单事务落库; - 会话列表校验 AuthVersion;吊销先 Del 后 SRem;删工具保护调用历史; - rejected 冷却 24h;限流被拒补偿;maintenance 清理限流窗口。 前端/菜单: - 修复 gatewayChildren late-append 导致 reports/tenants/channels 菜单不可见; - 聊天改名 PUT 对齐;渠道编辑清空凭据防串写+启用开关; - 聊天响应防串扰;报表本地时区日期。
This commit is contained in:
@@ -50,8 +50,8 @@ func (s *AgentPolicyService) Set(ctx context.Context, portalUserID string, polic
|
||||
if s == nil || s.pool == nil {
|
||||
return errors.New("安全策略服务不可用")
|
||||
}
|
||||
if policy.RateLimitMultiplier < 1 || policy.RateLimitMultiplier > 100 {
|
||||
return errors.New("限流倍数必须在 1-100 之间")
|
||||
if policy.RateLimitMultiplier < 1 || policy.RateLimitMultiplier > 10 {
|
||||
return errors.New("限流倍数必须在 1-10 之间")
|
||||
}
|
||||
_, err := s.pool.Exec(ctx, `INSERT INTO gateway.portal_agent_policies(portal_user_id,auto_approve_tools,rate_limit_multiplier) VALUES($1,$2,$3)
|
||||
ON CONFLICT(portal_user_id) DO UPDATE SET auto_approve_tools=$2,rate_limit_multiplier=$3,updated_at=clock_timestamp()`,
|
||||
|
||||
+30
-16
@@ -11,6 +11,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"aigateway.local/core/internal/platform/cryptox"
|
||||
@@ -184,6 +185,14 @@ func (s *ToolService) Delete(ctx context.Context, id, actorID string) error {
|
||||
if used {
|
||||
return ErrConflict
|
||||
}
|
||||
// 有调用历史的工具禁止删除:tool_runs 级联删除会永久丢失报表/审计数据。
|
||||
var hasRuns bool
|
||||
if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.tool_runs WHERE tool_id=$1)`, id).Scan(&hasRuns); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasRuns {
|
||||
return errors.New("工具存在调用历史,不能删除;请停用以保留报表数据")
|
||||
}
|
||||
tag, err := tx.Exec(ctx, `DELETE FROM gateway.tool_definitions WHERE id=$1`, id)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -234,7 +243,12 @@ func (s *ToolService) enforceGovernance(ctx context.Context, tool Tool, apiKeyID
|
||||
if err := s.assets.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.tool_approval_requests WHERE tool_id=$1 AND status='approved')`, tool.ID).Scan(&approved); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !approved {
|
||||
// rejected 后 24 小时冷却:避免每次调用都重新发起申请、通知轰炸管理员。
|
||||
var recentlyRejected bool
|
||||
if err := s.assets.pool.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM gateway.tool_approval_requests WHERE tool_id=$1 AND status='rejected' AND decided_at>clock_timestamp()-interval '24 hours')`, tool.ID).Scan(&recentlyRejected); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if !approved && !recentlyRejected {
|
||||
// 自动发起待审申请(唯一部分索引防重复),通知管理员。
|
||||
requestID, err := newUUID()
|
||||
if err != nil {
|
||||
@@ -283,6 +297,9 @@ func (s *ToolService) enforceGovernance(ctx context.Context, tool Tool, apiKeyID
|
||||
return false, err
|
||||
}
|
||||
if count > int64(limit) {
|
||||
// 被拒调用补偿递减:该次计数不消耗窗口额度,避免故障重试风暴
|
||||
// 打满窗口波及合法调用者。
|
||||
_, _ = s.assets.pool.Exec(ctx, `UPDATE gateway.tool_rate_usage SET call_count=greatest(call_count-1,0) WHERE tool_id=$1 AND portal_user_id=$2 AND window_start=date_trunc('minute',clock_timestamp())`, tool.ID, userKey)
|
||||
return false, ErrToolRateLimited
|
||||
}
|
||||
}
|
||||
@@ -510,24 +527,21 @@ func safeToolDial(allowPrivate bool) func(context.Context, string, string) (net.
|
||||
if allowPrivate {
|
||||
return dialer.DialContext
|
||||
}
|
||||
return func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
host, port, err := net.SplitHostPort(address)
|
||||
// 在系统 connect 阶段用 Dialer.Control 校验最终目标 IP:内核完成
|
||||
// 解析后、TCP 握手前回调,校验与连接之间不存在 DNS rebinding 窗口。
|
||||
dialer.Control = func(_, address string, _ syscall.RawConn) error {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
addresses, err := net.DefaultResolver.LookupIPAddr(ctx, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return errors.New("工具目标不是 IP 地址")
|
||||
}
|
||||
if len(addresses) == 0 {
|
||||
return nil, errors.New("工具主机没有解析结果")
|
||||
if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() {
|
||||
return fmt.Errorf("工具目标为受限地址 %s", ip)
|
||||
}
|
||||
for _, candidate := range addresses {
|
||||
ip := candidate.IP
|
||||
if ip == nil || ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() || ip.IsMulticast() || ip.IsUnspecified() {
|
||||
return nil, fmt.Errorf("工具主机解析到受限地址 %s", ip)
|
||||
}
|
||||
}
|
||||
return dialer.DialContext(ctx, network, net.JoinHostPort(addresses[0].IP.String(), port))
|
||||
return nil
|
||||
}
|
||||
return dialer.DialContext
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user