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:
LLMGuardX Dev
2026-08-13 15:22:19 +08:00
parent 8000bccde3
commit 58535fda7b
21 changed files with 287 additions and 124 deletions
+4 -1
View File
@@ -65,6 +65,8 @@ const (
PermissionApplicationManage = "application:manage"
PermissionNotificationRead = "notification:read"
PermissionNotificationManage = "notification:manage"
PermissionChannelRead = "channel:read"
PermissionChannelManage = "channel:manage"
PermissionMCPServerRead = "mcp_server:read"
PermissionMCPServerManage = "mcp_server:manage"
PermissionSkillRead = "skill:read"
@@ -99,6 +101,7 @@ var rolePermissions = map[string][]string{
PermissionToolRead, PermissionToolManage,
PermissionApplicationRead, PermissionApplicationManage,
PermissionNotificationRead, PermissionNotificationManage,
PermissionChannelRead, PermissionChannelManage,
PermissionMCPServerRead, PermissionMCPServerManage,
PermissionSkillRead, PermissionSkillManage,
PermissionDigitalEmployeeRead, PermissionDigitalEmployeeManage,
@@ -110,7 +113,7 @@ var rolePermissions = map[string][]string{
PermissionAgentNodeRead, PermissionAgentNodeManage,
PermissionSystemManage,
},
"auditor": {PermissionProviderRead, PermissionAPIKeyRead, PermissionAuditRead, PermissionUsageRead, PermissionOutboxRead, PermissionContentPolicyRead, PermissionPricingRead, PermissionPromptRead, PermissionKnowledgeRead, PermissionToolRead, PermissionApplicationRead, PermissionNotificationRead, PermissionMCPServerRead, PermissionSkillRead, PermissionDigitalEmployeeRead, PermissionMarketplaceRead, PermissionFileRead, PermissionInboxRead, PermissionScheduledTaskRead, PermissionTraceRead, PermissionAgentNodeRead},
"auditor": {PermissionProviderRead, PermissionAPIKeyRead, PermissionAuditRead, PermissionUsageRead, PermissionOutboxRead, PermissionContentPolicyRead, PermissionPricingRead, PermissionPromptRead, PermissionKnowledgeRead, PermissionToolRead, PermissionApplicationRead, PermissionNotificationRead, PermissionChannelRead, PermissionMCPServerRead, PermissionSkillRead, PermissionDigitalEmployeeRead, PermissionMarketplaceRead, PermissionFileRead, PermissionInboxRead, PermissionScheduledTaskRead, PermissionTraceRead, PermissionAgentNodeRead},
"member": {},
}
+10 -7
View File
@@ -468,6 +468,16 @@ func adminMenus(account Account) []map[string]any {
if HasPermission(account, PermissionPricingRead) || HasPermission(account, PermissionPricingManage) {
gatewayChildren = append(gatewayChildren, map[string]any{"name": "ModelPrices", "path": "model-prices", "component": "/gateway/model-prices", "meta": map[string]any{"title": "模型价格"}})
}
if HasPermission(account, PermissionUsageRead) {
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Reports", "path": "reports", "component": "/gateway/reports", "meta": map[string]any{"title": "企业报表"}})
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Tenants", "path": "tenants", "component": "/gateway/tenants", "meta": map[string]any{"title": "租户概览"}})
}
if HasPermission(account, PermissionNotificationRead) || HasPermission(account, PermissionNotificationManage) {
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Channels", "path": "channels", "component": "/gateway/channels", "meta": map[string]any{"title": "渠道管理"}})
}
// 注意:所有子菜单必须在 push 进 menus 前 append 完——gatewayChildren 容量
// 有限,先 push 后 append 会因扩容导致 menus 里的 children 指向旧数组,
// 后加页面在菜单中不可见。
if len(gatewayChildren) > 0 {
menus = append(menus, map[string]any{"name": "Gateway", "path": "/gateway", "component": "/index/index", "meta": map[string]any{"title": "网关接入", "icon": "ri:router-line"}, "children": gatewayChildren})
}
@@ -534,13 +544,6 @@ func adminMenus(account Account) []map[string]any {
menus = append(menus, map[string]any{"name": "ResourceMarket", "path": "/resource-market", "component": "/index/index", "meta": map[string]any{"title": "资源市场", "icon": "ri:store-3-line"}, "children": marketChildren})
}
if HasPermission(account, PermissionUsageRead) {
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Reports", "path": "reports", "component": "/gateway/reports", "meta": map[string]any{"title": "企业报表"}})
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Tenants", "path": "tenants", "component": "/gateway/tenants", "meta": map[string]any{"title": "租户概览"}})
}
if HasPermission(account, PermissionNotificationRead) || HasPermission(account, PermissionNotificationManage) {
gatewayChildren = append(gatewayChildren, map[string]any{"name": "Channels", "path": "channels", "component": "/gateway/channels", "meta": map[string]any{"title": "渠道管理"}})
}
// 系统管理:账号权限、事件投递与通知。
systemChildren := make([]map[string]any, 0, 3)
if HasPermission(account, PermissionIdentityManage) {
+10 -3
View File
@@ -248,6 +248,11 @@ func (s *SessionStore) ListSessions(ctx context.Context, kind Kind, subjectID, c
_ = s.client.SRem(ctx, sessionIndexKey(kind, subjectID), hex).Err()
continue
}
// 凭据版本不匹配的会话(改密/2FA 变更后)实际已失效,不展示并清理索引。
if principal.AuthVersion != s.AuthVersion(ctx, principal.Kind, principal.SubjectID) {
_ = s.client.SRem(ctx, sessionIndexKey(kind, subjectID), hex).Err()
continue
}
items = append(items, SessionView{ID: hex, IP: principal.IP, UserAgent: principal.UserAgent, IssuedAt: principal.IssuedAt,
Current: s.sessionHexMatches(currentAuthorization, hex)})
}
@@ -269,6 +274,11 @@ func (s *SessionStore) RevokeSession(ctx context.Context, kind Kind, subjectID,
if s.sessionHexMatches(currentAuthorization, sessionID) {
return ErrRevokeCurrentSession
}
if err := s.client.Del(ctx, sessionKeyFromHex(sessionID)).Err(); err != nil {
return fmt.Errorf("%w: %v", ErrUnavailable, err)
}
// 先删会话键再删索引:Del 失败时两者都保留(列表仍显示,可重试);
// SRem 失败只留脏索引,由 ListSessions 惰性清理自愈。
removed, err := s.client.SRem(ctx, sessionIndexKey(kind, subjectID), sessionID).Result()
if err != nil {
return fmt.Errorf("%w: %v", ErrUnavailable, err)
@@ -276,9 +286,6 @@ func (s *SessionStore) RevokeSession(ctx context.Context, kind Kind, subjectID,
if removed == 0 {
return ErrInvalidSession
}
if err := s.client.Del(ctx, sessionKeyFromHex(sessionID)).Err(); err != nil {
return fmt.Errorf("%w: %v", ErrUnavailable, err)
}
return nil
}
+13 -1
View File
@@ -1,6 +1,7 @@
package identity
import (
"errors"
"net/http"
"net/url"
"strings"
@@ -52,7 +53,18 @@ func (h *HTTPHandler) callbackSocial(w http.ResponseWriter, r *http.Request) {
}
result, err := h.service.CompleteSocialLogin(r.Context(), provider.Kind, state, code, SessionMeta{IP: h.service.ClientIP(r), UserAgent: r.UserAgent()})
if err != nil {
h.socialRedirect(w, r, provider, "sso_error", err.Error())
// 只回传固定错误码,内部细节写服务端日志,避免内部信息进浏览器
// 地址栏/历史/Referer。
code := "login_failed"
switch {
case errors.Is(err, ErrSocialUnbound):
code = "unbound"
case errors.Is(err, ErrAccountDisabled):
code = "disabled"
case errors.Is(err, ErrInvalidSession):
code = "expired"
}
h.socialRedirect(w, r, provider, "sso_error", code)
return
}
switch result.Purpose {