package server import ( "context" "errors" "net/http" "chat-agent-system/internal/auth" "chat-agent-system/internal/authz" "chat-agent-system/internal/contracts/vocabulary" "chat-agent-system/internal/ctxmgr" ) // failClosedAuthz 生产/DB 模式在授权依赖异常时默认拒绝。 func (s *Server) failClosedAuthz() bool { return s == nil || s.cfg == nil || !s.cfg.AllowInMemory } func (s *Server) userHasSystemPermission(ctx context.Context, user *auth.UserAccount, permKey string) (bool, error) { if user == nil || permKey == "" { return false, nil } if s.isSystemAdminUser(ctx, user) { return true, nil } caps, err := s.authzSvc().ListCapabilities(ctx, user.ID) if err != nil { return false, err } if hasPermInCapabilityList(caps, permKey) { return true, nil } if user.RoleID != "" && hasPermInCapabilityList(authz.FallbackCapabilitiesByRoleID(user.RoleID), permKey) { return true, nil } return false, nil } func hasPermInCapabilityList(caps []string, permKey string) bool { for _, k := range caps { if k == permKey || k == authz.PermManage { return true } } return false } func writeAuthzError(w http.ResponseWriter, err error) { switch { case errors.Is(err, authz.ErrForbidden): writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权访问该项目", }) case errors.Is(err, authz.ErrUnauthenticated): writeJSONResponse(w, http.StatusUnauthorized, map[string]string{ "error_code": "ERR_UNAUTHENTICATED", "message": "请先登录", }) default: writeDomainError(w, nil, http.StatusInternalServerError, err) } } // requireAuthenticatedUser 校验请求已登录,未登录返回 false 并写入 401。 func (s *Server) requireAuthenticatedUser(w http.ResponseWriter, r *http.Request) (*auth.UserAccount, bool) { if user := s.principalFromRequest(r); user != nil { return user, true } user, err := s.authenticateUser(r) if err != nil || user == nil { writeSafeError(w, r, http.StatusUnauthorized, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`) return nil, false } return user, true } // assertSessionOwnedByUser 校验会话归属当前用户;会话不存在或归属他人返回 false。 func (s *Server) assertSessionOwnedByUser(w http.ResponseWriter, r *http.Request, sessionID string, userID int64) bool { if s.ctxMgr() == nil { writeSafeError(w, r, http.StatusServiceUnavailable, "context manager not initialized") return false } ownerID, err := s.ctxMgr().GetSessionOwnerID(r.Context(), sessionID) if err != nil || ownerID <= 0 { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "SESSION_NOT_FOUND", "message": "指定会话不存在或已被删除", }) return false } if ownerID != userID { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "SESSION_FORBIDDEN", "message": "无权访问该会话", }) return false } return true } // assertMessageOwnedByUser 校验消息所属会话归属当前用户。 func (s *Server) assertMessageOwnedByUser(w http.ResponseWriter, r *http.Request, msg *ctxmgr.ChatMessageRecord, userID int64) bool { if msg == nil { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "MESSAGE_NOT_FOUND", "message": "指定消息不存在或已被删除", }) return false } return s.assertSessionOwnedByUser(w, r, msg.SessionID, userID) } // requireProjectAccess 校验当前用户可访问指定项目。 func (s *Server) requireProjectAccess(w http.ResponseWriter, r *http.Request, projectID int64) (*auth.UserAccount, bool) { user, ok := s.requirePrincipal(w, r) if !ok { return nil, false } if s.isSystemAdminUser(r.Context(), user) { return user, true } if err := s.authorizer().RequireProjectAccess(r.Context(), projectID, user.ID); err != nil { if err == authz.ErrForbidden { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权访问该项目", }) return nil, false } writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "ERR_PROJECT_NOT_FOUND", "message": "项目不存在或无权限访问", }) return nil, false } return user, true } // requireDocGovDocAccess 校验当前用户可访问指定受控文档(经 doc_id 解析 project_id 后做项目成员校验)。 func (s *Server) requireDocGovDocAccess(w http.ResponseWriter, r *http.Request, docID string) (*auth.UserAccount, bool) { user, ok := s.requirePrincipal(w, r) if !ok { return nil, false } if s.gov() == nil { writeJSONResponse(w, http.StatusServiceUnavailable, map[string]string{ "error_code": "ERR_DOCGOV_UNAVAILABLE", "message": "docgov engine not initialized", }) return nil, false } if s.isSystemAdminUser(r.Context(), user) { return user, true } doc, err := s.gov().GetDocument(r.Context(), docID) if err != nil || doc == nil { writeJSONResponse(w, http.StatusNotFound, map[string]string{ "error_code": "ERR_DOCUMENT_NOT_FOUND", "message": "文档不存在或无权限访问", }) return nil, false } if doc.ProjectID <= 0 { // 无项目归属的文档仅管理员可见 writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权访问该文档", }) return nil, false } if err := s.authorizer().RequireProjectAccess(r.Context(), doc.ProjectID, user.ID); err != nil { writeJSONResponse(w, http.StatusForbidden, map[string]string{ "error_code": "ERR_FORBIDDEN", "message": "无权访问该文档所属项目", }) return nil, false } return user, true } // resolveDocGovRole 服务端按文档职责分配解析 docgov 角色,不信任客户端自报角色。 func (s *Server) resolveDocGovRole(ctx context.Context, docID string, user *auth.UserAccount) vocabulary.UserRole { if user == nil || s.gov() == nil { return vocabulary.UserRole("") } doc, err := s.gov().GetDocument(ctx, docID) if err != nil || doc == nil { return vocabulary.UserRole("") } switch user.ID { case doc.AuthorID: return vocabulary.RoleAuthor case doc.ReviewerID: return vocabulary.RoleReviewer case doc.ApproverID: return vocabulary.RoleApprover } return vocabulary.UserRole("") }