Files
Logan.Li fd60361d93 chore: 统一本地服务器与远程仓库源码
- 合并 main 与 master 历史并以服务器结构为基线
- 保留迁移、数据保留、AWS 账单与部署安全能力
- 建立 SDD 规格和 CI 验证闭环
2026-08-05 20:03:25 +08:00

165 lines
5.4 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}