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:
@@ -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": {},
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user