Files
seaweedfs/weed/command/admin.go
LeeXN c5f15aaa25 fix(admin): resolve login redirect loop in admin interface (#7272) (#7280)
- Configure proper cookie session options in admin server:
  * Set Path, MaxAge attributes
  * Ensure session cookies are correctly saved and retrieved

This resolves the issue where users entering correct admin credentials
would be redirected back to the login page due to improperly configured
session storage.

Fixes #7272
2025-09-30 20:20:40 -07:00

377 lines
11 KiB
Go

package command
import (
"context"
"crypto/rand"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"os/user"
"path/filepath"
"strings"
"syscall"
"time"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/spf13/viper"
"github.com/seaweedfs/seaweedfs/weed/admin"
"github.com/seaweedfs/seaweedfs/weed/admin/dash"
"github.com/seaweedfs/seaweedfs/weed/admin/handlers"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/security"
"github.com/seaweedfs/seaweedfs/weed/util"
)
var (
a AdminOptions
)
type AdminOptions struct {
port *int
grpcPort *int
masters *string
adminUser *string
adminPassword *string
dataDir *string
}
func init() {
cmdAdmin.Run = runAdmin // break init cycle
a.port = cmdAdmin.Flag.Int("port", 23646, "admin server port")
a.grpcPort = cmdAdmin.Flag.Int("port.grpc", 0, "gRPC server port for worker connections (default: http port + 10000)")
a.masters = cmdAdmin.Flag.String("masters", "localhost:9333", "comma-separated master servers")
a.dataDir = cmdAdmin.Flag.String("dataDir", "", "directory to store admin configuration and data files")
a.adminUser = cmdAdmin.Flag.String("adminUser", "admin", "admin interface username")
a.adminPassword = cmdAdmin.Flag.String("adminPassword", "", "admin interface password (if empty, auth is disabled)")
}
var cmdAdmin = &Command{
UsageLine: "admin -port=23646 -masters=localhost:9333 [-port.grpc=33646] [-dataDir=/path/to/data]",
Short: "start SeaweedFS web admin interface",
Long: `Start a web admin interface for SeaweedFS cluster management.
The admin interface provides a modern web interface for:
- Cluster topology visualization and monitoring
- Volume management and operations
- File browser and management
- System metrics and performance monitoring
- Configuration management
- Maintenance operations
The admin interface automatically discovers filers from the master servers.
A gRPC server for worker connections runs on the configured gRPC port (default: HTTP port + 10000).
Example Usage:
weed admin -port=23646 -masters="master1:9333,master2:9333"
weed admin -port=23646 -masters="localhost:9333" -dataDir="/var/lib/seaweedfs-admin"
weed admin -port=23646 -port.grpc=33646 -masters="localhost:9333" -dataDir="~/seaweedfs-admin"
weed admin -port=9900 -port.grpc=19900 -masters="localhost:9333"
Data Directory:
- If dataDir is specified, admin configuration and maintenance data is persisted
- The directory will be created if it doesn't exist
- Configuration files are stored in JSON format for easy editing
- Without dataDir, all configuration is kept in memory only
Authentication:
- If adminPassword is not set, the admin interface runs without authentication
- If adminPassword is set, users must login with adminUser/adminPassword
- Sessions are secured with auto-generated session keys
Security Configuration:
- The admin server reads TLS configuration from security.toml
- Configure [https.admin] section in security.toml for HTTPS support
- If https.admin.key is set, the server will start in TLS mode
- If https.admin.ca is set, mutual TLS authentication is enabled
- Set strong adminPassword for production deployments
- Configure firewall rules to restrict admin interface access
security.toml Example:
[https.admin]
cert = "/etc/ssl/admin.crt"
key = "/etc/ssl/admin.key"
ca = "/etc/ssl/ca.crt" # optional, for mutual TLS
Worker Communication:
- Workers connect via gRPC on HTTP port + 10000
- Workers use [grpc.admin] configuration from security.toml
- TLS is automatically used if certificates are configured
- Workers fall back to insecure connections if TLS is unavailable
Configuration File:
- The security.toml file is read from ".", "$HOME/.seaweedfs/",
"/usr/local/etc/seaweedfs/", or "/etc/seaweedfs/", in that order
- Generate example security.toml: weed scaffold -config=security
`,
}
func runAdmin(cmd *Command, args []string) bool {
// Load security configuration
util.LoadSecurityConfiguration()
// Validate required parameters
if *a.masters == "" {
fmt.Println("Error: masters parameter is required")
fmt.Println("Usage: weed admin -masters=master1:9333,master2:9333")
return false
}
// Validate that masters string can be parsed
masterAddresses := pb.ServerAddresses(*a.masters).ToAddresses()
if len(masterAddresses) == 0 {
fmt.Println("Error: no valid master addresses found")
fmt.Println("Usage: weed admin -masters=master1:9333,master2:9333")
return false
}
// Set default gRPC port if not specified
if *a.grpcPort == 0 {
*a.grpcPort = *a.port + 10000
}
// Security warnings
if *a.adminPassword == "" {
fmt.Println("WARNING: Admin interface is running without authentication!")
fmt.Println(" Set -adminPassword for production use")
}
fmt.Printf("Starting SeaweedFS Admin Interface on port %d\n", *a.port)
fmt.Printf("Worker gRPC server will run on port %d\n", *a.grpcPort)
fmt.Printf("Masters: %s\n", *a.masters)
fmt.Printf("Filers will be discovered automatically from masters\n")
if *a.dataDir != "" {
fmt.Printf("Data Directory: %s\n", *a.dataDir)
} else {
fmt.Printf("Data Directory: Not specified (configuration will be in-memory only)\n")
}
if *a.adminPassword != "" {
fmt.Printf("Authentication: Enabled (user: %s)\n", *a.adminUser)
} else {
fmt.Printf("Authentication: Disabled\n")
}
// Set up graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle interrupt signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigChan
fmt.Printf("\nReceived signal %v, shutting down gracefully...\n", sig)
cancel()
}()
// Start the admin server with all masters
err := startAdminServer(ctx, a)
if err != nil {
fmt.Printf("Admin server error: %v\n", err)
return false
}
fmt.Println("Admin server stopped")
return true
}
// startAdminServer starts the actual admin server
func startAdminServer(ctx context.Context, options AdminOptions) error {
// Set Gin mode
gin.SetMode(gin.ReleaseMode)
// Create router
r := gin.New()
r.Use(gin.Logger(), gin.Recovery())
// Session store - always auto-generate session key
sessionKeyBytes := make([]byte, 32)
_, err := rand.Read(sessionKeyBytes)
if err != nil {
return fmt.Errorf("failed to generate session key: %w", err)
}
store := cookie.NewStore(sessionKeyBytes)
// Configure session options to ensure cookies are properly saved
store.Options(sessions.Options{
Path: "/",
MaxAge: 3600 * 24, // 24 hours
})
r.Use(sessions.Sessions("admin-session", store))
// Static files - serve from embedded filesystem
staticFS, err := admin.GetStaticFS()
if err != nil {
log.Printf("Warning: Failed to load embedded static files: %v", err)
} else {
r.StaticFS("/static", http.FS(staticFS))
}
// Create data directory if specified
var dataDir string
if *options.dataDir != "" {
// Expand tilde (~) to home directory
expandedDir, err := expandHomeDir(*options.dataDir)
if err != nil {
return fmt.Errorf("failed to expand dataDir path %s: %v", *options.dataDir, err)
}
dataDir = expandedDir
// Show path expansion if it occurred
if dataDir != *options.dataDir {
fmt.Printf("Expanded dataDir: %s -> %s\n", *options.dataDir, dataDir)
}
if err := os.MkdirAll(dataDir, 0755); err != nil {
return fmt.Errorf("failed to create data directory %s: %v", dataDir, err)
}
fmt.Printf("Data directory created/verified: %s\n", dataDir)
}
// Create admin server
adminServer := dash.NewAdminServer(*options.masters, nil, dataDir)
// Show discovered filers
filers := adminServer.GetAllFilers()
if len(filers) > 0 {
fmt.Printf("Discovered filers: %s\n", strings.Join(filers, ", "))
} else {
fmt.Printf("No filers discovered from masters\n")
}
// Start worker gRPC server for worker connections
err = adminServer.StartWorkerGrpcServer(*options.grpcPort)
if err != nil {
return fmt.Errorf("failed to start worker gRPC server: %w", err)
}
// Set up cleanup for gRPC server
defer func() {
if stopErr := adminServer.StopWorkerGrpcServer(); stopErr != nil {
log.Printf("Error stopping worker gRPC server: %v", stopErr)
}
}()
// Create handlers and setup routes
adminHandlers := handlers.NewAdminHandlers(adminServer)
adminHandlers.SetupRoutes(r, *options.adminPassword != "", *options.adminUser, *options.adminPassword)
// Server configuration
addr := fmt.Sprintf(":%d", *options.port)
server := &http.Server{
Addr: addr,
Handler: r,
}
// Start server
go func() {
log.Printf("Starting SeaweedFS Admin Server on port %d", *options.port)
// start http or https server with security.toml
var (
clientCertFile,
certFile,
keyFile string
)
useTLS := false
useMTLS := false
if viper.GetString("https.admin.key") != "" {
useTLS = true
certFile = viper.GetString("https.admin.cert")
keyFile = viper.GetString("https.admin.key")
}
if viper.GetString("https.admin.ca") != "" {
useMTLS = true
clientCertFile = viper.GetString("https.admin.ca")
}
if useMTLS {
server.TLSConfig = security.LoadClientTLSHTTP(clientCertFile)
}
if useTLS {
log.Printf("Starting SeaweedFS Admin Server with TLS on port %d", *options.port)
err = server.ListenAndServeTLS(certFile, keyFile)
} else {
err = server.ListenAndServe()
}
if err != nil && err != http.ErrServerClosed {
log.Printf("Failed to start server: %v", err)
}
}()
// Wait for context cancellation
<-ctx.Done()
// Graceful shutdown
log.Println("Shutting down admin server...")
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("admin server forced to shutdown: %w", err)
}
return nil
}
// GetAdminOptions returns the admin command options for testing
func GetAdminOptions() *AdminOptions {
return &AdminOptions{}
}
// expandHomeDir expands the tilde (~) in a path to the user's home directory
func expandHomeDir(path string) (string, error) {
if path == "" {
return path, nil
}
if !strings.HasPrefix(path, "~") {
return path, nil
}
// Get current user
currentUser, err := user.Current()
if err != nil {
return "", fmt.Errorf("failed to get current user: %w", err)
}
// Handle different tilde patterns
if path == "~" {
return currentUser.HomeDir, nil
}
if strings.HasPrefix(path, "~/") {
return filepath.Join(currentUser.HomeDir, path[2:]), nil
}
// Handle ~username/ patterns
if strings.HasPrefix(path, "~") {
parts := strings.SplitN(path[1:], "/", 2)
username := parts[0]
targetUser, err := user.Lookup(username)
if err != nil {
return "", fmt.Errorf("user %s not found: %v", username, err)
}
if len(parts) == 1 {
return targetUser.HomeDir, nil
}
return filepath.Join(targetUser.HomeDir, parts[1]), nil
}
return path, nil
}