Fix sftp performances and add seaweedfs all-in-one deployment (#6792)

* improve perfs & fix rclone & refactoring
Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com>

* improve perfs on download + add seaweedfs all-in-one deployment

Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com>

* use helper for topologySpreadConstraints and fix create home dir of sftp users

Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com>

* fix helm lint

Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com>

* add missing ctx param

Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com>

---------

Signed-off-by: Mohamed Sekour <mohamed.sekour@exfo.com>
This commit is contained in:
Mohamed Sekour
2025-05-26 09:50:48 +02:00
committed by GitHub
parent ea70d17c5f
commit 27a392f706
31 changed files with 1174 additions and 808 deletions

View File

@@ -17,12 +17,11 @@ type Manager struct {
userStore user.Store
passwordAuth *PasswordAuthenticator
publicKeyAuth *PublicKeyAuthenticator
permissionChecker *PermissionChecker
enabledAuthMethods []string
}
// NewManager creates a new authentication manager
func NewManager(userStore user.Store, fsHelper FileSystemHelper, enabledAuthMethods []string) *Manager {
func NewManager(userStore user.Store, enabledAuthMethods []string) *Manager {
manager := &Manager{
userStore: userStore,
enabledAuthMethods: enabledAuthMethods,
@@ -43,7 +42,6 @@ func NewManager(userStore user.Store, fsHelper FileSystemHelper, enabledAuthMeth
manager.passwordAuth = NewPasswordAuthenticator(userStore, passwordEnabled)
manager.publicKeyAuth = NewPublicKeyAuthenticator(userStore, publicKeyEnabled)
manager.permissionChecker = NewPermissionChecker(fsHelper)
return manager
}
@@ -65,11 +63,6 @@ func (m *Manager) GetSSHServerConfig() *ssh.ServerConfig {
return config
}
// CheckPermission checks if a user has the required permission on a path
func (m *Manager) CheckPermission(user *user.User, path, permission string) error {
return m.permissionChecker.CheckFilePermission(user, path, permission)
}
// GetUser retrieves a user from the user store
func (m *Manager) GetUser(username string) (*user.User, error) {
return m.userStore.GetUser(username)

View File

@@ -51,14 +51,3 @@ func (a *PasswordAuthenticator) Authenticate(conn ssh.ConnMetadata, password []b
return nil, fmt.Errorf("authentication failed")
}
// ValidatePassword checks if the provided password is valid for the user
func ValidatePassword(store user.Store, username string, password []byte) bool {
user, err := store.GetUser(username)
if err != nil {
return false
}
// Compare plaintext password
return string(password) == user.Password
}

View File

@@ -1,7 +1,6 @@
package auth
import (
"crypto/subtle"
"fmt"
"github.com/seaweedfs/seaweedfs/weed/sftpd/user"
@@ -40,7 +39,7 @@ func (a *PublicKeyAuthenticator) Authenticate(conn ssh.ConnMetadata, key ssh.Pub
keyData := string(key.Marshal())
// Validate public key
if ValidatePublicKey(a.userStore, username, keyData) {
if a.userStore.ValidatePublicKey(username, keyData) {
return &ssh.Permissions{
Extensions: map[string]string{
"username": username,
@@ -50,19 +49,3 @@ func (a *PublicKeyAuthenticator) Authenticate(conn ssh.ConnMetadata, key ssh.Pub
return nil, fmt.Errorf("authentication failed")
}
// ValidatePublicKey checks if the provided public key is valid for the user
func ValidatePublicKey(store user.Store, username string, keyData string) bool {
user, err := store.GetUser(username)
if err != nil {
return false
}
for _, key := range user.PublicKeys {
if subtle.ConstantTimeCompare([]byte(key), []byte(keyData)) == 1 {
return true
}
}
return false
}

View File

@@ -0,0 +1,99 @@
package sftpd
import (
"fmt"
"io"
"sync"
"github.com/seaweedfs/seaweedfs/weed/filer"
filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/sftpd/utils"
)
type SeaweedFileReaderAt struct {
fs *SftpServer
entry *filer_pb.Entry
reader io.ReadSeeker
mu sync.Mutex
bufferSize int
cache *utils.LruCache
fileSize int64
}
func NewSeaweedFileReaderAt(fs *SftpServer, entry *filer_pb.Entry) *SeaweedFileReaderAt {
return &SeaweedFileReaderAt{
fs: fs,
entry: entry,
bufferSize: 5 * 1024 * 1024, // 5MB
cache: utils.NewLRUCache(10), // Max 10 chunks = ~50MB
fileSize: int64(entry.Attributes.FileSize),
}
}
func (ra *SeaweedFileReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
ra.mu.Lock()
defer ra.mu.Unlock()
if off >= ra.fileSize {
return 0, io.EOF
}
remaining := len(p)
readOffset := off
totalRead := 0
for remaining > 0 && readOffset < ra.fileSize {
bufferKey := (readOffset / int64(ra.bufferSize)) * int64(ra.bufferSize)
bufferOffset := int(readOffset - bufferKey)
buffer, ok := ra.cache.Get(bufferKey)
if !ok {
readSize := ra.bufferSize
if bufferKey+int64(readSize) > ra.fileSize {
readSize = int(ra.fileSize - bufferKey)
}
if ra.reader == nil {
r := filer.NewFileReader(ra.fs, ra.entry)
if rs, ok := r.(io.ReadSeeker); ok {
ra.reader = rs
} else {
return 0, fmt.Errorf("reader is not seekable")
}
}
if _, err := ra.reader.Seek(bufferKey, io.SeekStart); err != nil {
return 0, fmt.Errorf("seek error: %v", err)
}
buffer = make([]byte, readSize)
readBytes, err := io.ReadFull(ra.reader, buffer)
if err != nil && err != io.ErrUnexpectedEOF {
return 0, fmt.Errorf("read error: %v", err)
}
buffer = buffer[:readBytes]
ra.cache.Put(bufferKey, buffer)
}
toCopy := len(buffer) - bufferOffset
if toCopy > remaining {
toCopy = remaining
}
if toCopy <= 0 {
break
}
copy(p[totalRead:], buffer[bufferOffset:bufferOffset+toCopy])
totalRead += toCopy
readOffset += int64(toCopy)
remaining -= toCopy
}
if totalRead == 0 {
return 0, io.EOF
}
if totalRead < len(p) {
return totalRead, io.EOF
}
return totalRead, nil
}

View File

@@ -8,8 +8,6 @@ import (
"time"
"github.com/pkg/sftp"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// FileInfo implements os.FileInfo.
@@ -70,57 +68,43 @@ func (l listerat) ListAt(ls []os.FileInfo, offset int64) (int, error) {
return n, nil
}
// filerFileWriter buffers writes and flushes on Close.
type filerFileWriter struct {
// SeaweedSftpFileWriter buffers writes and flushes on Close.
type SeaweedSftpFileWriter struct {
fs SftpServer
req *sftp.Request
mu sync.Mutex
data []byte
tmpFile *os.File
permissions os.FileMode
uid uint32
gid uint32
offset int64
}
func (w *filerFileWriter) Write(p []byte) (int, error) {
func (w *SeaweedSftpFileWriter) Write(p []byte) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
end := w.offset + int64(len(p))
if end > int64(len(w.data)) {
newBuf := make([]byte, end)
copy(newBuf, w.data)
w.data = newBuf
}
n := copy(w.data[w.offset:], p)
n, err := w.tmpFile.WriteAt(p, w.offset)
w.offset += int64(n)
return n, nil
return n, err
}
func (w *filerFileWriter) WriteAt(p []byte, off int64) (int, error) {
func (w *SeaweedSftpFileWriter) WriteAt(p []byte, off int64) (int, error) {
w.mu.Lock()
defer w.mu.Unlock()
end := int(off) + len(p)
if end > len(w.data) {
newBuf := make([]byte, end)
copy(newBuf, w.data)
w.data = newBuf
}
n := copy(w.data[off:], p)
return n, nil
return w.tmpFile.WriteAt(p, off)
}
func (w *filerFileWriter) Close() error {
func (w *SeaweedSftpFileWriter) Close() error {
w.mu.Lock()
defer w.mu.Unlock()
dir, _ := util.FullPath(w.req.Filepath).DirAndName()
defer os.Remove(w.tmpFile.Name()) // Clean up temp file
defer w.tmpFile.Close()
// Check permissions based on file metadata and user permissions
if err := w.fs.checkFilePermission(dir, "write"); err != nil {
glog.Errorf("Permission denied for %s", dir)
if _, err := w.tmpFile.Seek(0, io.SeekStart); err != nil {
return err
}
// Call the extracted putFile method on SftpServer
return w.fs.putFile(w.req.Filepath, w.data, w.fs.user)
// Stream the file instead of loading it
return w.fs.putFile(w.req.Filepath, w.tmpFile, w.fs.user)
}

View File

@@ -2,7 +2,6 @@
package sftpd
import (
"bytes"
"context"
"crypto/md5"
"encoding/json"
@@ -15,7 +14,6 @@ import (
"time"
"github.com/pkg/sftp"
"github.com/seaweedfs/seaweedfs/weed/filer"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
@@ -48,6 +46,9 @@ func (fs *SftpServer) getEntry(p string) (*filer_pb.Entry, error) {
err := fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
r, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{Directory: dir, Name: name})
if err != nil {
if isNotExistError(err) {
return os.ErrNotExist
}
return err
}
if r.Entry == nil {
@@ -57,11 +58,21 @@ func (fs *SftpServer) getEntry(p string) (*filer_pb.Entry, error) {
return nil
})
if err != nil {
if isNotExistError(err) {
return nil, os.ErrNotExist
}
return nil, fmt.Errorf("lookup %s: %w", p, err)
}
return entry, nil
}
func isNotExistError(err error) bool {
return strings.Contains(err.Error(), "not found") ||
strings.Contains(err.Error(), "no entry is found") ||
strings.Contains(err.Error(), "file does not exist") ||
err == os.ErrNotExist
}
// updateEntry sends an UpdateEntryRequest for the given entry.
func (fs *SftpServer) updateEntry(dir string, entry *filer_pb.Entry) error {
return fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
@@ -116,94 +127,30 @@ func (fs *SftpServer) readFile(r *sftp.Request) (io.ReaderAt, error) {
if err != nil {
return nil, err
}
return &SeaweedFileReaderAt{fs: fs, entry: entry}, nil
}
// putFile uploads a file to the filer and sets ownership metadata.
func (fs *SftpServer) putFile(filepath string, data []byte, user *user.User) error {
dir, filename := util.FullPath(filepath).DirAndName()
uploadUrl := fmt.Sprintf("http://%s%s", fs.filerAddr, filepath)
// Create a reader from our buffered data and calculate MD5 hash
hash := md5.New()
reader := bytes.NewReader(data)
body := io.TeeReader(reader, hash)
fileSize := int64(len(data))
// Create and execute HTTP request
proxyReq, err := http.NewRequest(http.MethodPut, uploadUrl, body)
if err != nil {
return fmt.Errorf("create request: %v", err)
}
proxyReq.ContentLength = fileSize
proxyReq.Header.Set("Content-Type", "application/octet-stream")
client := &http.Client{}
resp, err := client.Do(proxyReq)
if err != nil {
return fmt.Errorf("upload to filer: %v", err)
}
defer resp.Body.Close()
// Process response
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %v", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(respBody))
}
var result weed_server.FilerPostResult
if err := json.Unmarshal(respBody, &result); err != nil {
return fmt.Errorf("parse response: %v", err)
}
if result.Error != "" {
return fmt.Errorf("filer error: %s", result.Error)
}
// Update file ownership using the same pattern as other functions
if user != nil {
err := fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
// Look up the file to get its current entry
lookupResp, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: filename,
})
if err != nil {
return fmt.Errorf("lookup file for attribute update: %v", err)
}
if lookupResp.Entry == nil {
return fmt.Errorf("file not found after upload: %s/%s", dir, filename)
}
// Update the entry with new uid/gid
entry := lookupResp.Entry
entry.Attributes.Uid = user.Uid
entry.Attributes.Gid = user.Gid
// Update the entry in the filer
_, err = client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{
Directory: dir,
Entry: entry,
})
return err
})
if err != nil {
// Log the error but don't fail the whole operation
glog.Errorf("Failed to update file ownership for %s: %v", filepath, err)
}
}
return nil
return NewSeaweedFileReaderAt(fs, entry), nil
}
func (fs *SftpServer) newFileWriter(r *sftp.Request) (io.WriterAt, error) {
return &filerFileWriter{fs: *fs, req: r, permissions: 0644, uid: fs.user.Uid, gid: fs.user.Gid}, nil
dir, _ := util.FullPath(r.Filepath).DirAndName()
if err := fs.checkFilePermission(dir, "write"); err != nil {
glog.Errorf("Permission denied for %s", dir)
return nil, err
}
// Create a temporary file to buffer writes
tmpFile, err := os.CreateTemp("", "sftp-upload-*")
if err != nil {
return nil, fmt.Errorf("failed to create temp file: %v", err)
}
return &SeaweedSftpFileWriter{
fs: *fs,
req: r,
tmpFile: tmpFile,
permissions: 0644,
uid: fs.user.Uid,
gid: fs.user.Gid,
offset: 0,
}, nil
}
func (fs *SftpServer) removeEntry(r *sftp.Request) error {
@@ -317,7 +264,7 @@ func (fs *SftpServer) makeDir(r *sftp.Request) error {
return fmt.Errorf("cannot create directory: no user info")
}
dir, name := util.FullPath(r.Filepath).DirAndName()
if err := fs.checkFilePermission(dir, "mkdir"); err != nil {
if err := fs.checkFilePermission(r.Filepath, "mkdir"); err != nil {
return err
}
// default mode and ownership
@@ -345,6 +292,81 @@ func (fs *SftpServer) removeDir(r *sftp.Request) error {
return fs.deleteEntry(r.Filepath, false)
}
func (fs *SftpServer) putFile(filepath string, reader io.Reader, user *user.User) error {
dir, filename := util.FullPath(filepath).DirAndName()
uploadUrl := fmt.Sprintf("http://%s%s", fs.filerAddr, filepath)
// Compute MD5 while uploading
hash := md5.New()
body := io.TeeReader(reader, hash)
// We can skip ContentLength if unknown (chunked transfer encoding)
req, err := http.NewRequest(http.MethodPut, uploadUrl, body)
if err != nil {
return fmt.Errorf("create request: %v", err)
}
req.Header.Set("Content-Type", "application/octet-stream")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("upload to filer: %v", err)
}
defer resp.Body.Close()
respBody, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("read response: %v", err)
}
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
return fmt.Errorf("upload failed with status %d: %s", resp.StatusCode, string(respBody))
}
var result weed_server.FilerPostResult
if err := json.Unmarshal(respBody, &result); err != nil {
return fmt.Errorf("parse response: %v", err)
}
if result.Error != "" {
return fmt.Errorf("filer error: %s", result.Error)
}
// Update file ownership using the same pattern as other functions
if user != nil {
err := fs.callWithClient(false, func(ctx context.Context, client filer_pb.SeaweedFilerClient) error {
// Look up the file to get its current entry
lookupResp, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: filename,
})
if err != nil {
return fmt.Errorf("lookup file for attribute update: %v", err)
}
if lookupResp.Entry == nil {
return fmt.Errorf("file not found after upload: %s/%s", dir, filename)
}
// Update the entry with new uid/gid
entry := lookupResp.Entry
entry.Attributes.Uid = user.Uid
entry.Attributes.Gid = user.Gid
// Update the entry in the filer
_, err = client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{
Directory: dir,
Entry: entry,
})
return err
})
if err != nil {
// Log the error but don't fail the whole operation
glog.Errorf("Failed to update file ownership for %s: %v", filepath, err)
}
}
return nil
}
// ==================== Common Arguments Helpers ====================
func FileInfoFromEntry(e *filer_pb.Entry) FileInfo {
@@ -390,73 +412,6 @@ func (fi *EnhancedFileInfo) Owner() (uid, gid int) {
return int(fi.uid), int(fi.gid)
}
// SeaweedFileReaderAt implements io.ReaderAt for SeaweedFS files
type SeaweedFileReaderAt struct {
fs *SftpServer
entry *filer_pb.Entry
}
func (ra *SeaweedFileReaderAt) ReadAt(p []byte, off int64) (n int, err error) {
// Create a new reader for each ReadAt call
reader := filer.NewFileReader(ra.fs, ra.entry)
if reader == nil {
return 0, fmt.Errorf("failed to create file reader")
}
// Check if we're reading past the end of the file
fileSize := int64(ra.entry.Attributes.FileSize)
if off >= fileSize {
return 0, io.EOF
}
// Seek to the offset
if seeker, ok := reader.(io.Seeker); ok {
_, err = seeker.Seek(off, io.SeekStart)
if err != nil {
return 0, fmt.Errorf("seek error: %v", err)
}
} else {
// If the reader doesn't implement Seek, we need to read and discard bytes
toSkip := off
skipBuf := make([]byte, 8192)
for toSkip > 0 {
skipSize := int64(len(skipBuf))
if skipSize > toSkip {
skipSize = toSkip
}
read, err := reader.Read(skipBuf[:skipSize])
if err != nil {
return 0, fmt.Errorf("skip error: %v", err)
}
if read == 0 {
return 0, fmt.Errorf("unable to skip to offset %d", off)
}
toSkip -= int64(read)
}
}
// Adjust read length if it would go past EOF
readLen := len(p)
remaining := fileSize - off
if int64(readLen) > remaining {
readLen = int(remaining)
if readLen == 0 {
return 0, io.EOF
}
}
// Read the data
n, err = io.ReadFull(reader, p[:readLen])
// Handle EOF correctly
if err == io.ErrUnexpectedEOF || (err == nil && n < len(p)) {
err = io.EOF
}
return n, err
}
func (fs *SftpServer) checkFilePermission(filepath string, permissions string) error {
return fs.authManager.CheckPermission(fs.user, filepath, permissions)
return fs.CheckFilePermission(filepath, permissions)
}

View File

@@ -1,11 +1,12 @@
package auth
package sftpd
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/sftpd/user"
)
@@ -23,17 +24,6 @@ const (
PermReadWrite = "readwrite"
)
// PermissionChecker handles permission checking for file operations
// It verifies both Unix-style permissions and explicit ACLs defined in user configuration.
type PermissionChecker struct {
fsHelper FileSystemHelper
}
// FileSystemHelper provides necessary filesystem operations for permission checking
type FileSystemHelper interface {
GetEntry(path string) (*Entry, error)
}
// Entry represents a filesystem entry with attributes
type Entry struct {
IsDirectory bool
@@ -50,23 +40,7 @@ type EntryAttributes struct {
SymlinkTarget string
}
// PermissionError represents a permission-related error
type PermissionError struct {
Path string
Perm string
User string
}
func (e *PermissionError) Error() string {
return fmt.Sprintf("permission denied: %s required on %s for user %s", e.Perm, e.Path, e.User)
}
// NewPermissionChecker creates a new permission checker
func NewPermissionChecker(fsHelper FileSystemHelper) *PermissionChecker {
return &PermissionChecker{
fsHelper: fsHelper,
}
}
// PermissionError represents a permission-related erro
// CheckFilePermission verifies if a user has the required permission on a path
// It first checks if the path is in the user's home directory with explicit permissions.
@@ -78,45 +52,53 @@ func NewPermissionChecker(fsHelper FileSystemHelper) *PermissionChecker {
//
// Returns:
// - nil if permission is granted, error otherwise
func (pc *PermissionChecker) CheckFilePermission(user *user.User, path string, perm string) error {
if user == nil {
return &PermissionError{Path: path, Perm: perm, User: "unknown"}
func (fs *SftpServer) CheckFilePermission(path string, perm string) error {
if fs.user == nil {
glog.V(0).Infof("permission denied. No user associated with the SftpServer.")
return os.ErrPermission
}
// Retrieve metadata via helper
entry, err := pc.fsHelper.GetEntry(path)
// Special case for "create" or "write" permissions on non-existent paths
// Check parent directory permissions instead
entry, err := fs.getEntry(path)
if err != nil {
// If the path doesn't exist and we're checking for create/write/mkdir permission,
// check permissions on the parent directory instead
if err == os.ErrNotExist {
parentPath := filepath.Dir(path)
// Check if user can write to the parent directory
return fs.CheckFilePermission(parentPath, perm)
}
return fmt.Errorf("failed to get entry for path %s: %w", path, err)
}
// Rest of the function remains the same...
// Handle symlinks by resolving them
if entry.IsSymlink {
if entry.Attributes.GetSymlinkTarget() != "" {
// Get the actual entry for the resolved path
entry, err = pc.fsHelper.GetEntry(entry.Attributes.SymlinkTarget)
entry, err = fs.getEntry(entry.Attributes.GetSymlinkTarget())
if err != nil {
return fmt.Errorf("failed to get entry for resolved path %s: %w", entry.Attributes.SymlinkTarget, err)
}
// Store the original target
entry.Target = entry.Attributes.SymlinkTarget
}
// Special case: root user always has permission
if user.Username == "root" || user.Uid == 0 {
if fs.user.Username == "root" || fs.user.Uid == 0 {
return nil
}
// Check if path is within user's home directory and has explicit permissions
if isPathInHomeDirectory(user, path) {
if isPathInHomeDirectory(fs.user, path) {
// Check if user has explicit permissions for this path
if HasExplicitPermission(user, path, perm, entry.IsDirectory) {
if HasExplicitPermission(fs.user, path, perm, entry.IsDirectory) {
return nil
}
} else {
// For paths outside home directory or without explicit home permissions,
// check UNIX-style perms first
isOwner := user.Uid == entry.Attributes.Uid
isGroup := user.Gid == entry.Attributes.Gid
isOwner := fs.user.Uid == entry.Attributes.Uid
isGroup := fs.user.Gid == entry.Attributes.Gid
mode := os.FileMode(entry.Attributes.FileMode)
if HasUnixPermission(isOwner, isGroup, mode, entry.IsDirectory, perm) {
@@ -124,23 +106,12 @@ func (pc *PermissionChecker) CheckFilePermission(user *user.User, path string, p
}
// Then check explicit ACLs
if HasExplicitPermission(user, path, perm, entry.IsDirectory) {
if HasExplicitPermission(fs.user, path, perm, entry.IsDirectory) {
return nil
}
}
return &PermissionError{Path: path, Perm: perm, User: user.Username}
}
// CheckFilePermissionWithContext is a context-aware version of CheckFilePermission
// that supports cancellation and timeouts
func (pc *PermissionChecker) CheckFilePermissionWithContext(ctx context.Context, user *user.User, path string, perm string) error {
// Check for context cancellation
if ctx.Err() != nil {
return ctx.Err()
}
return pc.CheckFilePermission(user, path, perm)
glog.V(0).Infof("permission denied for user %s on path %s for permission %s", fs.user.Username, path, perm)
return os.ErrPermission
}
// isPathInHomeDirectory checks if a path is in the user's home directory

View File

@@ -2,12 +2,18 @@
package sftpd
import (
"context"
"fmt"
"io"
"os"
"time"
"github.com/pkg/sftp"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/sftpd/auth"
filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/sftpd/user"
"github.com/seaweedfs/seaweedfs/weed/util"
"google.golang.org/grpc"
)
@@ -17,16 +23,10 @@ type SftpServer struct {
dataCenter string
filerGroup string
user *user.User
authManager *auth.Manager
}
// NewSftpServer constructs the server.
func NewSftpServer(filerAddr pb.ServerAddress, grpcDialOption grpc.DialOption, dataCenter, filerGroup string, user *user.User) SftpServer {
// Create a file system helper for the auth manager
fsHelper := NewFileSystemHelper(filerAddr, grpcDialOption, dataCenter, filerGroup)
// Create an auth manager for permission checking
authManager := auth.NewManager(nil, fsHelper, []string{})
return SftpServer{
filerAddr: filerAddr,
@@ -34,7 +34,6 @@ func NewSftpServer(filerAddr pb.ServerAddress, grpcDialOption grpc.DialOption, d
dataCenter: dataCenter,
filerGroup: filerGroup,
user: user,
authManager: authManager,
}
}
@@ -57,3 +56,51 @@ func (fs *SftpServer) Filecmd(req *sftp.Request) error {
func (fs *SftpServer) Filelist(req *sftp.Request) (sftp.ListerAt, error) {
return fs.listDir(req)
}
// EnsureHomeDirectory creates the user's home directory if it doesn't exist
func (fs *SftpServer) EnsureHomeDirectory() error {
if fs.user.HomeDir == "" {
return fmt.Errorf("user has no home directory configured")
}
glog.V(0).Infof("Ensuring home directory exists for user %s: %s", fs.user.Username, fs.user.HomeDir)
// Check if home directory already exists
entry, err := fs.getEntry(fs.user.HomeDir)
if err == nil && entry != nil {
// Directory exists, just ensure proper ownership
if entry.Attributes.Uid != fs.user.Uid || entry.Attributes.Gid != fs.user.Gid {
dir, _ := util.FullPath(fs.user.HomeDir).DirAndName()
entry.Attributes.Uid = fs.user.Uid
entry.Attributes.Gid = fs.user.Gid
return fs.updateEntry(dir, entry)
}
return nil
}
// Skip permission check for home directory creation
// This is a special case where we want to create the directory regardless
dir, name := util.FullPath(fs.user.HomeDir).DirAndName()
// Create the directory with proper permissions using filer_pb.Mkdir
err = filer_pb.Mkdir(context.Background(), fs, dir, name, func(entry *filer_pb.Entry) {
mode := uint32(0700 | os.ModeDir) // Default to private permissions for home dirs
entry.Attributes.FileMode = mode
entry.Attributes.Uid = fs.user.Uid
entry.Attributes.Gid = fs.user.Gid
now := time.Now().Unix()
entry.Attributes.Crtime = now
entry.Attributes.Mtime = now
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
entry.Extended["creator"] = []byte(fs.user.Username)
})
if err != nil {
return fmt.Errorf("failed to create home directory: %v", err)
}
glog.V(0).Infof("Successfully created home directory for user %s: %s", fs.user.Username, fs.user.HomeDir)
return nil
}

View File

@@ -5,7 +5,6 @@ import (
"context"
"fmt"
"io"
"log"
"net"
"os"
"path/filepath"
@@ -14,10 +13,8 @@ import (
"github.com/pkg/sftp"
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/sftpd/auth"
"github.com/seaweedfs/seaweedfs/weed/sftpd/user"
"github.com/seaweedfs/seaweedfs/weed/util"
"golang.org/x/crypto/ssh"
"google.golang.org/grpc"
)
@@ -27,7 +24,6 @@ type SFTPService struct {
options SFTPServiceOptions
userStore user.Store
authManager *auth.Manager
homeManager *user.HomeManager
}
// SFTPServiceOptions contains all configuration options for the SFTP service.
@@ -64,100 +60,12 @@ func NewSFTPService(options *SFTPServiceOptions) *SFTPService {
}
service.userStore = userStore
// Initialize file system helper for permission checking
fsHelper := NewFileSystemHelper(
options.Filer,
options.GrpcDialOption,
options.DataCenter,
options.FilerGroup,
)
// Initialize auth manager
service.authManager = auth.NewManager(userStore, fsHelper, options.AuthMethods)
// Initialize home directory manager
service.homeManager = user.NewHomeManager(fsHelper)
service.authManager = auth.NewManager(userStore, options.AuthMethods)
return &service
}
// FileSystemHelper implements auth.FileSystemHelper interface
type FileSystemHelper struct {
filerAddr pb.ServerAddress
grpcDialOption grpc.DialOption
dataCenter string
filerGroup string
}
func NewFileSystemHelper(filerAddr pb.ServerAddress, grpcDialOption grpc.DialOption, dataCenter, filerGroup string) *FileSystemHelper {
return &FileSystemHelper{
filerAddr: filerAddr,
grpcDialOption: grpcDialOption,
dataCenter: dataCenter,
filerGroup: filerGroup,
}
}
// GetEntry implements auth.FileSystemHelper interface
func (fs *FileSystemHelper) GetEntry(path string) (*auth.Entry, error) {
dir, name := util.FullPath(path).DirAndName()
var entry *filer_pb.Entry
err := fs.withTimeoutContext(func(ctx context.Context) error {
return fs.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
resp, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: name,
})
if err != nil {
return err
}
if resp.Entry == nil {
return fmt.Errorf("entry not found")
}
entry = resp.Entry
return nil
})
})
if err != nil {
return nil, err
}
return &auth.Entry{
IsDirectory: entry.IsDirectory,
Attributes: &auth.EntryAttributes{
Uid: entry.Attributes.GetUid(),
Gid: entry.Attributes.GetGid(),
FileMode: entry.Attributes.GetFileMode(),
SymlinkTarget: entry.Attributes.GetSymlinkTarget(),
},
IsSymlink: entry.Attributes.GetSymlinkTarget() != "",
}, nil
}
// Implement FilerClient interface for FileSystemHelper
func (fs *FileSystemHelper) AdjustedUrl(location *filer_pb.Location) string {
return location.Url
}
func (fs *FileSystemHelper) GetDataCenter() string {
return fs.dataCenter
}
func (fs *FileSystemHelper) WithFilerClient(streamingMode bool, fn func(filer_pb.SeaweedFilerClient) error) error {
addr := fs.filerAddr.ToGrpcAddress()
return pb.WithGrpcClient(streamingMode, util.RandomInt32(), func(conn *grpc.ClientConn) error {
return fn(filer_pb.NewSeaweedFilerClient(conn))
}, addr, false, fs.grpcDialOption)
}
func (fs *FileSystemHelper) withTimeoutContext(fn func(ctx context.Context) error) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
return fn(ctx)
}
// Serve accepts incoming connections on the provided listener and handles them.
func (s *SFTPService) Serve(listener net.Listener) error {
// Build SSH server config
@@ -212,14 +120,14 @@ func (s *SFTPService) buildSSHConfig() (*ssh.ServerConfig, error) {
keyPath := filepath.Join(s.options.HostKeysFolder, file.Name())
if err := s.addHostKey(config, keyPath); err != nil {
// Log the error but continue with other keys
log.Printf("Warning: failed to add host key %s: %v", keyPath, err)
glog.V(0).Info(fmt.Sprintf("Failed to add host key %s: %v", keyPath, err))
continue
}
hostKeysAdded++
}
if hostKeysAdded == 0 {
log.Printf("Warning: no valid host keys found in folder %s", s.options.HostKeysFolder)
glog.V(0).Info(fmt.Sprintf("Warning: no valid host keys found in folder %s", s.options.HostKeysFolder))
}
}
@@ -296,7 +204,7 @@ func (s *SFTPService) handleSSHConnection(conn net.Conn, config *ssh.ServerConfi
)
// Ensure home directory exists with proper permissions
if err := s.homeManager.EnsureHomeDirectory(sftpUser); err != nil {
if err := userFs.EnsureHomeDirectory(); err != nil {
glog.Errorf("Failed to ensure home directory for user %s: %v", username, err)
// We don't close the connection here, as the user might still be able to access other directories
}

View File

@@ -1,143 +0,0 @@
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
}

View File

@@ -17,6 +17,39 @@ type FileStore struct {
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{
@@ -128,7 +161,7 @@ func (s *FileStore) ValidatePublicKey(username string, keyData string) bool {
}
for _, key := range user.PublicKeys {
if key == keyData {
if subtle.ConstantTimeCompare([]byte(key), []byte(keyData)) == 1 {
return true
}
}

View File

@@ -1,204 +0,0 @@
package user
import (
"context"
"fmt"
"os"
"path/filepath"
"strings"
"time"
"github.com/seaweedfs/seaweedfs/weed/glog"
filer_pb "github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
"github.com/seaweedfs/seaweedfs/weed/util"
)
// HomeManager handles user home directory operations
type HomeManager struct {
filerClient FilerClient
}
// FilerClient defines the interface for interacting with the filer
type FilerClient interface {
WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error
GetDataCenter() string
AdjustedUrl(location *filer_pb.Location) string
}
// NewHomeManager creates a new home directory manager
func NewHomeManager(filerClient FilerClient) *HomeManager {
return &HomeManager{
filerClient: filerClient,
}
}
// EnsureHomeDirectory creates the user's home directory if it doesn't exist
func (hm *HomeManager) EnsureHomeDirectory(user *User) error {
if user.HomeDir == "" {
return fmt.Errorf("user has no home directory configured")
}
glog.V(0).Infof("Ensuring home directory exists for user %s: %s", user.Username, user.HomeDir)
// Check if home directory exists and create it if needed
err := hm.createDirectoryIfNotExists(user.HomeDir, user)
if err != nil {
return fmt.Errorf("failed to ensure home directory: %v", err)
}
// Update user permissions map to include the home directory with full access if not already present
if user.Permissions == nil {
user.Permissions = make(map[string][]string)
}
// Only add permissions if not already present
if _, exists := user.Permissions[user.HomeDir]; !exists {
user.Permissions[user.HomeDir] = []string{"all"}
glog.V(0).Infof("Added full permissions for user %s to home directory %s",
user.Username, user.HomeDir)
}
return nil
}
// createDirectoryIfNotExists creates a directory path if it doesn't exist
func (hm *HomeManager) createDirectoryIfNotExists(dirPath string, user *User) error {
// Split the path into components
components := strings.Split(strings.Trim(dirPath, "/"), "/")
currentPath := "/"
for _, component := range components {
if component == "" {
continue
}
nextPath := filepath.Join(currentPath, component)
err := hm.createSingleDirectory(nextPath, user)
if err != nil {
return err
}
currentPath = nextPath
}
return nil
}
// createSingleDirectory creates a single directory if it doesn't exist
func (hm *HomeManager) createSingleDirectory(dirPath string, user *User) error {
var dirExists bool
err := hm.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
dir, name := util.FullPath(dirPath).DirAndName()
// Check if directory exists
resp, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: name,
})
if err != nil || resp.Entry == nil {
// Directory doesn't exist, create it
glog.V(0).Infof("Creating directory %s for user %s", dirPath, user.Username)
err = filer_pb.Mkdir(context.Background(), hm, string(dir), name, func(entry *filer_pb.Entry) {
// Set appropriate permissions
entry.Attributes.FileMode = uint32(0700 | os.ModeDir) // rwx------ for user
entry.Attributes.Uid = user.Uid
entry.Attributes.Gid = user.Gid
// Set creation and modification times
now := time.Now().Unix()
entry.Attributes.Crtime = now
entry.Attributes.Mtime = now
// Add extended attributes
if entry.Extended == nil {
entry.Extended = make(map[string][]byte)
}
entry.Extended["creator"] = []byte(user.Username)
entry.Extended["auto_created"] = []byte("true")
})
if err != nil {
return fmt.Errorf("failed to create directory %s: %v", dirPath, err)
}
} else if !resp.Entry.IsDirectory {
return fmt.Errorf("path %s exists but is not a directory", dirPath)
} else {
dirExists = true
// Update ownership if needed
if resp.Entry.Attributes.Uid != user.Uid || resp.Entry.Attributes.Gid != user.Gid {
glog.V(0).Infof("Updating ownership of directory %s for user %s", dirPath, user.Username)
entry := resp.Entry
entry.Attributes.Uid = user.Uid
entry.Attributes.Gid = user.Gid
_, updateErr := client.UpdateEntry(ctx, &filer_pb.UpdateEntryRequest{
Directory: dir,
Entry: entry,
})
if updateErr != nil {
glog.Warningf("Failed to update directory ownership: %v", updateErr)
}
}
}
return nil
})
if err != nil {
return err
}
if !dirExists {
// Verify the directory was created
verifyErr := hm.filerClient.WithFilerClient(false, func(client filer_pb.SeaweedFilerClient) error {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
dir, name := util.FullPath(dirPath).DirAndName()
resp, err := client.LookupDirectoryEntry(ctx, &filer_pb.LookupDirectoryEntryRequest{
Directory: dir,
Name: name,
})
if err != nil || resp.Entry == nil {
return fmt.Errorf("directory not found after creation")
}
if !resp.Entry.IsDirectory {
return fmt.Errorf("path exists but is not a directory")
}
dirExists = true
return nil
})
if verifyErr != nil {
return fmt.Errorf("failed to verify directory creation: %v", verifyErr)
}
}
return nil
}
// Implement necessary methods to satisfy the filer_pb.FilerClient interface
func (hm *HomeManager) AdjustedUrl(location *filer_pb.Location) string {
return hm.filerClient.AdjustedUrl(location)
}
func (hm *HomeManager) GetDataCenter() string {
return hm.filerClient.GetDataCenter()
}
// WithFilerClient delegates to the underlying filer client
func (hm *HomeManager) WithFilerClient(streamingMode bool, fn func(client filer_pb.SeaweedFilerClient) error) error {
return hm.filerClient.WithFilerClient(streamingMode, fn)
}

View File

@@ -2,7 +2,6 @@
package user
import (
"fmt"
"math/rand"
"path/filepath"
)
@@ -18,39 +17,6 @@ type User struct {
Gid uint32 // Group ID for file ownership
}
// 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)
}
// NewUser creates a new user with default settings
func NewUser(username string) *User {
// Generate a random UID/GID between 1000 and 60000

View File

@@ -0,0 +1,52 @@
package utils
import (
"container/list"
)
type CacheEntry struct {
key int64
value []byte
}
type LruCache struct {
capacity int
ll *list.List
cache map[int64]*list.Element
}
func NewLRUCache(capacity int) *LruCache {
return &LruCache{
capacity: capacity,
ll: list.New(),
cache: make(map[int64]*list.Element),
}
}
func (c *LruCache) Get(key int64) ([]byte, bool) {
if ele, ok := c.cache[key]; ok {
c.ll.MoveToFront(ele)
return ele.Value.(*CacheEntry).value, true
}
return nil, false
}
func (c *LruCache) Put(key int64, value []byte) {
if ele, ok := c.cache[key]; ok {
c.ll.MoveToFront(ele)
ele.Value.(*CacheEntry).value = value
return
}
if c.ll.Len() >= c.capacity {
oldest := c.ll.Back()
if oldest != nil {
c.ll.Remove(oldest)
delete(c.cache, oldest.Value.(*CacheEntry).key)
}
}
entry := &CacheEntry{key, value}
ele := c.ll.PushFront(entry)
c.cache[key] = ele
}