mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-10-13 09:38:36 +02:00
- SetPassword now returns an error instead of silently truncating passwords >72 bytes (the prior fallback could also panic for short passwords when bcrypt returned any non-ErrPasswordTooLong error). - CheckPassword is now pure and reports (ok, legacy); migration moves to FileStore.migrateLegacyPassword under a write lock to fix a data race on concurrent logins and a double-check avoids redundant re-hashing. - ValidatePassword only rewrites the user store when a migration actually happened, eliminating a full file rewrite on every successful login. - Legacy plaintext comparison uses subtle.ConstantTimeCompare again, restoring the pre-PR constant-time property. - CreateUser propagates SetPassword errors and releases the write lock before calling saveUsers to avoid recursive RWMutex locking. - Add unit tests covering new-user hashing, legacy migration, no-rewrite on normal login, wrong/empty passwords, oversize password rejection, and concurrent migration under -race.
105 lines
3.4 KiB
Go
105 lines
3.4 KiB
Go
// Package user provides user management functionality for the SFTP server
|
|
package user
|
|
|
|
import (
|
|
"crypto/subtle"
|
|
"fmt"
|
|
"math/rand/v2"
|
|
"path/filepath"
|
|
|
|
"golang.org/x/crypto/bcrypt"
|
|
)
|
|
|
|
// User represents an SFTP user with authentication and permission details
|
|
type User struct {
|
|
Username string `json:"Username"`
|
|
HashedPassword string `json:"HashedPassword"` // bcrypt hash
|
|
Password string `json:"Password,omitempty"` // deprecated: plaintext, migrated to HashedPassword on first successful login
|
|
PublicKeys []string `json:"PublicKeys,omitempty"`
|
|
HomeDir string `json:"HomeDir"`
|
|
Permissions map[string][]string `json:"Permissions,omitempty"`
|
|
Uid uint32 `json:"Uid"`
|
|
Gid uint32 `json:"Gid"`
|
|
}
|
|
|
|
// NewUser creates a new user with default settings
|
|
func NewUser(username string) *User {
|
|
// Generate a random UID/GID between 1000 and 60000
|
|
// This range is typically safe for regular users in most systems
|
|
// 0-999 are often reserved for system users
|
|
randomId := 1000 + rand.IntN(59000)
|
|
|
|
return &User{
|
|
Username: username,
|
|
Permissions: make(map[string][]string),
|
|
HomeDir: filepath.Join("/home", username),
|
|
Uid: uint32(randomId),
|
|
Gid: uint32(randomId),
|
|
}
|
|
}
|
|
|
|
// SetPassword hashes and stores the password using bcrypt. It clears any
|
|
// legacy plaintext value. Returns an error if the password cannot be hashed
|
|
// (e.g. exceeds bcrypt's 72-byte limit).
|
|
func (u *User) SetPassword(password string) error {
|
|
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
|
if err != nil {
|
|
return fmt.Errorf("hash password: %w", err)
|
|
}
|
|
u.HashedPassword = string(hash)
|
|
u.Password = ""
|
|
return nil
|
|
}
|
|
|
|
// CheckPassword verifies a password against the stored credentials without
|
|
// mutating the receiver. The legacy return is true when the match succeeded
|
|
// via the deprecated plaintext field; the caller is expected to re-hash and
|
|
// persist the password under an appropriate lock.
|
|
func (u *User) CheckPassword(password string) (ok bool, legacy bool) {
|
|
if u.HashedPassword != "" {
|
|
return bcrypt.CompareHashAndPassword([]byte(u.HashedPassword), []byte(password)) == nil, false
|
|
}
|
|
if u.Password != "" && subtle.ConstantTimeCompare([]byte(u.Password), []byte(password)) == 1 {
|
|
return true, true
|
|
}
|
|
return false, false
|
|
}
|
|
|
|
// AddPublicKey adds a public key to the user
|
|
func (u *User) AddPublicKey(key string) {
|
|
// Check if key already exists
|
|
for _, existingKey := range u.PublicKeys {
|
|
if existingKey == key {
|
|
return
|
|
}
|
|
}
|
|
u.PublicKeys = append(u.PublicKeys, key)
|
|
}
|
|
|
|
// RemovePublicKey removes a public key from the user
|
|
func (u *User) RemovePublicKey(key string) bool {
|
|
for i, existingKey := range u.PublicKeys {
|
|
if existingKey == key {
|
|
// Remove the key by replacing it with the last element and truncating
|
|
u.PublicKeys[i] = u.PublicKeys[len(u.PublicKeys)-1]
|
|
u.PublicKeys = u.PublicKeys[:len(u.PublicKeys)-1]
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// SetPermission sets permissions for a specific path
|
|
func (u *User) SetPermission(path string, permissions []string) {
|
|
u.Permissions[path] = permissions
|
|
}
|
|
|
|
// RemovePermission removes permissions for a specific path
|
|
func (u *User) RemovePermission(path string) bool {
|
|
if _, exists := u.Permissions[path]; exists {
|
|
delete(u.Permissions, path)
|
|
return true
|
|
}
|
|
return false
|
|
}
|