mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-08 15:41:15 +02:00
* Add GitHub Actions workflow for codespell on master * Add rudimentary codespell config * Tune codespell config: skip generated code, ignore camelCase, whitelist domain terms Add camelCase/PascalCase regex to ignore common Go/Rust/JS identifiers like allLocations, publishErr, ReadInside, FlushInterval. Also skip templ-generated *_templ.go files, and whitelist a handful of short/domain-specific words (visibles, fo, te, ser, bject, unparseable, keep-alives, tread, anc, ue) that show up as false positives across the tree. Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Fix ambiguous typos and protect false positives Fixes typos that codespell reports with multiple candidate suggestions (so `codespell -w` cannot auto-apply them), plus one inline pragma and one config entry to protect legitimate identifiers. Manual fixes (single correct answer chosen from context): - pattens -> patterns (5x) in filer/upload/shell flag help strings - finded -> found (2x) in tarantool storage.lua comment - spacify -> specify (2x) in helm chart values.yaml comment - wether -> whether in skiplist.go docstring - simpe -> simple in mq schema test case name False-positive protection: - Add `//codespell:ignore` next to `source GET's` (possessive of HTTP verb) in s3api_object_handlers_copy_stream.go - Whitelist `auther` in .codespellrc — it's a local variable meaning "authenticator" in weed/security/tls.go, not a typo of "author". Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Extend codespell ignore list: .git-meta path and thirdparty groupId Also skip `.git-meta` (scratch dir for commit messages that may contain typo words verbatim) and whitelist `thirdparty` — it appears as the literal Maven groupId `org.apache.hadoop.thirdparty` in hdfs3 poms and cannot be renamed. Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> * [DATALAD RUNCMD] Fix non-ambiguous typos with codespell -w Auto-applied fixes to the 44 remaining single-suggestion typos across docs, comments, log messages, tests, config, and one Java pom. === Do not change lines below === { "chain": [], "cmd": "uvx codespell -w", "exit": 0, "extra_inputs": [], "inputs": [], "outputs": [], "pwd": "." } ^^^ Do not change lines above ^^^ * Revert breaking codespell fixes; whitelist unknwon and atleast Two of the auto-applied `codespell -w` fixes were false positives that would break the build/tests: - go.mod: `github.com/unknwon/goconfig` is a real Go module path — the upstream author's GitHub handle is literally `unknwon`. Renaming to `unknown` would fail dependency resolution. - test/benchmark/fuse_db/bin/{sqlite_verify.py,run_mysql.sh,run_sqlite.sh}: `atleast` is a literal CLI mode value (a string constant compared and passed as a positional argument). Rewriting to `at least` splits it into two arguments and breaks the mode check. Reverted those files and whitelisted both words in .codespellrc so future runs won't re-suggest the same broken fixes. Co-Authored-By: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Code 2.1.217 / Claude Opus 4.7 (1M context) <noreply@anthropic.com>
254 lines
7.0 KiB
Go
254 lines
7.0 KiB
Go
package sftpd
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
stdpath "path"
|
|
"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 error
|
|
|
|
// 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 := stdpath.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
|
|
}
|
|
|
|
// pathWithin reports whether candidate is base itself or a descendant of base,
|
|
// on path-component boundaries so /a does not match /a-sibling.
|
|
func pathWithin(base, candidate string) bool {
|
|
base = stdpath.Clean(base)
|
|
candidate = stdpath.Clean(candidate)
|
|
if base == "/" {
|
|
return strings.HasPrefix(candidate, "/")
|
|
}
|
|
return candidate == base || strings.HasPrefix(candidate, base+"/")
|
|
}
|
|
|
|
// isPathInHomeDirectory checks if a path is in the user's home directory
|
|
func isPathInHomeDirectory(user *user.User, path string) bool {
|
|
if user.HomeDir == "" {
|
|
return true
|
|
}
|
|
return pathWithin(user.HomeDir, path)
|
|
}
|
|
|
|
// 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 {
|
|
// The path must be the permission path exactly or under that path
|
|
cleaned := stdpath.Clean(p)
|
|
if pathWithin(cleaned, filepath) && len(cleaned) > len(bestMatch) {
|
|
bestMatch = cleaned
|
|
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
|
|
}
|