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.
This commit is contained in:
Chris Lu
2026-06-19 13:04:04 -07:00
committed by GitHub
parent df1a25fd3e
commit 53342c9ba6
2 changed files with 66 additions and 1 deletions
+25 -1
View File
@@ -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)
}
}()
+41
View File
@@ -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)
}
}
}