From c40a74d983f85f0a0ccf49e1028cefb15dc10a83 Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 3 Jun 2026 09:33:37 +0800 Subject: [PATCH 1/2] fix(risk-control): exempt admins from moderation auto-ban --- .../internal/service/content_moderation.go | 5 ++ .../service/content_moderation_test.go | 90 +++++++++++++++++++ 2 files changed, 95 insertions(+) diff --git a/backend/internal/service/content_moderation.go b/backend/internal/service/content_moderation.go index ee1fca41fa..42b909c90f 100644 --- a/backend/internal/service/content_moderation.go +++ b/backend/internal/service/content_moderation.go @@ -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) 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 { user.Status = StatusDisabled if err := s.userRepo.Update(ctx, user); err != nil { diff --git a/backend/internal/service/content_moderation_test.go b/backend/internal/service/content_moderation_test.go index 6c6fef4447..9cfdc1e4b8 100644 --- a/backend/internal/service/content_moderation_test.go +++ b/backend/internal/service/content_moderation_test.go @@ -1,9 +1,11 @@ package service import ( + "bytes" "context" "encoding/json" "fmt" + "log/slog" "net/http" "net/http/httptest" "strings" @@ -1484,6 +1486,94 @@ func TestContentModerationCheck_HashBlockLogsDoNotIncreaseNextViolationCount(t * 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) { requestCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From 134687782ce24866f585818f46df35f0aafb5f1c Mon Sep 17 00:00:00 2001 From: wucm667 Date: Wed, 3 Jun 2026 09:48:46 +0800 Subject: [PATCH 2/2] build(go): bump toolchain to 1.26.4 --- .github/workflows/backend-ci.yml | 4 ++-- .github/workflows/release.yml | 2 +- .github/workflows/security-scan.yml | 2 +- Dockerfile | 2 +- backend/Dockerfile | 2 +- backend/go.mod | 2 +- deploy/Dockerfile | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/backend-ci.yml b/.github/workflows/backend-ci.yml index 15ff97fe00..fb4d0ce652 100644 --- a/.github/workflows/backend-ci.yml +++ b/.github/workflows/backend-ci.yml @@ -20,7 +20,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.3' + go version | grep -q 'go1.26.4' - name: Unit tests working-directory: backend run: make test-unit @@ -60,7 +60,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.3' + go version | grep -q 'go1.26.4' - name: golangci-lint uses: golangci/golangci-lint-action@v9 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 80bc9850da..7d48131aa6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -115,7 +115,7 @@ jobs: - name: Verify Go version run: | - go version | grep -q 'go1.26.3' + go version | grep -q 'go1.26.4' # Docker setup for GoReleaser - name: Set up QEMU diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml index ef8e59e54a..e102b5f86e 100644 --- a/.github/workflows/security-scan.yml +++ b/.github/workflows/security-scan.yml @@ -23,7 +23,7 @@ jobs: cache-dependency-path: backend/go.sum - name: Verify Go version run: | - go version | grep -q 'go1.26.3' + go version | grep -q 'go1.26.4' - name: Run govulncheck working-directory: backend run: | diff --git a/Dockerfile b/Dockerfile index d556008b79..f9a03a2bf3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,7 +7,7 @@ # ============================================================================= 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 POSTGRES_IMAGE=postgres:18-alpine ARG GOPROXY=https://goproxy.cn,direct diff --git a/backend/Dockerfile b/backend/Dockerfile index f153d68667..26b1dc3337 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,4 +1,4 @@ -FROM golang:1.26.3-alpine +FROM golang:1.26.4-alpine WORKDIR /app diff --git a/backend/go.mod b/backend/go.mod index 587d53701f..62be56c86e 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -1,6 +1,6 @@ module github.com/Wei-Shaw/sub2api -go 1.26.3 +go 1.26.4 require ( entgo.io/ent v0.14.5 diff --git a/deploy/Dockerfile b/deploy/Dockerfile index a947158f19..d39dd17d5c 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -7,7 +7,7 @@ # ============================================================================= 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 GOPROXY=https://goproxy.cn,direct ARG GOSUMDB=sum.golang.google.cn