Browse Source

Add read only user (#7862)

* add readonly user

* add args

* address comments

* avoid same user name

* Prevents timing attacks

* doc

---------

Co-authored-by: Chris Lu <chris.lu@gmail.com>
pull/7183/merge
Deyu Han 2 days ago
committed by GitHub
parent
commit
225e3d0302
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 4
      weed/admin/Makefile
  2. 19
      weed/admin/dash/auth_middleware.go
  3. 51
      weed/admin/dash/middleware.go
  4. 61
      weed/admin/handlers/admin_handlers.go
  5. 13
      weed/admin/handlers/auth_handlers.go
  6. 52
      weed/command/admin.go
  7. 21
      weed/command/mini.go

4
weed/admin/Makefile

@ -160,6 +160,4 @@ $(WEED_BINARY): $(TEMPL_GO_FILES) $(GO_FILES)
# Auto-generate templ files when .templ files change # Auto-generate templ files when .templ files change
%_templ.go: %.templ %_templ.go: %.templ
@echo "Regenerating $@ from $<" @echo "Regenerating $@ from $<"
@templ generate
.PHONY: $(TEMPL_GO_FILES)
@templ generate

19
weed/admin/dash/auth_middleware.go

@ -1,6 +1,7 @@
package dash package dash
import ( import (
"crypto/subtle"
"net/http" "net/http"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
@ -25,17 +26,31 @@ func (s *AdminServer) ShowLogin(c *gin.Context) {
} }
// HandleLogin handles login form submission // HandleLogin handles login form submission
func (s *AdminServer) HandleLogin(username, password string) gin.HandlerFunc {
func (s *AdminServer) HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
loginUsername := c.PostForm("username") loginUsername := c.PostForm("username")
loginPassword := c.PostForm("password") loginPassword := c.PostForm("password")
if loginUsername == username && loginPassword == password {
var role string
var authenticated bool
// Check admin credentials
if adminPassword != "" && loginUsername == adminUser && subtle.ConstantTimeCompare([]byte(loginPassword), []byte(adminPassword)) == 1 {
role = "admin"
authenticated = true
} else if readOnlyPassword != "" && loginUsername == readOnlyUser && subtle.ConstantTimeCompare([]byte(loginPassword), []byte(readOnlyPassword)) == 1 {
// Check read-only credentials
role = "readonly"
authenticated = true
}
if authenticated {
session := sessions.Default(c) session := sessions.Default(c)
// Clear any existing invalid session data before setting new values // Clear any existing invalid session data before setting new values
session.Clear() session.Clear()
session.Set("authenticated", true) session.Set("authenticated", true)
session.Set("username", loginUsername) session.Set("username", loginUsername)
session.Set("role", role)
if err := session.Save(); err != nil { if err := session.Save(); err != nil {
// Log the detailed error server-side for diagnostics // Log the detailed error server-side for diagnostics
glog.Errorf("Failed to save session for user %s: %v", loginUsername, err) glog.Errorf("Failed to save session for user %s: %v", loginUsername, err)

51
weed/admin/dash/middleware.go

@ -2,17 +2,30 @@ package dash
import ( import (
"net/http" "net/http"
"strings"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// setAuthContext sets username and role in context for use in handlers
func setAuthContext(c *gin.Context, username, role interface{}) {
c.Set("username", username)
if role != nil {
c.Set("role", role)
} else {
// Default to admin for backward compatibility
c.Set("role", "admin")
}
}
// RequireAuth checks if user is authenticated // RequireAuth checks if user is authenticated
func RequireAuth() gin.HandlerFunc { func RequireAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
session := sessions.Default(c) session := sessions.Default(c)
authenticated := session.Get("authenticated") authenticated := session.Get("authenticated")
username := session.Get("username") username := session.Get("username")
role := session.Get("role")
if authenticated != true || username == nil { if authenticated != true || username == nil {
c.Redirect(http.StatusTemporaryRedirect, "/login") c.Redirect(http.StatusTemporaryRedirect, "/login")
@ -20,8 +33,8 @@ func RequireAuth() gin.HandlerFunc {
return return
} }
// Set username in context for use in handlers
c.Set("username", username)
// Set username and role in context for use in handlers
setAuthContext(c, username, role)
c.Next() c.Next()
} }
} }
@ -33,6 +46,7 @@ func RequireAuthAPI() gin.HandlerFunc {
session := sessions.Default(c) session := sessions.Default(c)
authenticated := session.Get("authenticated") authenticated := session.Get("authenticated")
username := session.Get("username") username := session.Get("username")
role := session.Get("role")
if authenticated != true || username == nil { if authenticated != true || username == nil {
c.JSON(http.StatusUnauthorized, gin.H{ c.JSON(http.StatusUnauthorized, gin.H{
@ -43,8 +57,37 @@ func RequireAuthAPI() gin.HandlerFunc {
return return
} }
// Set username in context for use in handlers
c.Set("username", username)
// Set username and role in context for use in handlers
setAuthContext(c, username, role)
c.Next()
}
}
// RequireWriteAccess checks if user has admin role (write access)
// Returns JSON error for API endpoints, redirects for HTML endpoints
func RequireWriteAccess() gin.HandlerFunc {
return func(c *gin.Context) {
role, exists := c.Get("role")
if !exists {
role = "admin" // Default for backward compatibility
}
roleStr, ok := role.(string)
if !ok || roleStr != "admin" {
// Check if this is an API request (path starts with /api) or HTML request
path := c.Request.URL.Path
if strings.HasPrefix(path, "/api") {
c.JSON(http.StatusForbidden, gin.H{
"error": "Insufficient permissions",
"message": "This operation requires admin access. Read-only users can only view data.",
})
} else {
c.Redirect(http.StatusSeeOther, "/admin?error=Insufficient permissions")
}
c.Abort()
return
}
c.Next() c.Next()
} }
} }

61
weed/admin/handlers/admin_handlers.go

@ -5,9 +5,11 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/seaweedfs/seaweedfs/weed/admin/dash" "github.com/seaweedfs/seaweedfs/weed/admin/dash"
"github.com/seaweedfs/seaweedfs/weed/admin/view/app" "github.com/seaweedfs/seaweedfs/weed/admin/view/app"
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
"github.com/seaweedfs/seaweedfs/weed/stats"
) )
// AdminHandlers contains all the HTTP handlers for the admin interface // AdminHandlers contains all the HTTP handlers for the admin interface
@ -44,10 +46,13 @@ func NewAdminHandlers(adminServer *dash.AdminServer) *AdminHandlers {
} }
// SetupRoutes configures all the routes for the admin interface // SetupRoutes configures all the routes for the admin interface
func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username, password string) {
func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, adminUser, adminPassword, readOnlyUser, readOnlyPassword string) {
// Health check (no auth required) // Health check (no auth required)
r.GET("/health", h.HealthCheck) r.GET("/health", h.HealthCheck)
// Prometheus metrics endpoint (no auth required)
r.GET("/metrics", gin.WrapH(promhttp.HandlerFor(stats.Gather, promhttp.HandlerOpts{})))
// Favicon route (no auth required) - redirect to static version // Favicon route (no auth required) - redirect to static version
r.GET("/favicon.ico", func(c *gin.Context) { r.GET("/favicon.ico", func(c *gin.Context) {
c.Redirect(http.StatusMovedPermanently, "/static/favicon.ico") c.Redirect(http.StatusMovedPermanently, "/static/favicon.ico")
@ -56,7 +61,7 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
if authRequired { if authRequired {
// Authentication routes (no auth required) // Authentication routes (no auth required)
r.GET("/login", h.authHandlers.ShowLogin) r.GET("/login", h.authHandlers.ShowLogin)
r.POST("/login", h.authHandlers.HandleLogin(username, password))
r.POST("/login", h.authHandlers.HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword))
r.GET("/logout", h.authHandlers.HandleLogout) r.GET("/logout", h.authHandlers.HandleLogout)
// Protected routes group // Protected routes group
@ -96,9 +101,9 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
protected.GET("/maintenance", h.maintenanceHandlers.ShowMaintenanceQueue) protected.GET("/maintenance", h.maintenanceHandlers.ShowMaintenanceQueue)
protected.GET("/maintenance/workers", h.maintenanceHandlers.ShowMaintenanceWorkers) protected.GET("/maintenance/workers", h.maintenanceHandlers.ShowMaintenanceWorkers)
protected.GET("/maintenance/config", h.maintenanceHandlers.ShowMaintenanceConfig) protected.GET("/maintenance/config", h.maintenanceHandlers.ShowMaintenanceConfig)
protected.POST("/maintenance/config", h.maintenanceHandlers.UpdateMaintenanceConfig)
protected.POST("/maintenance/config", dash.RequireWriteAccess(), h.maintenanceHandlers.UpdateMaintenanceConfig)
protected.GET("/maintenance/config/:taskType", h.maintenanceHandlers.ShowTaskConfig) protected.GET("/maintenance/config/:taskType", h.maintenanceHandlers.ShowTaskConfig)
protected.POST("/maintenance/config/:taskType", h.maintenanceHandlers.UpdateTaskConfig)
protected.POST("/maintenance/config/:taskType", dash.RequireWriteAccess(), h.maintenanceHandlers.UpdateTaskConfig)
protected.GET("/maintenance/tasks/:id", h.maintenanceHandlers.ShowTaskDetail) protected.GET("/maintenance/tasks/:id", h.maintenanceHandlers.ShowTaskDetail)
// API routes for AJAX calls // API routes for AJAX calls
@ -115,45 +120,45 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
s3Api := api.Group("/s3") s3Api := api.Group("/s3")
{ {
s3Api.GET("/buckets", h.adminServer.ListBucketsAPI) s3Api.GET("/buckets", h.adminServer.ListBucketsAPI)
s3Api.POST("/buckets", h.adminServer.CreateBucket)
s3Api.DELETE("/buckets/:bucket", h.adminServer.DeleteBucket)
s3Api.POST("/buckets", dash.RequireWriteAccess(), h.adminServer.CreateBucket)
s3Api.DELETE("/buckets/:bucket", dash.RequireWriteAccess(), h.adminServer.DeleteBucket)
s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails) s3Api.GET("/buckets/:bucket", h.adminServer.ShowBucketDetails)
s3Api.PUT("/buckets/:bucket/quota", h.adminServer.UpdateBucketQuota)
s3Api.PUT("/buckets/:bucket/owner", h.adminServer.UpdateBucketOwner)
s3Api.PUT("/buckets/:bucket/quota", dash.RequireWriteAccess(), h.adminServer.UpdateBucketQuota)
s3Api.PUT("/buckets/:bucket/owner", dash.RequireWriteAccess(), h.adminServer.UpdateBucketOwner)
} }
// User management API routes // User management API routes
usersApi := api.Group("/users") usersApi := api.Group("/users")
{ {
usersApi.GET("", h.userHandlers.GetUsers) usersApi.GET("", h.userHandlers.GetUsers)
usersApi.POST("", h.userHandlers.CreateUser)
usersApi.POST("", dash.RequireWriteAccess(), h.userHandlers.CreateUser)
usersApi.GET("/:username", h.userHandlers.GetUserDetails) usersApi.GET("/:username", h.userHandlers.GetUserDetails)
usersApi.PUT("/:username", h.userHandlers.UpdateUser)
usersApi.DELETE("/:username", h.userHandlers.DeleteUser)
usersApi.POST("/:username/access-keys", h.userHandlers.CreateAccessKey)
usersApi.DELETE("/:username/access-keys/:accessKeyId", h.userHandlers.DeleteAccessKey)
usersApi.PUT("/:username", dash.RequireWriteAccess(), h.userHandlers.UpdateUser)
usersApi.DELETE("/:username", dash.RequireWriteAccess(), h.userHandlers.DeleteUser)
usersApi.POST("/:username/access-keys", dash.RequireWriteAccess(), h.userHandlers.CreateAccessKey)
usersApi.DELETE("/:username/access-keys/:accessKeyId", dash.RequireWriteAccess(), h.userHandlers.DeleteAccessKey)
usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies) usersApi.GET("/:username/policies", h.userHandlers.GetUserPolicies)
usersApi.PUT("/:username/policies", h.userHandlers.UpdateUserPolicies)
usersApi.PUT("/:username/policies", dash.RequireWriteAccess(), h.userHandlers.UpdateUserPolicies)
} }
// Object Store Policy management API routes // Object Store Policy management API routes
objectStorePoliciesApi := api.Group("/object-store/policies") objectStorePoliciesApi := api.Group("/object-store/policies")
{ {
objectStorePoliciesApi.GET("", h.policyHandlers.GetPolicies) objectStorePoliciesApi.GET("", h.policyHandlers.GetPolicies)
objectStorePoliciesApi.POST("", h.policyHandlers.CreatePolicy)
objectStorePoliciesApi.POST("", dash.RequireWriteAccess(), h.policyHandlers.CreatePolicy)
objectStorePoliciesApi.GET("/:name", h.policyHandlers.GetPolicy) objectStorePoliciesApi.GET("/:name", h.policyHandlers.GetPolicy)
objectStorePoliciesApi.PUT("/:name", h.policyHandlers.UpdatePolicy)
objectStorePoliciesApi.DELETE("/:name", h.policyHandlers.DeletePolicy)
objectStorePoliciesApi.PUT("/:name", dash.RequireWriteAccess(), h.policyHandlers.UpdatePolicy)
objectStorePoliciesApi.DELETE("/:name", dash.RequireWriteAccess(), h.policyHandlers.DeletePolicy)
objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy) objectStorePoliciesApi.POST("/validate", h.policyHandlers.ValidatePolicy)
} }
// File management API routes // File management API routes
filesApi := api.Group("/files") filesApi := api.Group("/files")
{ {
filesApi.DELETE("/delete", h.fileBrowserHandlers.DeleteFile)
filesApi.DELETE("/delete-multiple", h.fileBrowserHandlers.DeleteMultipleFiles)
filesApi.POST("/create-folder", h.fileBrowserHandlers.CreateFolder)
filesApi.POST("/upload", h.fileBrowserHandlers.UploadFile)
filesApi.DELETE("/delete", dash.RequireWriteAccess(), h.fileBrowserHandlers.DeleteFile)
filesApi.DELETE("/delete-multiple", dash.RequireWriteAccess(), h.fileBrowserHandlers.DeleteMultipleFiles)
filesApi.POST("/create-folder", dash.RequireWriteAccess(), h.fileBrowserHandlers.CreateFolder)
filesApi.POST("/upload", dash.RequireWriteAccess(), h.fileBrowserHandlers.UploadFile)
filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile) filesApi.GET("/download", h.fileBrowserHandlers.DownloadFile)
filesApi.GET("/view", h.fileBrowserHandlers.ViewFile) filesApi.GET("/view", h.fileBrowserHandlers.ViewFile)
filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties) filesApi.GET("/properties", h.fileBrowserHandlers.GetFileProperties)
@ -162,32 +167,32 @@ func (h *AdminHandlers) SetupRoutes(r *gin.Engine, authRequired bool, username,
// Volume management API routes // Volume management API routes
volumeApi := api.Group("/volumes") volumeApi := api.Group("/volumes")
{ {
volumeApi.POST("/:id/:server/vacuum", h.clusterHandlers.VacuumVolume)
volumeApi.POST("/:id/:server/vacuum", dash.RequireWriteAccess(), h.clusterHandlers.VacuumVolume)
} }
// Maintenance API routes // Maintenance API routes
maintenanceApi := api.Group("/maintenance") maintenanceApi := api.Group("/maintenance")
{ {
maintenanceApi.POST("/scan", h.adminServer.TriggerMaintenanceScan)
maintenanceApi.POST("/scan", dash.RequireWriteAccess(), h.adminServer.TriggerMaintenanceScan)
maintenanceApi.GET("/tasks", h.adminServer.GetMaintenanceTasks) maintenanceApi.GET("/tasks", h.adminServer.GetMaintenanceTasks)
maintenanceApi.GET("/tasks/:id", h.adminServer.GetMaintenanceTask) maintenanceApi.GET("/tasks/:id", h.adminServer.GetMaintenanceTask)
maintenanceApi.GET("/tasks/:id/detail", h.adminServer.GetMaintenanceTaskDetailAPI) maintenanceApi.GET("/tasks/:id/detail", h.adminServer.GetMaintenanceTaskDetailAPI)
maintenanceApi.POST("/tasks/:id/cancel", h.adminServer.CancelMaintenanceTask)
maintenanceApi.POST("/tasks/:id/cancel", dash.RequireWriteAccess(), h.adminServer.CancelMaintenanceTask)
maintenanceApi.GET("/workers", h.adminServer.GetMaintenanceWorkersAPI) maintenanceApi.GET("/workers", h.adminServer.GetMaintenanceWorkersAPI)
maintenanceApi.GET("/workers/:id", h.adminServer.GetMaintenanceWorker) maintenanceApi.GET("/workers/:id", h.adminServer.GetMaintenanceWorker)
maintenanceApi.GET("/workers/:id/logs", h.adminServer.GetWorkerLogs) maintenanceApi.GET("/workers/:id/logs", h.adminServer.GetWorkerLogs)
maintenanceApi.GET("/stats", h.adminServer.GetMaintenanceStats) maintenanceApi.GET("/stats", h.adminServer.GetMaintenanceStats)
maintenanceApi.GET("/config", h.adminServer.GetMaintenanceConfigAPI) maintenanceApi.GET("/config", h.adminServer.GetMaintenanceConfigAPI)
maintenanceApi.PUT("/config", h.adminServer.UpdateMaintenanceConfigAPI)
maintenanceApi.PUT("/config", dash.RequireWriteAccess(), h.adminServer.UpdateMaintenanceConfigAPI)
} }
// Message Queue API routes // Message Queue API routes
mqApi := api.Group("/mq") mqApi := api.Group("/mq")
{ {
mqApi.GET("/topics/:namespace/:topic", h.mqHandlers.GetTopicDetailsAPI) mqApi.GET("/topics/:namespace/:topic", h.mqHandlers.GetTopicDetailsAPI)
mqApi.POST("/topics/create", h.mqHandlers.CreateTopicAPI)
mqApi.POST("/topics/retention/update", h.mqHandlers.UpdateTopicRetentionAPI)
mqApi.POST("/retention/purge", h.adminServer.TriggerTopicRetentionPurgeAPI)
mqApi.POST("/topics/create", dash.RequireWriteAccess(), h.mqHandlers.CreateTopicAPI)
mqApi.POST("/topics/retention/update", dash.RequireWriteAccess(), h.mqHandlers.UpdateTopicRetentionAPI)
mqApi.POST("/retention/purge", dash.RequireWriteAccess(), h.adminServer.TriggerTopicRetentionPurgeAPI)
} }
} }
} else { } else {

13
weed/admin/handlers/auth_handlers.go

@ -3,6 +3,7 @@ package handlers
import ( import (
"net/http" "net/http"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/seaweedfs/seaweedfs/weed/admin/dash" "github.com/seaweedfs/seaweedfs/weed/admin/dash"
"github.com/seaweedfs/seaweedfs/weed/admin/view/layout" "github.com/seaweedfs/seaweedfs/weed/admin/view/layout"
@ -22,6 +23,14 @@ func NewAuthHandlers(adminServer *dash.AdminServer) *AuthHandlers {
// ShowLogin displays the login page // ShowLogin displays the login page
func (a *AuthHandlers) ShowLogin(c *gin.Context) { func (a *AuthHandlers) ShowLogin(c *gin.Context) {
session := sessions.Default(c)
// If already authenticated, redirect to admin
if session.Get("authenticated") == true {
c.Redirect(http.StatusSeeOther, "/admin")
return
}
errorMessage := c.Query("error") errorMessage := c.Query("error")
// Render login template // Render login template
@ -35,8 +44,8 @@ func (a *AuthHandlers) ShowLogin(c *gin.Context) {
} }
// HandleLogin handles login form submission // HandleLogin handles login form submission
func (a *AuthHandlers) HandleLogin(username, password string) gin.HandlerFunc {
return a.adminServer.HandleLogin(username, password)
func (a *AuthHandlers) HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword string) gin.HandlerFunc {
return a.adminServer.HandleLogin(adminUser, adminPassword, readOnlyUser, readOnlyPassword)
} }
// HandleLogout handles user logout // HandleLogout handles user logout

52
weed/command/admin.go

@ -33,13 +33,15 @@ var (
) )
type AdminOptions struct { type AdminOptions struct {
port *int
grpcPort *int
master *string
masters *string // deprecated, for backward compatibility
adminUser *string
adminPassword *string
dataDir *string
port *int
grpcPort *int
master *string
masters *string // deprecated, for backward compatibility
adminUser *string
adminPassword *string
readOnlyUser *string
readOnlyPassword *string
dataDir *string
} }
func init() { func init() {
@ -52,6 +54,8 @@ func init() {
a.adminUser = cmdAdmin.Flag.String("adminUser", "admin", "admin interface username") a.adminUser = cmdAdmin.Flag.String("adminUser", "admin", "admin interface username")
a.adminPassword = cmdAdmin.Flag.String("adminPassword", "", "admin interface password (if empty, auth is disabled)") a.adminPassword = cmdAdmin.Flag.String("adminPassword", "", "admin interface password (if empty, auth is disabled)")
a.readOnlyUser = cmdAdmin.Flag.String("readOnlyUser", "", "read-only user username (optional, for view-only access)")
a.readOnlyPassword = cmdAdmin.Flag.String("readOnlyPassword", "", "read-only user password (optional, for view-only access; requires adminPassword to be set)")
} }
var cmdAdmin = &Command{ var cmdAdmin = &Command{
@ -84,7 +88,11 @@ var cmdAdmin = &Command{
Authentication: Authentication:
- If adminPassword is not set, the admin interface runs without authentication - If adminPassword is not set, the admin interface runs without authentication
- If adminPassword is set, users must login with adminUser/adminPassword
- If adminPassword is set, users must login with adminUser/adminPassword (full access)
- Optional read-only access: set readOnlyUser and readOnlyPassword for view-only access
- Read-only users can view cluster status and configurations but cannot make changes
- IMPORTANT: When read-only credentials are configured, adminPassword MUST also be set
- This ensures an admin account exists to manage and authorize read-only access
- Sessions are secured with auto-generated session keys - Sessions are secured with auto-generated session keys
Security Configuration: Security Configuration:
@ -139,6 +147,26 @@ func runAdmin(cmd *Command, args []string) bool {
return false return false
} }
// Security validation: prevent empty username when password is set
if *a.adminPassword != "" && *a.adminUser == "" {
fmt.Println("Error: -adminUser cannot be empty when -adminPassword is set")
return false
}
if *a.readOnlyPassword != "" && *a.readOnlyUser == "" {
fmt.Println("Error: -readOnlyUser is required when -readOnlyPassword is set")
return false
}
// Security validation: prevent username conflicts between admin and read-only users
if *a.adminUser != "" && *a.readOnlyUser != "" && *a.adminUser == *a.readOnlyUser {
fmt.Println("Error: -adminUser and -readOnlyUser must be different when both are configured")
return false
}
// Security validation: admin password is required for read-only user
if *a.readOnlyPassword != "" && *a.adminPassword == "" {
fmt.Println("Error: -adminPassword must be set when -readOnlyPassword is configured")
return false
}
// Set default gRPC port if not specified // Set default gRPC port if not specified
if *a.grpcPort == 0 { if *a.grpcPort == 0 {
*a.grpcPort = *a.port + 10000 *a.grpcPort = *a.port + 10000
@ -160,7 +188,10 @@ func runAdmin(cmd *Command, args []string) bool {
fmt.Printf("Data Directory: Not specified (configuration will be in-memory only)\n") fmt.Printf("Data Directory: Not specified (configuration will be in-memory only)\n")
} }
if *a.adminPassword != "" { if *a.adminPassword != "" {
fmt.Printf("Authentication: Enabled (user: %s)\n", *a.adminUser)
fmt.Printf("Authentication: Enabled (admin user: %s)\n", *a.adminUser)
if *a.readOnlyPassword != "" {
fmt.Printf("Read-only access: Enabled (read-only user: %s)\n", *a.readOnlyUser)
}
} else { } else {
fmt.Printf("Authentication: Disabled\n") fmt.Printf("Authentication: Disabled\n")
} }
@ -274,8 +305,9 @@ func startAdminServer(ctx context.Context, options AdminOptions) error {
}() }()
// Create handlers and setup routes // Create handlers and setup routes
authRequired := *options.adminPassword != ""
adminHandlers := handlers.NewAdminHandlers(adminServer) adminHandlers := handlers.NewAdminHandlers(adminServer)
adminHandlers.SetupRoutes(r, *options.adminPassword != "", *options.adminUser, *options.adminPassword)
adminHandlers.SetupRoutes(r, authRequired, *options.adminUser, *options.adminPassword, *options.readOnlyUser, *options.readOnlyPassword)
// Server configuration // Server configuration
addr := fmt.Sprintf(":%d", *options.port) addr := fmt.Sprintf(":%d", *options.port)

21
weed/command/mini.go

@ -72,7 +72,7 @@ This command starts all components in one process (master, volume, filer,
S3 gateway, WebDAV gateway, and Admin UI). S3 gateway, WebDAV gateway, and Admin UI).
All settings are optimized for small/dev use cases: All settings are optimized for small/dev use cases:
- Volume size limit: 128MB (small files)
- Volume size limit: auto configured based on disk space (64MB-1024MB)
- Volume max: 0 (auto-configured based on free disk space) - Volume max: 0 (auto-configured based on free disk space)
- Pre-stop seconds: 1 (faster shutdown) - Pre-stop seconds: 1 (faster shutdown)
- Master peers: none (single master mode) - Master peers: none (single master mode)
@ -260,6 +260,8 @@ func initMiniAdminFlags() {
miniAdminOptions.dataDir = cmdMini.Flag.String("admin.dataDir", "", "directory to store admin configuration and data files") miniAdminOptions.dataDir = cmdMini.Flag.String("admin.dataDir", "", "directory to store admin configuration and data files")
miniAdminOptions.adminUser = cmdMini.Flag.String("admin.user", "admin", "admin interface username") miniAdminOptions.adminUser = cmdMini.Flag.String("admin.user", "admin", "admin interface username")
miniAdminOptions.adminPassword = cmdMini.Flag.String("admin.password", "", "admin interface password (if empty, auth is disabled)") 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)")
} }
func init() { func init() {
@ -921,6 +923,23 @@ func startMiniAdminWithWorker(allServicesReady chan struct{}) {
// Set admin options // Set admin options
*miniAdminOptions.master = masterAddr *miniAdminOptions.master = masterAddr
// 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")
}
if *miniAdminOptions.readOnlyPassword != "" && *miniAdminOptions.readOnlyUser == "" {
glog.Fatalf("Error: -admin.readOnlyUser is required when -admin.readOnlyPassword is set")
}
// Security validation: prevent username conflicts between admin and read-only users
if *miniAdminOptions.adminUser != "" && *miniAdminOptions.readOnlyUser != "" &&
*miniAdminOptions.adminUser == *miniAdminOptions.readOnlyUser {
glog.Fatalf("Error: -admin.user and -admin.readOnlyUser must be different when both are configured")
}
// Security validation: admin password is required for read-only user
if *miniAdminOptions.readOnlyPassword != "" && *miniAdminOptions.adminPassword == "" {
glog.Fatalf("Error: -admin.password must be set when -admin.readOnlyPassword is configured")
}
// gRPC port should have been initialized by ensureAllPortsAvailableOnIP in runMini // gRPC port should have been initialized by ensureAllPortsAvailableOnIP in runMini
// If it's still 0, that indicates a problem with the port initialization sequence // If it's still 0, that indicates a problem with the port initialization sequence
if *miniAdminOptions.grpcPort == 0 { if *miniAdminOptions.grpcPort == 0 {

Loading…
Cancel
Save