feat: 将安全策略模板化,新建商户时自动复制初始策略
This commit is contained in:
@@ -38,10 +38,6 @@ func (c *cImSecurityPolicy) Save(ctx context.Context, req *imsecuritypolicy.Save
|
||||
}
|
||||
return &imsecuritypolicy.SaveRes{Id: id}, nil
|
||||
}
|
||||
func (c *cImSecurityPolicy) Publish(ctx context.Context, req *imsecuritypolicy.PublishReq) (res *imsecuritypolicy.PublishRes, err error) {
|
||||
err = service.SysImSecurityPolicy().Publish(ctx, &req.ImSecurityPolicyPublishInp)
|
||||
return
|
||||
}
|
||||
func (c *cImSecurityPolicy) RestoreDefault(ctx context.Context, req *imsecuritypolicy.RestoreDefaultReq) (res *imsecuritypolicy.RestoreDefaultRes, err error) {
|
||||
id, err := service.SysImSecurityPolicy().RestoreDefault(ctx, &req.ImSecurityPolicyRestoreInp)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,8 +10,10 @@ import (
|
||||
"hotgo/internal/library/imkafka"
|
||||
"hotgo/internal/library/imtenant"
|
||||
"hotgo/internal/model/entity"
|
||||
"hotgo/internal/model/input/sysin"
|
||||
"hotgo/internal/service"
|
||||
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/encoding/gjson"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
)
|
||||
@@ -43,6 +45,38 @@ func policyJSON(value *gjson.Json, fallback string) string {
|
||||
return value.String()
|
||||
}
|
||||
|
||||
// imPolicyTemplateTable 安全策略模板表(平台级单行,新建商户初始策略来源)
|
||||
const imPolicyTemplateTable = "im_security_policy_template"
|
||||
|
||||
// loadPolicyTemplateRow 读取安全策略模板单行(取最小 id),返回 nil 表示模板未初始化。
|
||||
func loadPolicyTemplateRow(model *gdb.Model) (gdb.Record, error) {
|
||||
row, err := model.Order("id ASC").Limit(1).One()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if row.IsEmpty() {
|
||||
return nil, nil
|
||||
}
|
||||
return row, nil
|
||||
}
|
||||
|
||||
// templateCurfewRules 解析模板行 curfew_rules JSON 为宵禁规则输入列表
|
||||
func templateCurfewRules(tpl gdb.Record) []*sysin.ImMessageCurfewRuleInp {
|
||||
rules := make([]*sysin.ImMessageCurfewRuleInp, 0)
|
||||
for _, item := range gjson.New(tpl["curfew_rules"].String()).Array() {
|
||||
j := gjson.New(item)
|
||||
rules = append(rules, &sysin.ImMessageCurfewRuleInp{
|
||||
RuleCode: j.Get("ruleCode").String(), RuleName: j.Get("ruleName").String(), DateType: j.Get("dateType").String(),
|
||||
CustomDates: gjson.New(j.Get("customDates").String()), Weekdays: gjson.New(j.Get("weekdays").String()),
|
||||
Timezone: j.Get("timezone").String(), StartTime: j.Get("startTime").String(), EndTime: j.Get("endTime").String(),
|
||||
Scope: j.Get("scope").String(),
|
||||
WhitelistUserIds: gjson.New(j.Get("whitelistUserIds").String()), WhitelistGroupIds: gjson.New(j.Get("whitelistGroupIds").String()),
|
||||
Message: j.Get("message").String(), Status: j.Get("status").Int(), Sort: j.Get("sort").Int(),
|
||||
})
|
||||
}
|
||||
return rules
|
||||
}
|
||||
|
||||
func publishImPolicyEvent(ctx context.Context, tenantID int64, tenantCode, eventType, group string, version uint) error {
|
||||
return imkafka.PublishConfigEvent(ctx, imkafka.ConfigEventInput{
|
||||
TenantID: tenantID,
|
||||
|
||||
@@ -65,8 +65,34 @@ func (s *sSysImSecurityPolicy) View(ctx context.Context, in *sysin.ImSecurityPol
|
||||
err = baseModel.Where(c.TenantId, tenantID).
|
||||
Where(c.PublishStatus, consts.ImSecurityPublishPublished).
|
||||
OrderDesc(c.PolicyVersion).Scan(&res)
|
||||
if err == nil && res == nil {
|
||||
err = baseModel.Where(c.TenantId, tenantID).OrderDesc(c.PolicyVersion).Scan(&res)
|
||||
if err != nil {
|
||||
return nil, gerror.Wrap(err, "获取安全策略失败")
|
||||
}
|
||||
if res == nil {
|
||||
// 无自建策略时回落默认模板展示,保存后成为本商户自己的策略
|
||||
tpl, tplErr := loadPolicyTemplateRow(imtenant.RegistryDB().Model(imPolicyTemplateTable).Safe().Ctx(ctx))
|
||||
if tplErr != nil {
|
||||
return nil, gerror.Wrap(tplErr, "读取安全策略模板失败")
|
||||
}
|
||||
if tpl != nil {
|
||||
res = &sysin.ImSecurityPolicyModel{
|
||||
TenantId: tenantID,
|
||||
PolicyName: tpl["policy_name"].String(),
|
||||
PasswordPolicy: tpl["password_policy"].String(),
|
||||
LoginRateLimit: tpl["login_rate_limit"].String(),
|
||||
SensitiveWordPolicy: tpl["sensitive_word_policy"].String(),
|
||||
FileReviewPolicy: tpl["file_review_policy"].String(),
|
||||
RealnamePolicy: tpl["realname_policy"].String(),
|
||||
RiskChallengePolicy: tpl["risk_challenge_policy"].String(),
|
||||
GlobalFriendEnabled: tpl["global_friend_enabled"].Int(),
|
||||
FriendDisabledPolicy: tpl["friend_disabled_policy"].String(),
|
||||
MessageCurfewEnabled: tpl["message_curfew_enabled"].Int(),
|
||||
}
|
||||
for _, rule := range templateCurfewRules(tpl) {
|
||||
res.CurfewRules = append(res.CurfewRules, &sysin.ImMessageCurfewRuleModel{RuleCode: rule.RuleCode, RuleName: rule.RuleName, DateType: rule.DateType, CustomDates: rule.CustomDates.String(), Weekdays: rule.Weekdays.String(), Timezone: rule.Timezone, StartTime: rule.StartTime, EndTime: rule.EndTime, Scope: rule.Scope, WhitelistUserIds: rule.WhitelistUserIds.String(), WhitelistGroupIds: rule.WhitelistGroupIds.String(), Message: rule.Message, Status: rule.Status, Sort: rule.Sort})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
@@ -88,6 +114,8 @@ func (s *sSysImSecurityPolicy) curfewRules(ctx context.Context, policyID int64)
|
||||
return
|
||||
}
|
||||
|
||||
// Save 保存安全策略并立即生效:每租户仅一条当前生效记录(单记录模型,无草稿/发布工作流)。
|
||||
// 已有记录则原地更新(版本号自增仅作审计),无则创建;同步 im_tenant 冗余开关并投递策略变更事件。
|
||||
func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPolicySaveInp) (id int64, err error) {
|
||||
if err = in.Filter(ctx); err != nil {
|
||||
return
|
||||
@@ -96,55 +124,52 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if tenantID == 0 {
|
||||
// 默认模板不可通过商户策略保存入口修改
|
||||
return 0, gerror.New("默认模板不可修改,请选择具体商户")
|
||||
}
|
||||
db := imtenant.RegistryDB()
|
||||
c := dao.ImSecurityPolicy.Columns()
|
||||
rc := dao.ImMessageCurfewRules.Columns()
|
||||
tc := dao.ImTenant.Columns()
|
||||
operator := contexts.GetUserId(ctx)
|
||||
now := gtime.Now()
|
||||
version := uint(1)
|
||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
version := uint(0)
|
||||
if in.Id > 0 {
|
||||
current, readErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One()
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
if current.IsEmpty() || current[c.TenantId].Int64() != tenantID {
|
||||
return gerror.New("安全策略不存在")
|
||||
}
|
||||
if current[c.PublishStatus].String() != consts.ImSecurityPublishDraft {
|
||||
return gerror.New("只有草稿状态的安全策略可以编辑")
|
||||
}
|
||||
version = current[c.PolicyVersion].Uint()
|
||||
id = in.Id
|
||||
} else {
|
||||
maxValue, maxErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.TenantId, tenantID).Max(c.PolicyVersion)
|
||||
if maxErr != nil {
|
||||
return maxErr
|
||||
}
|
||||
version = uint(maxValue) + 1
|
||||
current, readErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Where(c.TenantId, tenantID).Where(c.PublishStatus, consts.ImSecurityPublishPublished).
|
||||
WhereNull(c.DeletedAt).OrderDesc(c.PolicyVersion).One()
|
||||
if readErr != nil {
|
||||
return readErr
|
||||
}
|
||||
data := g.Map{c.TenantId: tenantID, c.PolicyVersion: version, c.PolicyName: in.PolicyName, c.PublishStatus: consts.ImSecurityPublishDraft, c.PasswordPolicy: policyJSON(in.PasswordPolicy, "{}"), c.LoginRateLimit: policyJSON(in.LoginRateLimit, "{}"), c.SensitiveWordPolicy: normalizedModerationPolicy(in.SensitiveWordPolicy, tenantCode), c.FileReviewPolicy: policyJSON(in.FileReviewPolicy, "{}"), c.RealnamePolicy: policyJSON(in.RealnamePolicy, "{}"), c.RiskChallengePolicy: policyJSON(in.RiskChallengePolicy, "{}"), c.GlobalFriendEnabled: in.GlobalFriendEnabled, c.FriendDisabledPolicy: policyJSON(in.FriendDisabledPolicy, `{"hide_profile":true,"block_qrcode":true,"block_group_member_entry":true,"blocked_entries":["search","profile_card","group_member","qrcode","invite_auto_add","recommendation"],"message":"当前时段暂不允许添加好友"}`), c.MessageCurfewEnabled: in.MessageCurfewEnabled, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: operator}
|
||||
if in.Id > 0 && in.LoginRateLimit == nil {
|
||||
// 未提交登录失败策略时保留原值(登录失败锁定已迁移至独立配置入口,避免保存草稿清空配置)
|
||||
data := g.Map{c.PolicyName: in.PolicyName, c.PublishStatus: consts.ImSecurityPublishPublished, c.PasswordPolicy: policyJSON(in.PasswordPolicy, "{}"), c.LoginRateLimit: policyJSON(in.LoginRateLimit, "{}"), c.SensitiveWordPolicy: normalizedModerationPolicy(in.SensitiveWordPolicy, tenantCode), c.FileReviewPolicy: policyJSON(in.FileReviewPolicy, "{}"), c.RealnamePolicy: policyJSON(in.RealnamePolicy, "{}"), c.RiskChallengePolicy: policyJSON(in.RiskChallengePolicy, "{}"), c.GlobalFriendEnabled: in.GlobalFriendEnabled, c.FriendDisabledPolicy: policyJSON(in.FriendDisabledPolicy, `{"hide_profile":true,"block_qrcode":true,"block_group_member_entry":true,"blocked_entries":["search","profile_card","group_member","qrcode","invite_auto_add","recommendation"],"message":"当前时段暂不允许添加好友"}`), c.MessageCurfewEnabled: in.MessageCurfewEnabled, c.Remark: in.Remark, c.UpdatedAt: now, c.UpdatedBy: operator}
|
||||
if in.LoginRateLimit == nil {
|
||||
// 未提交登录失败策略时保留原值(登录失败锁定由独立配置入口管理,避免保存时清空配置)
|
||||
delete(data, c.LoginRateLimit)
|
||||
}
|
||||
if in.Id > 0 {
|
||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(data).Update(); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
} else {
|
||||
if current.IsEmpty() {
|
||||
version = 1
|
||||
data[c.TenantId] = tenantID
|
||||
data[c.PolicyVersion] = version
|
||||
data[c.CreatedAt] = now
|
||||
data[c.CreatedBy] = operator
|
||||
result, insertErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Data(data).Insert()
|
||||
if insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
id, insertErr = result.LastInsertId()
|
||||
if insertErr != nil {
|
||||
if id, insertErr = result.LastInsertId(); insertErr != nil {
|
||||
return insertErr
|
||||
}
|
||||
} else {
|
||||
id = current[c.Id].Int64()
|
||||
version = current[c.PolicyVersion].Uint() + 1
|
||||
data[c.PolicyVersion] = version
|
||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, id).Data(data).Update(); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
}
|
||||
if _, deleteErr := tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Where(rc.PolicyId, id).Delete(); deleteErr != nil {
|
||||
// 规则整体重建:uk_im_message_curfew_rule 唯一键含 tenant_id+rule_code,软删行仍占键会冲突,必须硬删
|
||||
if _, deleteErr := tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Where(rc.PolicyId, id).Unscoped().Delete(); deleteErr != nil {
|
||||
return deleteErr
|
||||
}
|
||||
for _, rule := range in.CurfewRules {
|
||||
@@ -153,12 +178,23 @@ func (s *sSysImSecurityPolicy) Save(ctx context.Context, in *sysin.ImSecurityPol
|
||||
return insertErr
|
||||
}
|
||||
}
|
||||
if tenantID > 0 {
|
||||
// 上游已将商户配置并入 im_tenant 表,此处同步冗余开关与策略版本(商户行必然存在,无需补建)
|
||||
if _, cfgErr := tx.Model(dao.ImTenant.Table()).Safe().Ctx(ctx).Where(tc.Id, tenantID).
|
||||
Data(g.Map{tc.SecurityPolicyVersion: version, tc.GlobalFriendEnabled: in.GlobalFriendEnabled, tc.MessageCurfewEnabled: in.MessageCurfewEnabled, tc.UpdatedAt: now, tc.UpdatedBy: operator}).Update(); cfgErr != nil {
|
||||
return cfgErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "保存安全策略草稿失败")
|
||||
return 0, gerror.Wrap(err, "保存安全策略失败")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_save", "im_security_policy", id, "success", g.Map{"version": version, "global_friend_enabled": in.GlobalFriendEnabled, "message_curfew_enabled": in.MessageCurfewEnabled})
|
||||
// 保存即生效:投递策略变更事件,IM 端消费后失效策略缓存并回源拉取新配置
|
||||
if err = publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", version); err != nil {
|
||||
return id, err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_save", "im_security_policy", id, "success", g.Map{"global_friend_enabled": in.GlobalFriendEnabled, "message_curfew_enabled": in.MessageCurfewEnabled})
|
||||
return id, nil
|
||||
}
|
||||
|
||||
@@ -176,51 +212,6 @@ func normalizedModerationPolicy(value *gjson.Json, _ string) string {
|
||||
return gjson.New(policy).String()
|
||||
}
|
||||
|
||||
func (s *sSysImSecurityPolicy) Publish(ctx context.Context, in *sysin.ImSecurityPolicyPublishInp) error {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
db := imtenant.RegistryDB()
|
||||
c := dao.ImSecurityPolicy.Columns()
|
||||
var row gdb.Record
|
||||
var err error
|
||||
if row, err = db.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).WhereNull(c.DeletedAt).One(); err != nil {
|
||||
return err
|
||||
}
|
||||
if row.IsEmpty() {
|
||||
return gerror.New("安全策略不存在")
|
||||
}
|
||||
if row[c.PublishStatus].String() != consts.ImSecurityPublishDraft {
|
||||
return gerror.New("只有草稿状态可以发布")
|
||||
}
|
||||
tenantID, tenantCode, err := resolveImPolicyTenant(ctx, row[c.TenantId].Int64())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
operator := contexts.GetUserId(ctx)
|
||||
now := gtime.Now()
|
||||
err = db.Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
|
||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.TenantId, tenantID).Where(c.PublishStatus, consts.ImSecurityPublishPublished).WhereNot(c.Id, in.Id).WhereNull(c.DeletedAt).Data(g.Map{c.PublishStatus: consts.ImSecurityPublishArchived, c.UpdatedAt: now, c.UpdatedBy: operator}).Update(); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
if _, updateErr := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Where(c.Id, in.Id).Data(g.Map{c.PublishStatus: consts.ImSecurityPublishPublished, c.PublishedAt: now, c.PublishedBy: operator, c.UpdatedAt: now, c.UpdatedBy: operator}).Update(); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
if tenantID > 0 {
|
||||
tc := dao.ImTenant.Columns()
|
||||
if _, updateErr := tx.Model(dao.ImTenant.Table()).Safe().Ctx(ctx).Where(tc.Id, tenantID).Data(g.Map{tc.SecurityPolicyVersion: row[c.PolicyVersion].Uint(), tc.GlobalFriendEnabled: row[c.GlobalFriendEnabled].Int(), tc.MessageCurfewEnabled: row[c.MessageCurfewEnabled].Int(), tc.UpdatedAt: now, tc.UpdatedBy: operator}).Update(); updateErr != nil {
|
||||
return updateErr
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "发布安全策略失败")
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_publish", "im_security_policy", in.Id, "success", g.Map{"reason": in.Reason, "version": row[c.PolicyVersion].Uint()})
|
||||
return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", row[c.PolicyVersion].Uint())
|
||||
}
|
||||
|
||||
func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImSecurityPolicyRestoreInp) (int64, error) {
|
||||
if err := in.Filter(ctx); err != nil {
|
||||
return 0, err
|
||||
@@ -229,27 +220,15 @@ func (s *sSysImSecurityPolicy) RestoreDefault(ctx context.Context, in *sysin.ImS
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
c := dao.ImSecurityPolicy.Columns()
|
||||
var source *sysin.ImSecurityPolicyModel
|
||||
err = imtenant.RegistryDB().Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Fields(securityPolicyFields("")...).Where(c.TenantId, 0).
|
||||
Where(c.PublishStatus, consts.ImSecurityPublishPublished).WhereNull(c.DeletedAt).
|
||||
OrderDesc(c.PolicyVersion).Scan(&source)
|
||||
// 读取安全策略模板,将模板内容覆盖为当前商户的策略(复制后商户可自行修改)
|
||||
tpl, err := loadPolicyTemplateRow(imtenant.RegistryDB().Model(imPolicyTemplateTable).Safe().Ctx(ctx))
|
||||
if err != nil {
|
||||
return 0, gerror.Wrap(err, "读取全局默认安全策略失败")
|
||||
return 0, gerror.Wrap(err, "读取安全策略模板失败")
|
||||
}
|
||||
if source == nil {
|
||||
return 0, gerror.New("全局默认安全策略不存在")
|
||||
if tpl == nil {
|
||||
return 0, gerror.New("安全策略模板未初始化")
|
||||
}
|
||||
source.CurfewRules, err = s.curfewRules(ctx, source.Id)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
rules := make([]*sysin.ImMessageCurfewRuleInp, 0, len(source.CurfewRules))
|
||||
for _, rule := range source.CurfewRules {
|
||||
rules = append(rules, &sysin.ImMessageCurfewRuleInp{RuleCode: rule.RuleCode, RuleName: rule.RuleName, DateType: rule.DateType, CustomDates: gjson.New(rule.CustomDates), Weekdays: gjson.New(rule.Weekdays), Timezone: rule.Timezone, StartTime: rule.StartTime, EndTime: rule.EndTime, Scope: rule.Scope, WhitelistUserIds: gjson.New(rule.WhitelistUserIds), WhitelistGroupIds: gjson.New(rule.WhitelistGroupIds), Message: rule.Message, Status: rule.Status, Sort: rule.Sort})
|
||||
}
|
||||
id, err := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: "恢复默认安全策略", PasswordPolicy: gjson.New(source.PasswordPolicy), LoginRateLimit: gjson.New(source.LoginRateLimit), SensitiveWordPolicy: gjson.New(source.SensitiveWordPolicy), FileReviewPolicy: gjson.New(source.FileReviewPolicy), RealnamePolicy: gjson.New(source.RealnamePolicy), RiskChallengePolicy: gjson.New(source.RiskChallengePolicy), GlobalFriendEnabled: source.GlobalFriendEnabled, FriendDisabledPolicy: gjson.New(source.FriendDisabledPolicy), MessageCurfewEnabled: source.MessageCurfewEnabled, Remark: in.Reason, CurfewRules: rules})
|
||||
id, err := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: tpl["policy_name"].String(), PasswordPolicy: gjson.New(tpl["password_policy"].String()), LoginRateLimit: gjson.New(tpl["login_rate_limit"].String()), SensitiveWordPolicy: gjson.New(tpl["sensitive_word_policy"].String()), FileReviewPolicy: gjson.New(tpl["file_review_policy"].String()), RealnamePolicy: gjson.New(tpl["realname_policy"].String()), RiskChallengePolicy: gjson.New(tpl["risk_challenge_policy"].String()), GlobalFriendEnabled: tpl["global_friend_enabled"].Int(), FriendDisabledPolicy: gjson.New(tpl["friend_disabled_policy"].String()), MessageCurfewEnabled: tpl["message_curfew_enabled"].Int(), Remark: in.Reason, CurfewRules: templateCurfewRules(tpl)})
|
||||
if err == nil {
|
||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_restore_default", "im_security_policy", id, "success", g.Map{"reason": in.Reason})
|
||||
}
|
||||
@@ -328,14 +307,11 @@ func (s *sSysImSecurityPolicy) SaveLoginLockoutConfig(ctx context.Context, in *s
|
||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_login_lockout_save", "im_security_policy", policyID, "success", g.Map{"enabled": in.Enabled, "password_max_attempts": in.PasswordMaxAttempts, "lock_minutes": in.LockMinutes, "window_minutes": in.WindowMinutes})
|
||||
return publishImPolicyEvent(ctx, tenantID, tenantCode, "security.policy.updated", "security_policy", version)
|
||||
}
|
||||
// 租户尚无已发布策略:创建默认策略草稿并直接发布(Publish 内部已发送策略变更事件)
|
||||
// 租户尚无策略:保存默认策略,保存即生效并投递策略变更事件
|
||||
id, saveErr := s.Save(ctx, &sysin.ImSecurityPolicySaveInp{TenantId: tenantID, PolicyName: "默认安全策略", LoginRateLimit: gjson.New(value), GlobalFriendEnabled: 1, MessageCurfewEnabled: 0})
|
||||
if saveErr != nil {
|
||||
return saveErr
|
||||
}
|
||||
if err = s.Publish(ctx, &sysin.ImSecurityPolicyPublishInp{Id: id, Reason: "登录失败策略首次配置"}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = writeImAdminAudit(ctx, tenantID, "im_security_policy_login_lockout_save", "im_security_policy", id, "success", g.Map{"enabled": in.Enabled, "password_max_attempts": in.PasswordMaxAttempts, "lock_minutes": in.LockMinutes, "window_minutes": in.WindowMinutes})
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"github.com/gogf/gf/v2/database/gdb"
|
||||
"github.com/gogf/gf/v2/errors/gerror"
|
||||
"github.com/gogf/gf/v2/frame/g"
|
||||
"github.com/gogf/gf/v2/os/gtime"
|
||||
)
|
||||
|
||||
type sSysImTenant struct{}
|
||||
@@ -210,6 +211,10 @@ func (s *sSysImTenant) saveTenant(ctx context.Context, in *sysin.ImTenantEditInp
|
||||
return gerror.Wrap(err, "新增IM租户失败,请稍后重试!")
|
||||
}
|
||||
tenantID = in.Id
|
||||
// 新建商户时复制安全策略模板为本商户初始策略(保存即生效模型)
|
||||
if err = copyDefaultPolicyForTenant(ctx, tx, in.Id, userId); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
@@ -345,7 +350,94 @@ func (s *sSysImTenant) resetTenantAdminPassword(ctx context.Context, tenantId in
|
||||
|
||||
// rollbackTenantCreation 商户创建补偿:删除已写入的商户及配置记录
|
||||
func (s *sSysImTenant) rollbackTenantCreation(ctx context.Context, tenantId int64) {
|
||||
_, _ = s.Model(ctx, &handler.Option{FilterAuth: false}).WherePri(tenantId).Delete()
|
||||
_ = imtenant.RegistryDB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) (err error) {
|
||||
if _, err = s.Model(ctx, &handler.Option{FilterAuth: false}).
|
||||
WherePri(tenantId).Delete(); err != nil {
|
||||
return
|
||||
}
|
||||
// 清理建户时复制的安全策略与宵禁规则,避免残留孤儿数据(商户未建成,硬删不留软删垃圾)
|
||||
if _, err = tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImSecurityPolicy.Columns().TenantId, tenantId).Unscoped().Delete(); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).
|
||||
Where(dao.ImMessageCurfewRules.Columns().TenantId, tenantId).Unscoped().Delete()
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
// copyDefaultPolicyForTenant 新建商户时复制安全策略模板为本商户自己的初始策略。
|
||||
// 模板未初始化时跳过(运行时由内部配置接口的默认值兜底),不阻断建户。
|
||||
func copyDefaultPolicyForTenant(ctx context.Context, tx gdb.TX, tenantID, operator int64) (err error) {
|
||||
tpl, err := loadPolicyTemplateRow(tx.Model(imPolicyTemplateTable).Safe().Ctx(ctx))
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "读取安全策略模板失败")
|
||||
}
|
||||
if tpl == nil {
|
||||
return nil
|
||||
}
|
||||
pc := dao.ImSecurityPolicy.Columns()
|
||||
now := gtime.Now()
|
||||
data := g.Map{
|
||||
pc.TenantId: tenantID,
|
||||
pc.PolicyName: tpl["policy_name"].String(),
|
||||
pc.PolicyVersion: 1,
|
||||
pc.PublishStatus: consts.ImSecurityPublishPublished,
|
||||
pc.PasswordPolicy: tpl["password_policy"].String(),
|
||||
pc.LoginRateLimit: tpl["login_rate_limit"].String(),
|
||||
pc.SensitiveWordPolicy: tpl["sensitive_word_policy"].String(),
|
||||
pc.FileReviewPolicy: tpl["file_review_policy"].String(),
|
||||
pc.RealnamePolicy: tpl["realname_policy"].String(),
|
||||
pc.RiskChallengePolicy: tpl["risk_challenge_policy"].String(),
|
||||
pc.GlobalFriendEnabled: tpl["global_friend_enabled"].Int(),
|
||||
pc.FriendDisabledPolicy: tpl["friend_disabled_policy"].String(),
|
||||
pc.MessageCurfewEnabled: tpl["message_curfew_enabled"].Int(),
|
||||
pc.Remark: "由默认模板复制",
|
||||
pc.PublishedAt: now,
|
||||
pc.PublishedBy: operator,
|
||||
pc.CreatedAt: now,
|
||||
pc.CreatedBy: operator,
|
||||
pc.UpdatedAt: now,
|
||||
pc.UpdatedBy: operator,
|
||||
}
|
||||
result, err := tx.Model(dao.ImSecurityPolicy.Table()).Safe().Ctx(ctx).Data(data).Insert()
|
||||
if err != nil {
|
||||
return gerror.Wrap(err, "复制安全策略模板失败")
|
||||
}
|
||||
policyID, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 同步复制模板宵禁规则
|
||||
rc := dao.ImMessageCurfewRules.Columns()
|
||||
for _, rule := range templateCurfewRules(tpl) {
|
||||
ruleData := g.Map{
|
||||
rc.TenantId: tenantID,
|
||||
rc.PolicyId: policyID,
|
||||
rc.RuleCode: rule.RuleCode,
|
||||
rc.RuleName: rule.RuleName,
|
||||
rc.DateType: rule.DateType,
|
||||
rc.CustomDates: policyJSON(rule.CustomDates, "[]"),
|
||||
rc.Weekdays: policyJSON(rule.Weekdays, "[]"),
|
||||
rc.Timezone: rule.Timezone,
|
||||
rc.StartTime: rule.StartTime,
|
||||
rc.EndTime: rule.EndTime,
|
||||
rc.Scope: rule.Scope,
|
||||
rc.WhitelistUserIds: policyJSON(rule.WhitelistUserIds, "[]"),
|
||||
rc.WhitelistGroupIds: policyJSON(rule.WhitelistGroupIds, "[]"),
|
||||
rc.Message: rule.Message,
|
||||
rc.Status: rule.Status,
|
||||
rc.Sort: rule.Sort,
|
||||
rc.CreatedAt: now,
|
||||
rc.CreatedBy: operator,
|
||||
rc.UpdatedAt: now,
|
||||
rc.UpdatedBy: operator,
|
||||
}
|
||||
if _, err = tx.Model(dao.ImMessageCurfewRules.Table()).Safe().Ctx(ctx).Data(ruleData).Insert(); err != nil {
|
||||
return gerror.Wrap(err, "复制模板宵禁规则失败")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *sSysImTenant) checkCodeUnique(ctx context.Context, id int64, code string) (err error) {
|
||||
|
||||
@@ -78,16 +78,6 @@ func TestImSecurityPolicySaveFilterForcesEveryFriendEntryOff(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestImSecurityPolicyPublishFilter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
if err := (&ImSecurityPolicyPublishInp{Id: 1, Reason: " "}).Filter(ctx); err == nil {
|
||||
t.Fatal("空发布原因应被拒绝")
|
||||
}
|
||||
if err := (&ImSecurityPolicyPublishInp{Id: 1, Reason: "管理员发布"}).Filter(ctx); err != nil {
|
||||
t.Fatalf("合法发布参数不应报错: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImSecurityIPRuleEditFilter(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
valid := &ImSecurityIPRuleEditInp{
|
||||
|
||||
@@ -255,25 +255,6 @@ func normalizeLoginRateLimit(value *gjson.Json) (*gjson.Json, error) {
|
||||
return gjson.New(map[string]any{"enabled": true, "password_max_attempts": maxAttempts, "lock_seconds": lockSeconds, "window_seconds": windowSeconds}), nil
|
||||
}
|
||||
|
||||
type ImSecurityPolicyPublishInp struct {
|
||||
Id int64 `json:"id" v:"required|min:1#策略ID不能为空|策略ID不合法"`
|
||||
Reason string `json:"reason" v:"required#发布原因不能为空"`
|
||||
}
|
||||
|
||||
func (in *ImSecurityPolicyPublishInp) Filter(ctx context.Context) error {
|
||||
in.Reason = strings.TrimSpace(in.Reason)
|
||||
if in.Id <= 0 {
|
||||
return gerror.New("策略ID不合法")
|
||||
}
|
||||
if in.Reason == "" {
|
||||
return gerror.New("发布原因不能为空")
|
||||
}
|
||||
if len([]rune(in.Reason)) > 500 {
|
||||
return gerror.New("发布原因不能超过500个字符")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImSecurityPolicyRestoreInp struct {
|
||||
TenantId int64 `json:"tenantId"`
|
||||
Reason string `json:"reason" v:"required#恢复原因不能为空"`
|
||||
|
||||
@@ -424,7 +424,6 @@ type (
|
||||
List(ctx context.Context, in *sysin.ImSecurityPolicyListInp) (list []*sysin.ImSecurityPolicyModel, totalCount int, err error)
|
||||
View(ctx context.Context, in *sysin.ImSecurityPolicyViewInp) (res *sysin.ImSecurityPolicyModel, err error)
|
||||
Save(ctx context.Context, in *sysin.ImSecurityPolicySaveInp) (id int64, err error)
|
||||
Publish(ctx context.Context, in *sysin.ImSecurityPolicyPublishInp) (err error)
|
||||
RestoreDefault(ctx context.Context, in *sysin.ImSecurityPolicyRestoreInp) (id int64, err error)
|
||||
GetLoginLockoutConfig(ctx context.Context, in *sysin.ImLoginLockoutConfigGetInp) (res *sysin.ImLoginLockoutConfigModel, err error)
|
||||
SaveLoginLockoutConfig(ctx context.Context, in *sysin.ImLoginLockoutConfigSaveInp) (err error)
|
||||
|
||||
Reference in New Issue
Block a user