fix: scope upload quotas and lock WeCom listener binding (#22076)

This commit is contained in:
buua436 authored and GitHub committed 2026-11-06 14:55:00 +08:00
1 parent 7125287b97
commit 92816b46c2
6 files changed
+241 -23

No files matched your search

+17 -11
View File
@@ -154,7 +154,6 @@ func newWeComChannelFromConfig(accountID string, cfg map[string]any) (*wecomChan
AccountID: accountID,
ConnectionType: connectionType,
Secret: firstString(cfg, "secret"),
WebhookHost: firstString(cfg, "webhook_host"),
APIBaseURL: firstString(cfg, "api_base_url"),
WSURL: firstString(cfg, "ws_url"),
}
@@ -189,29 +188,36 @@ func newWeComChannelFromConfig(accountID string, cfg map[string]any) (*wecomChan
if len(account.AESKey) != 43 {
return nil, fmt.Errorf("wecom account %q aes_key (EncodingAESKey) must be 43 characters, got %d", accountID, len(account.AESKey))
}
if rawHost, ok := cfg["webhook_host"]; ok {
host, ok := rawHost.(string)
if !ok {
return nil, fmt.Errorf("wecom account %q webhook_host must be %q", accountID, defaultWeComWebhookHost)
}
host = strings.TrimSpace(host)
if host != "" && host != defaultWeComWebhookHost {
return nil, fmt.Errorf("wecom account %q webhook_host must use the shared address %q", accountID, defaultWeComWebhookHost)
}
}
agentID, err := parseWeComInt(cfg["agent_id"], "agent_id")
if err != nil {
return nil, fmt.Errorf("wecom account %q %w", accountID, err)
}
account.AgentID = agentID
var configuredWebhookPort *int
if raw, ok := cfg["webhook_port"]; ok {
port, err := parseWeComInt(raw, "webhook_port")
if err != nil {
return nil, fmt.Errorf("wecom account %q %w", accountID, err)
}
if port <= 0 || port > 65535 {
return nil, fmt.Errorf("wecom account %q webhook_port must be between 1 and 65535", accountID)
}
if port != defaultWeComWebhookPort {
return nil, fmt.Errorf("wecom account %q webhook_port must use the shared port %d", accountID, defaultWeComWebhookPort)
}
account.WebhookPort = int(port)
configuredWebhookPort = &account.WebhookPort
}
channel, err := newWeComChannel(account)
if err != nil {
return nil, err
}
if configuredWebhookPort != nil {
channel.account.WebhookPort = *configuredWebhookPort
}
return channel, nil
return newWeComChannel(account)
}
// ChannelID returns the platform identifier used by the chat-channel runtime.
+64 -9
View File
@@ -90,15 +90,14 @@ func TestNewWeComChannelFromConfigTreatsUnknownConnectionTypeAsWebhook(t *testin
}
}
func TestNewWeComChannelFromConfigAcceptsIntegerAgentID(t *testing.T) {
func TestNewWeComChannelFromConfigAcceptsIntegerAgentIDAndDefaultsListener(t *testing.T) {
encodingAESKey, _ := testWeComAESKey()
channel, err := newWeComChannelFromConfig("account-1", map[string]any{
"corp_id": "corp-id",
"agent_id": 0,
"secret": "secret",
"token": "token",
"aes_key": encodingAESKey,
"webhook_port": 0,
"corp_id": "corp-id",
"agent_id": 0,
"secret": "secret",
"token": "token",
"aes_key": encodingAESKey,
})
if err != nil {
t.Fatalf("agent_id=0 error = %v", err)
@@ -106,8 +105,64 @@ func TestNewWeComChannelFromConfigAcceptsIntegerAgentID(t *testing.T) {
if channel.account.AgentID != 0 {
t.Fatalf("agent_id = %d, want 0", channel.account.AgentID)
}
if channel.account.WebhookPort != 0 {
t.Fatalf("webhook_port = %d, want 0", channel.account.WebhookPort)
if channel.account.WebhookPort != defaultWeComWebhookPort {
t.Fatalf("webhook_port = %d, want %d", channel.account.WebhookPort, defaultWeComWebhookPort)
}
if channel.account.WebhookHost != defaultWeComWebhookHost {
t.Fatalf("webhook_host = %q, want %q", channel.account.WebhookHost, defaultWeComWebhookHost)
}
}
func TestNewWeComChannelFromConfigRejectsTenantControlledListener(t *testing.T) {
encodingAESKey, _ := testWeComAESKey()
baseConfig := map[string]any{
"corp_id": "corp-id",
"agent_id": "1000001",
"secret": "secret",
"token": "token",
"aes_key": encodingAESKey,
}
for _, tc := range []struct {
name string
field string
value any
}{
{name: "ephemeral port", field: "webhook_port", value: 0},
{name: "out of range port", field: "webhook_port", value: 65536},
{name: "tenant selected port", field: "webhook_port", value: 3003},
{name: "loopback host", field: "webhook_host", value: "127.0.0.1"},
{name: "hostname", field: "webhook_host", value: "localhost"},
{name: "non-string host", field: "webhook_host", value: []string{"0.0.0.0"}},
} {
t.Run(tc.name, func(t *testing.T) {
config := make(map[string]any, len(baseConfig)+1)
for key, value := range baseConfig {
config[key] = value
}
config[tc.field] = tc.value
if _, err := newWeComChannelFromConfig("account-1", config); err == nil {
t.Fatalf("accepted tenant controlled %s=%v", tc.field, tc.value)
}
})
}
}
func TestNewWeComChannelFromConfigAllowsSharedListenerConfig(t *testing.T) {
encodingAESKey, _ := testWeComAESKey()
channel, err := newWeComChannelFromConfig("account-1", map[string]any{
"corp_id": "corp-id",
"agent_id": "1000001",
"secret": "secret",
"token": "token",
"aes_key": encodingAESKey,
"webhook_host": defaultWeComWebhookHost,
"webhook_port": defaultWeComWebhookPort,
})
if err != nil {
t.Fatalf("shared listener config error = %v", err)
}
if channel.account.WebhookHost != defaultWeComWebhookHost || channel.account.WebhookPort != defaultWeComWebhookPort {
t.Fatalf("listener address = %s:%d, want %s:%d", channel.account.WebhookHost, channel.account.WebhookPort, defaultWeComWebhookHost, defaultWeComWebhookPort)
}
}
+15 -2
View File
@@ -512,10 +512,23 @@ func (dao *DocumentDAO) GetByDocumentIDAndDatasetID(ctx context.Context, db *gor
return &document, err
}
// CountByTenantID counts documents by tenant ID
// CountByTenantID counts documents belonging to datasets owned by a tenant.
func (dao *DocumentDAO) CountByTenantID(ctx context.Context, db *gorm.DB, tenantID string) (int64, error) {
var count int64
err := db.WithContext(ctx).Model(&entity.Document{}).Where("created_by = ?", tenantID).Count(&count).Error
err := db.WithContext(ctx).Model(&entity.Document{}).
Joins("JOIN knowledgebase ON knowledgebase.id = document.kb_id").
Where("knowledgebase.tenant_id = ?", tenantID).
Count(&count).Error
return count, err
}
// CountByUserID counts documents attributed to a user. created_by may contain
// either a user ID or a tenant ID, so it is not a reliable user scope.
func (dao *DocumentDAO) CountByUserID(ctx context.Context, db *gorm.DB, userID string) (int64, error) {
var count int64
err := db.WithContext(ctx).Model(&entity.Document{}).
Where("user_id = ?", userID).
Count(&count).Error
return count, err
}
@@ -0,0 +1,90 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package dao
import (
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
)
func documentCountTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal("open sqlite:", err)
}
if err := db.Exec("CREATE TABLE knowledgebase (id TEXT PRIMARY KEY, tenant_id TEXT NOT NULL)").Error; err != nil {
t.Fatal("create knowledgebase table:", err)
}
if err := db.Exec("CREATE TABLE document (id TEXT PRIMARY KEY, kb_id TEXT NOT NULL, created_by TEXT NOT NULL, user_id TEXT NOT NULL)").Error; err != nil {
t.Fatal("create document table:", err)
}
if err := db.Exec("INSERT INTO knowledgebase (id, tenant_id) VALUES (?, ?), (?, ?)", "kb-1", "tenant-1", "kb-2", "tenant-2").Error; err != nil {
t.Fatal("insert knowledgebases:", err)
}
return db
}
func TestDocumentDAOCountByTenantIDUsesKnowledgebaseTenant(t *testing.T) {
db := documentCountTestDB(t)
if err := db.Exec("INSERT INTO document (id, kb_id, created_by, user_id) VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)",
"doc-1", "kb-1", "user-1", "user-1",
"doc-2", "kb-1", "user-2", "user-2",
"doc-3", "kb-2", "user-3", "user-3",
).Error; err != nil {
t.Fatal("insert documents:", err)
}
count, err := NewDocumentDAO().CountByTenantID(t.Context(), db, "tenant-1")
if err != nil {
t.Fatalf("CountByTenantID: %v", err)
}
if count != 2 {
t.Fatalf("CountByTenantID = %d, want 2 documents owned by tenant-1", count)
}
}
func TestDocumentDAOCountByUserIDUsesUserIDWhenCreatedByIsTenant(t *testing.T) {
db := documentCountTestDB(t)
if err := db.Exec("INSERT INTO document (id, kb_id, created_by, user_id) VALUES (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?), (?, ?, ?, ?)",
"doc-1", "kb-1", "tenant-1", "user-1",
"doc-2", "kb-1", "user-2", "user-1",
"doc-3", "kb-2", "user-1", "user-3",
"doc-4", "kb-2", "user-4", "user-4",
"doc-5", "kb-1", "tenant-1", "user-2",
"doc-6", "kb-1", "tenant-1", "tenant-1",
).Error; err != nil {
t.Fatalf("insert documents: %v", err)
}
count, err := NewDocumentDAO().CountByUserID(t.Context(), db, "user-1")
if err != nil {
t.Fatalf("CountByUserID: %v", err)
}
if count != 2 {
t.Fatalf("CountByUserID = %d, want 2 documents attributed to user-1", count)
}
tenantOwnerCount, err := NewDocumentDAO().CountByUserID(t.Context(), db, "tenant-1")
if err != nil {
t.Fatalf("CountByUserID for tenant owner: %v", err)
}
if tenantOwnerCount != 1 {
t.Fatalf("CountByUserID for tenant owner = %d, want 1 owner document", tenantOwnerCount)
}
}
+1 -1
View File
@@ -289,7 +289,7 @@ func (s *FileService) checkUploadInfoHealth(ctx context.Context, filename string
var maxNum int64
if _, err := fmt.Sscanf(maxFileNumPerUser, "%d", &maxNum); err == nil && maxNum > 0 {
var docCount int64
docCount, err = s.GetDocCount(ctx, userID)
docCount, err = dao.NewDocumentDAO().CountByUserID(ctx, dao.DB, userID)
if err != nil {
return fmt.Errorf("failed to get document count: %w", err)
}
@@ -0,0 +1,54 @@
//
// Copyright 2026 The InfiniFlow Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package file
import (
"strings"
"testing"
"github.com/glebarez/sqlite"
"gorm.io/gorm"
"ragflow/internal/common"
"ragflow/internal/dao"
"ragflow/internal/permission"
)
func TestCheckUploadInfoHealthEnforcesUserQuotaForTenantCreatedDocuments(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.Exec("CREATE TABLE document (id TEXT PRIMARY KEY, kb_id TEXT NOT NULL, created_by TEXT NOT NULL, user_id TEXT NOT NULL)").Error; err != nil {
t.Fatalf("create document table: %v", err)
}
if err := db.Exec("INSERT INTO document (id, kb_id, created_by, user_id) VALUES (?, ?, ?, ?), (?, ?, ?, ?)",
"doc-1", "kb-1", "tenant-1", "user-1",
"doc-2", "kb-1", "tenant-1", "user-1",
).Error; err != nil {
t.Fatalf("insert documents: %v", err)
}
oldDB := dao.DB
dao.DB = db
t.Cleanup(func() { dao.DB = oldDB })
t.Setenv(common.EnvMaxFileNumPerUser, "2")
ctx := permission.WithIdentity(t.Context(), "user-1", "tenant-1")
err = testFileService().checkUploadInfoHealth(ctx, "upload.txt")
if err == nil || !strings.Contains(err.Error(), "exceed the maximum file number") {
t.Fatalf("checkUploadInfoHealth() error = %v, want quota rejection", err)
}
}