feat(server): 为内部管理查询添加租户隔离支持

实现共库模式下的租户数据隔离,新增 TenantColumn 字段标识查询表的租户列,添加 internalAdminTenantCondition 函数生成租户过滤条件。修改所有内部管理查询的构建逻辑,在列表和详情查询中自动注入租户条件,防止跨租户数据泄漏。同时更新所有资源查询规格,配置对应表的租户列。
This commit is contained in:
Austin
2026-09-23 12:11:26 +08:00
parent a0d0bf574e
commit b8b21d3548
2 changed files with 102 additions and 45 deletions
+71 -37
View File
@@ -25,37 +25,48 @@ type internalQuerySpec struct {
Filters []internalQueryFilter
DetailKey string
ScopeColumn string
// TenantColumn 指定共库模式下的租户隔离列(如 "u.tenant_id")。
// 共库模式下所有租户共用同一物理库,列表与详情查询必须无条件注入 tenant_id 谓词。
TenantColumn string
}
// internalAdminTenantCondition 返回单条详情查询的租户过滤片段与追加参数。
func internalAdminTenantCondition(spec *internalQuerySpec, tenantID types.TenantID) (string, []any) {
if spec == nil || spec.TenantColumn == "" {
return "", nil
}
return " AND " + spec.TenantColumn + "=?", []any{int64(tenantID)}
}
var internalGroupResourceSpecs = map[string]*internalQuerySpec{
"join-requests": {
Select: "SELECT id,request_id,topic,applicant_user_id,inviter_user_id,source,message,status,reject_reason,handled_by,handled_at,expires_at,version,created_at,updated_at",
From: "FROM im_group_join_requests", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic",
From: "FROM im_group_join_requests", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"status", "status", "=", "string"}, {"applicant_user_id", "applicant_user_id", "=", "uid"}},
},
"invitations": {
Select: "SELECT id,invitation_id,topic,inviter_user_id,target_user_id,message,status,result,join_request_id,expires_at,handled_at,created_at,updated_at",
From: "FROM im_group_invitations", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic",
From: "FROM im_group_invitations", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"status", "status", "=", "string"}, {"target_user_id", "target_user_id", "=", "uid"}, {"inviter_user_id", "inviter_user_id", "=", "uid"}},
},
"announcements": {
Select: "SELECT id,announcement_id,topic,author_user_id,content,status,version,published_at,deleted_by,deleted_at,created_at,updated_at",
From: "FROM im_group_announcements", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic",
From: "FROM im_group_announcements", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"status", "status", "=", "string"}},
},
"mutes": {
Select: "SELECT id,topic,user_id,muted_by,muted_at,expires_at,reason,status,revoked_by,revoked_at,version,created_at,updated_at",
From: "FROM im_group_mutes", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic",
From: "FROM im_group_mutes", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"status", "status", "=", "string"}, {"user_id", "user_id", "=", "uid"}},
},
"admin-logs": {
Select: "SELECT id,log_id,topic,operator_id,target_user_id,action,object_type,object_id,before_data,after_data,request_id,idempotency_key,created_at",
From: "FROM im_group_admin_logs", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic",
From: "FROM im_group_admin_logs", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"action", "action", "=", "string"}, {"operator_id", "operator_id", "=", "uid"}},
},
"files": {
Select: "SELECT id,group_file_id,topic,file_id,source_topic,source_seq,uploader_id,file_name,mime_type,file_type,file_size,status,deleted_by,deleted_at,created_at,updated_at",
From: "FROM im_group_files", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic",
From: "FROM im_group_files", DefaultSort: "ORDER BY id DESC", ScopeColumn: "topic", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"status", "status", "=", "string"}, {"file_type", "file_type", "=", "string"}, {"file_name", "file_name", "LIKE", "string"}, {"uploader_id", "uploader_id", "=", "uid"}},
},
}
@@ -65,41 +76,46 @@ var internalTenantResourceSpecs = map[string]*internalQuerySpec{
"group-messages": groupMessagesInternalQuerySpec(),
"user-login-logs": {
Select: `SELECT id,CAST(user_id AS CHAR) AS user_id,username,account_type,account_mask,login_type,event_type,session_id,device_id,platform,app_version,ip,region,user_agent,risk_level,request_id,login_at,logout_at,metadata`,
From: "FROM im_user_login_logs", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
From: "FROM im_user_login_logs", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"user_id", "user_id", "=", "uid"}, {"account", "account_mask", "LIKE", "string"}, {"login_type", "login_type", "=", "string"}, {"event_type", "event_type", "=", "string"}, {"platform", "platform", "=", "string"}, {"ip", "ip", "LIKE", "string"}, {"login_at_start", "login_at", ">=", "time"}, {"login_at_end", "login_at", "<=", "time"}},
},
"login-failure-logs": {
Select: `SELECT id,CAST(user_id AS CHAR) AS user_id,account_type,account_mask,account_hash,failure_type,reason,detail,ip,region,device_id,platform,app_version,user_agent,risk_level,risk_action,request_id,attempt_at,metadata`,
From: "FROM im_login_failure_logs", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"user_id", "user_id", "=", "uid"}, {"account", "account_mask", "LIKE", "string"}, {"failure_type", "failure_type", "=", "string"}, {"platform", "platform", "=", "string"}, {"ip", "ip", "LIKE", "string"}, {"attempt_at_start", "attempt_at", ">=", "time"}, {"attempt_at_end", "attempt_at", "<=", "time"}},
},
"audit-logs": {
Select: `SELECT id,request_id,actor_uid AS actor_user_id,action,target_type,target_id,ip,user_agent,
JSON_OBJECT('actor_role',actor_role,'before_snapshot',before_snapshot,'after_snapshot',after_snapshot,'reason',reason,'metadata',metadata) AS detail,created_at`,
From: "FROM im_audit_logs", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
From: "FROM im_audit_logs", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"actor_user_id", "actor_uid", "=", "uid"}, {"action", "action", "=", "string"}, {"target_type", "target_type", "=", "string"}, {"target_id", "target_id", "=", "string"}, {"created_at_start", "created_at", ">=", "time"}, {"created_at_end", "created_at", "<=", "time"}},
},
"realname-verifications": {
Select: "SELECT id,user_id,submission_id,status,masked_name AS real_name,masked_id_number AS id_card_no_masked,review_reason AS reject_reason,verified_at AS reviewed_at,created_at,updated_at",
From: "FROM im_realname_verifications", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
From: "FROM im_realname_verifications", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"user_id", "user_id", "=", "uid"}, {"status", "status", "=", "string"}, {"real_name", "masked_name", "LIKE", "string"}, {"created_at_start", "created_at", ">=", "time"}, {"created_at_end", "created_at", "<=", "time"}},
},
"abuse-reports": {
Select: "SELECT id,report_id,reporter_id AS reporter_user_id,target_type,target_user_id,target_topic,target_seq,reason_code,description,evidence_file_ids,snapshot,status,created_at,updated_at",
From: "FROM im_abuse_reports", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
From: "FROM im_abuse_reports", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"reporter_user_id", "reporter_id", "=", "uid"}, {"target_type", "target_type", "=", "string"}, {"status", "status", "=", "string"}, {"created_at_start", "created_at", ">=", "time"}, {"created_at_end", "created_at", "<=", "time"}},
},
"feedback-submissions": {
Select: "SELECT id,feedback_id,user_id,category,content,contact,attachment_file_ids AS file_ids,client_context,include_diagnostics,status,created_at,updated_at",
From: "FROM im_feedback_submissions", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
From: "FROM im_feedback_submissions", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"user_id", "user_id", "=", "uid"}, {"category", "category", "=", "string"}, {"status", "status", "=", "string"}, {"created_at_start", "created_at", ">=", "time"}, {"created_at_end", "created_at", "<=", "time"}},
},
"feedback-config": {
Select: "SELECT id,categories,max_length,attachment_policy,allow_contact,diagnostics_notice,created_at,updated_at",
From: "FROM im_feedback_config", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
From: "FROM im_feedback_config", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
}, "abuse-reasons": {
Select: "SELECT id,target_type,reason_code,label,requires_description,allows_evidence,enabled,sort_order,created_at,updated_at",
From: "FROM im_abuse_reasons", DefaultSort: "ORDER BY sort_order ASC,id ASC", DetailKey: "id",
From: "FROM im_abuse_reasons", DefaultSort: "ORDER BY sort_order ASC,id ASC", DetailKey: "id", TenantColumn: "tenant_id",
Filters: []internalQueryFilter{{"target_type", "target_type", "=", "string"}},
},
"app-releases": {
Select: "SELECT id,platform,channel,latest_version,min_supported_version,update_type,release_notes,store_url,package_url,checksum,remind_after_seconds,published_at,created_at,updated_at",
From: "FROM im_app_releases", DefaultSort: "ORDER BY id DESC", DetailKey: "id",
From: "FROM im_app_releases", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id",
}}
var internalOutboxResourceSpecs = map[string]*internalQuerySpec{}
@@ -118,7 +134,7 @@ func serveInternalAdminResourceList(wrt http.ResponseWriter, req *http.Request,
return
}
}
query, countQuery, args, page, perPage, err := buildInternalAdminListQuery(req, spec, scope)
query, countQuery, args, page, perPage, err := buildInternalAdminListQuery(req, spec, scope, tenantID)
if err != nil {
writeIMError(wrt, req, http.StatusBadRequest, 30000, imReasonMalformedRequest, "malformed request", nil)
return
@@ -155,7 +171,7 @@ func queryEncryptedAdminMessages(req *http.Request, tenantID types.TenantID, spe
queryValues.Del("keyword")
urlCopy.RawQuery = queryValues.Encode()
requestCopy.URL = &urlCopy
query, countQuery, args, _, _, err := buildInternalAdminListQuery(requestCopy, spec, scope)
query, countQuery, args, _, _, err := buildInternalAdminListQuery(requestCopy, spec, scope, tenantID)
if err != nil {
return nil, err
}
@@ -230,7 +246,12 @@ func serveInternalAdminResourceDetail(wrt http.ResponseWriter, req *http.Request
return
}
query := spec.Select + " " + spec.From + " WHERE " + spec.DetailKey + "=?"
row, err := store.InternalAdminQueries.QueryOne(tenantID, query, []any{id})
args := []any{id}
if cond, extra := internalAdminTenantCondition(spec, tenantID); cond != "" {
query += cond
args = append(args, extra...)
}
row, err := store.InternalAdminQueries.QueryOne(tenantID, query, args)
if err != nil {
writeInternalQueryError(wrt, req, err)
return
@@ -299,13 +320,13 @@ func internalAdminQuerySpec(resource string) (*internalQuerySpec, bool) {
case "users":
return usersInternalQuerySpec(), true
case "user_devices":
return &internalQuerySpec{Select: "SELECT id,deviceid AS device_id,platform,lastseen AS last_seen,lang", From: "FROM im_devices", DefaultSort: "ORDER BY lastseen DESC", ScopeColumn: "userid", Filters: []internalQueryFilter{{"device_id", "deviceid", "LIKE", "string"}, {"platform", "platform", "=", "string"}}}, true
return &internalQuerySpec{Select: "SELECT id,deviceid AS device_id,platform,lastseen AS last_seen,lang", From: "FROM im_devices", DefaultSort: "ORDER BY lastseen DESC", ScopeColumn: "userid", TenantColumn: "tenant_id", Filters: []internalQueryFilter{{"device_id", "deviceid", "LIKE", "string"}, {"platform", "platform", "=", "string"}}}, true
case "groups":
return groupsInternalQuerySpec(), true
case "file_links":
return &internalQuerySpec{Select: "SELECT id,topic,msgid AS seq_id,userid AS user_id,createdat AS created_at", From: "FROM im_filemsglinks", DefaultSort: "ORDER BY id DESC", ScopeColumn: "fileid"}, true
return &internalQuerySpec{Select: "SELECT id,topic,msgid AS seq_id,userid AS user_id,createdat AS created_at", From: "FROM im_filemsglinks", DefaultSort: "ORDER BY id DESC", ScopeColumn: "fileid", TenantColumn: "tenant_id"}, true
case "files":
return &internalQuerySpec{Select: "SELECT id,id AS file_id,userid AS user_id,mimetype AS mime_type,size AS file_size,location,etag,status,createdat AS created_at,updatedat AS updated_at", From: "FROM im_fileuploads", DefaultSort: "ORDER BY id DESC", DetailKey: "id", Filters: []internalQueryFilter{{"file_id", "id", "=", "int"}, {"user_id", "userid", "=", "uid"}, {"status", "status", "=", "int"}, {"mime_type", "mimetype", "LIKE", "string"}, {"created_at_start", "createdat", ">=", "time"}, {"created_at_end", "createdat", "<=", "time"}}}, true
return &internalQuerySpec{Select: "SELECT id,id AS file_id,userid AS user_id,mimetype AS mime_type,size AS file_size,location,etag,status,createdat AS created_at,updatedat AS updated_at", From: "FROM im_fileuploads", DefaultSort: "ORDER BY id DESC", DetailKey: "id", TenantColumn: "tenant_id", Filters: []internalQueryFilter{{"file_id", "id", "=", "int"}, {"user_id", "userid", "=", "uid"}, {"status", "status", "=", "int"}, {"mime_type", "mimetype", "LIKE", "string"}, {"created_at_start", "createdat", ">=", "time"}, {"created_at_end", "createdat", "<=", "time"}}}, true
}
if strings.HasPrefix(resource, "group_") {
spec := internalGroupResourceSpecs[strings.TrimPrefix(resource, "group_")]
@@ -331,10 +352,11 @@ func usersInternalQuerySpec() *internalQuerySpec {
p.signature,sec.auth_version,sec.password_updated_at,u.createdat AS created_at,u.updatedat AS updated_at,
CASE WHEN EXISTS(SELECT 1 FROM im_registration_invite_uses AS invite_use WHERE invite_use.user_id=u.id) THEN 'invite_code' WHEN COALESCE(u.phone,'')<>'' THEN 'mobile' ELSE 'admin' END AS register_type,
(SELECT JSON_ARRAYAGG(JSON_OBJECT('method',c.method,'value',c.value,'done',c.done,'updated_at',c.updatedat)) FROM im_credentials AS c WHERE c.userid=u.id AND c.deletedat IS NULL) AS credentials`,
From: "FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id LEFT JOIN im_user_security AS sec ON sec.user_id=u.id LEFT JOIN im_fileuploads AS avatar ON avatar.id=CAST(p.avatar_file_id AS UNSIGNED) AND p.avatar_file_id REGEXP '^[0-9]+$'",
DefaultSort: "ORDER BY u.id DESC",
DetailKey: "u.id",
Filters: []internalQueryFilter{{"user_id", "u.id", "=", "uid"}, {"user_ids", "u.id", "IN", "uid_list"}, {"account_no", "u.account_no", "=", "int"}, {"keyword", "CONCAT_WS(' ',u.username,p.nickname,u.phone,CAST(u.account_no AS CHAR))", "LIKE", "string"}, {"nickname", "p.nickname", "LIKE", "string"}, {"auth_name", "u.username", "LIKE", "string"}, {"state", "u.state", "=", "int"}, {"created_at_start", "u.createdat", ">=", "time"}, {"created_at_end", "u.createdat", "<=", "time"}, {"last_seen_start", "u.lastseen", ">=", "time"}, {"last_seen_end", "u.lastseen", "<=", "time"}},
From: "FROM im_users AS u LEFT JOIN im_user_profiles AS p ON p.user_id=u.id LEFT JOIN im_user_security AS sec ON sec.user_id=u.id LEFT JOIN im_fileuploads AS avatar ON avatar.id=CAST(p.avatar_file_id AS UNSIGNED) AND p.avatar_file_id REGEXP '^[0-9]+$'",
DefaultSort: "ORDER BY u.id DESC",
DetailKey: "u.id",
TenantColumn: "u.tenant_id",
Filters: []internalQueryFilter{{"user_id", "u.id", "=", "uid"}, {"user_ids", "u.id", "IN", "uid_list"}, {"account_no", "u.account_no", "=", "int"}, {"keyword", "CONCAT_WS(' ',u.username,p.nickname,u.phone,CAST(u.account_no AS CHAR))", "LIKE", "string"}, {"nickname", "p.nickname", "LIKE", "string"}, {"auth_name", "u.username", "LIKE", "string"}, {"state", "u.state", "=", "int"}, {"created_at_start", "u.createdat", ">=", "time"}, {"created_at_end", "u.createdat", "<=", "time"}, {"last_seen_start", "u.lastseen", ">=", "time"}, {"last_seen_end", "u.lastseen", "<=", "time"}},
}
}
@@ -342,10 +364,11 @@ func groupsInternalQuerySpec() *internalQuerySpec {
return &internalQuerySpec{
Select: `SELECT t.name AS topic,t.state,t.owner AS owner_id,t.seqid AS seq_id,t.delid AS del_id,t.subcnt AS sub_count,t.touchedat AS touched_at,t.createdat AS created_at,t.updatedat AS updated_at,t.public,t.trusted,t.access,t.tags,t.aux,
gs.name AS group_name,gs.avatar_file_id,gs.description,gs.group_status,gs.join_approval,gs.mute_all,gs.member_upload_files,gs.member_private_chat,gs.member_invite,gs.max_member_count,gs.max_admin_count,gs.member_count AS business_member_count,gs.version AS settings_version,gs.updated_at AS settings_updated_at`,
From: "FROM (SELECT * FROM im_topics WHERE name LIKE 'grp%') AS t LEFT JOIN im_group_settings AS gs ON gs.topic=t.name",
DefaultSort: "ORDER BY t.id DESC",
DetailKey: "t.name",
Filters: []internalQueryFilter{{"topic", "t.name", "LIKE", "string"}, {"owner_id", "t.owner", "=", "uid"}, {"state", "t.state", "=", "int"}, {"group_status", "gs.group_status", "=", "string"}, {"created_at_start", "t.createdat", ">=", "time"}, {"created_at_end", "t.createdat", "<=", "time"}, {"touched_at_start", "t.touchedat", ">=", "time"}, {"touched_at_end", "t.touchedat", "<=", "time"}},
From: "FROM (SELECT * FROM im_topics WHERE name LIKE 'grp%') AS t LEFT JOIN im_group_settings AS gs ON gs.topic=t.name",
DefaultSort: "ORDER BY t.id DESC",
DetailKey: "t.name",
TenantColumn: "t.tenant_id",
Filters: []internalQueryFilter{{"topic", "t.name", "LIKE", "string"}, {"owner_id", "t.owner", "=", "uid"}, {"state", "t.state", "=", "int"}, {"group_status", "gs.group_status", "=", "string"}, {"created_at_start", "t.createdat", ">=", "time"}, {"created_at_end", "t.createdat", "<=", "time"}, {"touched_at_start", "t.touchedat", ">=", "time"}, {"touched_at_end", "t.touchedat", "<=", "time"}},
}
}
@@ -367,8 +390,9 @@ func privateMessagesInternalQuerySpec() *internalQuerySpec {
LEFT JOIN im_users AS ru ON ru.id=rs.userid
LEFT JOIN im_user_profiles AS rp ON rp.user_id=rs.userid
LEFT JOIN im_message_admin_actions AS aa ON aa.id=(SELECT MAX(a2.id) FROM im_message_admin_actions AS a2 WHERE a2.conversation_type='private' AND a2.topic=m.topic AND a2.seqid=m.seqid AND a2.action_status='success')`,
DefaultSort: "ORDER BY m.id DESC",
Filters: []internalQueryFilter{{"topic", "m.topic", "LIKE", "string"}, {"from_user_id", "m.`from`", "=", "uid"}, {"to_user_id", "rs.userid", "=", "uid"}, {"content_type", contentType, "=", "string"}, {"status", status, "=", "string"}, {"keyword", "CAST(m.content AS CHAR)", "LIKE", "string"}, {"created_at_start", "m.createdat", ">=", "time"}, {"created_at_end", "m.createdat", "<=", "time"}},
DefaultSort: "ORDER BY m.id DESC",
TenantColumn: "m.tenant_id",
Filters: []internalQueryFilter{{"topic", "m.topic", "LIKE", "string"}, {"from_user_id", "m.`from`", "=", "uid"}, {"to_user_id", "rs.userid", "=", "uid"}, {"content_type", contentType, "=", "string"}, {"status", status, "=", "string"}, {"keyword", "CAST(m.content AS CHAR)", "LIKE", "string"}, {"created_at_start", "m.createdat", ">=", "time"}, {"created_at_end", "m.createdat", "<=", "time"}},
}
}
@@ -388,7 +412,8 @@ func groupMessagesInternalQuerySpec() *internalQuerySpec {
LEFT JOIN im_user_profiles AS p ON p.user_id=m.from_user
LEFT JOIN im_group_settings AS gs ON gs.topic=m.topic COLLATE utf8mb4_0900_ai_ci
LEFT JOIN im_message_admin_actions AS aa ON aa.id=(SELECT MAX(a2.id) FROM im_message_admin_actions AS a2 WHERE a2.conversation_type='group' AND a2.topic=m.topic COLLATE utf8mb4_0900_ai_ci AND a2.seqid=m.seqid AND a2.action_status='success')`,
DefaultSort: "ORDER BY m.id DESC",
DefaultSort: "ORDER BY m.id DESC",
TenantColumn: "m.tenant_id",
Filters: []internalQueryFilter{
{"topic", "m.topic", "LIKE", "string"},
{"seq_id", "m.seqid", "=", "int"},
@@ -417,9 +442,10 @@ func groupMessagesInternalQuerySpecForTenant(tenantID types.TenantID) (*internal
}
func internalGroupMessageSource(tenantID types.TenantID) (string, error) {
shardFilter := "status<>'archived' AND tenant_id=?"
result, err := store.InternalAdminQueries.Query(tenantID,
"SELECT table_name FROM im_group_message_shards WHERE status<>'archived' ORDER BY shard_month DESC",
"SELECT COUNT(*) FROM im_group_message_shards WHERE status<>'archived'", nil, 1, 200)
"SELECT table_name FROM im_group_message_shards WHERE "+shardFilter,
"SELECT COUNT(*) FROM im_group_message_shards WHERE "+shardFilter, []any{int64(tenantID)}, 1, 200)
if err != nil {
return "", err
}
@@ -429,12 +455,15 @@ func internalGroupMessageSource(tenantID types.TenantID) (string, error) {
if !validInternalGroupMessageTable(table) {
continue
}
selects = append(selects, fmt.Sprintf(`SELECT message_id AS id,topic,seqid,from_user,msg_type,status,content,head,reply_seqid,mentioned_user_ids,created_at,updated_at FROM %s`, table))
selects = append(selects, fmt.Sprintf(`SELECT message_id AS id,tenant_id,topic,seqid,from_user,msg_type,status,content,head,reply_seqid,mentioned_user_ids,created_at,updated_at FROM %s`, table))
}
selects = append(selects, `SELECT id,topic COLLATE utf8mb4_0900_ai_ci AS topic,seqid,from_user,
// legacy 行内联租户条件(tenantID 来自租户注册表数值 ID,非用户输入)
selects = append(selects, fmt.Sprintf(`SELECT id,tenant_id,topic COLLATE utf8mb4_0900_ai_ci AS topic,seqid,from_user,
msg_type COLLATE utf8mb4_0900_ai_ci AS msg_type,status COLLATE utf8mb4_0900_ai_ci AS status,
content,head,reply_seqid,mentioned_user_ids,created_at,updated_at
FROM im_group_messages AS legacy`)
FROM im_group_messages AS legacy WHERE legacy.tenant_id=%d AND NOT EXISTS (
SELECT 1 FROM im_group_message_topic_shards AS route
WHERE route.topic=legacy.topic COLLATE utf8mb4_0900_ai_ci AND legacy.seqid BETWEEN route.min_seq AND route.max_seq)`, int64(tenantID)))
return "(" + strings.Join(selects, " UNION ALL ") + ")", nil
}
@@ -498,11 +527,16 @@ func serveInternalDashboardTrends(wrt http.ResponseWriter, req *http.Request, te
writeIMSuccess(wrt, req, map[string]any{"items": normalizeInternalAdminRowsForAPI(result.Items), "total": result.Total, "page": result.Page, "per_page": result.PerPage})
}
func buildInternalAdminListQuery(req *http.Request, spec *internalQuerySpec, scope string) (string, string, []any, int, int, error) {
func buildInternalAdminListQuery(req *http.Request, spec *internalQuerySpec, scope string, tenantID types.TenantID) (string, string, []any, int, int, error) {
page := parseIMPositiveInt(req.URL.Query().Get("page"), 1)
perPage := parseIMLimit(firstNonEmpty(req.URL.Query().Get("per_page"), req.URL.Query().Get("limit")), 20, 200)
where := []string{"1=1"}
args := []any{}
// 租户隔离必须先于 scope/filters 注入:共库模式下不注入即跨租户泄漏
if spec.TenantColumn != "" {
where = append(where, spec.TenantColumn+"=?")
args = append(args, int64(tenantID))
}
if spec.ScopeColumn != "" && strings.TrimSpace(scope) != "" {
value, err := internalAdminFilterValue("scope", scope, scopeKindForColumn(spec.ScopeColumn))
if err != nil {
+31 -8
View File
@@ -467,7 +467,12 @@ func serveInternalUserQuery(wrt http.ResponseWriter, req *http.Request, tenantID
}
if spec, ok := internalAdminQuerySpec("users"); ok {
query := spec.Select + " " + spec.From + " WHERE u.id=?"
if row, err := store.InternalAdminQueries.QueryOne(tenantID, query, []any{userDBID}); err == nil {
args := []any{userDBID}
if cond, extra := internalAdminTenantCondition(spec, tenantID); cond != "" {
query += cond
args = append(args, extra...)
}
if row, err := store.InternalAdminQueries.QueryOne(tenantID, query, args); err == nil {
writeIMSuccess(wrt, req, normalizeInternalAdminRowForAPI(row))
return
} else {
@@ -508,7 +513,12 @@ func serveInternalGroupQuery(wrt http.ResponseWriter, req *http.Request, tenantI
}
if spec, ok := internalAdminQuerySpec("groups"); ok {
query := spec.Select + " " + spec.From + " WHERE t.name=?"
if row, err := store.InternalAdminQueries.QueryOne(tenantID, query, []any{topic}); err == nil {
args := []any{topic}
if cond, extra := internalAdminTenantCondition(spec, tenantID); cond != "" {
query += cond
args = append(args, extra...)
}
if row, err := store.InternalAdminQueries.QueryOne(tenantID, query, args); err == nil {
writeIMSuccess(wrt, req, normalizeInternalAdminRowForAPI(row))
return
}
@@ -529,9 +539,10 @@ func serveInternalGroupMembersQuery(wrt http.ResponseWriter, req *http.Request,
spec := &internalQuerySpec{
Select: `SELECT gm.id,gm.topic,gm.user_id,u.state,p.nickname,p.public_account,p.avatar_file_id,p.signature,p.gender,p.region,s.modewant AS mode_want,s.modegiven AS mode_given,s.recvseqid AS recv_seq_id,s.readseqid AS read_seq_id,s.delid AS del_id,
gm.role,gm.member_status,gm.nickname AS group_nickname,gm.pinned,gm.mute_notifications,gm.join_source,gm.joined_by,gm.joined_at,gm.left_at,gm.removed_by,gm.version,gm.created_at,gm.updated_at`,
From: "FROM im_group_members AS gm LEFT JOIN im_users AS u ON u.id=gm.user_id LEFT JOIN im_user_profiles AS p ON p.user_id=gm.user_id LEFT JOIN im_subscriptions AS s ON s.topic=gm.topic AND s.userid=gm.user_id",
DefaultSort: "ORDER BY gm.id DESC",
ScopeColumn: "gm.topic",
From: "FROM im_group_members AS gm LEFT JOIN im_users AS u ON u.id=gm.user_id LEFT JOIN im_user_profiles AS p ON p.user_id=gm.user_id LEFT JOIN im_subscriptions AS s ON s.topic=gm.topic AND s.userid=gm.user_id",
DefaultSort: "ORDER BY gm.id DESC",
ScopeColumn: "gm.topic",
TenantColumn: "gm.tenant_id",
Filters: []internalQueryFilter{
{"user_id", "gm.user_id", "=", "uid"},
{"role", "gm.role", "=", "string"},
@@ -540,7 +551,7 @@ func serveInternalGroupMembersQuery(wrt http.ResponseWriter, req *http.Request,
{"eligible_role", "gm.role", "=", "string"},
},
}
query, countQuery, args, page, perPage, err := buildInternalAdminListQuery(req, spec, topic)
query, countQuery, args, page, perPage, err := buildInternalAdminListQuery(req, spec, topic, tenantID)
if err == nil {
if result, queryErr := store.InternalAdminQueries.Query(tenantID, query, countQuery, args, page, perPage); queryErr == nil {
writeIMSuccess(wrt, req, map[string]any{
@@ -606,7 +617,13 @@ func serveInternalMessageQuery(wrt http.ResponseWriter, req *http.Request, tenan
}
}
spec.Select += ",m.head,JSON_UNQUOTE(m.content) AS content"
row, err := store.InternalAdminQueries.QueryOne(tenantID, spec.Select+" "+spec.From+" WHERE "+where, []any{topic, seq})
query := spec.Select + " " + spec.From + " WHERE " + where
args := []any{topic, seq}
if cond, extra := internalAdminTenantCondition(spec, tenantID); cond != "" {
query += cond
args = append(args, extra...)
}
row, err := store.InternalAdminQueries.QueryOne(tenantID, query, args)
if err != nil {
writeInternalQueryError(wrt, req, err)
return
@@ -633,7 +650,13 @@ func serveInternalFileQuery(wrt http.ResponseWriter, req *http.Request, tenantID
return
}
spec, _ := internalAdminQuerySpec("files")
row, err := store.InternalAdminQueries.QueryOne(tenantID, spec.Select+" "+spec.From+" WHERE id=?", []any{fileDBID})
query := spec.Select + " " + spec.From + " WHERE id=?"
args := []any{fileDBID}
if cond, extra := internalAdminTenantCondition(spec, tenantID); cond != "" {
query += cond
args = append(args, extra...)
}
row, err := store.InternalAdminQueries.QueryOne(tenantID, query, args)
if err != nil {
writeInternalQueryError(wrt, req, err)
return