From 0b7af92761a898003a472d6e9f9c8a061e8d9125 Mon Sep 17 00:00:00 2001 From: Kalzu Rekku Date: Sat, 20 Sep 2025 21:32:30 +0300 Subject: [PATCH] First proto type for issue #3, initial root account. --- auth/auth.go | 27 ++++++++++++--- server/server.go | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 5 deletions(-) diff --git a/auth/auth.go b/auth/auth.go index bbb475a..c171a8f 100644 --- a/auth/auth.go +++ b/auth/auth.go @@ -39,7 +39,7 @@ func NewAuthService(db *badger.DB, logger *logrus.Logger) *AuthService { // StoreAPIToken stores an API token in BadgerDB with TTL func (s *AuthService) StoreAPIToken(tokenString string, userUUID string, scopes []string, expiresAt int64) error { tokenHash := utils.HashToken(tokenString) - + apiToken := types.APIToken{ TokenHash: tokenHash, UserUUID: userUUID, @@ -55,13 +55,13 @@ func (s *AuthService) StoreAPIToken(tokenString string, userUUID string, scopes return s.db.Update(func(txn *badger.Txn) error { entry := badger.NewEntry([]byte(TokenStorageKey(tokenHash)), tokenData) - + // Set TTL to the token expiration time ttl := time.Until(time.Unix(expiresAt, 0)) if ttl > 0 { entry = entry.WithTTL(ttl) } - + return txn.SetEntry(entry) }) } @@ -69,7 +69,7 @@ func (s *AuthService) StoreAPIToken(tokenString string, userUUID string, scopes // GetAPIToken retrieves an API token from BadgerDB by hash func (s *AuthService) GetAPIToken(tokenHash string) (*types.APIToken, error) { var apiToken types.APIToken - + err := s.db.View(func(txn *badger.Txn) error { item, err := txn.Get([]byte(TokenStorageKey(tokenHash))) if err != nil { @@ -202,4 +202,21 @@ func GetAuthContext(ctx context.Context) *AuthContext { return authCtx } return nil -} \ No newline at end of file +} + +// HasUsers checks if any users exist in the database +func (s *AuthService) HasUsers() (bool, error) { + var has bool + err := s.db.View(func(txn *badger.Txn) error { + opts := badger.DefaultIteratorOptions + opts.PrefetchValues = false // Only need keys + it := txn.NewIterator(opts) + defer it.Close() + + prefix := []byte("user:") // Adjust if UserStorageKey uses a different prefix + it.Seek(prefix) + has = it.ValidForPrefix(prefix) + return nil + }) + return has, err +} diff --git a/server/server.go b/server/server.go index 0c04348..4b248b0 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,89 @@ 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") // Adjust if function name differs + adminGroup := types.Group{ + UUID: adminGroupUUID, + Name: hashedGroupName, + CreatedAt: now, // Add if field exists; remove otherwise + // Members: []string{}, // Add if needed; e.g., add root later + } + 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(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") // Adjust if function name differs + rootUser := types.User{ + UUID: rootUUID, + Nickname: hashedNickname, + Groups: []string{adminGroupUUID}, + CreatedAt: now, // Add if field exists; remove otherwise + } + 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(UserStorageKey(rootUUID)), userData) + }) + if err != nil { + return nil, fmt.Errorf("failed to store root user: %v", err) + } + + // Optionally update group members if bidirectional + // adminGroup.Members = append(adminGroup.Members, rootUUID) + // Update group in DB if needed... + + // 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)