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.
		
		
		
		
		
			
		
			
				
					
					
						
							238 lines
						
					
					
						
							6.6 KiB
						
					
					
				
			
		
		
		
			
			
			
		
		
	
	
							238 lines
						
					
					
						
							6.6 KiB
						
					
					
				| package sftpd | |
| 
 | |
| import ( | |
| 	"fmt" | |
| 	"os" | |
| 	"path/filepath" | |
| 	"strings" | |
| 
 | |
| 	"github.com/seaweedfs/seaweedfs/weed/glog" | |
| 	"github.com/seaweedfs/seaweedfs/weed/sftpd/user" | |
| ) | |
| 
 | |
| // Permission constants for clarity and consistency | |
| const ( | |
| 	PermRead      = "read" | |
| 	PermWrite     = "write" | |
| 	PermExecute   = "execute" | |
| 	PermList      = "list" | |
| 	PermDelete    = "delete" | |
| 	PermMkdir     = "mkdir" | |
| 	PermTraverse  = "traverse" | |
| 	PermAll       = "*" | |
| 	PermAdmin     = "admin" | |
| 	PermReadWrite = "readwrite" | |
| ) | |
| 
 | |
| // Entry represents a filesystem entry with attributes | |
| type Entry struct { | |
| 	IsDirectory bool | |
| 	Attributes  *EntryAttributes | |
| 	IsSymlink   bool   // Added to track symlinks | |
| 	Target      string // For symlinks, stores the target path | |
| } | |
| 
 | |
| // EntryAttributes contains file attributes | |
| type EntryAttributes struct { | |
| 	Uid           uint32 | |
| 	Gid           uint32 | |
| 	FileMode      uint32 | |
| 	SymlinkTarget string | |
| } | |
| 
 | |
| // 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. | |
| // If not, it falls back to Unix permission checking followed by explicit permission checking. | |
| // Parameters: | |
| //   - user: The user requesting access | |
| //   - path: The filesystem path to check | |
| //   - perm: The permission being requested (read, write, execute, etc.) | |
| // | |
| // Returns: | |
| //   - nil if permission is granted, error otherwise | |
| 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 | |
| 	} | |
| 
 | |
| 	// 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.Attributes.GetSymlinkTarget() != "" { | |
| 		// Get the actual entry for the resolved path | |
| 		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) | |
| 		} | |
| 	} | |
| 
 | |
| 	// Special case: root user always has permission | |
| 	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(fs.user, path) { | |
| 		// Check if user has explicit permissions for this path | |
| 		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 := 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) { | |
| 			return nil | |
| 		} | |
| 
 | |
| 		// Then check explicit ACLs | |
| 		if HasExplicitPermission(fs.user, path, perm, entry.IsDirectory) { | |
| 			return nil | |
| 		} | |
| 	} | |
| 	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 | |
| func isPathInHomeDirectory(user *user.User, path string) bool { | |
| 	return strings.HasPrefix(path, user.HomeDir) | |
| } | |
| 
 | |
| // HasUnixPermission checks if the user has the required Unix permission | |
| // Uses bit masks for clarity and maintainability | |
| func HasUnixPermission(isOwner, isGroup bool, fileMode os.FileMode, isDirectory bool, requiredPerm string) bool { | |
| 	const ( | |
| 		ownerRead  = 0400 | |
| 		ownerWrite = 0200 | |
| 		ownerExec  = 0100 | |
| 		groupRead  = 0040 | |
| 		groupWrite = 0020 | |
| 		groupExec  = 0010 | |
| 		otherRead  = 0004 | |
| 		otherWrite = 0002 | |
| 		otherExec  = 0001 | |
| 	) | |
| 
 | |
| 	// Check read permission | |
| 	hasRead := (isOwner && (fileMode&ownerRead != 0)) || | |
| 		(isGroup && (fileMode&groupRead != 0)) || | |
| 		(fileMode&otherRead != 0) | |
| 
 | |
| 	// Check write permission | |
| 	hasWrite := (isOwner && (fileMode&ownerWrite != 0)) || | |
| 		(isGroup && (fileMode&groupWrite != 0)) || | |
| 		(fileMode&otherWrite != 0) | |
| 
 | |
| 	// Check execute permission | |
| 	hasExec := (isOwner && (fileMode&ownerExec != 0)) || | |
| 		(isGroup && (fileMode&groupExec != 0)) || | |
| 		(fileMode&otherExec != 0) | |
| 
 | |
| 	switch requiredPerm { | |
| 	case PermRead: | |
| 		return hasRead | |
| 	case PermWrite: | |
| 		return hasWrite | |
| 	case PermExecute: | |
| 		return hasExec | |
| 	case PermList: | |
| 		if isDirectory { | |
| 			return hasRead && hasExec | |
| 		} | |
| 		return hasRead | |
| 	case PermDelete: | |
| 		return hasWrite | |
| 	case PermMkdir: | |
| 		return isDirectory && hasWrite | |
| 	case PermTraverse: | |
| 		return isDirectory && hasExec | |
| 	case PermReadWrite: | |
| 		return hasRead && hasWrite | |
| 	case PermAll, PermAdmin: | |
| 		return hasRead && hasWrite && hasExec | |
| 	} | |
| 	return false | |
| } | |
| 
 | |
| // HasExplicitPermission checks if the user has explicit permission from user config | |
| func HasExplicitPermission(user *user.User, filepath, requiredPerm string, isDirectory bool) bool { | |
| 	// Find the most specific permission that applies to this path | |
| 	var bestMatch string | |
| 	var perms []string | |
| 
 | |
| 	for p, userPerms := range user.Permissions { | |
| 		// Check if the path is either the permission path exactly or is under that path | |
| 		if strings.HasPrefix(filepath, p) && len(p) > len(bestMatch) { | |
| 			bestMatch = p | |
| 			perms = userPerms | |
| 		} | |
| 	} | |
| 
 | |
| 	// No matching permissions found | |
| 	if bestMatch == "" { | |
| 		return false | |
| 	} | |
| 
 | |
| 	// Check if user has admin role | |
| 	if containsString(perms, PermAdmin) { | |
| 		return true | |
| 	} | |
| 
 | |
| 	// If user has list permission and is requesting traverse/execute permission, grant it | |
| 	if isDirectory && requiredPerm == PermExecute && containsString(perms, PermList) { | |
| 		return true | |
| 	} | |
| 
 | |
| 	// Check if the required permission is in the list | |
| 	for _, perm := range perms { | |
| 		if perm == requiredPerm || perm == PermAll { | |
| 			return true | |
| 		} | |
| 
 | |
| 		// Handle combined permissions | |
| 		if perm == PermReadWrite && (requiredPerm == PermRead || requiredPerm == PermWrite) { | |
| 			return true | |
| 		} | |
| 
 | |
| 		// Directory-specific permissions | |
| 		if isDirectory && perm == PermList && requiredPerm == PermRead { | |
| 			return true | |
| 		} | |
| 		if isDirectory && perm == PermTraverse && requiredPerm == PermExecute { | |
| 			return true | |
| 		} | |
| 	} | |
| 
 | |
| 	return false | |
| } | |
| 
 | |
| // Helper function to check if a string is in a slice | |
| func containsString(slice []string, s string) bool { | |
| 	for _, item := range slice { | |
| 		if item == s { | |
| 			return true | |
| 		} | |
| 	} | |
| 	return false | |
| }
 |