227 lines
5.7 KiB
Go
227 lines
5.7 KiB
Go
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("iam_log_scope_roles").
|
|
Joins("JOIN iam_user_log_scope_roles ON iam_user_log_scope_roles.log_scope_role_id = iam_log_scope_roles.id").
|
|
Where("iam_user_log_scope_roles.user_id = ? AND iam_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)
|
|
} |