Files
LokiStack/backend/services/log_policy.go
T
opsx f3733f0bfc feat: 品牌统一 LokiStack + 全站中英 i18n + 用户批量删除 + 主题定时切换与浅色侧栏
- 品牌: Go 模块/数据文件/二进制/构建部署资产全部 opsx→lokistack;前端 localStorage 命名空间迁移(读旧写新,含按用户 id 前缀迁移);修复 nginx upstream 与 Makefile 引用已改名服务的断裂
- i18n: react-i18next 落地,common + 11 个页面域命名空间全站中英(菜单/顶栏/登录/列表页/表单/确认弹窗/状态徽章);设置页语言即时切换;修复词典自注册导致的循环导入白屏
- 用户管理: 批量删除(POST /users/batch-delete,不删自己/最后管理员,部分失败不阻断)+ 行复选/全选/批量工具条
- 主题: 浅色侧栏(中性灰激活态)、四态模式(浅色/深色/跟随系统/定时切换,按时区判定)、顶栏太阳月亮二态、设置页紧凑分段选择器
- UI: 登录记住用户名、CardActionButton 统一五页卡片操作钮、shadcn Badge 替换手写徽章、语义色修正
- 清理: 移除主机 Web 终端(前后端)、k8s-helm/Helm 遗留代码与字段、README 重写精简
- 修复: 侧栏与日志告警 order 重复、路由标题 i18n key 未翻译、词典双逗号等
2026-10-27 17:11:38 +08:00

195 lines
5.2 KiB
Go

package services
import (
"fmt"
"strings"
"time"
"lokistack/models"
"lokistack/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
}