mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-09-29 05:56:38 +08:00
autoresearch iter 35: BUGFIX a failed whitelist read is no longer cached as an admin decision
This commit is contained in:
+51
-12
@@ -5,13 +5,18 @@
|
||||
package cache
|
||||
|
||||
import (
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/plugins/domain/upload/shared"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
uploadstorage "Wavelet/plugins/domain/upload/storage"
|
||||
)
|
||||
|
||||
@@ -65,41 +70,75 @@ func loadFileAccessWhitelist(ctx context.Context) map[string]struct{} {
|
||||
return fileAccessWhitelistTypes
|
||||
}
|
||||
|
||||
fileAccessWhitelistTypes = fetchFileAccessWhitelist(ctx)
|
||||
types, err := fetchFileAccessWhitelist(ctx)
|
||||
if err != nil {
|
||||
logger.ErrorF(ctx, "[Upload] 读取公开访问白名单失败: %v", err)
|
||||
if fileAccessWhitelistTypes != nil {
|
||||
// A previously loaded list is still the best answer; keep it until its
|
||||
// TTL rather than narrowing access because one read failed.
|
||||
fileAccessWhitelistValid = true
|
||||
fileAccessWhitelistCheckedAt = time.Now()
|
||||
return fileAccessWhitelistTypes
|
||||
}
|
||||
// Nothing cached yet: serve the restricted default but stay invalid so the
|
||||
// next request retries instead of pinning it for the whole TTL.
|
||||
return fallbackFileAccessWhitelist()
|
||||
}
|
||||
|
||||
fileAccessWhitelistTypes = types
|
||||
fileAccessWhitelistValid = true
|
||||
fileAccessWhitelistCheckedAt = time.Now()
|
||||
return fileAccessWhitelistTypes
|
||||
return types
|
||||
}
|
||||
|
||||
func fetchFileAccessWhitelist(ctx context.Context) map[string]struct{} {
|
||||
whitelist := parseFileAccessWhitelist(ctx)
|
||||
// fallbackFileAccessWhitelist is the restricted default used when nothing better is known.
|
||||
func fallbackFileAccessWhitelist() map[string]struct{} {
|
||||
return map[string]struct{}{strings.ToLower(shared.DefaultPublicUploadType): {}}
|
||||
}
|
||||
|
||||
func fetchFileAccessWhitelist(ctx context.Context) (map[string]struct{}, error) {
|
||||
whitelist, err := parseFileAccessWhitelist(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
types := make(map[string]struct{}, len(whitelist))
|
||||
for _, item := range whitelist {
|
||||
types[strings.ToLower(item)] = struct{}{}
|
||||
}
|
||||
return types
|
||||
return types, nil
|
||||
}
|
||||
|
||||
func parseFileAccessWhitelist(ctx context.Context) []string {
|
||||
// parseFileAccessWhitelist reads the configured public access types.
|
||||
//
|
||||
// A missing row or an empty value is a real answer and yields the default; only a
|
||||
// read that actually fails is reported as an error, so a database outage is no
|
||||
// longer mistaken for "the admin never configured a whitelist".
|
||||
func parseFileAccessWhitelist(ctx context.Context) ([]string, error) {
|
||||
var sc struct{ Value string }
|
||||
db := shared.GetDB(ctx)
|
||||
if db != nil {
|
||||
_ = db.Table("w_system_configs").Where("key = ?", "file_access_whitelist").First(&sc).Error
|
||||
if db == nil {
|
||||
return nil, errors.New("database not available")
|
||||
}
|
||||
if err := db.Table("w_system_configs").Where("key = ?", "file_access_whitelist").First(&sc).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return []string{shared.DefaultPublicUploadType}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("read file_access_whitelist: %w", err)
|
||||
}
|
||||
if sc.Value == "" {
|
||||
return []string{shared.DefaultPublicUploadType}
|
||||
return []string{shared.DefaultPublicUploadType}, nil
|
||||
}
|
||||
|
||||
var whitelist []string
|
||||
if err := json.Unmarshal([]byte(sc.Value), &whitelist); err == nil && len(whitelist) > 0 {
|
||||
return whitelist
|
||||
return whitelist, nil
|
||||
}
|
||||
|
||||
whitelist = parseCommaSeparatedWhitelist(sc.Value)
|
||||
if len(whitelist) == 0 {
|
||||
return []string{shared.DefaultPublicUploadType}
|
||||
return []string{shared.DefaultPublicUploadType}, nil
|
||||
}
|
||||
return whitelist
|
||||
return whitelist, nil
|
||||
}
|
||||
|
||||
func parseCommaSeparatedWhitelist(value string) []string {
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"Wavelet/plugins/domain/upload/shared"
|
||||
"context"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// configRow mirrors just enough of w_system_configs to rebuild it mid-test.
|
||||
type configRow struct {
|
||||
Key string `gorm:"primaryKey;size:64"`
|
||||
Value string `gorm:"type:text;not null"`
|
||||
Type string `gorm:"size:32;not null"`
|
||||
}
|
||||
|
||||
// TableName returns the system config table.
|
||||
func (configRow) TableName() string { return "w_system_configs" }
|
||||
|
||||
// TestWhitelistReadFailureIsNotCachedAsConfigured pins the defect where a failed
|
||||
// read of the public access whitelist was discarded and the resulting restricted
|
||||
// default was then cached as though the admin had configured it, narrowing access
|
||||
// for the whole TTL with nothing to retry it sooner.
|
||||
func TestWhitelistReadFailureIsNotCachedAsConfigured(t *testing.T) {
|
||||
db, cleanup := shared.SetupTestEnv(t)
|
||||
defer cleanup()
|
||||
ResetAccessCaches()
|
||||
t.Cleanup(ResetAccessCaches)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
// Make the config unreadable, then take one sample through the failure path.
|
||||
if err := db.Exec("DROP TABLE w_system_configs").Error; err != nil {
|
||||
t.Fatalf("drop config table: %v", err)
|
||||
}
|
||||
if IsFilePublic(ctx, "document") {
|
||||
t.Fatal(`document reported public while the whitelist cannot be read`)
|
||||
}
|
||||
|
||||
// Restore a configured whitelist that includes document.
|
||||
if err := db.AutoMigrate(&configRow{}); err != nil {
|
||||
t.Fatalf("recreate config table: %v", err)
|
||||
}
|
||||
row := configRow{Key: "file_access_whitelist", Value: `["avatar","document"]`, Type: "system"}
|
||||
if err := db.Create(&row).Error; err != nil {
|
||||
t.Fatalf("seed whitelist: %v", err)
|
||||
}
|
||||
|
||||
// A failed first read must not have been cached as a real answer, so this call
|
||||
// has to re-read and see the configured list.
|
||||
if !IsFilePublic(ctx, "document") {
|
||||
t.Error("whitelist stayed pinned to the default after a read failure; the failed lookup must be retried")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user