mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 11:33:18 +08:00
fix(audit): scope cyber policy events
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user