aboutsummaryrefslogtreecommitdiff
path: root/weed/sftpd/sftp_userstore.go
diff options
context:
space:
mode:
Diffstat (limited to 'weed/sftpd/sftp_userstore.go')
-rw-r--r--weed/sftpd/sftp_userstore.go143
1 files changed, 143 insertions, 0 deletions
diff --git a/weed/sftpd/sftp_userstore.go b/weed/sftpd/sftp_userstore.go
new file mode 100644
index 000000000..8c59ed576
--- /dev/null
+++ b/weed/sftpd/sftp_userstore.go
@@ -0,0 +1,143 @@
+package sftpd
+
+import (
+ "crypto/subtle"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+)
+
+// UserStore interface for user management.
+type UserStore interface {
+ GetUser(username string) (*User, error)
+ ValidatePassword(username string, password []byte) bool
+ ValidatePublicKey(username string, keyData string) bool
+ GetUserPermissions(username string, path string) []string
+}
+
+// User represents an SFTP user with authentication and permission details.
+type User struct {
+ Username string
+ Password string // Plaintext password
+ PublicKeys []string // Authorized public keys
+ HomeDir string // User's home directory
+ Permissions map[string][]string // path -> permissions (read, write, list, etc.)
+ Uid uint32 // User ID for file ownership
+ Gid uint32 // Group ID for file ownership
+}
+
+// FileUserStore implements UserStore using a JSON file.
+type FileUserStore struct {
+ filePath string
+ users map[string]*User
+ mu sync.RWMutex
+}
+
+// NewFileUserStore creates a new user store from a JSON file.
+func NewFileUserStore(filePath string) (*FileUserStore, error) {
+ store := &FileUserStore{
+ filePath: filePath,
+ users: make(map[string]*User),
+ }
+
+ if err := store.loadUsers(); err != nil {
+ return nil, err
+ }
+
+ return store, nil
+}
+
+// loadUsers loads users from the JSON file.
+func (s *FileUserStore) loadUsers() error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ // Check if file exists
+ if _, err := os.Stat(s.filePath); os.IsNotExist(err) {
+ return fmt.Errorf("user store file not found: %s", s.filePath)
+ }
+
+ 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)
+ }
+
+ for _, user := range users {
+ s.users[user.Username] = user
+ }
+
+ return nil
+}
+
+// GetUser returns a user by username.
+func (s *FileUserStore) GetUser(username string) (*User, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ user, ok := s.users[username]
+ if !ok {
+ return nil, fmt.Errorf("user not found: %s", username)
+ }
+
+ return user, nil
+}
+
+// ValidatePassword checks if the password is valid for the user.
+func (s *FileUserStore) 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 *FileUserStore) 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 *FileUserStore) 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 strings.HasPrefix(path, p) && len(p) > len(bestMatch) {
+ bestMatch = p
+ bestPerms = perms
+ }
+ }
+
+ return bestPerms
+}