diff --git a/server/server.go b/server/server.go index 0c04348..670027b 100644 --- a/server/server.go +++ b/server/server.go @@ -8,15 +8,18 @@ import ( "path/filepath" "sync" "time" + "encoding/json" "github.com/dgraph-io/badger/v4" "github.com/robfig/cron/v3" "github.com/sirupsen/logrus" + "github.com/google/uuid" "kvs/auth" "kvs/cluster" "kvs/storage" "kvs/types" + "kvs/utils" ) // Server represents the KVS node @@ -117,6 +120,100 @@ func NewServer(config *types.Config) (*Server, error) { // Initialize authentication service server.authService = auth.NewAuthService(db, logger) + // New: Initial root account setup for empty DB with no seeds + if len(config.SeedNodes) == 0 { + hasUsers, err := server.authService.HasUsers() + if err != nil { + return nil, fmt.Errorf("failed to check for existing users: %v", err) + } + if !hasUsers { + server.logger.Info("Detected empty database with no seed nodes; creating initial root account") + + now := time.Now().Unix() + + // Create admin group + adminGroupUUID := uuid.NewString() + hashedGroupName := utils.HashGroupName("admin") + adminGroup := types.Group{ + UUID: adminGroupUUID, + NameHash: hashedGroupName, + Members: []string{}, + CreatedAt: now, + UpdatedAt: now, + } + groupData, err := json.Marshal(adminGroup) + if err != nil { + return nil, fmt.Errorf("failed to marshal admin group: %v", err) + } + err = db.Update(func(txn *badger.Txn) error { + return txn.Set([]byte(auth.GroupStorageKey(adminGroupUUID)), groupData) + }) + if err != nil { + return nil, fmt.Errorf("failed to store admin group: %v", err) + } + + // Create root user + rootUUID := uuid.NewString() + hashedNickname := utils.HashUserNickname("root") + rootUser := types.User{ + UUID: rootUUID, + NicknameHash: hashedNickname, + Groups: []string{adminGroupUUID}, + CreatedAt: now, + UpdatedAt: now, + } + userData, err := json.Marshal(rootUser) + if err != nil { + return nil, fmt.Errorf("failed to marshal root user: %v", err) + } + err = db.Update(func(txn *badger.Txn) error { + return txn.Set([]byte(auth.UserStorageKey(rootUUID)), userData) + }) + if err != nil { + return nil, fmt.Errorf("failed to store root user: %v", err) + } + + // Update admin group to include root user (bidirectional) + adminGroup.Members = append(adminGroup.Members, rootUUID) + groupData, err = json.Marshal(adminGroup) + if err != nil { + return nil, fmt.Errorf("failed to marshal updated admin group: %v", err) + } + err = db.Update(func(txn *badger.Txn) error { + return txn.Set([]byte(auth.GroupStorageKey(adminGroupUUID)), groupData) + }) + if err != nil { + return nil, fmt.Errorf("failed to update admin group: %v", err) + } + + // Generate and store API token + scopes := []string{"admin", "read", "write", "create", "delete"} + expirationHours := 8760 // 1 year + tokenString, expiresAt, err := auth.GenerateJWT(rootUUID, scopes, expirationHours) + if err != nil { + return nil, fmt.Errorf("failed to generate JWT: %v", err) + } + err = server.authService.StoreAPIToken(tokenString, rootUUID, scopes, expiresAt) + if err != nil { + return nil, fmt.Errorf("failed to store API token: %v", err) + } + + // Log the details securely (only once, to stderr) + fmt.Fprintf(os.Stderr, ` +*************************************************************************** +WARNING: Initial root user created for new server instance. +Save this information securely—it will not be shown again. + +Root User UUID: %s +API Token (Bearer): %s +Expires At: %s (UTC) + +Use this token for authentication in API requests. Change or revoke it immediately via the API for security. +*************************************************************************** +`, rootUUID, tokenString, time.Unix(expiresAt, 0).UTC()) + } + } + // Initialize Merkle tree using cluster service if err := server.syncService.InitializeMerkleTree(); err != nil { return nil, fmt.Errorf("failed to initialize Merkle tree: %v", err)