diff options
Diffstat (limited to 'weed/sftpd/auth/permissions.go')
| -rw-r--r-- | weed/sftpd/auth/permissions.go | 267 |
1 files changed, 267 insertions, 0 deletions
diff --git a/weed/sftpd/auth/permissions.go b/weed/sftpd/auth/permissions.go new file mode 100644 index 000000000..8a0a3eade --- /dev/null +++ b/weed/sftpd/auth/permissions.go @@ -0,0 +1,267 @@ +package auth + +import ( + "context" + "fmt" + "os" + "strings" + + "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" +) + +// 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 + 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 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, + } +} + +// 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 (pc *PermissionChecker) CheckFilePermission(user *user.User, path string, perm string) error { + if user == nil { + return &PermissionError{Path: path, Perm: perm, User: "unknown"} + } + + // Retrieve metadata via helper + entry, err := pc.fsHelper.GetEntry(path) + if err != nil { + return fmt.Errorf("failed to get entry for path %s: %w", path, err) + } + + // Handle symlinks by resolving them + if entry.IsSymlink { + // Get the actual entry for the resolved path + entry, err = pc.fsHelper.GetEntry(entry.Attributes.SymlinkTarget) + 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 { + return nil + } + + // Check if path is within user's home directory and has explicit permissions + if isPathInHomeDirectory(user, path) { + // Check if user has explicit permissions for this path + if HasExplicitPermission(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 + mode := os.FileMode(entry.Attributes.FileMode) + + if HasUnixPermission(isOwner, isGroup, mode, entry.IsDirectory, perm) { + return nil + } + + // Then check explicit ACLs + if HasExplicitPermission(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) +} + +// 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 +} |
