Merge pull request #2988 from wucm667: content audit auto-ban exempts admin accounts
fix(risk-control): 内容审计 auto-ban 豁免管理员账号,避免封禁管理员/超管
This commit is contained in:
commit
05f0326e99
@ -7,7 +7,7 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
ARG NODE_IMAGE=node:24-alpine
|
ARG NODE_IMAGE=node:24-alpine
|
||||||
ARG GOLANG_IMAGE=golang:1.26.3-alpine
|
ARG GOLANG_IMAGE=golang:1.26.4-alpine
|
||||||
ARG ALPINE_IMAGE=alpine:3.21
|
ARG ALPINE_IMAGE=alpine:3.21
|
||||||
ARG POSTGRES_IMAGE=postgres:18-alpine
|
ARG POSTGRES_IMAGE=postgres:18-alpine
|
||||||
ARG GOPROXY=https://goproxy.cn,direct
|
ARG GOPROXY=https://goproxy.cn,direct
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.26.3-alpine
|
FROM golang:1.26.4-alpine
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
|
|||||||
@ -1656,6 +1656,11 @@ func (s *ContentModerationService) applyFlaggedAccountSideEffects(ctx context.Co
|
|||||||
slog.Warn("content_moderation.ban_get_user_failed", "user_id", *log.UserID, "error", err)
|
slog.Warn("content_moderation.ban_get_user_failed", "user_id", *log.UserID, "error", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
if user.IsAdmin() {
|
||||||
|
slog.Warn("content_moderation.autoban_skipped_admin", "user_id", *log.UserID, "role", user.Role, "count", count, "threshold", cfg.BanThreshold)
|
||||||
|
// TODO: Disable the triggering API key instead when API key mutation is available here.
|
||||||
|
return false
|
||||||
|
}
|
||||||
if user.Status != StatusDisabled {
|
if user.Status != StatusDisabled {
|
||||||
user.Status = StatusDisabled
|
user.Status = StatusDisabled
|
||||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"strings"
|
"strings"
|
||||||
@ -1484,6 +1486,94 @@ func TestContentModerationCheck_HashBlockLogsDoNotIncreaseNextViolationCount(t *
|
|||||||
require.Equal(t, 1, logs[1].ViolationCount)
|
require.Equal(t, 1, logs[1].ViolationCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestContentModerationAutoBanSkipsAdminAccount(t *testing.T) {
|
||||||
|
var slogOutput bytes.Buffer
|
||||||
|
previousLogger := slog.Default()
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(&slogOutput, nil)))
|
||||||
|
t.Cleanup(func() {
|
||||||
|
slog.SetDefault(previousLogger)
|
||||||
|
})
|
||||||
|
|
||||||
|
cfg := defaultContentModerationConfig()
|
||||||
|
cfg.BanThreshold = 2
|
||||||
|
cfg.ViolationWindowHours = 24
|
||||||
|
|
||||||
|
userID := int64(1001)
|
||||||
|
repo := &contentModerationTestRepo{}
|
||||||
|
require.NoError(t, repo.CreateLog(context.Background(), newContentModerationFlaggedLog(userID)))
|
||||||
|
userRepo := &contentModerationTestUserRepo{user: &User{ID: userID, Role: RoleAdmin, Status: StatusActive}}
|
||||||
|
invalidator := &contentModerationTestAuthCacheInvalidator{}
|
||||||
|
svc := NewContentModerationService(nil, repo, nil, nil, userRepo, invalidator, nil)
|
||||||
|
|
||||||
|
svc.persistContentModerationLog(context.Background(), cfg, newContentModerationFlaggedLog(userID), "", false, true)
|
||||||
|
|
||||||
|
logs := requireContentModerationLogCount(t, repo, 2)
|
||||||
|
require.Equal(t, 2, logs[1].ViolationCount)
|
||||||
|
require.False(t, logs[1].AutoBanned)
|
||||||
|
require.Equal(t, StatusActive, userRepo.user.Status)
|
||||||
|
require.Empty(t, userRepo.updated)
|
||||||
|
require.Empty(t, invalidator.userIDs)
|
||||||
|
require.Contains(t, slogOutput.String(), "content_moderation.autoban_skipped_admin")
|
||||||
|
require.Contains(t, slogOutput.String(), "user_id=1001")
|
||||||
|
require.Contains(t, slogOutput.String(), "role=admin")
|
||||||
|
require.Contains(t, slogOutput.String(), "count=2")
|
||||||
|
require.Contains(t, slogOutput.String(), "threshold=2")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContentModerationAutoBanDisablesRegularUserAtThreshold(t *testing.T) {
|
||||||
|
cfg := defaultContentModerationConfig()
|
||||||
|
cfg.BanThreshold = 2
|
||||||
|
cfg.ViolationWindowHours = 24
|
||||||
|
|
||||||
|
userID := int64(1001)
|
||||||
|
repo := &contentModerationTestRepo{}
|
||||||
|
require.NoError(t, repo.CreateLog(context.Background(), newContentModerationFlaggedLog(userID)))
|
||||||
|
userRepo := &contentModerationTestUserRepo{user: &User{ID: userID, Role: RoleUser, Status: StatusActive}}
|
||||||
|
invalidator := &contentModerationTestAuthCacheInvalidator{}
|
||||||
|
svc := NewContentModerationService(nil, repo, nil, nil, userRepo, invalidator, nil)
|
||||||
|
|
||||||
|
svc.persistContentModerationLog(context.Background(), cfg, newContentModerationFlaggedLog(userID), "", false, true)
|
||||||
|
|
||||||
|
logs := requireContentModerationLogCount(t, repo, 2)
|
||||||
|
require.Equal(t, 2, logs[1].ViolationCount)
|
||||||
|
require.True(t, logs[1].AutoBanned)
|
||||||
|
require.Len(t, userRepo.updated, 1)
|
||||||
|
require.Equal(t, StatusDisabled, userRepo.user.Status)
|
||||||
|
require.Equal(t, []int64{userID}, invalidator.userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestContentModerationAdminBelowBanThresholdRecordsViolationOnly(t *testing.T) {
|
||||||
|
cfg := defaultContentModerationConfig()
|
||||||
|
cfg.BanThreshold = 2
|
||||||
|
cfg.ViolationWindowHours = 24
|
||||||
|
|
||||||
|
userID := int64(1001)
|
||||||
|
repo := &contentModerationTestRepo{}
|
||||||
|
userRepo := &contentModerationTestUserRepo{user: &User{ID: userID, Role: RoleAdmin, Status: StatusActive}}
|
||||||
|
invalidator := &contentModerationTestAuthCacheInvalidator{}
|
||||||
|
svc := NewContentModerationService(nil, repo, nil, nil, userRepo, invalidator, nil)
|
||||||
|
|
||||||
|
svc.persistContentModerationLog(context.Background(), cfg, newContentModerationFlaggedLog(userID), "", false, true)
|
||||||
|
|
||||||
|
logs := requireContentModerationLogCount(t, repo, 1)
|
||||||
|
require.Equal(t, 1, logs[0].ViolationCount)
|
||||||
|
require.False(t, logs[0].AutoBanned)
|
||||||
|
require.Equal(t, StatusActive, userRepo.user.Status)
|
||||||
|
require.Empty(t, userRepo.updated)
|
||||||
|
require.Empty(t, invalidator.userIDs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newContentModerationFlaggedLog(userID int64) *ContentModerationLog {
|
||||||
|
return &ContentModerationLog{
|
||||||
|
UserID: &userID,
|
||||||
|
Action: ContentModerationActionBlock,
|
||||||
|
Flagged: true,
|
||||||
|
HighestCategory: "sexual",
|
||||||
|
HighestScore: 0.9,
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestContentModerationCheck_PreBlockFlaggedWritesRedisHashCache(t *testing.T) {
|
func TestContentModerationCheck_PreBlockFlaggedWritesRedisHashCache(t *testing.T) {
|
||||||
requestCount := 0
|
requestCount := 0
|
||||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
ARG NODE_IMAGE=node:24-alpine
|
ARG NODE_IMAGE=node:24-alpine
|
||||||
ARG GOLANG_IMAGE=golang:1.26.3-alpine
|
ARG GOLANG_IMAGE=golang:1.26.4-alpine
|
||||||
ARG ALPINE_IMAGE=alpine:3.20
|
ARG ALPINE_IMAGE=alpine:3.20
|
||||||
ARG GOPROXY=https://goproxy.cn,direct
|
ARG GOPROXY=https://goproxy.cn,direct
|
||||||
ARG GOSUMDB=sum.golang.google.cn
|
ARG GOSUMDB=sum.golang.google.cn
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user