From 6601a483bee4393782f68414c11dba9c0de66c29 Mon Sep 17 00:00:00 2001 From: Windfarer Date: Wed, 29 Apr 2026 18:58:09 +0800 Subject: [PATCH] feat: add STORAGE_ALLOW_LIST env var --- .env.example | 3 + frontend/src/api/system/index.ts | 14 ++-- .../knowledge/settings/KBStorageSettings.vue | 34 ++++++++-- .../views/settings/StorageEngineSettings.vue | 51 ++++++++++---- internal/handler/initialization.go | 4 ++ internal/handler/knowledgebase.go | 6 ++ internal/handler/storage_allowlist.go | 68 +++++++++++++++++++ internal/handler/system.go | 37 ++++++++-- internal/handler/tenant.go | 14 ++++ 9 files changed, 198 insertions(+), 33 deletions(-) create mode 100644 internal/handler/storage_allowlist.go diff --git a/.env.example b/.env.example index ac3f14499..595153e15 100644 --- a/.env.example +++ b/.env.example @@ -37,6 +37,9 @@ DB_DRIVER=postgres # 向量存储类型(postgres/elasticsearch_v7/elasticsearch_v8/qdrant/milvus/weaviate) RETRIEVE_DRIVER=postgres +# 允许用户使用哪些文件存储类型,使用逗号分隔,留空则允许所有类型的存储 +# STORAGE_ALLOW_LIST=local,minio,cos,tos,s3 + # 文件存储类型(local/minio/cos/tos/s3) STORAGE_TYPE=local diff --git a/frontend/src/api/system/index.ts b/frontend/src/api/system/index.ts index 35ad2855b..8c50d2a62 100644 --- a/frontend/src/api/system/index.ts +++ b/frontend/src/api/system/index.ts @@ -184,9 +184,9 @@ export function reconnectDocReader(addr: string): Promise {{ opt.label }} - {{ $t('kbSettings.storage.notConfigured') }} + {{ $t('kbSettings.storage.unavailable') }} + {{ $t('kbSettings.storage.notConfigured') }} {{ $t('kbSettings.storage.unavailable') }} @@ -71,55 +72,64 @@ const localProvider = ref(props.storageProvider || 'local') const loading = ref(true) const engineStatus = ref([]) const defaultProvider = ref('local') +const allowedProviders = ref([]) const hasAnyConfig = ref(false) const engineOptions = computed(() => { const statusMap: Record = {} + const allowedMap: Record = {} for (const e of engineStatus.value) { statusMap[e.name] = e.available + allowedMap[e.name] = e.allowed !== false } return [ { value: 'local', label: t('kbSettings.storage.engineLocal'), desc: t('kbSettings.storage.engineLocalDesc'), + allowed: allowedMap.local !== false, available: statusMap.local !== false, - disabled: false, + disabled: allowedMap.local === false, }, { value: 'minio', label: 'MinIO', desc: t('kbSettings.storage.engineMinioDesc'), + allowed: allowedMap.minio !== false, available: statusMap.minio, - disabled: statusMap.minio === false, + disabled: allowedMap.minio === false || statusMap.minio === false, }, { value: 'cos', label: t('kbSettings.storage.engineCos'), desc: t('kbSettings.storage.engineCosDesc'), + allowed: allowedMap.cos !== false, available: statusMap.cos, - disabled: statusMap.cos === false, + disabled: allowedMap.cos === false || statusMap.cos === false, }, { value: 'tos', label: t('kbSettings.storage.engineTos'), desc: t('kbSettings.storage.engineTosDesc'), + allowed: allowedMap.tos !== false, available: statusMap.tos, - disabled: statusMap.tos === false, + disabled: allowedMap.tos === false || statusMap.tos === false, }, { value: 's3', label: t('kbSettings.storage.engineS3'), desc: t('kbSettings.storage.engineS3Desc'), + allowed: allowedMap.s3 !== false, available: statusMap.s3, - disabled: statusMap.s3 === false, + disabled: allowedMap.s3 === false || statusMap.s3 === false, }, { value: 'oss', label: t('kbSettings.storage.engineOss'), desc: t('kbSettings.storage.engineOssDesc'), + allowed: allowedMap.oss !== false, available: statusMap.oss, - disabled: statusMap.oss === false, + disabled: allowedMap.oss === false || statusMap.oss === false, }, ] }) @@ -136,6 +146,14 @@ function handleChange() { emit('update:storageProvider', localProvider.value) } +function ensureAllowedProvider() { + const current = engineOptions.value.find(o => o.value === localProvider.value && !o.disabled) + if (current) return + const fallback = engineOptions.value.find(o => !o.disabled)?.value || defaultProvider.value || 'local' + localProvider.value = fallback + emit('update:storageProvider', localProvider.value) +} + function goToStorageSettings() { uiStore.closeKBEditor?.() uiStore.openSettings?.('storage') @@ -150,6 +168,7 @@ async function load() { ]) const engines = statusRes?.data?.engines ?? [] engineStatus.value = engines + allowedProviders.value = statusRes?.data?.allowed_providers ?? [] defaultProvider.value = configRes?.data?.default_provider || 'local' const d = configRes?.data hasAnyConfig.value = !!(d?.local?.path_prefix || d?.minio?.bucket_name || d?.cos?.bucket_name || d?.tos?.bucket_name || d?.s3?.bucket_name) @@ -157,6 +176,7 @@ async function load() { localProvider.value = defaultProvider.value emit('update:storageProvider', localProvider.value) } + ensureAllowedProvider() } catch { engineStatus.value = [] } finally { diff --git a/frontend/src/views/settings/StorageEngineSettings.vue b/frontend/src/views/settings/StorageEngineSettings.vue index 694a5f45f..e3dafa3e3 100644 --- a/frontend/src/views/settings/StorageEngineSettings.vue +++ b/frontend/src/views/settings/StorageEngineSettings.vue @@ -28,20 +28,15 @@

