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:
1 parent
cde7bbe540
commit
7e1c8f583c
84 files changed
+7729
-1507
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user