201 lines
6.2 KiB
Go
201 lines
6.2 KiB
Go
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("")
|
|
}
|