{{ $t('settings.storage.defaultEngineDesc') }}

- - - - - - - + +
-
+
@@ -64,7 +59,7 @@
-
+
@@ -202,7 +197,7 @@
-
+
@@ -276,7 +271,7 @@
-
+
@@ -350,7 +345,7 @@
-
+
@@ -424,7 +419,7 @@
-
+
@@ -499,7 +494,7 @@
- {{ $t('settings.storage.saveConfig') }} + {{ $t('settings.storage.saveConfig') }} {{ saveMessage }} @@ -567,6 +562,7 @@ const defaultConfig = (): StorageEngineConfig => ({ const loading = ref(true) const error = ref('') const config = ref(defaultConfig()) +const allowedProviders = ref(null) const engineStatus = ref<{ local: boolean; minio: boolean; cos: boolean }>({ local: true, minio: false, @@ -590,6 +586,17 @@ const s3CheckResult = ref<{ ok: boolean; message: string } | null>(null) const checkingOss = ref(false) const ossCheckResult = ref<{ ok: boolean; message: string } | null>(null) +const providerOptions = computed(() => [ + { value: 'local', label: t('settings.storage.engineLocal'), allowed: isProviderAllowed('local') }, + { value: 'minio', label: 'MinIO', allowed: isProviderAllowed('minio') }, + { value: 'cos', label: t('settings.storage.engineCos'), allowed: isProviderAllowed('cos') }, + { value: 'tos', label: t('settings.storage.engineTos'), allowed: isProviderAllowed('tos') }, + { value: 's3', label: 'AWS S3', allowed: isProviderAllowed('s3') }, + { value: 'oss', label: t('settings.storage.engineOss'), allowed: isProviderAllowed('oss') }, +]) + +const hasAllowedProviders = computed(() => (allowedProviders.value?.length ?? 0) > 0) + const minioAvailable = computed(() => { if (config.value.minio?.mode === 'remote') { return !!(config.value.minio.endpoint && config.value.minio.access_key_id && config.value.minio.secret_access_key) @@ -597,6 +604,17 @@ const minioAvailable = computed(() => { return minioEnvAvailable.value }) +function isProviderAllowed(provider: string) { + if (allowedProviders.value === null) return true + return allowedProviders.value.includes(provider) +} + +function ensureAllowedDefaultProvider() { + if (isProviderAllowed(config.value.default_provider)) return + const fallback = allowedProviders.value?.[0] || 'local' + config.value.default_provider = fallback +} + async function loadConfig() { try { const res = await getStorageEngineConfig() @@ -670,6 +688,9 @@ async function loadStatus() { try { const res = await getStorageEngineStatus() const engines = res?.data?.engines ?? [] + allowedProviders.value = res?.data?.allowed_providers?.length + ? res.data.allowed_providers + : engines.filter(e => e.allowed !== false).map(e => e.name) const status = { local: true, minio: false, cos: true } for (const e of engines) { if (e.name === 'local') status.local = e.available @@ -680,6 +701,7 @@ async function loadStatus() { minioEnvAvailable.value = res?.data?.minio_env_available ?? false } catch { engineStatus.value = { local: true, minio: false, cos: true } + allowedProviders.value = ['local', 'minio', 'cos', 'tos', 's3', 'oss'] minioEnvAvailable.value = false } } @@ -704,6 +726,7 @@ async function loadAll() { error.value = '' try { await Promise.all([loadConfig(), loadStatus()]) + ensureAllowedDefaultProvider() if (minioEnvAvailable.value) loadMinioBuckets() } catch (e: unknown) { error.value = e instanceof Error ? e.message : t('settings.storage.loadFailed') diff --git a/internal/handler/initialization.go b/internal/handler/initialization.go index 766416bc3..b1ab8c823 100644 --- a/internal/handler/initialization.go +++ b/internal/handler/initialization.go @@ -328,6 +328,10 @@ func (h *InitializationHandler) UpdateKBConfig(c *gin.Context) { if provider == "" { provider = "local" } + if !isStorageProviderAllowed(provider) { + c.Error(errors.NewBadRequestError("Storage provider is not allowed by STORAGE_ALLOW_LIST")) + return + } oldProvider := kb.GetStorageProvider() if oldProvider == "" { oldProvider = "local" diff --git a/internal/handler/knowledgebase.go b/internal/handler/knowledgebase.go index 8ae600b37..94e0b44ed 100644 --- a/internal/handler/knowledgebase.go +++ b/internal/handler/knowledgebase.go @@ -5,6 +5,7 @@ import ( stderrors "errors" "net/http" "strconv" + "strings" "time" "github.com/Tencent/WeKnora/internal/application/repository" @@ -128,6 +129,11 @@ func (h *KnowledgeBaseHandler) CreateKnowledgeBase(c *gin.Context) { c.Error(err) return } + provider := strings.ToLower(strings.TrimSpace(req.GetStorageProvider())) + if provider != "" && !isStorageProviderAllowed(provider) { + c.Error(apperrors.NewBadRequestError("Storage provider is not allowed by STORAGE_ALLOW_LIST")) + return + } logger.Infof(ctx, "Creating knowledge base, name: %s", secutils.SanitizeForLog(req.Name)) // Create knowledge base using the service diff --git a/internal/handler/storage_allowlist.go b/internal/handler/storage_allowlist.go new file mode 100644 index 000000000..5bfd64ff0 --- /dev/null +++ b/internal/handler/storage_allowlist.go @@ -0,0 +1,68 @@ +package handler + +import ( + "os" + "strings" +) + +const storageAllowListEnv = "STORAGE_ALLOW_LIST" + +var supportedStorageProviders = []string{"local", "minio", "cos", "tos", "s3", "oss"} + +func getSupportedStorageProviders() []string { + providers := make([]string, len(supportedStorageProviders)) + copy(providers, supportedStorageProviders) + return providers +} + +func getAllowedStorageProviders() map[string]bool { + raw := strings.TrimSpace(os.Getenv(storageAllowListEnv)) + allowed := make(map[string]bool, len(supportedStorageProviders)) + + if raw == "" { + for _, provider := range supportedStorageProviders { + allowed[provider] = true + } + return allowed + } + + for _, item := range strings.FieldsFunc(raw, func(r rune) bool { + switch r { + case ',', ';', '|', '\n', '\t', ' ': + return true + default: + return false + } + }) { + provider := strings.ToLower(strings.TrimSpace(item)) + if provider == "" { + continue + } + for _, supported := range supportedStorageProviders { + if provider == supported { + allowed[provider] = true + break + } + } + } + + return allowed +} + +func isStorageProviderAllowed(provider string) bool { + provider = strings.ToLower(strings.TrimSpace(provider)) + if provider == "" { + return true + } + return getAllowedStorageProviders()[provider] +} + +func firstAllowedStorageProvider() string { + allowed := getAllowedStorageProviders() + for _, provider := range supportedStorageProviders { + if allowed[provider] { + return provider + } + } + return "" +} diff --git a/internal/handler/system.go b/internal/handler/system.go index f067f407c..75d2f0b77 100644 --- a/internal/handler/system.go +++ b/internal/handler/system.go @@ -457,6 +457,7 @@ type ListMinioBucketsResponse struct { // StorageEngineStatusItem describes one storage engine's availability and description. type StorageEngineStatusItem struct { Name string `json:"name"` // "local", "minio", "cos", "tos" + Allowed bool `json:"allowed"` Available bool `json:"available"` // whether the engine can be used Description string `json:"description"` // short description for UI } @@ -464,6 +465,7 @@ type StorageEngineStatusItem struct { // GetStorageEngineStatusResponse is the response for GET /system/storage-engine-status. type GetStorageEngineStatusResponse struct { Engines []StorageEngineStatusItem `json:"engines"` + AllowedProviders []string `json:"allowed_providers"` MinioEnvAvailable bool `json:"minio_env_available"` } @@ -479,18 +481,27 @@ func (h *SystemHandler) GetStorageEngineStatus(c *gin.Context) { minioEnvAvailable := h.isMinioEnvAvailable() cosConfigured := h.isCOSConfigured(c) tosConfigured := h.isTOSConfigured(c) + s3Configured := h.isS3Configured(c) ossConfigured := h.isOSSConfigured(c) + allowed := getAllowedStorageProviders() + allowedProviders := make([]string, 0, len(supportedStorageProviders)) + for _, provider := range getSupportedStorageProviders() { + if allowed[provider] { + allowedProviders = append(allowedProviders, provider) + } + } engines := []StorageEngineStatusItem{ - {Name: "local", Available: true, Description: "本地文件系统存储,仅适合单机部署"}, - {Name: "minio", Available: minioConfigured || minioEnvAvailable, Description: "S3 兼容的自托管对象存储,适合内网和私有云部署"}, - {Name: "cos", Available: cosConfigured, Description: "腾讯云对象存储服务,适合公有云部署,支持 CDN 加速"}, - {Name: "tos", Available: tosConfigured, Description: "火山引擎对象存储服务,适合公有云部署"}, - {Name: "oss", Available: ossConfigured, Description: "阿里云对象存储服务,适合公有云部署,支持 S3 兼容协议"}, + {Name: "local", Allowed: allowed["local"], Available: true, Description: "本地文件系统存储,仅适合单机部署"}, + {Name: "minio", Allowed: allowed["minio"], Available: minioConfigured || minioEnvAvailable, Description: "S3 兼容的自托管对象存储,适合内网和私有云部署"}, + {Name: "cos", Allowed: allowed["cos"], Available: cosConfigured, Description: "腾讯云对象存储服务,适合公有云部署,支持 CDN 加速"}, + {Name: "tos", Allowed: allowed["tos"], Available: tosConfigured, Description: "火山引擎对象存储服务,适合公有云部署"}, + {Name: "s3", Allowed: allowed["s3"], Available: s3Configured, Description: "AWS S3 与兼容对象存储服务,适合公有云与混合云部署"}, + {Name: "oss", Allowed: allowed["oss"], Available: ossConfigured, Description: "阿里云对象存储服务,适合公有云部署,支持 S3 兼容协议"}, } c.JSON(200, gin.H{ "code": 0, "msg": "success", - "data": GetStorageEngineStatusResponse{Engines: engines, MinioEnvAvailable: minioEnvAvailable}, + "data": GetStorageEngineStatusResponse{Engines: engines, AllowedProviders: allowedProviders, MinioEnvAvailable: minioEnvAvailable}, }) } @@ -801,6 +812,10 @@ func (h *SystemHandler) CheckStorageEngine(c *gin.Context) { c.JSON(400, gin.H{"code": 1, "msg": "请求体格式错误"}) return } + if !isStorageProviderAllowed(req.Provider) { + c.JSON(403, gin.H{"code": 1, "msg": "该存储引擎已被禁用"}) + return + } switch req.Provider { case "minio": @@ -818,6 +833,16 @@ func (h *SystemHandler) CheckStorageEngine(c *gin.Context) { } } +func (h *SystemHandler) isS3Configured(c *gin.Context) bool { + if v, exists := c.Get(types.TenantInfoContextKey.String()); exists { + if tenant, ok := v.(*types.Tenant); ok && tenant != nil && tenant.StorageEngineConfig != nil && tenant.StorageEngineConfig.S3 != nil { + s3Conf := tenant.StorageEngineConfig.S3 + return s3Conf.Endpoint != "" && s3Conf.Region != "" && s3Conf.AccessKey != "" && s3Conf.SecretKey != "" && s3Conf.BucketName != "" + } + } + return false +} + func (h *SystemHandler) checkMinio(c *gin.Context, ctx context.Context, cfg *types.MinIOEngineConfig) { if cfg == nil { c.JSON(200, gin.H{"code": 0, "data": StorageCheckResponse{OK: false, Message: "未提供 MinIO 配置"}}) diff --git a/internal/handler/tenant.go b/internal/handler/tenant.go index fdeadd800..90500aa00 100644 --- a/internal/handler/tenant.go +++ b/internal/handler/tenant.go @@ -3,6 +3,7 @@ package handler import ( "net/http" "strconv" + "strings" "github.com/gin-gonic/gin" @@ -842,6 +843,19 @@ func (h *TenantHandler) updateTenantStorageEngineConfigInternal(c *gin.Context) c.Error(errors.NewValidationError("Invalid request data").WithDetails(err.Error())) return } + provider := strings.ToLower(strings.TrimSpace(cfg.DefaultProvider)) + if provider == "" { + provider = firstAllowedStorageProvider() + } + if provider == "" { + c.Error(errors.NewBadRequestError("No storage provider is allowed by STORAGE_ALLOW_LIST")) + return + } + if !isStorageProviderAllowed(provider) { + c.Error(errors.NewBadRequestError("Storage provider is not allowed by STORAGE_ALLOW_LIST")) + return + } + cfg.DefaultProvider = provider tenant, _ := types.TenantInfoFromContext(ctx) if tenant == nil { logger.Error(ctx, "Tenant is empty")