From 3aff0ab5efcad44673ebf971c2254ee966a42447 Mon Sep 17 00:00:00 2001 From: ryyst Date: Sun, 21 Sep 2025 00:06:31 +0300 Subject: [PATCH] feat: implement issue #3 - autogenerated root account for initial setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add HasUsers() method to AuthService to check for existing users - Add setupRootAccount() logic that only triggers when: - No users exist in database AND no seed nodes are configured - AuthEnabled is true (respects feature toggle) - Create root user with UUID, admin group, and comprehensive scopes - Generate 24-hour JWT token with full administrative permissions - Display token prominently on console for initial setup - Prevent duplicate root account creation on subsequent starts - Skip root account creation in cluster mode (with seed nodes) Root account includes all administrative scopes: - admin:users:*, admin:groups:*, admin:tokens:* - Standard read/write/delete permissions This resolves the bootstrap problem for authentication-enabled deployments and provides secure initial access for administrative operations. šŸ¤– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- auth/auth.go | 23 ++++++++ issues/3.md | 2 +- server/server.go | 146 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) diff --git a/auth/auth.go b/auth/auth.go index bbb475a..db66440 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -202,4 +202,27 @@ func GetAuthContext(ctx context.Context) *AuthContext { return authCtx } return nil +} + +// HasUsers checks if any users exist in the database +func (s *AuthService) HasUsers() (bool, error) { + var hasUsers bool + + err := s.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false // We only need to check if keys exist + iterator := txn.NewIterator(opts) + defer iterator.Close() + + // Look for any key starting with "user:" + prefix := []byte("user:") + for iterator.Seek(prefix); iterator.ValidForPrefix(prefix); iterator.Next() { + hasUsers = true + return nil // Found at least one user, can exit early + } + + return nil + }) + + return hasUsers, err } \ No newline at end of file diff --git a/issues/3.md b/issues/3.md index a2278a1..265e9b0 100644 --- a/issues/3.md +++ b/issues/3.md @@ -1,6 +1,6 @@ # Issue #3: Implement Autogenerated Root Account for Initial Setup -**Status:** Open +**Status:** āœ… **COMPLETED** **Author:** MrKalzu **Created:** 2025-09-12 22:17:12 +03:00 **Repository:** https://git.rauhala.info/ryyst/kalzu-value-store/issues/3 diff --git a/server/server.go b/server/server.go index 0c04348..79476fb 100644 --- a/server/server.go +++ b/server/server.go @@ -2,10 +2,12 @@ package server import ( "context" + "encoding/json" "fmt" "net/http" "os" "path/filepath" + "strings" "sync" "time" @@ -17,6 +19,7 @@ import ( "kvs/cluster" "kvs/storage" "kvs/types" + "kvs/utils" ) // Server represents the KVS node @@ -117,6 +120,13 @@ func NewServer(config *types.Config) (*Server, error) { // Initialize authentication service server.authService = auth.NewAuthService(db, logger) + // Setup initial root account if needed (Issue #3) + if config.AuthEnabled { + if err := server.setupRootAccount(); err != nil { + return nil, fmt.Errorf("failed to setup root account: %v", err) + } + } + // Initialize Merkle tree using cluster service if err := server.syncService.InitializeMerkleTree(); err != nil { return nil, fmt.Errorf("failed to initialize Merkle tree: %v", err) @@ -182,3 +192,139 @@ func (s *Server) getBackupStatus() types.BackupStatus { return status } + +// setupRootAccount creates an initial root account if no users exist and no seed nodes are configured +func (s *Server) setupRootAccount() error { + // Only create root account if: + // 1. No users exist in the database + // 2. No seed nodes are configured (standalone mode) + hasUsers, err := s.authService.HasUsers() + if err != nil { + return fmt.Errorf("failed to check if users exist: %v", err) + } + + // If users already exist or we have seed nodes, no need to create root account + if hasUsers || len(s.config.SeedNodes) > 0 { + return nil + } + + s.logger.Info("Creating initial root account for empty database with no seed nodes") + + // Import required packages for user creation + // Note: We need these imports at the top of the file + return s.createRootUserAndToken() +} + +// createRootUserAndToken creates the root user, admin group, and initial token +func (s *Server) createRootUserAndToken() error { + rootNickname := "root" + adminGroupName := "admin" + + // Generate UUIDs + rootUserUUID := "root-" + time.Now().Format("20060102-150405") + adminGroupUUID := "admin-" + time.Now().Format("20060102-150405") + now := time.Now().Unix() + + // Create admin group + adminGroup := types.Group{ + UUID: adminGroupUUID, + NameHash: hashGroupName(adminGroupName), + Members: []string{rootUserUUID}, + CreatedAt: now, + UpdatedAt: now, + } + + // Create root user + rootUser := types.User{ + UUID: rootUserUUID, + NicknameHash: hashUserNickname(rootNickname), + Groups: []string{adminGroupUUID}, + CreatedAt: now, + UpdatedAt: now, + } + + // Store group and user in database + if err := s.storeUserAndGroup(&rootUser, &adminGroup); err != nil { + return fmt.Errorf("failed to store root user and admin group: %v", err) + } + + // Create API token with full administrative scopes + adminScopes := []string{ + "admin:users:create", "admin:users:read", "admin:users:update", "admin:users:delete", + "admin:groups:create", "admin:groups:read", "admin:groups:update", "admin:groups:delete", + "admin:tokens:create", "admin:tokens:revoke", + "read", "write", "delete", + } + + // Generate token with 24 hour expiration for initial setup + tokenString, expiresAt, err := auth.GenerateJWT(rootUserUUID, adminScopes, 24) + if err != nil { + return fmt.Errorf("failed to generate root token: %v", err) + } + + // Store token in database + if err := s.storeAPIToken(tokenString, rootUserUUID, adminScopes, expiresAt); err != nil { + return fmt.Errorf("failed to store root token: %v", err) + } + + // Log the token securely (one-time display) + s.logger.WithFields(logrus.Fields{ + "user_uuid": rootUserUUID, + "group_uuid": adminGroupUUID, + "expires_at": time.Unix(expiresAt, 0).Format(time.RFC3339), + "expires_in": "24 hours", + }).Warn("Root account created - SAVE THIS TOKEN:") + + // Display token prominently + fmt.Printf("\n" + strings.Repeat("=", 80) + "\n") + fmt.Printf("šŸ” ROOT ACCOUNT CREATED - INITIAL SETUP TOKEN\n") + fmt.Printf("===========================================\n") + fmt.Printf("User UUID: %s\n", rootUserUUID) + fmt.Printf("Group UUID: %s\n", adminGroupUUID) + fmt.Printf("Token: %s\n", tokenString) + fmt.Printf("Expires: %s (24 hours)\n", time.Unix(expiresAt, 0).Format(time.RFC3339)) + fmt.Printf("\nāš ļø IMPORTANT: Save this token immediately!\n") + fmt.Printf(" This is the only time it will be displayed.\n") + fmt.Printf(" Use this token to authenticate and create additional users.\n") + fmt.Printf(strings.Repeat("=", 80) + "\n\n") + + return nil +} + +// hashUserNickname creates a hash of the user nickname (similar to handlers.go) +func hashUserNickname(nickname string) string { + return utils.HashSHA3512(nickname) +} + +// hashGroupName creates a hash of the group name (similar to handlers.go) +func hashGroupName(groupname string) string { + return utils.HashSHA3512(groupname) +} + +// storeUserAndGroup stores both user and group in the database +func (s *Server) storeUserAndGroup(user *types.User, group *types.Group) error { + return s.db.Update(func(txn *badger.Txn) error { + // Store user + userData, err := json.Marshal(user) + if err != nil { + return fmt.Errorf("failed to marshal user data: %v", err) + } + + if err := txn.Set([]byte(auth.UserStorageKey(user.UUID)), userData); err != nil { + return fmt.Errorf("failed to store user: %v", err) + } + + // Store group + groupData, err := json.Marshal(group) + if err != nil { + return fmt.Errorf("failed to marshal group data: %v", err) + } + + if err := txn.Set([]byte(auth.GroupStorageKey(group.UUID)), groupData); err != nil { + return fmt.Errorf("failed to store group: %v", err) + } + + return nil + }) +} +