fix(logs): 「所有人」策略保存被拒 + 快捷 LogQL namespace 空且不认默认策略
=== 一、保存策略组报「用户ID无效或重复」===
validatePolicyGroup 里 `if id == 0 || seen[id]` 直接把「所有人」哨兵当非法。
修复:
- 新增常量 AnyoneUserID = 0(与 policyGroupAccess、前端 ANYONE 对齐)
- 0 跳过用户存在性校验,但必须独占:与具体用户混选时报
「「所有人」不能与具体用户同时选择」
- 重复检查独立出来,报错带具体 ID
=== 二、快捷 LogQL 的 namespace 下拉总是空的 ===
两个叠加原因:
1. 用的是原生 <datalist>,只在输入时才提示、样式弱,看着像"下拉是空的"
2. 候选只来自 Loki 实际值 —— 而默认策略配的 default 命名空间里没有业务 Pod,
Loki 里根本没有这个 label 值,所以永远不出现;前端也不会拿策略值当默认选中
修复:
后端新增 GET /api/v1/logs/my-scope?sourceId= → {labels, unrestricted}
(services.UserAccessLabels 暴露当前用户在数据源上的授权 labels;
admin 返回 unrestricted=true 但仍带上策略 labels 供 UI 做默认值)
前端:
- logsApi.myScope()
- QuickQueryTemplates 用策略里的 namespace 作为初始选中值
- namespaceOptions = 策略值 ∪ Loki 实际值(去重,策略值优先)
- namespace/app/pod 三个 <datalist> 全部换成 SearchableValueSelect
(点击即展开、可搜索,placeholder 显示「请选择(共 N 个)」)
验证:
PUT /logs/policy-groups/1 {userIds:[0]} → 200 ✅
POST userIds:[0,2](混选) → 400「「所有人」不能与具体用户同时选择」✅
GET /logs/my-scope?sourceId=1 → {"labels":{"namespace":"default"},"unrestricted":true} ✅
bundle: my-scope 1 处、datalist 残留 0 处 ✅
镜像:lghost/lokistack:latest + v0.1 = sha256:ad659f564665
This commit is contained in:
1 parent
4dda6e5ba8
commit
cf54be3f7d
6 files changed
+131
-16
No files matched your search
@@ -245,6 +245,39 @@ func (ctrl *LogsController) LabelValues(c *gin.Context) {
|
||||
utils.Success(c, values)
|
||||
}
|
||||
|
||||
// MyScope 返回当前登录用户在某数据源上的授权范围。
|
||||
//
|
||||
// 快捷 LogQL 用它做两件事:
|
||||
// 1. 把授权策略里配置的 namespace 作为默认选中值(管理员配了 default/astralx 后开箱即用)
|
||||
// 2. 把该值合并进 namespace 候选列表 —— 否则像 default 这种「策略里有、Loki 里没日志」
|
||||
// 的命名空间不会出现在下拉里,用户会以为策略没生效
|
||||
func (ctrl *LogsController) MyScope(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("sourceId"), 10, 64)
|
||||
src := services.GetLogSource(uint(id))
|
||||
if src == nil {
|
||||
utils.BadRequest(c, "数据源不存在")
|
||||
return
|
||||
}
|
||||
if c.GetString("role") == "admin" {
|
||||
// 管理员的查询范围不受策略限制(unrestricted=true),
|
||||
// 但仍返回策略里的 labels 作为快捷 LogQL 的默认选中值——
|
||||
// 否则管理员在小黑框里看到的下拉永远没有策略配的那个 namespace,
|
||||
// 会误以为「默认策略没生效」。
|
||||
labels := services.UserAccessLabels(src, c.GetUint("userId"))
|
||||
if labels == nil {
|
||||
labels = map[string]string{}
|
||||
}
|
||||
utils.Success(c, gin.H{"labels": labels, "unrestricted": true})
|
||||
return
|
||||
}
|
||||
labels := services.UserAccessLabels(src, c.GetUint("userId"))
|
||||
if labels == nil {
|
||||
utils.Forbidden(c, "无权查询此数据源")
|
||||
return
|
||||
}
|
||||
utils.Success(c, gin.H{"labels": labels, "unrestricted": len(labels) == 0})
|
||||
}
|
||||
|
||||
// Health Loki /ready 探活(查询页状态点)
|
||||
func (ctrl *LogsController) Health(c *gin.Context) {
|
||||
id, _ := strconv.ParseUint(c.Query("sourceId"), 10, 64)
|
||||
|
||||
@@ -84,6 +84,7 @@ func SetupRouter() *gin.Engine {
|
||||
logs.POST("/context", logsCtrl.Context)
|
||||
logs.GET("/labels", logsCtrl.Labels)
|
||||
logs.GET("/label/:name/values", logsCtrl.LabelValues)
|
||||
logs.GET("/my-scope", logsCtrl.MyScope)
|
||||
logs.POST("/histogram", logsCtrl.Histogram)
|
||||
logs.GET("/health", logsCtrl.Health)
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ func ListLogPolicyGroups() []models.LogPolicyGroup {
|
||||
return groups
|
||||
}
|
||||
|
||||
// AnyoneUserID 是 userIds 里的哨兵值,表示「所有人」(anyone / *)。
|
||||
// 与 policyGroupAccess 的匹配逻辑、前端 ANYONE 常量保持一致。
|
||||
const AnyoneUserID uint = 0
|
||||
|
||||
func validatePolicyGroup(db *repository.JsonDB, group models.LogPolicyGroup, excludeID uint) error {
|
||||
group.Name = strings.TrimSpace(group.Name)
|
||||
if group.Name == "" {
|
||||
@@ -53,10 +57,18 @@ func validatePolicyGroup(db *repository.JsonDB, group models.LogPolicyGroup, exc
|
||||
}
|
||||
seen := map[uint]bool{}
|
||||
for _, id := range group.UserIDs {
|
||||
if id == 0 || seen[id] {
|
||||
return fmt.Errorf("用户 ID 无效或重复")
|
||||
if seen[id] {
|
||||
return fmt.Errorf("用户 ID 重复: %d", id)
|
||||
}
|
||||
seen[id] = true
|
||||
// AnyoneUserID(0) 是「所有人」哨兵,不对应真实用户记录,
|
||||
// 跳过存在性校验;但它必须独占,不能与其他具体用户混选。
|
||||
if id == AnyoneUserID {
|
||||
if len(group.UserIDs) > 1 {
|
||||
return fmt.Errorf("「所有人」不能与具体用户同时选择")
|
||||
}
|
||||
continue
|
||||
}
|
||||
found := false
|
||||
for _, user := range db.Users {
|
||||
if user.ID == id && user.Role != "admin" {
|
||||
|
||||
@@ -58,6 +58,32 @@ func ListVisibleLogSources(userID uint, admin bool) []map[string]interface{} {
|
||||
return out
|
||||
}
|
||||
|
||||
// UserAccessLabels 返回某用户在指定数据源上的授权 labels。
|
||||
//
|
||||
// 返回值语义:
|
||||
// - nil → 该用户对此数据源完全无权限
|
||||
// - 空 map → 有权限但不限定范围(可查全部)
|
||||
// - 非空 map → 只能查匹配这些 label 的日志(如 {"namespace":"astralx"})
|
||||
func UserAccessLabels(src *models.LogSource, userID uint) map[string]string {
|
||||
db := repository.GetDB()
|
||||
var access *models.LogSourceAccess
|
||||
if db != nil {
|
||||
db.RLock()
|
||||
access = policyGroupAccess(db.LogPolicyGroups, src.ID, userID)
|
||||
db.RUnlock()
|
||||
}
|
||||
if access == nil {
|
||||
access = sourceAccess(src, userID)
|
||||
}
|
||||
if access == nil {
|
||||
return nil
|
||||
}
|
||||
if access.Labels == nil {
|
||||
return map[string]string{}
|
||||
}
|
||||
return access.Labels
|
||||
}
|
||||
|
||||
func sourceAccess(src *models.LogSource, userID uint) *models.LogSourceAccess {
|
||||
for i := range src.Access {
|
||||
if src.Access[i].UserID == userID {
|
||||
|
||||
Reference in new issue
Block a user