From 53342c9ba670fdd4ae9c6329f115d550480be169 Mon Sep 17 00:00:00 2001 From: Chris Lu Date: Fri, 19 Jun 2026 13:04:04 -0700 Subject: [PATCH] mini: resolve admin credentials from security.toml and env vars (#10021) * mini: resolve admin credentials from security.toml and env vars weed mini started the admin UI without resolving admin.user/admin.password (and the read-only pair) from security.toml [admin] or WEED_ADMIN_* env vars, so the only way to protect the UI was the -admin.password flag. The standalone weed admin command applies these fallbacks in runAdmin via applyViperFallback; the mini path calls startAdminServer directly and skipped it, leaving authRequired false and the UI unauthenticated. * mini: load admin.toml maintenance settings The mini admin path runs ApplyMaintenanceConfigFromToml (via startAdminServer) against the global viper, but runMini never merged admin.toml, so file-based maintenance task settings ([maintenance.vacuum], .balance, .erasure_coding) were ignored under mini while the standalone weed admin honored them. Load it alongside master/volume config. * mini: support -admin.urlPrefix for the admin UI Expose the reverse-proxy subdirectory prefix that the standalone weed admin already supports, so the mini admin UI can run under e.g. /seaweedfs. The prefix is normalized the same way and passed through to startAdminServer. --- weed/command/mini.go | 26 +++++++++++++++++- weed/command/mini_admin_auth_test.go | 41 ++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 weed/command/mini_admin_auth_test.go diff --git a/weed/command/mini.go b/weed/command/mini.go index 9a4e7451c..6fc47bc36 100644 --- a/weed/command/mini.go +++ b/weed/command/mini.go @@ -548,6 +548,7 @@ func initMiniAdminFlags() { miniAdminOptions.adminPassword = cmdMini.Flag.String("admin.password", "", "admin interface password (if empty, auth is disabled)") miniAdminOptions.readOnlyUser = cmdMini.Flag.String("admin.readOnlyUser", "", "read-only user username (optional, for view-only access)") miniAdminOptions.readOnlyPassword = cmdMini.Flag.String("admin.readOnlyPassword", "", "read-only user password (optional, for view-only access; requires admin.password to be set)") + miniAdminOptions.urlPrefix = cmdMini.Flag.String("admin.urlPrefix", "", "URL path prefix when running the admin UI behind a reverse proxy under a subdirectory (e.g. /seaweedfs)") } func init() { @@ -1164,6 +1165,7 @@ func runMini(cmd *Command, args []string) bool { util.LoadSecurityConfiguration() util.LoadConfiguration("master", false) util.LoadConfiguration("volume", false) + util.LoadConfiguration("admin", false) miniOptions.v.applyDiskIOProbeConfig() ensureMiniVolumeGrowthDefaults() @@ -1487,6 +1489,18 @@ func startS3Service() { miniS3Options.startS3Server() } +// applyMiniAdminCredentialFallback fills the admin credential flags from +// security.toml [admin] / WEED_ADMIN_* env vars when they were not set on the +// command line, mirroring the standalone `weed admin` command. CLI flags take +// precedence. Note the read-only viper keys (admin.readonly.*) differ from the +// mini flag names (admin.readOnly*). +func applyMiniAdminCredentialFallback(options *AdminOptions) { + applyViperFallback(cmdMini, options.adminUser, "admin.user", "admin.user") + applyViperFallback(cmdMini, options.adminPassword, "admin.password", "admin.password") + applyViperFallback(cmdMini, options.readOnlyUser, "admin.readOnlyUser", "admin.readonly.user") + applyViperFallback(cmdMini, options.readOnlyPassword, "admin.readOnlyPassword", "admin.readonly.password") +} + // startMiniAdminWithWorker starts the admin server with one worker func startMiniAdminWithWorker(allServicesReady chan struct{}) { defer close(allServicesReady) // Ensure channel is always closed on all paths @@ -1503,6 +1517,10 @@ func startMiniAdminWithWorker(allServicesReady chan struct{}) { // Set admin options *miniAdminOptions.master = masterAddr + // Resolve admin credentials from security.toml [admin] / WEED_ADMIN_* env + // vars, matching the standalone `weed admin` command. + applyMiniAdminCredentialFallback(&miniAdminOptions) + // Security validation: prevent empty username when password is set if *miniAdminOptions.adminPassword != "" && *miniAdminOptions.adminUser == "" { glog.Fatalf("Error: -admin.user cannot be empty when -admin.password is set") @@ -1532,6 +1550,12 @@ func startMiniAdminWithWorker(allServicesReady chan struct{}) { *miniAdminOptions.dataDir = filepath.Join(*miniDataFolders, "admin") } + // Normalize URL prefix the same way `weed admin` does. + urlPrefix := strings.TrimRight(*miniAdminOptions.urlPrefix, "/") + if urlPrefix != "" && !strings.HasPrefix(urlPrefix, "/") { + urlPrefix = "/" + urlPrefix + } + // Start admin server in background. trackMiniClient lets the Ctrl+C // handler wait for startAdminServer's graceful shutdown before filer/ // volume/master tear down. @@ -1546,7 +1570,7 @@ func startMiniAdminWithWorker(allServicesReady chan struct{}) { if miniS3Options.portIceberg != nil { icebergPort = *miniS3Options.portIceberg } - if err := startAdminServer(ctx, miniAdminOptions, *miniEnableAdminUI, icebergPort, ""); err != nil { + if err := startAdminServer(ctx, miniAdminOptions, *miniEnableAdminUI, icebergPort, urlPrefix); err != nil { glog.Errorf("Admin server error: %v", err) } }() diff --git a/weed/command/mini_admin_auth_test.go b/weed/command/mini_admin_auth_test.go new file mode 100644 index 000000000..ce487cdd9 --- /dev/null +++ b/weed/command/mini_admin_auth_test.go @@ -0,0 +1,41 @@ +package command + +import "testing" + +// weed mini must resolve admin credentials from security.toml [admin] / +// WEED_ADMIN_* env vars the same way the standalone `weed admin` command does. +// This exercises the production fallback so the flag-name -> viper-key mapping +// stays correct, in particular the read-only keys where the mini flag +// (admin.readOnlyUser) and viper key (admin.readonly.user) differ. +func TestApplyMiniAdminCredentialFallbackFromEnv(t *testing.T) { + adminUser, adminPassword, readOnlyUser, readOnlyPassword := "admin", "", "", "" + options := &AdminOptions{ + adminUser: &adminUser, + adminPassword: &adminPassword, + readOnlyUser: &readOnlyUser, + readOnlyPassword: &readOnlyPassword, + } + + t.Setenv("WEED_ADMIN_USER", "env-admin") + t.Setenv("WEED_ADMIN_PASSWORD", "env-secret") + t.Setenv("WEED_ADMIN_READONLY_USER", "env-ro") + t.Setenv("WEED_ADMIN_READONLY_PASSWORD", "env-ro-secret") + + applyMiniAdminCredentialFallback(options) + + checks := []struct { + name string + got string + want string + }{ + {"adminUser", *options.adminUser, "env-admin"}, + {"adminPassword", *options.adminPassword, "env-secret"}, + {"readOnlyUser", *options.readOnlyUser, "env-ro"}, + {"readOnlyPassword", *options.readOnlyPassword, "env-ro-secret"}, + } + for _, c := range checks { + if c.got != c.want { + t.Errorf("%s = %q, want %q", c.name, c.got, c.want) + } + } +}