Merge pull request #5514 from wucm667/fix/issue-5510-cyber-policy-audit-scope

fix(audit): scope cyber policy events
This commit is contained in:
Wesley Liddick
2026-08-12 09:58:32 +08:00
committed by GitHub
2 changed files with 142 additions and 6 deletions
+11 -6
View File
@@ -2987,18 +2987,23 @@ type CyberPolicyRecordInput struct {
// RecordCyberPolicyEvent 把一次 cyber_policy 硬阻断写入风控中心日志、计入违规计数、
// 并给用户发邮件。当前请求已由 gateway 透传给用户;本方法仅做事后记录/通知/计数。
// 仅受 risk_control_enabled 总开关约束(不受内容审核 Enabled/Mode/scope/sample 约束)。
// 受 risk_control_enabled 总开关和内容审核 group/model scope 约束,
// 不受内容审核 Enabled/Mode/sample 约束。
func (s *ContentModerationService) RecordCyberPolicyEvent(ctx context.Context, in CyberPolicyRecordInput) {
if s == nil || s.repo == nil {
return
}
if !s.isRiskControlEnabled(ctx) {
runtimeSnapshot, err := s.loadRuntimeSnapshot(ctx)
if err != nil {
slog.Warn("content_moderation.cyber_runtime_snapshot_load_failed", "error", err)
return
}
cfg, err := s.loadConfig(ctx)
if err != nil {
slog.Warn("content_moderation.cyber_load_config_failed", "error", err)
cfg = &ContentModerationConfig{}
if !runtimeSnapshot.riskControlEnabled {
return
}
cfg := runtimeSnapshot.config
if !cfg.includesGroup(in.GroupID) || !cfg.includesModel(in.Model) {
return
}
var userID *int64
if in.UserID > 0 {
@@ -2,6 +2,7 @@ package service
import (
"context"
"errors"
"strings"
"sync"
"testing"
@@ -156,6 +157,136 @@ func TestRecordCyberPolicyEvent_WritesLogWhenEnabled(t *testing.T) {
"Error should mention flagged or cyber_policy")
}
func TestRecordCyberPolicyEvent_RespectsContentModerationScope(t *testing.T) {
groupID := int64(7)
tests := []struct {
name string
config string
groupID *int64
model string
wantCalls []bool
wantLogs int
wantBanned bool
}{
{
name: "excluded group",
config: `{"all_groups":false,"group_ids":[8],"ban_threshold":1}`,
groupID: &groupID,
model: "gpt-5",
wantLogs: 0,
},
{
name: "ungrouped excluded by selected groups",
config: `{"all_groups":false,"group_ids":[7],"ban_threshold":1}`,
groupID: nil,
model: "gpt-5",
wantLogs: 0,
},
{
name: "excluded model",
config: `{"all_groups":true,"model_filter":{"type":"include","models":["gpt-4o"]},"ban_threshold":1}`,
groupID: &groupID,
model: "gpt-5",
wantLogs: 0,
},
{
name: "included group and model",
config: `{"enabled":false,"mode":"off","sample_rate":0,"all_groups":false,"group_ids":[7],"model_filter":{"type":"include","models":["gpt-5"]},"ban_threshold":1}`,
groupID: &groupID,
model: "gpt-5",
wantCalls: []bool{false},
wantLogs: 1,
wantBanned: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
repo := &banCountArgsTestRepo{}
userRepo := &contentModerationTestUserRepo{user: &User{ID: 1, Role: RoleUser, Status: StatusActive}}
svc := NewContentModerationService(
&contentModerationTestSettingRepo{values: map[string]string{
SettingKeyRiskControlEnabled: "true",
SettingKeyContentModerationConfig: tt.config,
}},
repo, nil, nil, userRepo, nil, nil, nil,
)
svc.RecordCyberPolicyEvent(context.Background(), CyberPolicyRecordInput{
UserID: 1,
GroupID: tt.groupID,
Model: tt.model,
})
if tt.wantCalls == nil {
require.Empty(t, repo.snapshotCountCalls())
} else {
require.Equal(t, tt.wantCalls, repo.snapshotCountCalls())
}
require.Len(t, repo.snapshotLogs(), tt.wantLogs)
require.Equal(t, tt.wantBanned, userRepo.user.Status == StatusDisabled)
if tt.wantBanned {
require.Len(t, userRepo.updated, 1)
} else {
require.Empty(t, userRepo.updated)
}
})
}
}
func TestRecordCyberPolicyEvent_InitialRuntimeSnapshotLoadFailureSkipsEvent(t *testing.T) {
repo := &banCountArgsTestRepo{}
settingRepo := &contentModerationRuntimeSettingRepo{values: map[string]string{
SettingKeyRiskControlEnabled: "true",
SettingKeyContentModerationConfig: `{invalid`,
}}
svc := NewContentModerationService(settingRepo, repo, nil, nil, nil, nil, nil, nil)
svc.RecordCyberPolicyEvent(context.Background(), CyberPolicyRecordInput{
UserID: 1,
Model: "gpt-5",
})
require.Empty(t, repo.snapshotCountCalls())
require.Empty(t, repo.snapshotLogs())
getValue, getMultiple := settingRepo.calls()
require.Zero(t, getValue)
require.GreaterOrEqual(t, getMultiple, 1)
}
func TestRecordCyberPolicyEvent_RuntimeSnapshotRefreshFailureKeepsStaleScope(t *testing.T) {
repo := &banCountArgsTestRepo{}
settingRepo := &contentModerationRuntimeSettingRepo{values: map[string]string{
SettingKeyRiskControlEnabled: "true",
SettingKeyContentModerationConfig: `{"all_groups":true,"model_filter":{"type":"include","models":["gpt-5"]}}`,
}}
svc := NewContentModerationService(settingRepo, repo, nil, nil, nil, nil, nil, nil)
svc.runtimeCacheTTL = time.Minute
_, err := svc.loadRuntimeSnapshot(context.Background())
require.NoError(t, err)
current := svc.runtimeSnapshot.Load()
require.NotNil(t, current)
expired := *current
expired.loadedAt = time.Now().Add(-2 * time.Minute)
svc.runtimeSnapshot.Store(&expired)
settingRepo.failMultiple(errors.New("database unavailable"))
svc.RecordCyberPolicyEvent(context.Background(), CyberPolicyRecordInput{
UserID: 1,
Model: "gpt-5",
})
require.Len(t, repo.snapshotLogs(), 1)
require.Eventually(t, func() bool {
_, calls := settingRepo.calls()
return calls == 2
}, time.Second, time.Millisecond)
getValue, getMultiple := settingRepo.calls()
require.Zero(t, getValue)
require.Equal(t, 2, getMultiple)
}
// TestRecordCyberPolicyEvent_CreateLogBeforeEmail verifies F7: the moderation
// log is persisted BEFORE email delivery, and EmailSent is patched afterwards —
// SMTP hangs can no longer swallow the audit record.