package handler import ( "net/http" "regexp" "strings" "github.com/gin-gonic/gin" "flowstack/internal/database" "flowstack/internal/model" ) // 三级流水线权限:viewer 查看 / operator 执行 / owner 管理;admin 全局豁免(等级 4) const ( LevelViewer = "viewer" LevelOperator = "operator" LevelOwner = "owner" adminAccessLevel = 4 ) // LevelRank 数值化权限等级,用于比较 func LevelRank(level string) int { switch level { case LevelOwner: return 3 case LevelOperator: return 2 case LevelViewer: return 1 } return 0 } // currentUser 从 AuthMiddleware 注入的上下文取当前用户 func currentUser(c *gin.Context) (username string, isAdmin bool) { return c.GetString("username"), c.GetString("role") == "admin" } func memberInList(members []string, username string) bool { for _, m := range members { if m == username { return true } } return false } // globToRegexp 通配符(* 任意字符、? 单字符)转正则,其余字符按字面量——操作员无需正则知识 func globToRegexp(glob string) *regexp.Regexp { expr := regexp.QuoteMeta(glob) expr = strings.ReplaceAll(expr, `\*`, `.*`) expr = strings.ReplaceAll(expr, `\?`, `.`) return regexp.MustCompile(expr) } // roleMatchPipeline 权限组是否覆盖某流水线:manual 按多选 ID、pattern 按通配符匹配名称 func roleMatchPipeline(role *model.PipelineRole, p *model.Pipeline) bool { if role.ScopeMode == "pattern" { if role.Pattern == "" { return false } re := globToRegexp(role.Pattern) return re.MatchString(p.Name) || (p.JenkinsJobName != "" && re.MatchString(p.JenkinsJobName)) } for _, id := range role.PipelineIDs { if id == p.ID { return true } } return false } // roleLevelForPipeline 用户通过权限组对某流水线获得的最高等级(manual + pattern 一并计算) func roleLevelForPipeline(username string, p *model.Pipeline) int { if username == "" { return 0 } var roles []model.PipelineRole database.DB.Find(&roles) best := 0 for i := range roles { role := &roles[i] if !memberInList(role.Members, username) { continue } if roleMatchPipeline(role, p) { if r := LevelRank(role.Level); r > best { best = r } } } return best } // roleLevelForName 按名称预检(AI 自动创建前):只有 pattern 模式能命中尚未创建的流水线 func roleLevelForName(username string, name string) int { if username == "" { return 0 } var roles []model.PipelineRole database.DB.Find(&roles) best := 0 for _, role := range roles { if !memberInList(role.Members, username) || role.ScopeMode != "pattern" || role.Pattern == "" { continue } if globToRegexp(role.Pattern).MatchString(name) { if r := LevelRank(role.Level); r > best { best = r } } } return best } // pipelineAccessLevel 用户对某流水线的最高权限等级: // admin 豁免 → 单流水线直接成员 → 权限组命中(manual/pattern,取最大值) func pipelineAccessLevel(username string, isAdmin bool, p *model.Pipeline) int { if isAdmin { return adminAccessLevel } best := 0 var member model.PipelineMember if err := database.DB.Where("pipeline_id = ? AND username = ?", p.ID, username).First(&member).Error; err == nil { best = LevelRank(member.Level) } if r := roleLevelForPipeline(username, p); r > best { best = r } return best } // AccessiblePipelineIDs 列表过滤用:直接成员 ∪ 权限组命中的流水线 ID。 // 第二个返回值表示是否需要过滤:admin 返回 (nil, false) 全量可见; // 普通用户返回 (ids, true),ids 可为空 = 无任何可见流水线 func AccessiblePipelineIDs(username string, isAdmin bool) ([]uint, bool) { if isAdmin { return nil, false } if username == "" { return []uint{}, true } ids := map[uint]bool{} var memberIDs []uint database.DB.Model(&model.PipelineMember{}).Where("username = ?", username).Pluck("pipeline_id", &memberIDs) for _, id := range memberIDs { ids[id] = true } var roles []model.PipelineRole database.DB.Find(&roles) patterns := make([]*regexp.Regexp, 0, len(roles)) for i := range roles { role := &roles[i] if !memberInList(role.Members, username) { continue } if role.ScopeMode == "pattern" && role.Pattern != "" { patterns = append(patterns, globToRegexp(role.Pattern)) } else { for _, id := range role.PipelineIDs { ids[id] = true } } } if len(patterns) > 0 { var pipelines []model.Pipeline database.DB.Select("id", "name", "jenkins_job_name").Find(&pipelines) for _, p := range pipelines { for _, re := range patterns { if re.MatchString(p.Name) || (p.JenkinsJobName != "" && re.MatchString(p.JenkinsJobName)) { ids[p.ID] = true break } } } } out := make([]uint, 0, len(ids)) for id := range ids { out = append(out, id) } return out, true } // RequirePipelineAccess 流水线级权限守卫:解析 :id 后按最低等级校验,通过后注入 pipeline func RequirePipelineAccess(minLevel int) gin.HandlerFunc { return func(c *gin.Context) { var p model.Pipeline if err := database.DB.First(&p, c.Param("id")).Error; err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "pipeline not found"}) return } username, isAdmin := currentUser(c) if pipelineAccessLevel(username, isAdmin, &p) < minLevel { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权访问该流水线"}) return } c.Set("pipeline", &p) c.Next() } } // RequireRunAccess 运行级权限守卫:runId → run → pipeline 再校验(防 runId 绕过流水线权限) func RequireRunAccess(minLevel int) gin.HandlerFunc { return func(c *gin.Context) { var run model.PipelineRun if err := database.DB.Where("run_id = ?", c.Param("runId")).First(&run).Error; err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "run not found"}) return } var p model.Pipeline if err := database.DB.First(&p, run.PipelineID).Error; err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"error": "pipeline not found"}) return } username, isAdmin := currentUser(c) if pipelineAccessLevel(username, isAdmin, &p) < minLevel { c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"error": "无权访问该流水线的运行记录"}) return } c.Set("pipeline", &p) c.Next() } }