feat: 工具配置增强、站点拨测与日志中心多项改进

- 工具配置:新增卸载 API/UI、Go 工具链、流水线构建自动安装依赖
- 修复 ListTools VerifyTool 递归栈溢出导致获取工具配置失败
- Node.js Alpine 安装改用 unofficial-builds musl 预编译包与 index.json 解析 LTS
- 站点拨测:详情页路由、告警自动恢复、并发拨测与默认可观测频率
- 日志中心:RBAC、查询构建器、ES/Loki 增强与审计 request/response 展示
- 发布配置:构建节点、流水线远程执行、Deploy RBAC 导航拆分
This commit is contained in:
Logan committed 2026-07-02 07:47:33 +08:00
1 parent cde7bbe540
commit 7e1c8f583c
84 files changed
+7729 -1507

No files matched your search

+227
View File
@@ -0,0 +1,227 @@
package logaccess
import (
"fmt"
"regexp"
"strings"
"ezops/model"
"gorm.io/gorm"
)
// QueryContext 日志查询授权上下文 // Logan.Li
type QueryContext struct {
Engine string
DatasourceID uint
Index string
Query string
}
var (
lokiSelectorRe = regexp.MustCompile(`\{([^}]*)\}`)
lokiLabelRe = regexp.MustCompile(`([a-zA-Z_][\w.-]*)\s*(=|!=|=~|!~)\s*("([^"]*)"|'([^']*)'|([^\s,}]+))`)
esAppRe = regexp.MustCompile(`(?i)\bapp:(\*|"[^"]+"|'[^']+'|[^\s)]+)`)
)
// GetUserLogScopeRoles 获取用户已启用的日志权限策略 // Logan.Li
func GetUserLogScopeRoles(db *gorm.DB, userID uint) ([]model.LogScopeRole, error) {
var roles []model.LogScopeRole
err := db.Table("monitor_log_scope_roles").
Joins("JOIN monitor_user_log_scope_roles ON monitor_user_log_scope_roles.log_scope_role_id = monitor_log_scope_roles.id").
Where("monitor_user_log_scope_roles.user_id = ? AND monitor_log_scope_roles.status = 1", userID).
Find(&roles).Error
return roles, err
}
// UserHasAssignments 用户是否已分配日志权限(未分配则保持兼容:不拦截) // Logan.Li
func UserHasAssignments(db *gorm.DB, userID uint) bool {
var n int64
db.Model(&model.UserLogScopeRole{}).Where("user_id = ?", userID).Count(&n)
return n > 0
}
// CanQuery 校验用户是否允许执行该日志查询 // Logan.Li
func CanQuery(db *gorm.DB, userID uint, isAdmin bool, qc QueryContext) (bool, string) {
if isAdmin {
return true, ""
}
if !UserHasAssignments(db, userID) {
return true, ""
}
roles, err := GetUserLogScopeRoles(db, userID)
if err != nil || len(roles) == 0 {
return false, "未分配可用的日志查询权限,请联系管理员"
}
for _, r := range roles {
if roleAllowsQuery(r, qc) {
return true, ""
}
}
return false, "无权查询该日志范围(索引/Label 未命中授权策略)"
}
// FilterDataSources 按授权过滤可见数据源 // Logan.Li
func FilterDataSources(db *gorm.DB, userID uint, isAdmin bool, sources []model.LogDataSource) []model.LogDataSource {
if isAdmin || !UserHasAssignments(db, userID) {
return sources
}
roles, _ := GetUserLogScopeRoles(db, userID)
if len(roles) == 0 {
return []model.LogDataSource{}
}
out := make([]model.LogDataSource, 0, len(sources))
for _, ds := range sources {
if datasourceAllowed(roles, ds) {
out = append(out, ds)
}
}
return out
}
// MatchRolesForPreview 返回命中预览的策略列表 // Logan.Li
func MatchRolesForPreview(roles []model.LogScopeRole, qc QueryContext, sample string) []model.LogScopeRole {
matched := make([]model.LogScopeRole, 0)
for _, r := range roles {
if roleAllowsQuery(r, qc) {
matched = append(matched, r)
continue
}
if sample != "" && regexpMatch(r.Pattern, sample) && engineMatches(r.Engine, qc.Engine) {
if r.DatasourceID == nil || *r.DatasourceID == 0 || *r.DatasourceID == qc.DatasourceID {
if fieldMatchesEngine(r.MatchField, qc.Engine) {
matched = append(matched, r)
}
}
}
}
return matched
}
func datasourceAllowed(roles []model.LogScopeRole, ds model.LogDataSource) bool {
for _, r := range roles {
if !engineMatches(r.Engine, ds.Type) {
continue
}
if r.DatasourceID == nil || *r.DatasourceID == 0 || *r.DatasourceID == ds.ID {
return true
}
}
return false
}
func roleAllowsQuery(r model.LogScopeRole, qc QueryContext) bool {
if !engineMatches(r.Engine, qc.Engine) {
return false
}
if r.DatasourceID != nil && *r.DatasourceID > 0 && *r.DatasourceID != qc.DatasourceID {
return false
}
switch r.MatchField {
case "index":
if qc.Engine != "elasticsearch" {
return false
}
idx := strings.TrimSpace(qc.Index)
if idx == "" {
idx = "*"
}
return regexpMatch(r.Pattern, idx)
case "app", "namespace", "job":
values := extractFieldValues(qc.Engine, qc.Query, r.MatchField)
if len(values) == 0 {
return false
}
for _, v := range values {
if regexpMatch(r.Pattern, v) {
return true
}
}
return false
default:
return false
}
}
func engineMatches(roleEngine, queryEngine string) bool {
re := strings.ToLower(strings.TrimSpace(roleEngine))
qe := strings.ToLower(strings.TrimSpace(queryEngine))
if re == "" || re == "*" {
return true
}
return re == qe
}
func fieldMatchesEngine(field, engine string) bool {
if field == "index" {
return engine == "elasticsearch"
}
return engine == "loki"
}
func regexpMatch(pattern, value string) bool {
if pattern == "" || value == "" {
return false
}
matched, err := regexp.MatchString(pattern, value)
return err == nil && matched
}
func extractFieldValues(engine, query, field string) []string {
if engine == "loki" {
return extractLokiLabelValues(query, field)
}
if field == "app" {
return extractESAppValues(query)
}
return nil
}
func extractLokiLabelValues(query, field string) []string {
m := lokiSelectorRe.FindStringSubmatch(query)
if len(m) < 2 {
return nil
}
inner := m[1]
var values []string
for _, part := range lokiLabelRe.FindAllStringSubmatch(inner, -1) {
if len(part) < 7 || part[1] != field {
continue
}
op := part[2]
val := part[4]
if val == "" {
val = part[5]
}
if val == "" {
val = part[6]
}
if op == "=" || op == "=~" {
values = append(values, val)
}
}
return values
}
func extractESAppValues(query string) []string {
var values []string
for _, m := range esAppRe.FindAllStringSubmatch(query, -1) {
if len(m) < 2 {
continue
}
v := strings.Trim(m[1], `"'`)
if v != "" {
values = append(values, v)
}
}
return values
}
// DescribeRole 策略可读描述 // Logan.Li
func DescribeRole(r model.LogScopeRole) string {
ds := "全部数据源"
if r.DatasourceID != nil && *r.DatasourceID > 0 {
ds = fmt.Sprintf("数据源#%d", *r.DatasourceID)
}
return fmt.Sprintf("%s · %s · %s=%s", r.Engine, ds, r.MatchField, r.Pattern)
}