Implements a master toggle for security features across config, CLI, and application logic. This allows granular control over whether the advanced security middleware and protections are active globally.
300 lines
9.7 KiB
Go
300 lines
9.7 KiB
Go
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"warpbox/lib/config"
|
|
)
|
|
|
|
func TestAdminSettingsRequiresAuth(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/admin/settings", nil)
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusSeeOther {
|
|
t.Fatalf("expected redirect, got %d", response.Code)
|
|
}
|
|
if location := response.Header().Get("Location"); location != "/admin/login" {
|
|
t.Fatalf("expected login redirect, got %q", location)
|
|
}
|
|
if app == nil {
|
|
t.Fatal("expected app setup")
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsPageRenders(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/admin/settings", nil)
|
|
request.AddCookie(authCookie(app))
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", response.Code)
|
|
}
|
|
body := response.Body.String()
|
|
if !strings.Contains(body, "WarpBox Settings") {
|
|
t.Fatalf("expected settings page title, got %s", body)
|
|
}
|
|
if !strings.Contains(body, "WARPBOX_API_ENABLED") {
|
|
t.Fatalf("expected API env var in page body")
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsExportIncludesCurrentValues(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodGet, "/admin/settings/export", nil)
|
|
request.AddCookie(authCookie(app))
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d", response.Code)
|
|
}
|
|
|
|
var payload struct {
|
|
Format string `json:"format"`
|
|
Settings map[string]string `json:"settings"`
|
|
}
|
|
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
|
}
|
|
if payload.Format != "warpbox.settings.export.v1" {
|
|
t.Fatalf("unexpected export format: %q", payload.Format)
|
|
}
|
|
if payload.Settings[config.SettingAPIEnabled] != "false" {
|
|
t.Fatalf("expected api_enabled to reflect environment false, got %q", payload.Settings[config.SettingAPIEnabled])
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsSavePersistsEditableOverrides(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"api_enabled":"true","box_poll_interval_ms":"6000","global_max_file_size_gb":"0.5"}}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.AddCookie(authCookie(app))
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", response.Code, response.Body.String())
|
|
}
|
|
if !app.config.APIEnabled {
|
|
t.Fatal("expected APIEnabled override to be applied")
|
|
}
|
|
if app.config.BoxPollIntervalMS != 6000 {
|
|
t.Fatalf("expected poll interval override, got %d", app.config.BoxPollIntervalMS)
|
|
}
|
|
if app.config.GlobalMaxFileSizeBytes != 512*1024*1024 {
|
|
t.Fatalf("expected size override in bytes, got %d", app.config.GlobalMaxFileSizeBytes)
|
|
}
|
|
|
|
overrides, err := config.ReadAdminSettingsOverrides(app.settingsOverridesPath)
|
|
if err != nil {
|
|
t.Fatalf("ReadAdminSettingsOverrides returned error: %v", err)
|
|
}
|
|
if overrides[config.SettingAPIEnabled] != "true" {
|
|
t.Fatalf("expected persisted API override, got %#v", overrides)
|
|
}
|
|
if _, exists := overrides[config.SettingBoxPollIntervalMS]; !exists {
|
|
t.Fatalf("expected changed poll interval override to be persisted, got %#v", overrides)
|
|
}
|
|
if _, exists := overrides[config.SettingSessionTTLSeconds]; exists {
|
|
t.Fatalf("expected untouched setting to stay out of overrides, got %#v", overrides)
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsSaveRejectsLockedSetting(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"data_dir":"./other"}}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.AddCookie(authCookie(app))
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", response.Code)
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsImportSkipsLockedAndUnknownKeys(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/settings/import", strings.NewReader(`{"settings":{"api_enabled":"true","data_dir":"./other","bogus":"x"}}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.AddCookie(authCookie(app))
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", response.Code, response.Body.String())
|
|
}
|
|
if !app.config.APIEnabled {
|
|
t.Fatal("expected editable import value to apply")
|
|
}
|
|
|
|
var payload struct {
|
|
Warnings []string `json:"warnings"`
|
|
}
|
|
if err := json.Unmarshal(response.Body.Bytes(), &payload); err != nil {
|
|
t.Fatalf("json.Unmarshal returned error: %v", err)
|
|
}
|
|
if len(payload.Warnings) != 2 {
|
|
t.Fatalf("expected 2 warnings, got %#v", payload.Warnings)
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsResetUsesBuiltInDefaults(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/settings/reset", strings.NewReader(`{}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.AddCookie(authCookie(app))
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", response.Code, response.Body.String())
|
|
}
|
|
if app.config.APIEnabled {
|
|
t.Fatal("expected reset to respect environment and restore APIEnabled=false")
|
|
}
|
|
}
|
|
|
|
func setupAdminSettingsTest(t *testing.T) (*App, *gin.Engine) {
|
|
t.Helper()
|
|
gin.SetMode(gin.TestMode)
|
|
cwd, err := os.Getwd()
|
|
if err != nil {
|
|
t.Fatalf("Getwd returned error: %v", err)
|
|
}
|
|
root := filepath.Clean(filepath.Join(cwd, "..", ".."))
|
|
if err := os.Chdir(root); err != nil {
|
|
t.Fatalf("Chdir returned error: %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
_ = os.Chdir(cwd)
|
|
})
|
|
clearAdminSettingsEnv(t)
|
|
t.Setenv("WARPBOX_DATA_DIR", t.TempDir())
|
|
t.Setenv("WARPBOX_ADMIN_PASSWORD", "secret")
|
|
t.Setenv("WARPBOX_ADMIN_ENABLED", "true")
|
|
t.Setenv("WARPBOX_API_ENABLED", "false")
|
|
|
|
cfg, err := config.Load()
|
|
if err != nil {
|
|
t.Fatalf("Load returned error: %v", err)
|
|
}
|
|
if err := cfg.EnsureDirectories(); err != nil {
|
|
t.Fatalf("EnsureDirectories returned error: %v", err)
|
|
}
|
|
|
|
app := &App{
|
|
config: cfg,
|
|
settingsOverridesPath: filepath.Join(cfg.DBDir, config.AdminSettingsOverrideFilename),
|
|
}
|
|
|
|
htmlTemplates, err := loadHTMLTemplates()
|
|
if err != nil {
|
|
t.Fatalf("loadHTMLTemplates returned error: %v", err)
|
|
}
|
|
|
|
router := gin.New()
|
|
router.SetHTMLTemplate(htmlTemplates)
|
|
admin := router.Group("/admin")
|
|
admin.GET("/login", app.handleAdminLogin)
|
|
protected := router.Group("/admin", app.adminAuthMiddleware)
|
|
protected.GET("/settings", app.handleAdminSettings)
|
|
protected.GET("/settings/export", app.handleAdminSettingsExport)
|
|
protected.POST("/settings/save", app.handleAdminSettingsSave)
|
|
protected.POST("/settings/import", app.handleAdminSettingsImport)
|
|
protected.POST("/settings/reset", app.handleAdminSettingsReset)
|
|
return app, router
|
|
}
|
|
|
|
func authCookie(app *App) *http.Cookie {
|
|
return &http.Cookie{Name: adminSessionCookie, Value: app.adminSessionToken()}
|
|
}
|
|
|
|
func clearAdminSettingsEnv(t *testing.T) {
|
|
t.Helper()
|
|
for _, name := range []string{
|
|
"WARPBOX_DATA_DIR",
|
|
"WARPBOX_ADMIN_PASSWORD",
|
|
"WARPBOX_ADMIN_USERNAME",
|
|
"WARPBOX_ADMIN_EMAIL",
|
|
"WARPBOX_ADMIN_ENABLED",
|
|
"WARPBOX_ALLOW_ADMIN_SETTINGS_OVERRIDE",
|
|
"WARPBOX_ADMIN_COOKIE_SECURE",
|
|
"WARPBOX_GUEST_UPLOADS_ENABLED",
|
|
"WARPBOX_API_ENABLED",
|
|
"WARPBOX_ZIP_DOWNLOADS_ENABLED",
|
|
"WARPBOX_ONE_TIME_DOWNLOADS_ENABLED",
|
|
"WARPBOX_ONE_TIME_DOWNLOAD_EXPIRY_SECONDS",
|
|
"WARPBOX_ONE_TIME_DOWNLOAD_RETRY_ON_FAILURE",
|
|
"WARPBOX_RENEW_ON_ACCESS_ENABLED",
|
|
"WARPBOX_RENEW_ON_DOWNLOAD_ENABLED",
|
|
"WARPBOX_DEFAULT_GUEST_EXPIRY_SECONDS",
|
|
"WARPBOX_MAX_GUEST_EXPIRY_SECONDS",
|
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_GB",
|
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_MB",
|
|
"WARPBOX_GLOBAL_MAX_FILE_SIZE_BYTES",
|
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_GB",
|
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_MB",
|
|
"WARPBOX_GLOBAL_MAX_BOX_SIZE_BYTES",
|
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_GB",
|
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_MB",
|
|
"WARPBOX_DEFAULT_USER_MAX_FILE_SIZE_BYTES",
|
|
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_GB",
|
|
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_MB",
|
|
"WARPBOX_DEFAULT_USER_MAX_BOX_SIZE_BYTES",
|
|
"WARPBOX_SESSION_TTL_SECONDS",
|
|
"WARPBOX_BOX_POLL_INTERVAL_MS",
|
|
"WARPBOX_THUMBNAIL_BATCH_SIZE",
|
|
"WARPBOX_THUMBNAIL_INTERVAL_SECONDS",
|
|
"WARPBOX_SECURITY_ENABLED",
|
|
"WARPBOX_SECURITY_IP_WHITELIST",
|
|
"WARPBOX_SECURITY_ADMIN_IP_WHITELIST",
|
|
"WARPBOX_TRUSTED_PROXY_CIDRS",
|
|
"WARPBOX_SECURITY_LOGIN_WINDOW_SECONDS",
|
|
"WARPBOX_SECURITY_LOGIN_MAX_ATTEMPTS",
|
|
"WARPBOX_SECURITY_BAN_SECONDS",
|
|
"WARPBOX_SECURITY_SCAN_WINDOW_SECONDS",
|
|
"WARPBOX_SECURITY_SCAN_MAX_ATTEMPTS",
|
|
"WARPBOX_SECURITY_UPLOAD_WINDOW_SECONDS",
|
|
"WARPBOX_SECURITY_UPLOAD_MAX_REQUESTS",
|
|
"WARPBOX_SECURITY_UPLOAD_MAX_GB",
|
|
"WARPBOX_SECURITY_UPLOAD_MAX_MB",
|
|
"WARPBOX_SECURITY_UPLOAD_MAX_BYTES",
|
|
} {
|
|
t.Setenv(name, "")
|
|
}
|
|
}
|
|
|
|
func TestAdminSettingsSaveRejectsInvalidTrustedProxyCIDR(t *testing.T) {
|
|
app, router := setupAdminSettingsTest(t)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/admin/settings/save", strings.NewReader(`{"values":{"trusted_proxy_cidrs":"not-a-cidr"}}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
request.AddCookie(authCookie(app))
|
|
response := httptest.NewRecorder()
|
|
router.ServeHTTP(response, request)
|
|
|
|
if response.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d", response.Code)
|
|
}
|
|
}
|