This commit is contained in:
Austin
2026-09-18 10:56:00 +08:00
14 changed files with 531 additions and 42 deletions
+15
View File
@@ -196,6 +196,21 @@ func (c *adminInternalClient) SensitiveWords(tenantCode string) (*types.Sensitiv
return &result, nil
}
func (c *adminInternalClient) SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error) {
var result types.SecurityPolicy
path := "/internal/im-config/tenants/" + url.PathEscape(strings.TrimSpace(tenantCode)) + "/security-policy"
if err := c.getJSON(path, nil, &result); err != nil {
return nil, err
}
if result.FriendDisabledPolicy == nil {
result.FriendDisabledPolicy = map[string]any{}
}
if result.CurfewRules == nil {
result.CurfewRules = []*types.MessageCurfewRule{}
}
return &result, nil
}
func (c *adminInternalClient) TenantLoginIPWhitelist(id types.TenantID) (*tenantLoginIPWhitelistConfig, error) {
if !id.IsValid() {
return nil, types.ErrMalformed
+3
View File
@@ -864,6 +864,9 @@ func createIMUserProductTables(tx *sql.Tx) error {
region VARCHAR(128) NOT NULL DEFAULT '',
email VARCHAR(128) NOT NULL DEFAULT '',
public_account VARCHAR(64) NOT NULL DEFAULT '',
friend_add_enabled TINYINT NOT NULL DEFAULT 1,
friend_add_disabled_reason VARCHAR(255) NOT NULL DEFAULT '',
friend_add_policy_version INT NOT NULL DEFAULT 1,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
+8 -2
View File
@@ -3192,7 +3192,7 @@ func (a *adapter) ensureUserProfile(ctx context.Context, uid t.Uid) error {
if err != nil {
return err
}
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,public_account) VALUES(?,?,?)`, profileID, decodeUID(uid), uid.UserId()); err != nil {
if _, err := a.db.ExecContext(ctx, `INSERT IGNORE INTO im_user_profiles(id,user_id,public_account,friend_add_enabled,friend_add_policy_version) VALUES(?,?,?,?,?)`, profileID, decodeUID(uid), uid.UserId(), true, 1); err != nil {
return err
}
return nil
@@ -3894,12 +3894,15 @@ func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfi
Region string
Email string
PublicAccount string `db:"public_account"`
FriendAddEnabled bool `db:"friend_add_enabled"`
FriendAddDisabledReason string `db:"friend_add_disabled_reason"`
FriendAddPolicyVersion int `db:"friend_add_policy_version"`
Version int
CreatedAt time.Time `db:"created_at"`
UpdatedAt time.Time `db:"updated_at"`
}{}
if err := sqlx.GetContext(ctx, a.db, &row, `SELECT user_id,avatar_file_id,nickname,birth_date,signature,gender,region,email,
public_account,version,created_at,updated_at FROM im_user_profiles WHERE user_id=?`, decodeUID(uid)); err != nil {
public_account,friend_add_enabled,friend_add_disabled_reason,friend_add_policy_version,version,created_at,updated_at FROM im_user_profiles WHERE user_id=?`, decodeUID(uid)); err != nil {
return nil, err
}
profile := &t.UserProfile{
@@ -3911,6 +3914,9 @@ func (a *adapter) userProfileByUID(ctx context.Context, uid t.Uid) (*t.UserProfi
Region: row.Region,
Email: row.Email,
PublicAccount: row.PublicAccount,
FriendAddEnabled: row.FriendAddEnabled,
FriendAddDisabledReason: row.FriendAddDisabledReason,
FriendAddPolicyVersion: row.FriendAddPolicyVersion,
Version: row.Version,
CreatedAt: row.CreatedAt,
UpdatedAt: row.UpdatedAt,
+26 -1
View File
@@ -65,7 +65,11 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',nickname VARCHAR(64) NOT NULL DEFAULT '',
birth_date DATE,signature VARCHAR(256) NOT NULL DEFAULT '',gender VARCHAR(16) NOT NULL DEFAULT '',
region VARCHAR(128) NOT NULL DEFAULT '',email VARCHAR(128) NOT NULL DEFAULT '',
public_account VARCHAR(64) NOT NULL DEFAULT '',version INT NOT NULL DEFAULT 1,
public_account VARCHAR(64) NOT NULL DEFAULT '',
friend_add_enabled TINYINT NOT NULL DEFAULT 1,
friend_add_disabled_reason VARCHAR(255) NOT NULL DEFAULT '',
friend_add_policy_version INT NOT NULL DEFAULT 1,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),UNIQUE INDEX uk_im_user_profiles_user(`+profileUniqueKey+`))`); err != nil {
@@ -99,6 +103,27 @@ func ensureUserIdentitySchema(db *sqlx.DB) error {
return err
}
}
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "friend_add_enabled"); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_profiles ADD COLUMN friend_add_enabled TINYINT NOT NULL DEFAULT 1 AFTER public_account"); err != nil {
return err
}
}
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "friend_add_disabled_reason"); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_profiles ADD COLUMN friend_add_disabled_reason VARCHAR(255) NOT NULL DEFAULT '' AFTER friend_add_enabled"); err != nil {
return err
}
}
if exists, err := identityColumnExists(ctx, conn, "im_user_profiles", "friend_add_policy_version"); err != nil {
return err
} else if !exists {
if _, err = conn.ExecContext(ctx, "ALTER TABLE im_user_profiles ADD COLUMN friend_add_policy_version INT NOT NULL DEFAULT 1 AFTER friend_add_disabled_reason"); err != nil {
return err
}
}
}
if err = backfillUserPhones(ctx, conn, usersScoped); err != nil {
+47 -1
View File
@@ -3635,6 +3635,10 @@ func serveIMFriendRequestCreate(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "invalid friend request source", nil)
return
}
if decision := imFriendAddPolicyDecision(principal.TenantID, "", principal.Uid, source); !decision.Allowed {
writeIMPolicyDecision(wrt, req, decision)
return
}
privacy, err := store.Profiles.GetPrivacy(principal.TenantID, target)
if err != nil {
writeIMRelationError(wrt, req, err)
@@ -4048,10 +4052,14 @@ func serveIMGroupMembers(wrt http.ResponseWriter, req *http.Request, topic strin
return
}
remarks := groupMemberRemarkMap(principal.TenantID, principal.Uid, members)
hideFriendEntrypoint := !imFriendGlobalDisabledDecision(principal.TenantID, "", "group").Allowed
items := make([]map[string]any, 0, limit)
for _, member := range members {
access := groupMemberAccess(member)
item := groupBusinessMemberPayload(member, remarks[member.UserID])
if hideFriendEntrypoint && member.UserID != principal.Uid {
maskIMGroupMemberProfileEntrypoint(item)
}
if eligibleRole != "" {
eligible, reason := groupRoleEligibility(selfAccess, access, eligibleRole)
item["eligible"] = eligible
@@ -5467,6 +5475,12 @@ func serveIMGroupMemberProfile(wrt http.ResponseWriter, req *http.Request, topic
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "user_id"})
return
}
if memberID != principal.Uid {
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "group"); !decision.Allowed {
writeIMPolicyDecision(wrt, req, decision)
return
}
}
selfSub, err := store.Subs.Get(principal.TenantID, topic, principal.Uid, false)
if err != nil {
writeIMError(wrt, req, http.StatusInternalServerError, 90000, imReasonInternal, "internal error", nil)
@@ -5519,6 +5533,10 @@ func serveIMMeQRCodes(wrt http.ResponseWriter, req *http.Request) {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", map[string]any{"field": "purpose"})
return
}
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "qr"); !decision.Allowed {
writeIMPolicyDecision(wrt, req, decision)
return
}
expiresIn := body.ExpiresInSeconds
if expiresIn <= 0 {
expiresIn = imQRCodeDefaultExpiresInSeconds
@@ -5624,6 +5642,10 @@ func serveIMQRCodesResolve(wrt http.ResponseWriter, req *http.Request) {
writeIMRelationError(wrt, req, err)
return
}
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "qr"); !decision.Allowed {
writeIMPolicyDecision(wrt, req, decision)
return
}
canAdd := ctx.Relationship == types.RelationshipNone && imPrivacyAllowsAdd(privacy, "qr")
action := imRelationshipAction(ctx.Relationship)
if ctx.Relationship == types.RelationshipNone && !canAdd {
@@ -5665,8 +5687,14 @@ func serveIMUserSearch(wrt http.ResponseWriter, req *http.Request) {
return
}
payload := make([]map[string]any, 0, len(items))
friendSearchAllowed := imFriendGlobalDisabledDecision(principal.TenantID, "", "search").Allowed
for _, item := range items {
payload = append(payload, userSearchItemPayload(item))
data := userSearchItemPayload(item)
if !friendSearchAllowed && item.Relationship == types.RelationshipNone {
data["can_add"] = false
data["action"] = "none"
}
payload = append(payload, data)
}
writeIMSuccess(wrt, req, map[string]any{"items": payload, "next_cursor": nextCursor})
}
@@ -5738,6 +5766,12 @@ func serveIMUserProfile(wrt http.ResponseWriter, req *http.Request, identifier s
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "profile is private", nil)
return
}
if ctx.Relationship == types.RelationshipNone {
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "profile"); !decision.Allowed {
writeIMPolicyDecision(wrt, req, decision)
return
}
}
writeIMSuccess(wrt, req, userProfilePayload(user, ctx))
}
@@ -5795,6 +5829,10 @@ func serveIMUserQRCodes(wrt http.ResponseWriter, req *http.Request, identifier s
writeIMError(wrt, req, http.StatusForbidden, 30005, imReasonPermissionDenied, "permission denied", nil)
return
}
if decision := imFriendGlobalDisabledDecision(principal.TenantID, "", "qr"); !decision.Allowed {
writeIMPolicyDecision(wrt, req, decision)
return
}
}
expiresIn := body.ExpiresInSeconds
if expiresIn <= 0 {
@@ -6932,6 +6970,14 @@ func groupBusinessMemberPayload(member types.GroupMember, remark string) map[str
return data
}
func maskIMGroupMemberProfileEntrypoint(data map[string]any) {
for _, key := range []string{"avatar_file_id", "public_account", "signature", "gender", "region"} {
delete(data, key)
}
data["can_add"] = false
data["action"] = "none"
}
func copyUserNickname(data map[string]any, public any) {
var fields map[string]any
if value, ok := public.(map[string]any); ok {
+330
View File
@@ -0,0 +1,330 @@
package main
import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"time"
"github.com/tinode/chat/server/logs"
"github.com/tinode/chat/server/store"
"github.com/tinode/chat/server/store/types"
)
const (
imReasonFriendAddDisabled = "FRIEND_ADD_DISABLED"
imReasonMessageCurfew = "MESSAGE_CURFEW_ACTIVE"
)
type imPolicyDecision struct {
Allowed bool
Reason string
Message string
}
func imAllowDecision() imPolicyDecision {
return imPolicyDecision{Allowed: true}
}
func imDenyDecision(reason, message string) imPolicyDecision {
return imPolicyDecision{Allowed: false, Reason: reason, Message: strings.TrimSpace(message)}
}
func imTenantCodeForPolicy(tenantID types.TenantID) (string, error) {
tenant, err := store.Tenants.GetByID(tenantID)
if err != nil || tenant == nil {
return "", err
}
return strings.TrimSpace(tenant.Code), nil
}
func imSecurityPolicy(tenantID types.TenantID, tenantCode string) (*types.SecurityPolicy, error) {
tenantCode = strings.TrimSpace(tenantCode)
if tenantCode == "" {
code, err := imTenantCodeForPolicy(tenantID)
if err != nil {
return nil, err
}
tenantCode = code
}
if tenantCode == "" {
return nil, types.ErrNotFound
}
policy, err := store.ModerationConfig.SecurityPolicy(tenantCode)
if err != nil {
return nil, err
}
return policy, nil
}
func imFriendAddPolicyDecision(tenantID types.TenantID, tenantCode string, actor types.Uid, source string) imPolicyDecision {
policy, err := imSecurityPolicy(tenantID, tenantCode)
if err != nil {
if !errors.Is(err, types.ErrUnsupported) && !errors.Is(err, types.ErrNotFound) {
logs.Warn.Printf("friend add policy lookup failed: tenant=%d code=%s user=%s err=%v", tenantID, tenantCode, actor.UserId(), err)
}
return imAllowDecision()
}
if policy != nil && !policy.GlobalFriendEnabled {
entry := imNormalizeFriendAddEntry(source)
if imFriendEntryBlocked(policy.FriendDisabledPolicy, entry) {
return imDenyDecision(imReasonFriendAddDisabled, imPolicyString(policy.FriendDisabledPolicy, "message", "当前时段暂不允许添加好友"))
}
}
profile, err := store.Profiles.Get(tenantID, actor)
if err != nil {
logs.Warn.Printf("friend add user policy lookup failed: tenant=%d user=%s err=%v", tenantID, actor.UserId(), err)
return imAllowDecision()
}
if profile != nil && !profile.FriendAddEnabled {
message := strings.TrimSpace(profile.FriendAddDisabledReason)
if message == "" {
message = "该用户暂不允许发起加好友"
}
return imDenyDecision(imReasonFriendAddDisabled, message)
}
return imAllowDecision()
}
func imFriendGlobalDisabledDecision(tenantID types.TenantID, tenantCode, source string) imPolicyDecision {
policy, err := imSecurityPolicy(tenantID, tenantCode)
if err != nil {
if !errors.Is(err, types.ErrUnsupported) && !errors.Is(err, types.ErrNotFound) {
logs.Warn.Printf("friend global policy lookup failed: tenant=%d code=%s source=%s err=%v", tenantID, tenantCode, source, err)
}
return imAllowDecision()
}
if policy != nil && !policy.GlobalFriendEnabled && imFriendEntryBlocked(policy.FriendDisabledPolicy, imNormalizeFriendAddEntry(source)) {
return imDenyDecision(imReasonFriendAddDisabled, imPolicyString(policy.FriendDisabledPolicy, "message", "当前时段暂不允许添加好友"))
}
return imAllowDecision()
}
func imNormalizeFriendAddEntry(source string) string {
switch strings.TrimSpace(source) {
case "qr":
return "qrcode"
case "group":
return "group_member"
case "profile", "profile_card":
return "profile_card"
case "invite", "invite_auto_add":
return "invite_auto_add"
case "recommend", "recommendation":
return "recommendation"
default:
return "search"
}
}
func imFriendEntryBlocked(policy map[string]any, entry string) bool {
if len(policy) == 0 {
return true
}
raw, ok := policy["blocked_entries"]
if !ok {
return true
}
entries := imAnyStringSlice(raw)
if len(entries) == 0 {
return true
}
entry = strings.TrimSpace(entry)
for _, item := range entries {
if strings.TrimSpace(item) == entry {
return true
}
}
return false
}
func imMessageCurfewDecision(tenantID types.TenantID, tenantCode string, actor types.Uid, topic string, cat types.TopicCat) imPolicyDecision {
if cat != types.TopicCatP2P {
return imAllowDecision()
}
policy, err := imSecurityPolicy(tenantID, tenantCode)
if err != nil {
if !errors.Is(err, types.ErrUnsupported) && !errors.Is(err, types.ErrNotFound) {
logs.Warn.Printf("message curfew policy lookup failed: tenant=%d code=%s user=%s topic=%s err=%v", tenantID, tenantCode, actor.UserId(), topic, err)
}
return imAllowDecision()
}
if policy == nil || !policy.MessageCurfewEnabled {
return imAllowDecision()
}
scope := "private_message"
if cat == types.TopicCatGrp {
scope = "group_message"
}
for _, rule := range policy.CurfewRules {
if rule == nil || rule.Status != 1 || !imCurfewRuleScopeMatches(rule.Scope, scope) {
continue
}
if imCurfewWhitelisted(rule, actor, topic, cat) {
continue
}
if imCurfewRuleActive(rule, time.Now()) {
message := strings.TrimSpace(rule.Message)
if message == "" {
message = "当前时段暂不能发送消息"
}
return imDenyDecision(imReasonMessageCurfew, message)
}
}
return imAllowDecision()
}
func imCurfewRuleScopeMatches(ruleScope, scope string) bool {
ruleScope = strings.TrimSpace(ruleScope)
return ruleScope == "" || ruleScope == "all" || ruleScope == scope
}
func imCurfewWhitelisted(rule *types.MessageCurfewRule, actor types.Uid, topic string, cat types.TopicCat) bool {
uidValues := map[string]bool{
actor.UserId(): true,
actor.String(): true,
}
if raw := store.DecodeUid(actor); raw > 0 {
uidValues[strconv.FormatInt(raw, 10)] = true
}
for _, item := range rule.WhitelistUserIds {
if uidValues[strings.TrimSpace(item)] {
return true
}
}
if cat == types.TopicCatGrp {
for _, item := range rule.WhitelistGroupIds {
if strings.TrimSpace(item) == topic {
return true
}
}
}
return false
}
func imCurfewRuleActive(rule *types.MessageCurfewRule, now time.Time) bool {
loc := time.Local
if tz := strings.TrimSpace(rule.Timezone); tz != "" {
if loaded, err := time.LoadLocation(tz); err == nil {
loc = loaded
}
}
localNow := now.In(loc)
if !imCurfewDateMatches(rule, localNow) {
return false
}
start, okStart := imParseClock(rule.StartTime)
end, okEnd := imParseClock(rule.EndTime)
if !okStart || !okEnd {
return false
}
minute := localNow.Hour()*60 + localNow.Minute()
if start == end {
return true
}
if start < end {
return minute >= start && minute < end
}
return minute >= start || minute < end
}
func imCurfewDateMatches(rule *types.MessageCurfewRule, now time.Time) bool {
switch strings.TrimSpace(rule.DateType) {
case "", "daily":
return true
case "weekday":
return now.Weekday() >= time.Monday && now.Weekday() <= time.Friday
case "weekend":
return now.Weekday() == time.Saturday || now.Weekday() == time.Sunday
case "custom":
date := now.Format("2006-01-02")
for _, item := range rule.CustomDates {
if strings.TrimSpace(item) == date {
return true
}
}
return false
default:
if len(rule.Weekdays) == 0 {
return false
}
weekday := int(now.Weekday())
if weekday == 0 {
weekday = 7
}
for _, item := range rule.Weekdays {
if item == weekday {
return true
}
}
return false
}
}
func imParseClock(value string) (int, bool) {
value = strings.TrimSpace(value)
if value == "" {
return 0, false
}
parts := strings.Split(value, ":")
if len(parts) < 2 {
return 0, false
}
hour, errHour := strconv.Atoi(parts[0])
minute, errMinute := strconv.Atoi(parts[1])
if errHour != nil || errMinute != nil || hour < 0 || hour > 23 || minute < 0 || minute > 59 {
return 0, false
}
return hour*60 + minute, true
}
func imPolicyString(policy map[string]any, key, fallback string) string {
if value := strings.TrimSpace(fmt.Sprint(policy[key])); value != "" && value != "<nil>" {
return value
}
return fallback
}
func imAnyStringSlice(raw any) []string {
switch value := raw.(type) {
case []string:
return value
case []any:
out := make([]string, 0, len(value))
for _, item := range value {
if text := strings.TrimSpace(fmt.Sprint(item)); text != "" && text != "<nil>" {
out = append(out, text)
}
}
return out
default:
return nil
}
}
func writeIMPolicyDecision(wrt http.ResponseWriter, req *http.Request, decision imPolicyDecision) {
message := strings.TrimSpace(decision.Message)
if message == "" {
message = "permission denied"
}
reason := strings.TrimSpace(decision.Reason)
if reason == "" {
reason = imReasonPermissionDenied
}
writeIMError(wrt, req, http.StatusForbidden, 30005, reason, message, map[string]any{
"reason": reason,
"message": message,
})
}
func imCurfewPolicyReply(msg *ClientComMessage, decision imPolicyDecision) *ServerComMessage {
reply := ErrPolicyReply(msg, types.TimeNow())
reply.Ctrl.Text = "message curfew active"
reply.Ctrl.Params = map[string]any{
"reason": decision.Reason,
"message": decision.Message,
}
return reply
}
+1 -1
View File
@@ -239,7 +239,7 @@ func internalAdminQuerySpec(resource string) (*internalQuerySpec, bool) {
func usersInternalQuerySpec() *internalQuerySpec {
return &internalQuerySpec{
Select: `SELECT CAST(u.id AS CHAR) AS user_id,u.account_no AS display_no,u.username,u.phone,u.state,u.stateat AS state_at,u.access,u.lastseen AS last_seen,u.useragent AS user_agent,u.public,u.trusted,
p.nickname,p.public_account,p.gender,p.region,p.avatar_file_id,
p.nickname,p.public_account,p.gender,p.region,p.avatar_file_id,COALESCE(p.friend_add_enabled,1) AS friend_add_enabled,
COALESCE(
NULLIF(avatar.location,''),
NULLIF(CASE WHEN p.avatar_file_id REGEXP '^(https?://|data:)' THEN p.avatar_file_id ELSE '' END,''),
+2
View File
@@ -99,6 +99,7 @@ func executeKafkaConfigEvent(envelope kafkaEnvelope) error {
store.InvalidateAppReleaseConfigCache()
case configEventSecurityPolicyUpdated:
store.InvalidateModerationConfigCache(tenantCode, false, true, false)
store.InvalidateSecurityPolicyCache(tenantCode)
case configEventSensitiveWordsUpdated:
store.InvalidateModerationConfigCache(tenantCode, false, false, true)
default:
@@ -107,6 +108,7 @@ func executeKafkaConfigEvent(envelope kafkaEnvelope) error {
store.InvalidateRegistrationConfigCache()
store.InvalidateAppReleaseConfigCache()
store.InvalidateModerationConfigCache(tenantCode, true, true, true)
store.InvalidateSecurityPolicyCache(tenantCode)
}
logs.Info.Printf("kafka config event applied: event=%s type=%s tenant=%s group=%s", envelope.EventID, envelope.EventType, tenantCode, payload.Group)
return nil
+1
View File
@@ -19,6 +19,7 @@ func (c testConfig) Provider() (*types.ModerationProviderConfig, error) {
}
func (c testConfig) Policy(string) (*types.ModerationPolicy, error) { return c.policy, nil }
func (c testConfig) Words(string) (*types.SensitiveWordSet, error) { return c.words, nil }
func (c testConfig) SecurityPolicy(string) (*types.SecurityPolicy, error) { return nil, nil }
type testAudit struct{}
+9
View File
@@ -64,9 +64,18 @@ var (
moderationProviderCache = newConfigCache[*types.ModerationProviderConfig]()
moderationPolicyCache = newConfigCache[*types.ModerationPolicy]()
moderationWordsCache = newConfigCache[*types.SensitiveWordSet]()
securityPolicyCache = newConfigCache[*types.SecurityPolicy]()
thirdPartyConfigAllKeys = "*"
)
func InvalidateSecurityPolicyCache(tenantCode string) {
if strings.TrimSpace(tenantCode) == "" {
securityPolicyCache.clear()
return
}
securityPolicyCache.delete(strings.TrimSpace(tenantCode))
}
func InvalidateModerationConfigCache(tenantCode string, provider, policy, words bool) {
if provider {
moderationProviderCache.clear()
+17
View File
@@ -173,6 +173,7 @@ type AdminInternalConfigProvider interface {
ModerationProvider() (*types.ModerationProviderConfig, error)
ModerationPolicy(tenantCode string) (*types.ModerationPolicy, error)
SensitiveWords(tenantCode string) (*types.SensitiveWordSet, error)
SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error)
}
var adminInternalConfigProvider AdminInternalConfigProvider
@@ -185,6 +186,7 @@ type ModerationConfigPersistenceInterface interface {
Provider() (*types.ModerationProviderConfig, error)
Policy(tenantCode string) (*types.ModerationPolicy, error)
Words(tenantCode string) (*types.SensitiveWordSet, error)
SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error)
}
type moderationConfigMapper struct{}
@@ -235,6 +237,21 @@ func (moderationConfigMapper) Words(tenantCode string) (*types.SensitiveWordSet,
return value, err
}
func (moderationConfigMapper) SecurityPolicy(tenantCode string) (*types.SecurityPolicy, error) {
tenantCode = strings.TrimSpace(tenantCode)
if value, ok := securityPolicyCache.get(tenantCode); ok {
return value, nil
}
if adminInternalConfigProvider == nil {
return nil, types.ErrUnsupported
}
value, err := adminInternalConfigProvider.SecurityPolicy(tenantCode)
if err == nil && value != nil {
securityPolicyCache.set(tenantCode, value)
}
return value, err
}
// RegistrationConfigPersistenceInterface exposes global registration policy.
type RegistrationConfigPersistenceInterface interface {
SMSEnabled() (bool, error)
+3
View File
@@ -12,6 +12,9 @@ type UserProfile struct {
Region string
Email string
PublicAccount string
FriendAddEnabled bool
FriendAddDisabledReason string
FriendAddPolicyVersion int
Version int
CreatedAt time.Time
UpdatedAt time.Time
+28
View File
@@ -0,0 +1,28 @@
package types
type SecurityPolicy struct {
TenantID TenantID `json:"tenant_id"`
TenantCode string `json:"tenant_code"`
PolicyVersion uint `json:"policy_version"`
GlobalFriendEnabled bool `json:"global_friend_enabled"`
FriendDisabledPolicy map[string]any `json:"friend_disabled_policy"`
MessageCurfewEnabled bool `json:"message_curfew_enabled"`
CurfewRules []*MessageCurfewRule `json:"curfew_rules"`
}
type MessageCurfewRule struct {
RuleCode string `json:"rule_code"`
RuleName string `json:"rule_name"`
DateType string `json:"date_type"`
CustomDates []string `json:"custom_dates"`
Weekdays []int `json:"weekdays"`
Timezone string `json:"timezone"`
StartTime string `json:"start_time"`
EndTime string `json:"end_time"`
Scope string `json:"scope"`
WhitelistUserIds []string `json:"whitelist_user_ids"`
WhitelistGroupIds []string `json:"whitelist_group_ids"`
Message string `json:"message"`
Status int `json:"status"`
Sort int `json:"sort"`
}
+4
View File
@@ -1496,6 +1496,10 @@ func (t *Topic) handlePubBroadcast(msg *ClientComMessage) {
msg.Pub.Content = content
}
if msg.sess != nil && msg.sess.authLvl != auth.LevelRoot && !isCall {
if decision := imMessageCurfewDecision(t.tenantID, msg.sess.tenantCode, asUid, t.name, t.cat); !decision.Allowed {
msg.sess.queueOut(imCurfewPolicyReply(msg, decision))
return
}
scene := moderation.ScenePrivateMessage
if t.cat == types.TopicCatGrp {
scene = moderation.SceneGroupMessage