package middleware import ( "context" "net/http" "os" "strconv" "strings" "github.com/gin-gonic/gin" "github.com/sundynix/sundynix-gateway/internal/auth" "github.com/sundynix/sundynix-gateway/internal/store" ) // CtxUserID 是鉴权后写入 gin.Context 的已验证用户 ID 键。 const CtxUserID = "uid" // Auth 解析 Authorization: Bearer ,校验通过则把已验证 userID 写入上下文。 // 非阻断:无 token / 无效 token 时不报错,由各 handler(经 userID 兜底 header)或 // 后续 RequireAuth 决定是否放行。 func Auth() gin.HandlerFunc { return func(c *gin.Context) { h := c.GetHeader("Authorization") if strings.HasPrefix(h, "Bearer ") { if uid, err := auth.Parse(strings.TrimSpace(h[len("Bearer "):])); err == nil { c.Set(CtxUserID, uid) } } c.Next() } } // RequireAuth 在 Auth 之后使用:上下文无已验证 userID 则 401 拒绝。 // 用于 owner 作用域的业务路由;SSE/导出等按 task_id 寻址的端点不挂(EventSource 无法带头)。 func RequireAuth() gin.HandlerFunc { return func(c *gin.Context) { if v, ok := c.Get(CtxUserID); ok { if s, _ := v.(string); s != "" { c.Next() return } } c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "需要登录"}) } } // RequireAdmin 保护运维控制面:必须登录,且(设了 ADMIN_USER_IDS 时)uid 须在白名单内。 // ADMIN_USER_IDS 为空:开发期放行任意登录用户;生产期(APP_ENV=prod/GIN_MODE=release)直接拒绝 // ——逼运维显式配置管理员,杜绝"任意账号改模型/密钥配置"。 func RequireAdmin() gin.HandlerFunc { allow := splitEnv("ADMIN_USER_IDS") prod := strings.EqualFold(os.Getenv("APP_ENV"), "production") || strings.EqualFold(os.Getenv("APP_ENV"), "prod") || strings.EqualFold(os.Getenv("GIN_MODE"), "release") return func(c *gin.Context) { uid, _ := c.Get(CtxUserID) id, _ := uid.(string) if id == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "需要登录"}) return } if len(allow) == 0 { if prod { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "未配置管理员(ADMIN_USER_IDS)"}) return } c.Next() // 开发期放行 return } for _, a := range allow { if a == id { c.Next() return } } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"}) } } // MemberRoleResolver 解析用户在某租户的角色(*store.Postgres 天然满足)。抽成接口便于单测门控逻辑。 type MemberRoleResolver interface { MemberRole(ctx context.Context, tenantID, userID string) string } // RequireTenantRole 要求当前用户在活跃租户中的角色不低于 minRole(按 store.RoleRank 阶梯)。 // 须挂在 RequireAuth + TenantContext 之后(依赖已注入的 uid 与 tenant_id)。 // 无租户上下文(正常不会:TenantContext 兜底补建默认租户)或非成员 → rank 0 → 拦下。 // 复用中间件:当前仅接 SubmitTask(≥member,挡只读 viewer 烧租户积分);将来成员自管/共享 // 工作区上线,直接给对应写路由挂上更高 minRole 即生效,无需改 handler。 func RequireTenantRole(db MemberRoleResolver, minRole string) gin.HandlerFunc { min := store.RoleRank(minRole) return func(c *gin.Context) { uid, _ := c.Get(CtxUserID) id, _ := uid.(string) if id == "" { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "需要登录"}) return } tid, _ := c.Get(CtxTenantID) tenant, _ := tid.(string) if store.RoleRank(db.MemberRole(c.Request.Context(), tenant, id)) < min { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "当前角色为只读,无权执行该操作"}) return } c.Next() } } func splitEnv(key string) []string { var out []string for _, p := range strings.Split(os.Getenv(key), ",") { if p = strings.TrimSpace(p); p != "" { out = append(out, p) } } return out } // envInt 读正整数环境变量,缺省回退 def。 func envInt(key string, def int) int { if v := os.Getenv(key); v != "" { if n, err := strconv.Atoi(v); err == nil && n > 0 { return n } } return def }