fix(server): getOrCreateSession 增加会话归属校验(R-1a)
调用方传入 callerUserID;持久层与内存会话归属不匹配时返回 SESSION_FORBIDDEN;覆盖 NewSession 默认 UserID=1 占位。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -143,8 +143,8 @@ func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sess := s.getOrCreateSession(sessionID, "", "", "", "", "", "", "", "")
|
||||
if sess == nil || sess.UserCtx == nil {
|
||||
sess, err := s.getOrCreateSession(0, sessionID, "", "", "", "", "", "", "", "")
|
||||
if err != nil || sess == nil || sess.UserCtx == nil {
|
||||
writeSafeError(w, r, http.StatusNotFound, "session or project context not found")
|
||||
return
|
||||
}
|
||||
@@ -169,7 +169,8 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
req.WorkDir = ""
|
||||
|
||||
sess := s.getOrCreateSession(
|
||||
sess, err := s.getOrCreateSession(
|
||||
user.ID,
|
||||
req.SessionID,
|
||||
req.ProjectName,
|
||||
req.CustomerName,
|
||||
@@ -180,7 +181,17 @@ func (s *Server) handleChat(w http.ResponseWriter, r *http.Request) {
|
||||
req.UserName,
|
||||
req.UserRole,
|
||||
)
|
||||
sess.UserID = user.ID
|
||||
if err != nil {
|
||||
if errors.Is(err, ctxmgr.ErrSessionOwnerMismatch) {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "SESSION_FORBIDDEN",
|
||||
"message": "无权访问该会话",
|
||||
})
|
||||
return
|
||||
}
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "resolve session failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
s.hydrateVisibleHistory(r.Context(), sess, user.ID)
|
||||
if req.ProjectID > 0 {
|
||||
sess.ProjectID = req.ProjectID
|
||||
@@ -982,6 +993,10 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
|
||||
writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized")
|
||||
return
|
||||
}
|
||||
user, ok := s.requirePrincipal(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
var req upsertChatSessionRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
@@ -992,9 +1007,17 @@ func (s *Server) handleUpsertChatSession(w http.ResponseWriter, r *http.Request)
|
||||
writeSafeError(w, r, http.StatusBadRequest, "missing session_id")
|
||||
return
|
||||
}
|
||||
req.UserID = user.ID
|
||||
|
||||
summary, err := s.ctxMgr.EnsureChatSession(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.Title)
|
||||
summary, err := s.ctxMgr.EnsureChatSessionOwned(r.Context(), req.SessionID, req.UserID, req.ProjectID, req.Title)
|
||||
if err != nil {
|
||||
if errors.Is(err, ctxmgr.ErrSessionOwnerMismatch) {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "SESSION_FORBIDDEN",
|
||||
"message": "无权操作该会话",
|
||||
})
|
||||
return
|
||||
}
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "upsert chat session failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@@ -391,8 +391,9 @@ func (s *Server) cleanStaleSessionsLocked(ttl time.Duration) int {
|
||||
return cleaned
|
||||
}
|
||||
|
||||
// getOrCreateSession 获取或动态构建会话
|
||||
// getOrCreateSession 获取或动态构建会话。callerUserID>0 时校验持久层归属,不匹配返回 ErrSessionOwnerMismatch。
|
||||
func (s *Server) getOrCreateSession(
|
||||
callerUserID int64,
|
||||
sessionID string,
|
||||
projectName string,
|
||||
customerName string,
|
||||
@@ -402,25 +403,38 @@ func (s *Server) getOrCreateSession(
|
||||
workDir string,
|
||||
userName string,
|
||||
userRole string,
|
||||
) *chat.Session {
|
||||
) (*chat.Session, error) {
|
||||
if sessionID == "" {
|
||||
sessionID = fmt.Sprintf("sess_%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
// 归属校验必须先于任何缓存命中/持久层灌入
|
||||
if callerUserID > 0 && s.ctxMgr != nil {
|
||||
if owner, err := s.ctxMgr.GetSessionOwnerID(context.Background(), sessionID); err == nil && owner > 0 && owner != callerUserID {
|
||||
return nil, ctxmgr.ErrSessionOwnerMismatch
|
||||
}
|
||||
}
|
||||
|
||||
s.sessionsMu.RLock()
|
||||
existing, exists := s.sessions[sessionID]
|
||||
s.sessionsMu.RUnlock()
|
||||
if exists && existing != nil {
|
||||
if callerUserID > 0 && existing.UserID > 0 && existing.UserID != callerUserID {
|
||||
return nil, ctxmgr.ErrSessionOwnerMismatch
|
||||
}
|
||||
existing.LastAccessedAt = time.Now()
|
||||
return existing
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
s.sessionsMu.Lock()
|
||||
defer s.sessionsMu.Unlock()
|
||||
// 双重检查
|
||||
if existing, exists := s.sessions[sessionID]; exists && existing != nil {
|
||||
if callerUserID > 0 && existing.UserID > 0 && existing.UserID != callerUserID {
|
||||
return nil, ctxmgr.ErrSessionOwnerMismatch
|
||||
}
|
||||
existing.LastAccessedAt = time.Now()
|
||||
return existing
|
||||
return existing, nil
|
||||
}
|
||||
|
||||
// 惰性内存防泄漏保护 (会话数超过 200 时清理空闲超 120 分钟会话)
|
||||
@@ -549,9 +563,14 @@ func (s *Server) getOrCreateSession(
|
||||
}
|
||||
}
|
||||
|
||||
// 新建会话时立即绑定调用者。NewSession 历史占位 UserID=1 不能当作已绑定归属。
|
||||
if callerUserID > 0 && (restored == nil || restored.UserID <= 0) {
|
||||
sess.UserID = callerUserID
|
||||
}
|
||||
|
||||
sess.LastAccessedAt = time.Now()
|
||||
s.sessions[sessionID] = sess
|
||||
return sess
|
||||
return sess, nil
|
||||
}
|
||||
|
||||
func (s *Server) hydrateVisibleHistory(ctx context.Context, sess *chat.Session, viewerUserID int64) {
|
||||
|
||||
+13
-3
@@ -173,7 +173,8 @@ func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) {
|
||||
req.WorkDir = ""
|
||||
|
||||
// 会话与项目绑定必须在 SSE headers/Flush 之前完成授权(与 tasks/run/stream 对齐)
|
||||
sess := s.getOrCreateSession(
|
||||
sess, err := s.getOrCreateSession(
|
||||
user.ID,
|
||||
req.SessionID,
|
||||
req.ProjectName,
|
||||
req.CustomerName,
|
||||
@@ -184,7 +185,17 @@ func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) {
|
||||
req.UserName,
|
||||
req.UserRole,
|
||||
)
|
||||
sess.UserID = req.UserID
|
||||
if err != nil {
|
||||
if errors.Is(err, ctxmgr.ErrSessionOwnerMismatch) {
|
||||
writeJSONResponse(w, http.StatusForbidden, map[string]string{
|
||||
"error_code": "SESSION_FORBIDDEN",
|
||||
"message": "无权访问该会话",
|
||||
})
|
||||
return
|
||||
}
|
||||
writeSafeError(w, r, http.StatusInternalServerError, "resolve session failed: "+err.Error())
|
||||
return
|
||||
}
|
||||
s.hydrateVisibleHistory(r.Context(), sess, user.ID)
|
||||
if req.ProjectID > 0 {
|
||||
sess.ProjectID = req.ProjectID
|
||||
@@ -306,7 +317,6 @@ func (s *Server) handleChatStream(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// 4. 同 session 串行执行后发起流式对话
|
||||
var err error
|
||||
execErr := sess.WithExecLock(func() error {
|
||||
var sendErr error
|
||||
_, sendErr = sess.SendMessageStreamTurn(ctx, req.Message, listener, req.IncludeDetails, turnOpts)
|
||||
|
||||
Reference in New Issue
Block a user