现象:userIds:[0] 新建策略组时报
「用户 0 已属于此数据源的另一个策略组」HTTP 400
根因:validatePolicyGroup 的跨组冲突检查把 EveryoneUserID(0) 当真实用户,
只要已有任何一个「所有人」策略组(如内置默认策略),
再新建含 0 的组必然冲突。
修复:跨组冲突检查跳过 AnyoneUserID(0) —— 它是哨兵值不是真实用户,
允许多个策略组同时包含「所有人」。
验证:
POST /logs/policy-groups {userIds:[0]} → 200,id=2 正常创建
POST userIds:[0,2](混选) → 400「「所有人」不能与具体用户同时选择」
GET /logs/policy-groups → 默认策略(所有人 · default 命名空间)
镜像:lghost/lokistack:latest + v0.1 = sha256:3222a8716fdd
195 lines
5.2 KiB
Go
195 lines
5.2 KiB
Go
package services
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"opsxloki/models"
|
|
"opsxloki/repository"
|
|
)
|
|
|
|
func ListLogPolicyGroups() []models.LogPolicyGroup {
|
|
db := repository.GetDB()
|
|
db.RLock()
|
|
defer db.RUnlock()
|
|
groups := make([]models.LogPolicyGroup, len(db.LogPolicyGroups))
|
|
for i, group := range db.LogPolicyGroups {
|
|
groups[i] = group
|
|
groups[i].UserIDs = append([]uint(nil), group.UserIDs...)
|
|
groups[i].Labels = make(map[string]string, len(group.Labels))
|
|
for key, value := range group.Labels {
|
|
groups[i].Labels[key] = value
|
|
}
|
|
}
|
|
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 == "" {
|
|
return fmt.Errorf("策略组名称必填")
|
|
}
|
|
if group.SourceID == 0 {
|
|
return fmt.Errorf("请选择数据源")
|
|
}
|
|
foundSource := false
|
|
for _, source := range db.LogSources {
|
|
if source.ID == group.SourceID {
|
|
foundSource = true
|
|
break
|
|
}
|
|
}
|
|
if !foundSource {
|
|
return fmt.Errorf("数据源不存在")
|
|
}
|
|
if len(group.UserIDs) == 0 {
|
|
return fmt.Errorf("请选择至少一个用户")
|
|
}
|
|
for key := range group.Labels {
|
|
if !ValidLogLabelName(key) {
|
|
return fmt.Errorf("无效标签名: %s", key)
|
|
}
|
|
}
|
|
seen := map[uint]bool{}
|
|
for _, id := range group.UserIDs {
|
|
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" {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return fmt.Errorf("用户 %d 不存在或不是普通用户", id)
|
|
}
|
|
}
|
|
for _, other := range db.LogPolicyGroups {
|
|
if other.ID == excludeID || other.SourceID != group.SourceID {
|
|
continue
|
|
}
|
|
if other.Name == group.Name {
|
|
return fmt.Errorf("此数据源已有同名策略组")
|
|
}
|
|
for _, id := range other.UserIDs {
|
|
// AnyoneUserID(0) 是「所有人」哨兵,不是真实用户,
|
|
// 允许多个策略组同时包含它(否则新建任何「所有人」策略组都会被这条冲突检查拒掉)
|
|
if id == AnyoneUserID {
|
|
continue
|
|
}
|
|
if seen[id] {
|
|
return fmt.Errorf("用户 %d 已属于此数据源的另一个策略组", id)
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Group membership replaces legacy per-user grants for the same source.
|
|
func removeDirectGrants(db *repository.JsonDB, sourceID uint, userIDs []uint) {
|
|
members := map[uint]bool{}
|
|
for _, id := range userIDs {
|
|
members[id] = true
|
|
}
|
|
for i := range db.LogSources {
|
|
if db.LogSources[i].ID != sourceID {
|
|
continue
|
|
}
|
|
kept := db.LogSources[i].Access[:0]
|
|
for _, rule := range db.LogSources[i].Access {
|
|
if !members[rule.UserID] {
|
|
kept = append(kept, rule)
|
|
}
|
|
}
|
|
db.LogSources[i].Access = kept
|
|
return
|
|
}
|
|
}
|
|
|
|
func CreateLogPolicyGroup(group models.LogPolicyGroup) (*models.LogPolicyGroup, error) {
|
|
db := repository.GetDB()
|
|
id := db.NextID("logPolicyGroups")
|
|
db.Lock()
|
|
defer db.Unlock()
|
|
if err := validatePolicyGroup(db, group, 0); err != nil {
|
|
return nil, err
|
|
}
|
|
group.ID = id
|
|
group.Name = strings.TrimSpace(group.Name)
|
|
group.CreatedAt = time.Now()
|
|
group.UpdatedAt = group.CreatedAt
|
|
removeDirectGrants(db, group.SourceID, group.UserIDs)
|
|
db.LogPolicyGroups = append(db.LogPolicyGroups, group)
|
|
db.RequestSave()
|
|
return &group, nil
|
|
}
|
|
|
|
func UpdateLogPolicyGroup(id uint, group models.LogPolicyGroup) (*models.LogPolicyGroup, error) {
|
|
db := repository.GetDB()
|
|
db.Lock()
|
|
defer db.Unlock()
|
|
for i := range db.LogPolicyGroups {
|
|
if db.LogPolicyGroups[i].ID != id {
|
|
continue
|
|
}
|
|
if err := validatePolicyGroup(db, group, id); err != nil {
|
|
return nil, err
|
|
}
|
|
group.ID = id
|
|
group.Name = strings.TrimSpace(group.Name)
|
|
group.CreatedAt = db.LogPolicyGroups[i].CreatedAt
|
|
group.UpdatedAt = time.Now()
|
|
removeDirectGrants(db, group.SourceID, group.UserIDs)
|
|
db.LogPolicyGroups[i] = group
|
|
db.RequestSave()
|
|
return &group, nil
|
|
}
|
|
return nil, fmt.Errorf("策略组不存在")
|
|
}
|
|
|
|
func DeleteLogPolicyGroup(id uint) error {
|
|
db := repository.GetDB()
|
|
db.Lock()
|
|
defer db.Unlock()
|
|
for i := range db.LogPolicyGroups {
|
|
if db.LogPolicyGroups[i].ID == id {
|
|
db.LogPolicyGroups = append(db.LogPolicyGroups[:i], db.LogPolicyGroups[i+1:]...)
|
|
db.RequestSave()
|
|
return nil
|
|
}
|
|
}
|
|
return fmt.Errorf("策略组不存在")
|
|
}
|
|
|
|
func policyGroupAccess(groups []models.LogPolicyGroup, sourceID, userID uint) *models.LogSourceAccess {
|
|
for _, group := range groups {
|
|
if group.SourceID != sourceID {
|
|
continue
|
|
}
|
|
for _, id := range group.UserIDs {
|
|
// 0 是哨兵值:表示「所有人」(anyone / *),匹配任意用户。
|
|
// 用于默认策略——让所有账号开箱即可查询业务命名空间。
|
|
if id == 0 || id == userID {
|
|
return &models.LogSourceAccess{UserID: userID, Labels: group.Labels}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|