package middleware import ( "net/http" "regexp" "strconv" "ezops/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // PipelineAccess 流水线权限中间件 // 路由参数 :code(流水线编码)→ 查找 pipeline → 注入 pipelineID 到 context // 权限检查顺序:管理员 → 直接成员 → 用户组 → Item Role (regex) func PipelineAccess(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // 1) 解析 code → 查找流水线(所有用户都需要) code := c.Param("code") if code == "" { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": 400, "message": "缺少流水线编码"}) return } var pipeline model.Pipeline if err := db.Where("code = ?", code).First(&pipeline).Error; err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"code": 404, "message": "流水线不存在"}) return } if authorizePipelineAccess(c, db, &pipeline) { c.Next() } } } // PipelineRunAccess 通过运行记录反查流水线并校验访问权限,防止按 runId 越权读取日志或工作区。 // Logan.Li func PipelineRunAccess(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { runID, err := strconv.ParseUint(c.Param("runId"), 10, 32) if err != nil { c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": 400, "message": "无效的运行记录 ID"}) return } var run model.PipelineRun if err := db.Select("id", "pipeline_id").Take(&run, uint(runID)).Error; err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"code": 404, "message": "流水线运行记录不存在"}) return } var pipeline model.Pipeline if err := db.Take(&pipeline, run.PipelineID).Error; err != nil { c.AbortWithStatusJSON(http.StatusNotFound, gin.H{"code": 404, "message": "关联流水线不存在"}) return } if authorizePipelineAccess(c, db, &pipeline) { c.Next() } } } // authorizePipelineAccess 集中复用流水线成员、用户组与 Item Role 授权规则。 // Logan.Li func authorizePipelineAccess(c *gin.Context, db *gorm.DB, pipeline *model.Pipeline) bool { c.Set("pipelineID", pipeline.ID) c.Set("pipelineCode", pipeline.Code) if isAdmin, _ := c.Get("isAdmin"); isAdmin == true { return true } username, _ := c.Get("username") usernameStr, _ := username.(string) userID, _ := c.Get("userID") userIDUint, ok := userID.(uint) if usernameStr == "" || !ok { c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"code": 401, "message": "未认证"}) return false } var directCount int64 db.Model(&model.DeployPipelineMember{}).Where("pipeline_id = ? AND username = ?", pipeline.ID, usernameStr).Count(&directCount) if directCount > 0 { return true } var groupCount int64 db.Table("deploy_pipeline_members").Joins("JOIN deploy_user_group_members ON deploy_user_group_members.group_id = deploy_pipeline_members.group_id").Where("deploy_pipeline_members.pipeline_id = ? AND deploy_user_group_members.username = ?", pipeline.ID, usernameStr).Count(&groupCount) if groupCount > 0 { return true } var itemRoles []model.ItemRole db.Table("iam_item_roles").Joins("JOIN iam_user_item_roles ON iam_user_item_roles.item_role_id = iam_item_roles.id").Where("iam_item_roles.status = 1 AND iam_user_item_roles.user_id = ?", userIDUint).Find(&itemRoles) for _, ir := range itemRoles { if pipelineMatchesItemRole(*pipeline, ir.Pattern) { return true } } c.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": 403, "message": "无权访问此流水线,请联系流水线 Owner 添加你为成员"}) return false } // GetAccessiblePipelineIDs 获取当前用户有权访问的流水线 ID 列表 // 包括:直接成员 + 用户组 + Item Role (regex) func GetAccessiblePipelineIDs(db *gorm.DB, username string, userID uint) ([]uint, bool) { var directIDs []uint db.Model(&model.DeployPipelineMember{}). Where("username = ?", username). Pluck("pipeline_id", &directIDs) var groupIDs []uint db.Model(&model.DeployPipelineMember{}). Joins("JOIN deploy_user_group_members ON deploy_user_group_members.group_id = deploy_pipeline_members.group_id"). Where("deploy_user_group_members.username = ?", username). Pluck("deploy_pipeline_members.pipeline_id", &groupIDs) idSet := make(map[uint]bool) for _, id := range directIDs { idSet[id] = true } for _, id := range groupIDs { idSet[id] = true } // Item Role regex matching: get all item roles for user, match against pipeline codes var itemRoles []model.ItemRole db.Table("iam_item_roles"). Joins("JOIN iam_user_item_roles ON iam_user_item_roles.item_role_id = iam_item_roles.id"). Where("iam_item_roles.status = 1 AND iam_user_item_roles.user_id = ?", userID). Find(&itemRoles) if len(itemRoles) > 0 { var allPipelines []model.Pipeline db.Find(&allPipelines) for _, p := range allPipelines { for _, ir := range itemRoles { if pipelineMatchesItemRole(p, ir.Pattern) { idSet[p.ID] = true break } } } } ids := make([]uint, 0, len(idSet)) for id := range idSet { ids = append(ids, id) } return ids, len(ids) > 0 } // pipelineMatchesItemRole 正则匹配流水线 code 或 Job 名称(Webhook Job) // Logan.Li func pipelineMatchesItemRole(p model.Pipeline, pattern string) bool { if pattern == "" { return false } if matched, err := regexp.MatchString(pattern, p.Code); err == nil && matched { return true } if p.JobName != "" { if matched, err := regexp.MatchString(pattern, p.JobName); err == nil && matched { return true } } return false }