Files
Agent-System/internal/server/auth_helpers.go
T
weichenandCursor 90f77eac93 feat(authz): implement six-phase permissions, settings and workflow v2
Close P0 auth gaps for lineage/runs, stage ABAC fail-closed, and
project:create; add IAM migrations 00021-23, me settings APIs, schema v2
manifests, orchestrator skeleton, and gate A regression coverage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-13 02:50:23 +00:00

138 lines
4.2 KiB
Go

package server
import (
"context"
"errors"
"net/http"
"chat-agent-system/internal/auth"
"chat-agent-system/internal/authz"
"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:
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
// 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 {
http.Error(w, `{"error":"ERR_UNAUTHENTICATED","message":"请先登录"}`, http.StatusUnauthorized)
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.svc == nil || s.svc.ContextManager() == nil {
http.Error(w, "context manager not initialized", http.StatusServiceUnavailable)
return false
}
ownerID, err := s.svc.ContextManager().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
}