You can not select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
					
					
						
							261 lines
						
					
					
						
							6.0 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							261 lines
						
					
					
						
							6.0 KiB
						
					
					
				
								package user
							 | 
						|
								
							 | 
						|
								import (
							 | 
						|
									"crypto/subtle"
							 | 
						|
									"encoding/json"
							 | 
						|
									"fmt"
							 | 
						|
									"os"
							 | 
						|
									"sync"
							 | 
						|
								
							 | 
						|
									"golang.org/x/crypto/ssh"
							 | 
						|
								)
							 | 
						|
								
							 | 
						|
								// FileStore implements Store using a JSON file
							 | 
						|
								type FileStore struct {
							 | 
						|
									filePath string
							 | 
						|
									users    map[string]*User
							 | 
						|
									mu       sync.RWMutex
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// Store defines the interface for user storage and retrieval
							 | 
						|
								type Store interface {
							 | 
						|
									// GetUser retrieves a user by username
							 | 
						|
									GetUser(username string) (*User, error)
							 | 
						|
								
							 | 
						|
									// ValidatePassword checks if the password is valid for the user
							 | 
						|
									ValidatePassword(username string, password []byte) bool
							 | 
						|
								
							 | 
						|
									// ValidatePublicKey checks if the public key is valid for the user
							 | 
						|
									ValidatePublicKey(username string, keyData string) bool
							 | 
						|
								
							 | 
						|
									// GetUserPermissions returns the permissions for a user on a path
							 | 
						|
									GetUserPermissions(username string, path string) []string
							 | 
						|
								
							 | 
						|
									// SaveUser saves or updates a user
							 | 
						|
									SaveUser(user *User) error
							 | 
						|
								
							 | 
						|
									// DeleteUser removes a user
							 | 
						|
									DeleteUser(username string) error
							 | 
						|
								
							 | 
						|
									// ListUsers returns all usernames
							 | 
						|
									ListUsers() ([]string, error)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// UserNotFoundError is returned when a user is not found
							 | 
						|
								type UserNotFoundError struct {
							 | 
						|
									Username string
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								func (e *UserNotFoundError) Error() string {
							 | 
						|
									return fmt.Sprintf("user not found: %s", e.Username)
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// NewFileStore creates a new user store from a JSON file
							 | 
						|
								func NewFileStore(filePath string) (*FileStore, error) {
							 | 
						|
									store := &FileStore{
							 | 
						|
										filePath: filePath,
							 | 
						|
										users:    make(map[string]*User),
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create the file if it doesn't exist
							 | 
						|
									if _, err := os.Stat(filePath); os.IsNotExist(err) {
							 | 
						|
										// Create an empty users array
							 | 
						|
										if err := os.WriteFile(filePath, []byte("[]"), 0600); err != nil {
							 | 
						|
											return nil, fmt.Errorf("failed to create user store file: %w", err)
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := store.loadUsers(); err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return store, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// loadUsers loads users from the JSON file
							 | 
						|
								func (s *FileStore) loadUsers() error {
							 | 
						|
									s.mu.Lock()
							 | 
						|
									defer s.mu.Unlock()
							 | 
						|
								
							 | 
						|
									data, err := os.ReadFile(s.filePath)
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to read user store file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									var users []*User
							 | 
						|
									if err := json.Unmarshal(data, &users); err != nil {
							 | 
						|
										return fmt.Errorf("failed to parse user store file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Clear existing users and add the loaded ones
							 | 
						|
									s.users = make(map[string]*User)
							 | 
						|
									for _, user := range users {
							 | 
						|
										// Process public keys to ensure they're in the correct format
							 | 
						|
										for i, keyData := range user.PublicKeys {
							 | 
						|
											// Try to parse the key as an authorized key format
							 | 
						|
											pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(keyData))
							 | 
						|
											if err == nil {
							 | 
						|
												// If successful, store the marshaled binary format
							 | 
						|
												user.PublicKeys[i] = string(pubKey.Marshal())
							 | 
						|
											}
							 | 
						|
										}
							 | 
						|
										s.users[user.Username] = user
							 | 
						|
								
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// saveUsers saves users to the JSON file
							 | 
						|
								func (s *FileStore) saveUsers() error {
							 | 
						|
									s.mu.RLock()
							 | 
						|
									defer s.mu.RUnlock()
							 | 
						|
								
							 | 
						|
									// Convert map to slice for JSON serialization
							 | 
						|
									var users []*User
							 | 
						|
									for _, user := range s.users {
							 | 
						|
										users = append(users, user)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									data, err := json.MarshalIndent(users, "", "  ")
							 | 
						|
									if err != nil {
							 | 
						|
										return fmt.Errorf("failed to serialize users: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									if err := os.WriteFile(s.filePath, data, 0600); err != nil {
							 | 
						|
										return fmt.Errorf("failed to write user store file: %w", err)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetUser returns a user by username
							 | 
						|
								func (s *FileStore) GetUser(username string) (*User, error) {
							 | 
						|
									s.mu.RLock()
							 | 
						|
									defer s.mu.RUnlock()
							 | 
						|
								
							 | 
						|
									user, ok := s.users[username]
							 | 
						|
									if !ok {
							 | 
						|
										return nil, &UserNotFoundError{Username: username}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return user, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ValidatePassword checks if the password is valid for the user
							 | 
						|
								func (s *FileStore) ValidatePassword(username string, password []byte) bool {
							 | 
						|
									user, err := s.GetUser(username)
							 | 
						|
									if err != nil {
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Compare plaintext password using constant time comparison for security
							 | 
						|
									return subtle.ConstantTimeCompare([]byte(user.Password), password) == 1
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ValidatePublicKey checks if the public key is valid for the user
							 | 
						|
								func (s *FileStore) ValidatePublicKey(username string, keyData string) bool {
							 | 
						|
									user, err := s.GetUser(username)
							 | 
						|
									if err != nil {
							 | 
						|
										return false
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									for _, key := range user.PublicKeys {
							 | 
						|
										if subtle.ConstantTimeCompare([]byte(key), []byte(keyData)) == 1 {
							 | 
						|
											return true
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return false
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// GetUserPermissions returns the permissions for a user on a path
							 | 
						|
								func (s *FileStore) GetUserPermissions(username string, path string) []string {
							 | 
						|
									user, err := s.GetUser(username)
							 | 
						|
									if err != nil {
							 | 
						|
										return nil
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check exact path match first
							 | 
						|
									if perms, ok := user.Permissions[path]; ok {
							 | 
						|
										return perms
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Check parent directories
							 | 
						|
									var bestMatch string
							 | 
						|
									var bestPerms []string
							 | 
						|
								
							 | 
						|
									for p, perms := range user.Permissions {
							 | 
						|
										if len(p) > len(bestMatch) && os.IsPathSeparator(p[len(p)-1]) && path[:len(p)] == p {
							 | 
						|
											bestMatch = p
							 | 
						|
											bestPerms = perms
							 | 
						|
										}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return bestPerms
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// SaveUser saves or updates a user
							 | 
						|
								func (s *FileStore) SaveUser(user *User) error {
							 | 
						|
									s.mu.Lock()
							 | 
						|
									s.users[user.Username] = user
							 | 
						|
									s.mu.Unlock()
							 | 
						|
								
							 | 
						|
									return s.saveUsers()
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// DeleteUser removes a user
							 | 
						|
								func (s *FileStore) DeleteUser(username string) error {
							 | 
						|
									s.mu.Lock()
							 | 
						|
									_, exists := s.users[username]
							 | 
						|
									if !exists {
							 | 
						|
										s.mu.Unlock()
							 | 
						|
										return &UserNotFoundError{Username: username}
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									delete(s.users, username)
							 | 
						|
									s.mu.Unlock()
							 | 
						|
								
							 | 
						|
									return s.saveUsers()
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// ListUsers returns all usernames
							 | 
						|
								func (s *FileStore) ListUsers() ([]string, error) {
							 | 
						|
									s.mu.RLock()
							 | 
						|
									defer s.mu.RUnlock()
							 | 
						|
								
							 | 
						|
									usernames := make([]string, 0, len(s.users))
							 | 
						|
									for username := range s.users {
							 | 
						|
										usernames = append(usernames, username)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return usernames, nil
							 | 
						|
								}
							 | 
						|
								
							 | 
						|
								// CreateUser creates a new user with the given username and password
							 | 
						|
								func (s *FileStore) CreateUser(username, password string) (*User, error) {
							 | 
						|
									s.mu.Lock()
							 | 
						|
									defer s.mu.Unlock()
							 | 
						|
								
							 | 
						|
									// Check if user already exists
							 | 
						|
									if _, exists := s.users[username]; exists {
							 | 
						|
										return nil, fmt.Errorf("user already exists: %s", username)
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									// Create new user
							 | 
						|
									user := NewUser(username)
							 | 
						|
								
							 | 
						|
									// Store plaintext password
							 | 
						|
									user.Password = password
							 | 
						|
								
							 | 
						|
									// Add default permissions
							 | 
						|
									user.Permissions[user.HomeDir] = []string{"all"}
							 | 
						|
								
							 | 
						|
									// Save the user
							 | 
						|
									s.users[username] = user
							 | 
						|
									if err := s.saveUsers(); err != nil {
							 | 
						|
										return nil, err
							 | 
						|
									}
							 | 
						|
								
							 | 
						|
									return user, nil
							 | 
						|
								}
							 |