sftp_permissions.go 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  1. package sftpd
  2. import (
  3. "fmt"
  4. "os"
  5. "path/filepath"
  6. "strings"
  7. "github.com/seaweedfs/seaweedfs/weed/glog"
  8. "github.com/seaweedfs/seaweedfs/weed/sftpd/user"
  9. )
  10. // Permission constants for clarity and consistency
  11. const (
  12. PermRead = "read"
  13. PermWrite = "write"
  14. PermExecute = "execute"
  15. PermList = "list"
  16. PermDelete = "delete"
  17. PermMkdir = "mkdir"
  18. PermTraverse = "traverse"
  19. PermAll = "*"
  20. PermAdmin = "admin"
  21. PermReadWrite = "readwrite"
  22. )
  23. // Entry represents a filesystem entry with attributes
  24. type Entry struct {
  25. IsDirectory bool
  26. Attributes *EntryAttributes
  27. IsSymlink bool // Added to track symlinks
  28. Target string // For symlinks, stores the target path
  29. }
  30. // EntryAttributes contains file attributes
  31. type EntryAttributes struct {
  32. Uid uint32
  33. Gid uint32
  34. FileMode uint32
  35. SymlinkTarget string
  36. }
  37. // PermissionError represents a permission-related erro
  38. // CheckFilePermission verifies if a user has the required permission on a path
  39. // It first checks if the path is in the user's home directory with explicit permissions.
  40. // If not, it falls back to Unix permission checking followed by explicit permission checking.
  41. // Parameters:
  42. // - user: The user requesting access
  43. // - path: The filesystem path to check
  44. // - perm: The permission being requested (read, write, execute, etc.)
  45. //
  46. // Returns:
  47. // - nil if permission is granted, error otherwise
  48. func (fs *SftpServer) CheckFilePermission(path string, perm string) error {
  49. if fs.user == nil {
  50. glog.V(0).Infof("permission denied. No user associated with the SftpServer.")
  51. return os.ErrPermission
  52. }
  53. // Special case for "create" or "write" permissions on non-existent paths
  54. // Check parent directory permissions instead
  55. entry, err := fs.getEntry(path)
  56. if err != nil {
  57. // If the path doesn't exist and we're checking for create/write/mkdir permission,
  58. // check permissions on the parent directory instead
  59. if err == os.ErrNotExist {
  60. parentPath := filepath.Dir(path)
  61. // Check if user can write to the parent directory
  62. return fs.CheckFilePermission(parentPath, perm)
  63. }
  64. return fmt.Errorf("failed to get entry for path %s: %w", path, err)
  65. }
  66. // Rest of the function remains the same...
  67. // Handle symlinks by resolving them
  68. if entry.Attributes.GetSymlinkTarget() != "" {
  69. // Get the actual entry for the resolved path
  70. entry, err = fs.getEntry(entry.Attributes.GetSymlinkTarget())
  71. if err != nil {
  72. return fmt.Errorf("failed to get entry for resolved path %s: %w", entry.Attributes.SymlinkTarget, err)
  73. }
  74. }
  75. // Special case: root user always has permission
  76. if fs.user.Username == "root" || fs.user.Uid == 0 {
  77. return nil
  78. }
  79. // Check if path is within user's home directory and has explicit permissions
  80. if isPathInHomeDirectory(fs.user, path) {
  81. // Check if user has explicit permissions for this path
  82. if HasExplicitPermission(fs.user, path, perm, entry.IsDirectory) {
  83. return nil
  84. }
  85. } else {
  86. // For paths outside home directory or without explicit home permissions,
  87. // check UNIX-style perms first
  88. isOwner := fs.user.Uid == entry.Attributes.Uid
  89. isGroup := fs.user.Gid == entry.Attributes.Gid
  90. mode := os.FileMode(entry.Attributes.FileMode)
  91. if HasUnixPermission(isOwner, isGroup, mode, entry.IsDirectory, perm) {
  92. return nil
  93. }
  94. // Then check explicit ACLs
  95. if HasExplicitPermission(fs.user, path, perm, entry.IsDirectory) {
  96. return nil
  97. }
  98. }
  99. glog.V(0).Infof("permission denied for user %s on path %s for permission %s", fs.user.Username, path, perm)
  100. return os.ErrPermission
  101. }
  102. // isPathInHomeDirectory checks if a path is in the user's home directory
  103. func isPathInHomeDirectory(user *user.User, path string) bool {
  104. return strings.HasPrefix(path, user.HomeDir)
  105. }
  106. // HasUnixPermission checks if the user has the required Unix permission
  107. // Uses bit masks for clarity and maintainability
  108. func HasUnixPermission(isOwner, isGroup bool, fileMode os.FileMode, isDirectory bool, requiredPerm string) bool {
  109. const (
  110. ownerRead = 0400
  111. ownerWrite = 0200
  112. ownerExec = 0100
  113. groupRead = 0040
  114. groupWrite = 0020
  115. groupExec = 0010
  116. otherRead = 0004
  117. otherWrite = 0002
  118. otherExec = 0001
  119. )
  120. // Check read permission
  121. hasRead := (isOwner && (fileMode&ownerRead != 0)) ||
  122. (isGroup && (fileMode&groupRead != 0)) ||
  123. (fileMode&otherRead != 0)
  124. // Check write permission
  125. hasWrite := (isOwner && (fileMode&ownerWrite != 0)) ||
  126. (isGroup && (fileMode&groupWrite != 0)) ||
  127. (fileMode&otherWrite != 0)
  128. // Check execute permission
  129. hasExec := (isOwner && (fileMode&ownerExec != 0)) ||
  130. (isGroup && (fileMode&groupExec != 0)) ||
  131. (fileMode&otherExec != 0)
  132. switch requiredPerm {
  133. case PermRead:
  134. return hasRead
  135. case PermWrite:
  136. return hasWrite
  137. case PermExecute:
  138. return hasExec
  139. case PermList:
  140. if isDirectory {
  141. return hasRead && hasExec
  142. }
  143. return hasRead
  144. case PermDelete:
  145. return hasWrite
  146. case PermMkdir:
  147. return isDirectory && hasWrite
  148. case PermTraverse:
  149. return isDirectory && hasExec
  150. case PermReadWrite:
  151. return hasRead && hasWrite
  152. case PermAll, PermAdmin:
  153. return hasRead && hasWrite && hasExec
  154. }
  155. return false
  156. }
  157. // HasExplicitPermission checks if the user has explicit permission from user config
  158. func HasExplicitPermission(user *user.User, filepath, requiredPerm string, isDirectory bool) bool {
  159. // Find the most specific permission that applies to this path
  160. var bestMatch string
  161. var perms []string
  162. for p, userPerms := range user.Permissions {
  163. // Check if the path is either the permission path exactly or is under that path
  164. if strings.HasPrefix(filepath, p) && len(p) > len(bestMatch) {
  165. bestMatch = p
  166. perms = userPerms
  167. }
  168. }
  169. // No matching permissions found
  170. if bestMatch == "" {
  171. return false
  172. }
  173. // Check if user has admin role
  174. if containsString(perms, PermAdmin) {
  175. return true
  176. }
  177. // If user has list permission and is requesting traverse/execute permission, grant it
  178. if isDirectory && requiredPerm == PermExecute && containsString(perms, PermList) {
  179. return true
  180. }
  181. // Check if the required permission is in the list
  182. for _, perm := range perms {
  183. if perm == requiredPerm || perm == PermAll {
  184. return true
  185. }
  186. // Handle combined permissions
  187. if perm == PermReadWrite && (requiredPerm == PermRead || requiredPerm == PermWrite) {
  188. return true
  189. }
  190. // Directory-specific permissions
  191. if isDirectory && perm == PermList && requiredPerm == PermRead {
  192. return true
  193. }
  194. if isDirectory && perm == PermTraverse && requiredPerm == PermExecute {
  195. return true
  196. }
  197. }
  198. return false
  199. }
  200. // Helper function to check if a string is in a slice
  201. func containsString(slice []string, s string) bool {
  202. for _, item := range slice {
  203. if item == s {
  204. return true
  205. }
  206. }
  207. return false
  208. }