feat: enforce tenant content moderation

This commit is contained in:
richjohnrambo
2026-09-16 22:21:25 +08:00
parent 0ae4af0c31
commit 0f8e3b82b7
26 changed files with 891 additions and 1 deletions
+23
View File
@@ -61,9 +61,32 @@ var (
registrationSMSCache = newConfigCache[bool]()
thirdPartyConfigCache = newConfigCache[map[string]string]()
appReleaseConfigCache = newConfigCache[*types.AppRelease]()
moderationProviderCache = newConfigCache[*types.ModerationProviderConfig]()
moderationPolicyCache = newConfigCache[*types.ModerationPolicy]()
moderationWordsCache = newConfigCache[*types.SensitiveWordSet]()
thirdPartyConfigAllKeys = "*"
)
func InvalidateModerationConfigCache(tenantCode string, provider, policy, words bool) {
if provider {
moderationProviderCache.clear()
}
if policy {
if strings.TrimSpace(tenantCode) == "" {
moderationPolicyCache.clear()
} else {
moderationPolicyCache.delete(strings.TrimSpace(tenantCode))
}
}
if words {
if strings.TrimSpace(tenantCode) == "" {
moderationWordsCache.clear()
} else {
moderationWordsCache.delete(strings.TrimSpace(tenantCode))
}
}
}
func cloneTenant(t *types.Tenant) *types.Tenant {
if t == nil {
return nil
+9
View File
@@ -12,6 +12,15 @@ type SecurityAuditPersistenceInterface interface {
RecordLoginFailure(types.TenantID, *types.LoginFailureEvent) error
CloseLogin(types.TenantID, string, time.Time) error
ApplyMessageAction(types.TenantID, *types.MessageAdminAction) error
RecordContentModeration(types.TenantID, *types.ContentModerationLog) error
}
func (securityAuditMapper) RecordContentModeration(tenantID types.TenantID, event *types.ContentModerationLog) error {
audit, err := tenantSecurityAuditAdapter(tenantID)
if err != nil {
return err
}
return audit.TenantContentModerationLogCreate(tenantID, event)
}
type securityAuditMapper struct{}
+57
View File
@@ -170,6 +170,9 @@ type AdminInternalConfigProvider interface {
RegistrationSMSEnabled() (bool, error)
ThirdPartyConfig(group string, keys ...string) (map[string]string, error)
LatestAppRelease(platform, channel string) (*types.AppRelease, error)
ModerationProvider() (*types.ModerationProviderConfig, error)
ModerationPolicy(tenantCode string) (*types.ModerationPolicy, error)
SensitiveWords(tenantCode string) (*types.SensitiveWordSet, error)
}
var adminInternalConfigProvider AdminInternalConfigProvider
@@ -178,6 +181,60 @@ func SetAdminInternalConfigProvider(provider AdminInternalConfigProvider) {
adminInternalConfigProvider = provider
}
type ModerationConfigPersistenceInterface interface {
Provider() (*types.ModerationProviderConfig, error)
Policy(tenantCode string) (*types.ModerationPolicy, error)
Words(tenantCode string) (*types.SensitiveWordSet, error)
}
type moderationConfigMapper struct{}
var ModerationConfig ModerationConfigPersistenceInterface = moderationConfigMapper{}
func (moderationConfigMapper) Provider() (*types.ModerationProviderConfig, error) {
if value, ok := moderationProviderCache.get("provider"); ok {
return value, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
value, err := adminInternalConfigProvider.ModerationProvider()
if err == nil && value != nil {
moderationProviderCache.set("provider", value)
}
return value, err
}
func (moderationConfigMapper) Policy(tenantCode string) (*types.ModerationPolicy, error) {
tenantCode = strings.TrimSpace(tenantCode)
if value, ok := moderationPolicyCache.get(tenantCode); ok {
return value, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
value, err := adminInternalConfigProvider.ModerationPolicy(tenantCode)
if err == nil && value != nil {
moderationPolicyCache.set(tenantCode, value)
}
return value, err
}
func (moderationConfigMapper) Words(tenantCode string) (*types.SensitiveWordSet, error) {
tenantCode = strings.TrimSpace(tenantCode)
if value, ok := moderationWordsCache.get(tenantCode); ok {
return value, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
value, err := adminInternalConfigProvider.SensitiveWords(tenantCode)
if err == nil && value != nil {
moderationWordsCache.set(tenantCode, value)
}
return value, err
}
// RegistrationConfigPersistenceInterface exposes global registration policy.
type RegistrationConfigPersistenceInterface interface {
SMSEnabled() (bool, error)
+17
View File
@@ -57,3 +57,20 @@ type MessageAdminAction struct {
RequestID string
ProcessedAt time.Time
}
type ContentModerationLog struct {
RequestID string
Scene string
Topic string
SeqID *int64
ActorID Uid
Provider string
ProviderRequestID string
RiskLevel string
RiskLabels []string
Action string
ContentHash string
LatencyMS int64
ErrorCode string
CreatedAt time.Time
}
+45
View File
@@ -0,0 +1,45 @@
package types
type ModerationProviderConfig struct {
Provider string `json:"provider"`
Endpoint string `json:"endpoint"`
Enabled bool `json:"enabled"`
RegionID string `json:"region_id"`
NicknameService string `json:"nickname_service"`
ChatService string `json:"chat_service"`
TimeoutMS int `json:"timeout_ms"`
FailureMode string `json:"failure_mode"`
AccessKeyID string `json:"access_key_id"`
AccessKeySecret string `json:"access_key_secret"`
}
type ModerationPolicy struct {
TenantID TenantID `json:"tenant_id"`
TenantCode string `json:"tenant_code"`
PolicyVersion uint `json:"policy_version"`
Enabled bool `json:"enabled"`
AliyunEnabled bool `json:"aliyun_enabled"`
LocalWordsEnabled bool `json:"local_words_enabled"`
Scenes map[string]bool `json:"scenes"`
RiskActions map[string]string `json:"risk_actions"`
FailureMode string `json:"failure_mode"`
BlockMessage string `json:"block_message"`
}
type SensitiveWordRule struct {
ID int64 `json:"id"`
Word string `json:"word"`
MatchType string `json:"match_type"`
Scenes []string `json:"scenes"`
Action string `json:"action"`
Replacement string `json:"replacement"`
RiskLevel string `json:"risk_level"`
Priority int `json:"priority"`
}
type SensitiveWordSet struct {
TenantID TenantID `json:"tenant_id"`
TenantCode string `json:"tenant_code"`
Version int64 `json:"version"`
Items []*SensitiveWordRule `json:"items"`
}