mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 23:56:37 +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
|
package cache
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"Wavelet/pkg/logger"
|
||||||
"Wavelet/plugins/domain/upload/shared"
|
"Wavelet/plugins/domain/upload/shared"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
uploadstorage "Wavelet/plugins/domain/upload/storage"
|
uploadstorage "Wavelet/plugins/domain/upload/storage"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -65,41 +70,75 @@ func loadFileAccessWhitelist(ctx context.Context) map[string]struct{} {
|
|||||||
return fileAccessWhitelistTypes
|
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
|
fileAccessWhitelistValid = true
|
||||||
fileAccessWhitelistCheckedAt = time.Now()
|
fileAccessWhitelistCheckedAt = time.Now()
|
||||||
return fileAccessWhitelistTypes
|
return types
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchFileAccessWhitelist(ctx context.Context) map[string]struct{} {
|
// fallbackFileAccessWhitelist is the restricted default used when nothing better is known.
|
||||||
whitelist := parseFileAccessWhitelist(ctx)
|
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))
|
types := make(map[string]struct{}, len(whitelist))
|
||||||
for _, item := range whitelist {
|
for _, item := range whitelist {
|
||||||
types[strings.ToLower(item)] = struct{}{}
|
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 }
|
var sc struct{ Value string }
|
||||||
db := shared.GetDB(ctx)
|
db := shared.GetDB(ctx)
|
||||||
if db != nil {
|
if db == nil {
|
||||||
_ = db.Table("w_system_configs").Where("key = ?", "file_access_whitelist").First(&sc).Error
|
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 == "" {
|
if sc.Value == "" {
|
||||||
return []string{shared.DefaultPublicUploadType}
|
return []string{shared.DefaultPublicUploadType}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var whitelist []string
|
var whitelist []string
|
||||||
if err := json.Unmarshal([]byte(sc.Value), &whitelist); err == nil && len(whitelist) > 0 {
|
if err := json.Unmarshal([]byte(sc.Value), &whitelist); err == nil && len(whitelist) > 0 {
|
||||||
return whitelist
|
return whitelist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
whitelist = parseCommaSeparatedWhitelist(sc.Value)
|
whitelist = parseCommaSeparatedWhitelist(sc.Value)
|
||||||
if len(whitelist) == 0 {
|
if len(whitelist) == 0 {
|
||||||
return []string{shared.DefaultPublicUploadType}
|
return []string{shared.DefaultPublicUploadType}, nil
|
||||||
}
|
}
|
||||||
return whitelist
|
return whitelist, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseCommaSeparatedWhitelist(value string) []string {
|
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