user_hierarchy.go 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. package middleware
  2. import (
  3. "bytes"
  4. "context"
  5. "encoding/json"
  6. "io"
  7. "net/http"
  8. "strconv"
  9. "strings"
  10. "github.com/Wei-Shaw/sub2api/internal/service"
  11. "github.com/gin-gonic/gin"
  12. )
  13. type hierarchyUserLookup interface {
  14. GetByID(context.Context, int64) (*service.User, error)
  15. }
  16. // UserHierarchyGuard belongs after AdminAuth on the admin route group. It
  17. // protects all user writes, including balance, TOTP, identities and bulk limits.
  18. func UserHierarchyGuard(userService *service.UserService) gin.HandlerFunc {
  19. if userService == nil {
  20. return userHierarchyGuard(nil)
  21. }
  22. return userHierarchyGuard(userService)
  23. }
  24. func userHierarchyGuard(users hierarchyUserLookup) gin.HandlerFunc {
  25. return func(c *gin.Context) {
  26. path := c.Request.URL.Path
  27. role, ok := GetUserRoleFromContext(c)
  28. if subject, present := GetAuthSubjectFromContext(c); present && subject.UserID > 0 {
  29. c.Request = c.Request.WithContext(service.WithUserManagementActor(c.Request.Context(), subject.UserID))
  30. }
  31. if superAdminOperation(path, c.Request.Method) && (!ok || role != service.RoleSuperAdmin) {
  32. AbortWithError(c, 403, "SUPER_ADMIN_REQUIRED", "只有超级管理员可以管理全局安全设置、备份和插件")
  33. return
  34. }
  35. if !strings.HasPrefix(path, "/api/v1/admin/users") || c.Request.Method == http.MethodGet || c.Request.Method == http.MethodHead || c.Request.Method == http.MethodOptions {
  36. c.Next()
  37. return
  38. }
  39. if !ok || !service.IsAdminRole(role) {
  40. AbortWithError(c, 403, "FORBIDDEN", "Admin access required")
  41. return
  42. }
  43. if role == service.RoleSuperAdmin {
  44. c.Next()
  45. return
  46. }
  47. var body struct {
  48. Role string `json:"role"`
  49. UserIDs []int64 `json:"user_ids"`
  50. All bool `json:"all"`
  51. }
  52. if c.Request.Body != nil && c.Request.Body != http.NoBody {
  53. data, err := io.ReadAll(io.LimitReader(c.Request.Body, 2<<20))
  54. if err != nil || len(data) >= 2<<20 {
  55. AbortWithError(c, 400, "INVALID_REQUEST", "User management request is too large")
  56. return
  57. }
  58. c.Request.Body = io.NopCloser(bytes.NewReader(data))
  59. if len(data) > 0 && json.Unmarshal(data, &body) != nil {
  60. AbortWithError(c, 400, "INVALID_REQUEST", "Invalid JSON request")
  61. return
  62. }
  63. }
  64. // All=true includes administrators. The super-admin may use that legacy
  65. // operation; an ordinary admin selects specific ordinary users instead.
  66. if body.All || service.IsAdminRole(body.Role) {
  67. AbortWithError(c, 403, "SUPER_ADMIN_REQUIRED", "只有超级管理员可以修改角色或管理管理员账号")
  68. return
  69. }
  70. ids := body.UserIDs
  71. if len(ids) > 500 {
  72. AbortWithError(c, 400, "INVALID_REQUEST", "At most 500 users may be selected")
  73. return
  74. }
  75. if raw := c.Param("id"); raw != "" {
  76. if id, err := strconv.ParseInt(raw, 10, 64); err == nil && id > 0 {
  77. ids = append(ids, id)
  78. }
  79. }
  80. for _, id := range ids {
  81. if users == nil {
  82. AbortWithError(c, 503, "UNAVAILABLE", "User authorization unavailable")
  83. return
  84. }
  85. u, err := users.GetByID(c.Request.Context(), id)
  86. if err != nil {
  87. AbortWithError(c, 403, "FORBIDDEN", "Cannot authorize target user")
  88. return
  89. }
  90. if u.IsAdmin() {
  91. AbortWithError(c, 403, "SUPER_ADMIN_REQUIRED", "只有超级管理员可以管理管理员账号")
  92. return
  93. }
  94. }
  95. c.Next()
  96. }
  97. }
  98. func superAdminOperation(path, method string) bool {
  99. for _, prefix := range []string{"/api/v1/admin/settings/admin-api-key", "/api/v1/admin/backups", "/api/v1/admin/data-management"} {
  100. if path == prefix || strings.HasPrefix(path, prefix+"/") {
  101. return true
  102. }
  103. }
  104. write := method != http.MethodGet && method != http.MethodHead && method != http.MethodOptions
  105. return write && (strings.HasPrefix(path, "/api/v1/admin/system/") || path == "/api/v1/admin/settings" || strings.HasPrefix(path, "/api/v1/admin/settings/") || path == "/api/v1/admin/plugins" || strings.HasPrefix(path, "/api/v1/admin/plugins/"))
  106. }