aboutsummaryrefslogtreecommitdiff
path: root/weed/sftpd/user/filestore.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/sftpd/user/filestore.go')
-rw-r--r--weed/sftpd/user/filestore.go228
1 files changed, 228 insertions, 0 deletions
diff --git a/weed/sftpd/user/filestore.go b/weed/sftpd/user/filestore.go
new file mode 100644
index 000000000..d40d77c8c
--- /dev/null
+++ b/weed/sftpd/user/filestore.go
@@ -0,0 +1,228 @@
+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
+}
+
+// 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: %v", 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: %v", err)
+ }
+
+ var users []*User
+ if err := json.Unmarshal(data, &users); err != nil {
+ return fmt.Errorf("failed to parse user store file: %v", 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: %v", err)
+ }
+
+ if err := os.WriteFile(s.filePath, data, 0600); err != nil {
+ return fmt.Errorf("failed to write user store file: %v", 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 key == keyData {
+ 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
+}