Add SFTP Server Support (#6753)
* Add SFTP Server Support Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com> * fix s3 tests and helm lint Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com> * increase helm chart version * adjust version --------- Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com> Co-authored-by: chrislu <chris.lu@gmail.com>
This commit is contained in:
143
weed/sftpd/sftp_userstore.go
Normal file
143
weed/sftpd/sftp_userstore.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user