From 02db00d008225a5ced621672b375c16c708bbefc Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Mon, 31 Jan 2022 18:01:37 +0100 Subject: [PATCH] dataprovider: add naming rules naming rules allow to support case insensitive usernames, trim trailing and leading white spaces, and accept any valid UTF-8 characters in usernames. If you were enabling `skip_natural_keys_validation` now you need to set `naming_rules` to `1` Fixes #687 Signed-off-by: Nicola Murino --- config/config.go | 4 +-- dataprovider/admin.go | 2 +- dataprovider/dataprovider.go | 55 ++++++++++++++++++++++++++++++++---- dataprovider/memory.go | 5 +++- docs/full-configuration.md | 2 +- httpd/api_admin.go | 1 + httpd/api_folder.go | 1 + httpd/api_maintenance.go | 3 ++ httpd/api_user.go | 3 +- httpd/httpd_test.go | 40 +++++++++++++++++--------- httpdtest/httpdtest.go | 6 ++-- sftpd/sftpd_test.go | 43 ++++++++++++++++++++++++++++ sftpgo.json | 2 +- 13 files changed, 137 insertions(+), 30 deletions(-) diff --git a/config/config.go b/config/config.go index 11d66e1e..b8a66af5 100644 --- a/config/config.go +++ b/config/config.go @@ -270,9 +270,9 @@ func Init() { PasswordCaching: true, UpdateMode: 0, PreferDatabaseCredentials: false, - SkipNaturalKeysValidation: false, DelayedQuotaUpdate: 0, CreateDefaultAdmin: false, + NamingRules: 0, IsShared: 0, }, HTTPDConfig: httpd.Conf{ @@ -1308,9 +1308,9 @@ func setViperDefaults() { viper.SetDefault("data_provider.password_validation.users.min_entropy", globalConf.ProviderConf.PasswordValidation.Users.MinEntropy) viper.SetDefault("data_provider.password_caching", globalConf.ProviderConf.PasswordCaching) viper.SetDefault("data_provider.update_mode", globalConf.ProviderConf.UpdateMode) - viper.SetDefault("data_provider.skip_natural_keys_validation", globalConf.ProviderConf.SkipNaturalKeysValidation) viper.SetDefault("data_provider.delayed_quota_update", globalConf.ProviderConf.DelayedQuotaUpdate) viper.SetDefault("data_provider.create_default_admin", globalConf.ProviderConf.CreateDefaultAdmin) + viper.SetDefault("data_provider.naming_rules", globalConf.ProviderConf.NamingRules) viper.SetDefault("data_provider.is_shared", globalConf.ProviderConf.IsShared) viper.SetDefault("httpd.templates_path", globalConf.HTTPDConfig.TemplatesPath) viper.SetDefault("httpd.static_files_path", globalConf.HTTPDConfig.StaticFilesPath) diff --git a/dataprovider/admin.go b/dataprovider/admin.go index ca1a9a45..a6caff18 100644 --- a/dataprovider/admin.go +++ b/dataprovider/admin.go @@ -209,7 +209,7 @@ func (a *Admin) validate() error { if err := a.validateRecoveryCodes(); err != nil { return err } - if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(a.Username) { + if config.NamingRules&1 == 0 && !usernameRegex.MatchString(a.Username) { return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", a.Username)) } if err := a.hashPassword(); err != nil { diff --git a/dataprovider/dataprovider.go b/dataprovider/dataprovider.go index dd6a9e91..20e4358b 100644 --- a/dataprovider/dataprovider.go +++ b/dataprovider/dataprovider.go @@ -349,10 +349,6 @@ type Config struct { // Cloud Storage) should be stored in the database instead of in the directory specified by // CredentialsPath. PreferDatabaseCredentials bool `json:"prefer_database_credentials" mapstructure:"prefer_database_credentials"` - // SkipNaturalKeysValidation allows to use any UTF-8 character for natural keys as username, admin name, - // folder name. These keys are used in URIs for REST API and Web admin. By default only unreserved URI - // characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". - SkipNaturalKeysValidation bool `json:"skip_natural_keys_validation" mapstructure:"skip_natural_keys_validation"` // PasswordValidation defines the password validation rules PasswordValidation PasswordValidation `json:"password_validation" mapstructure:"password_validation"` // Verifying argon2 passwords has a high memory and computational cost, @@ -370,6 +366,18 @@ type Config struct { // on first start. // You can also create the first admin user by using the web interface or by loading initial data. CreateDefaultAdmin bool `json:"create_default_admin" mapstructure:"create_default_admin"` + // Rules for usernames and folder names: + // - 0 means no rules + // - 1 means you can use any UTF-8 character. The names are used in URIs for REST API and Web admin. + // By default only unreserved URI characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". + // - 2 means names are converted to lowercase before saving/matching and so case + // insensitive matching is possible + // - 4 means trimming trailing and leading white spaces before saving/matching + // Rules can be combined, for example 3 means both converting to lowercase and allowing any UTF-8 character. + // Enabling these options for existing installations could be backward incompatible, some users + // could be unable to login, for example existing users with mixed cases in their usernames. + // You have to ensure that all existing users respect the defined rules. + NamingRules int `json:"naming_rules" mapstructure:"naming_rules"` // If the data provider is shared across multiple SFTPGo instances, set this parameter to 1. // MySQL, PostgreSQL and CockroachDB can be shared, this setting is ignored for other data // providers. For shared data providers, SFTPGo periodically reloads the latest updated users, @@ -388,6 +396,20 @@ func (c *Config) GetShared() int { return c.IsShared } +func (c *Config) convertName(name string) string { + if c.NamingRules == 0 { + return name + } + if c.NamingRules&2 != 0 { + name = strings.ToLower(name) + } + if c.NamingRules&4 != 0 { + name = strings.TrimSpace(name) + } + + return name +} + // IsDefenderSupported returns true if the configured provider supports the defender func (c *Config) IsDefenderSupported() bool { switch c.Driver { @@ -409,6 +431,11 @@ func (c *Config) requireCustomTLSForMySQL() bool { return false } +// ConvertName converts the given name based on the configured rules +func ConvertName(name string) string { + return config.convertName(name) +} + // ActiveTransfer defines an active protocol transfer type ActiveTransfer struct { ID int64 @@ -823,6 +850,7 @@ func ResetDatabase(cnf Config, basePath string) error { // CheckAdminAndPass validates the given admin and password connecting from ip func CheckAdminAndPass(username, password, ip string) (Admin, error) { + username = config.convertName(username) return provider.validateAdminAndPass(username, password, ip) } @@ -861,6 +889,7 @@ func CheckCachedUserCredentials(user *CachedUser, password, loginMethod, protoco // CheckCompositeCredentials checks multiple credentials. // WebDAV users can send both a password and a TLS certificate within the same request func CheckCompositeCredentials(username, password, ip, loginMethod, protocol string, tlsCert *x509.Certificate) (User, string, error) { + username = config.convertName(username) if loginMethod == LoginMethodPassword { user, err := CheckUserAndPass(username, password, ip, protocol) return user, loginMethod, err @@ -900,6 +929,7 @@ func CheckCompositeCredentials(username, password, ip, loginMethod, protocol str // CheckUserBeforeTLSAuth checks if a user exits before trying mutual TLS func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) { + username = config.convertName(username) if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) { return doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate) } @@ -915,6 +945,7 @@ func CheckUserBeforeTLSAuth(username, ip, protocol string, tlsCert *x509.Certifi // CheckUserAndTLSCert returns the SFTPGo user with the given username and check if the // given TLS certificate allow authentication without password func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificate) (User, error) { + username = config.convertName(username) if plugin.Handler.HasAuthScope(plugin.AuthScopeTLSCertificate) { user, err := doPluginAuth(username, "", nil, ip, protocol, tlsCert, plugin.AuthScopeTLSCertificate) if err != nil { @@ -941,6 +972,7 @@ func CheckUserAndTLSCert(username, ip, protocol string, tlsCert *x509.Certificat // CheckUserAndPass retrieves the SFTPGo user with the given username and password if a match is found or an error func CheckUserAndPass(username, password, ip, protocol string) (User, error) { + username = config.convertName(username) if plugin.Handler.HasAuthScope(plugin.AuthScopePassword) { user, err := doPluginAuth(username, password, nil, ip, protocol, nil, plugin.AuthScopePassword) if err != nil { @@ -967,6 +999,7 @@ func CheckUserAndPass(username, password, ip, protocol string) (User, error) { // CheckUserAndPubKey retrieves the SFTP user with the given username and public key if a match is found or an error func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (User, string, error) { + username = config.convertName(username) if plugin.Handler.HasAuthScope(plugin.AuthScopePublicKey) { user, err := doPluginAuth(username, "", pubKey, ip, protocol, nil, plugin.AuthScopePublicKey) if err != nil { @@ -996,6 +1029,7 @@ func CheckUserAndPubKey(username string, pubKey []byte, ip, protocol string) (Us func CheckKeyboardInteractiveAuth(username, authHook string, client ssh.KeyboardInteractiveChallenge, ip, protocol string) (User, error) { var user User var err error + username = config.convertName(username) if plugin.Handler.HasAuthScope(plugin.AuthScopeKeyboardInteractive) { user, err = doPluginAuth(username, "", nil, ip, protocol, nil, plugin.AuthScopeKeyboardInteractive) } else if config.ExternalAuthHook != "" && (config.ExternalAuthScope == 0 || config.ExternalAuthScope&4 != 0) { @@ -1271,6 +1305,7 @@ func AddAdmin(admin *Admin, executor, ipAddress string) error { admin.Filters.TOTPConfig = AdminTOTPConfig{ Enabled: false, } + admin.Username = config.convertName(admin.Username) err := provider.addAdmin(admin) if err == nil { atomic.StoreInt32(&isAdminCreated, 1) @@ -1290,6 +1325,7 @@ func UpdateAdmin(admin *Admin, executor, ipAddress string) error { // DeleteAdmin deletes an existing SFTPGo admin func DeleteAdmin(username, executor, ipAddress string) error { + username = config.convertName(username) admin, err := provider.adminExists(username) if err != nil { return err @@ -1303,11 +1339,13 @@ func DeleteAdmin(username, executor, ipAddress string) error { // AdminExists returns the admin with the given username if it exists func AdminExists(username string) (Admin, error) { + username = config.convertName(username) return provider.adminExists(username) } // UserExists checks if the given SFTPGo username exists, returns an error if no match is found func UserExists(username string) (User, error) { + username = config.convertName(username) return provider.userExists(username) } @@ -1317,6 +1355,7 @@ func AddUser(user *User, executor, ipAddress string) error { user.Filters.TOTPConfig = UserTOTPConfig{ Enabled: false, } + user.Username = config.convertName(user.Username) err := provider.addUser(user) if err == nil { executeAction(operationAdd, executor, ipAddress, actionObjectUser, user.Username, user) @@ -1337,6 +1376,7 @@ func UpdateUser(user *User, executor, ipAddress string) error { // DeleteUser deletes an existing SFTPGo user. func DeleteUser(username, executor, ipAddress string) error { + username = config.convertName(username) user, err := provider.userExists(username) if err != nil { return err @@ -1425,6 +1465,7 @@ func GetUsersForQuotaCheck(toFetch map[string]bool) ([]User, error) { // AddFolder adds a new virtual folder. func AddFolder(folder *vfs.BaseVirtualFolder) error { + folder.Name = config.convertName(folder.Name) return provider.addFolder(folder) } @@ -1448,6 +1489,7 @@ func UpdateFolder(folder *vfs.BaseVirtualFolder, users []string, executor, ipAdd // DeleteFolder deletes an existing folder. func DeleteFolder(folderName, executor, ipAddress string) error { + folderName = config.convertName(folderName) folder, err := provider.getFolderByName(folderName) if err != nil { return err @@ -1469,6 +1511,7 @@ func DeleteFolder(folderName, executor, ipAddress string) error { // GetFolderByName returns the folder with the specified name if any func GetFolderByName(name string) (vfs.BaseVirtualFolder, error) { + name = config.convertName(name) return provider.getFolderByName(name) } @@ -2049,7 +2092,7 @@ func validateBaseParams(user *User) error { if user.Email != "" && !emailRegex.MatchString(user.Email) { return util.NewValidationError(fmt.Sprintf("email %#v is not valid", user.Email)) } - if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(user.Username) { + if config.NamingRules&1 == 0 && !usernameRegex.MatchString(user.Username) { return util.NewValidationError(fmt.Sprintf("username %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", user.Username)) } @@ -2107,7 +2150,7 @@ func ValidateFolder(folder *vfs.BaseVirtualFolder) error { if folder.Name == "" { return util.NewValidationError("folder name is mandatory") } - if !config.SkipNaturalKeysValidation && !usernameRegex.MatchString(folder.Name) { + if config.NamingRules&1 == 0 && !usernameRegex.MatchString(folder.Name) { return util.NewValidationError(fmt.Sprintf("folder name %#v is not valid, the following characters are allowed: a-zA-Z0-9-_.~", folder.Name)) } diff --git a/dataprovider/memory.go b/dataprovider/memory.go index cb089880..9bdad0bb 100644 --- a/dataprovider/memory.go +++ b/dataprovider/memory.go @@ -1536,8 +1536,9 @@ func (p *MemoryProvider) restoreAPIKeys(dump *BackupData) error { func (p *MemoryProvider) restoreAdmins(dump *BackupData) error { for _, admin := range dump.Admins { - a, err := p.adminExists(admin.Username) admin := admin // pin + admin.Username = config.convertName(admin.Username) + a, err := p.adminExists(admin.Username) if err == nil { admin.ID = a.ID err = UpdateAdmin(&admin, ActionExecutorSystem, "") @@ -1559,6 +1560,7 @@ func (p *MemoryProvider) restoreAdmins(dump *BackupData) error { func (p *MemoryProvider) restoreFolders(dump *BackupData) error { for _, folder := range dump.Folders { folder := folder // pin + folder.Name = config.convertName(folder.Name) f, err := p.getFolderByName(folder.Name) if err == nil { folder.ID = f.ID @@ -1582,6 +1584,7 @@ func (p *MemoryProvider) restoreFolders(dump *BackupData) error { func (p *MemoryProvider) restoreUsers(dump *BackupData) error { for _, user := range dump.Users { user := user // pin + user.Username = config.convertName(user.Username) u, err := p.userExists(user.Username) if err == nil { user.ID = u.ID diff --git a/docs/full-configuration.md b/docs/full-configuration.md index 4dde9c28..de20cfd2 100644 --- a/docs/full-configuration.md +++ b/docs/full-configuration.md @@ -215,8 +215,8 @@ The configuration file contains the following sections: - `min_entropy`, float. Default: `0`. - `password_caching`, boolean. Verifying argon2id passwords has a high memory and computational cost, verifying bcrypt passwords has a high computational cost, by enabling, in memory, password caching you reduce these costs. Default: `true` - `update_mode`, integer. Defines how the database will be initialized/updated. 0 means automatically. 1 means manually using the initprovider sub-command. - - `skip_natural_keys_validation`, boolean. If `true` you can use any UTF-8 character for natural keys as username, admin name, folder name. These keys are used in URIs for REST API and Web admin. If `false` only unreserved URI characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". Default: `false`. - `create_default_admin`, boolean. Before you can use SFTPGo you need to create an admin account. If you open the admin web UI, a setup screen will guide you in creating the first admin account. You can automatically create the first admin account by enabling this setting and setting the environment variables `SFTPGO_DEFAULT_ADMIN_USERNAME` and `SFTPGO_DEFAULT_ADMIN_PASSWORD`. You can also create the first admin by loading initial data. This setting has no effect if an admin account is already found within the data provider. Default `false`. + - `naming_rules`, integer. Naming rules for usernames and folder names. `0` means no rules. `1` means you can use any UTF-8 character. The names are used in URIs for REST API and Web admin. If not set only unreserved URI characters are allowed: ALPHA / DIGIT / "-" / "." / "_" / "~". `2` means names are converted to lowercase before saving/matching and so case insensitive matching is possible. `3` means trimming trailing and leading white spaces before saving/matching. Rules can be combined, for example `3` means both converting to lowercase and allowing any UTF-8 character. Enabling these options for existing installations could be backward incompatible, some users could be unable to login, for example existing users with mixed cases in their usernames. You have to ensure that all existing users respect the defined rules. Default: `0`. - `is_shared`, integer. If the data provider is shared across multiple SFTPGo instances, set this parameter to `1`. `MySQL`, `PostgreSQL` and `CockroachDB` can be shared, this setting is ignored for other data providers. For shared data providers, SFTPGo periodically reloads the latest updated users, based on the `updated_at` field, and updates its internal caches if users are updated from a different instance. This check, if enabled, is executed every 10 minutes. For shared data providers, active transfers are persisted in the database and thus quota checks between ongoing transfers will work cross multiple instances. Default: `0`. - **"httpd"**, the configuration for the HTTP server used to serve REST API and to expose the built-in web interface - `bindings`, list of structs. Each struct has the following fields: diff --git a/httpd/api_admin.go b/httpd/api_admin.go index a944d498..1df4beaf 100644 --- a/httpd/api_admin.go +++ b/httpd/api_admin.go @@ -103,6 +103,7 @@ func updateAdmin(w http.ResponseWriter, r *http.Request) { } adminID := admin.ID + username = admin.Username totpConfig := admin.Filters.TOTPConfig recoveryCodes := admin.Filters.RecoveryCodes admin.Filters.TOTPConfig = dataprovider.AdminTOTPConfig{} diff --git a/httpd/api_folder.go b/httpd/api_folder.go index e7270fdd..6938fa01 100644 --- a/httpd/api_folder.go +++ b/httpd/api_folder.go @@ -58,6 +58,7 @@ func updateFolder(w http.ResponseWriter, r *http.Request) { } users := folder.Users folderID := folder.ID + name = folder.Name currentS3AccessSecret := folder.FsConfig.S3Config.AccessSecret currentAzAccountKey := folder.FsConfig.AzBlobConfig.AccountKey currentAzSASUrl := folder.FsConfig.AzBlobConfig.SASURL diff --git a/httpd/api_maintenance.go b/httpd/api_maintenance.go index 8dcd3a47..c157087a 100644 --- a/httpd/api_maintenance.go +++ b/httpd/api_maintenance.go @@ -228,6 +228,7 @@ func RestoreFolders(folders []vfs.BaseVirtualFolder, inputFile string, mode, sca continue } folder.ID = f.ID + folder.Name = f.Name err = dataprovider.UpdateFolder(&folder, f.Users, executor, ipAddress) logger.Debug(logSender, "", "restoring existing folder: %+v, dump file: %#v, error: %v", folder, inputFile, err) } else { @@ -318,6 +319,7 @@ func RestoreAdmins(admins []dataprovider.Admin, inputFile string, mode int, exec continue } admin.ID = a.ID + admin.Username = a.Username err = dataprovider.UpdateAdmin(&admin, executor, ipAddress) admin.Password = redactedSecret logger.Debug(logSender, "", "restoring existing admin: %+v, dump file: %#v, error: %v", admin, inputFile, err) @@ -345,6 +347,7 @@ func RestoreUsers(users []dataprovider.User, inputFile string, mode, scanQuota i continue } user.ID = u.ID + user.Username = u.Username err = dataprovider.UpdateUser(&user, executor, ipAddress) user.Password = redactedSecret logger.Debug(logSender, "", "restoring existing user: %+v, dump file: %#v, error: %v", user, inputFile, err) diff --git a/httpd/api_user.go b/httpd/api_user.go index 41921760..8d9eee85 100644 --- a/httpd/api_user.go +++ b/httpd/api_user.go @@ -123,6 +123,7 @@ func updateUser(w http.ResponseWriter, r *http.Request) { return } userID := user.ID + username = user.Username totpConfig := user.Filters.TOTPConfig recoveryCodes := user.Filters.RecoveryCodes currentPermissions := user.Permissions @@ -184,7 +185,7 @@ func deleteUser(w http.ResponseWriter, r *http.Request) { return } sendAPIResponse(w, r, err, "User deleted", http.StatusOK) - disconnectUser(username) + disconnectUser(dataprovider.ConvertName(username)) } func forgotUserPassword(w http.ResponseWriter, r *http.Request) { diff --git a/httpd/httpd_test.go b/httpd/httpd_test.go index e5e71e3c..a1791905 100644 --- a/httpd/httpd_test.go +++ b/httpd/httpd_test.go @@ -3753,7 +3753,7 @@ func TestCloseConnectionAfterUserUpdateDelete(t *testing.T) { assert.Len(t, common.Connections.GetStats(), 0) } -func TestSkipNaturalKeysValidation(t *testing.T) { +func TestNamingRules(t *testing.T) { smtpCfg := smtp.Config{ Host: "127.0.0.1", Port: 3525, @@ -3766,44 +3766,56 @@ func TestSkipNaturalKeysValidation(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - providerConf.SkipNaturalKeysValidation = true + providerConf.NamingRules = 7 err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) u := getTestUser() - u.Username = "user@user.me" - u.Email = u.Username + u.Username = " uSeR@user.me " + u.Email = dataprovider.ConvertName(u.Username) user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) + assert.Equal(t, "user@user.me", user.Username) + user.Username = u.Username user.AdditionalInfo = "info" user, _, err = httpdtest.UpdateUser(user, http.StatusOK, "") assert.NoError(t, err) - user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK) + user, _, err = httpdtest.GetUserByUsername(u.Username, http.StatusOK) assert.NoError(t, err) a := getTestAdmin() - a.Username = "admin@example.com" + a.Username = "admiN@example.com " admin, _, err := httpdtest.AddAdmin(a, http.StatusCreated) assert.NoError(t, err) - admin.Email = admin.Username + assert.Equal(t, "admin@example.com", admin.Username) + admin.Email = dataprovider.ConvertName(a.Username) + admin.Username = a.Username admin, _, err = httpdtest.UpdateAdmin(admin, http.StatusOK) assert.NoError(t, err) - admin, _, err = httpdtest.GetAdminByUsername(admin.Username, http.StatusOK) + admin, _, err = httpdtest.GetAdminByUsername(a.Username, http.StatusOK) assert.NoError(t, err) f := vfs.BaseVirtualFolder{ - Name: "文件夹", + Name: "文件夹AB", MappedPath: filepath.Clean(os.TempDir()), } folder, resp, err := httpdtest.AddFolder(f, http.StatusCreated) assert.NoError(t, err, string(resp)) + assert.Equal(t, "文件夹ab", folder.Name) + folder.Name = f.Name folder.Description = folder.Name folder, resp, err = httpdtest.UpdateFolder(folder, http.StatusOK) assert.NoError(t, err, string(resp)) - folder, resp, err = httpdtest.GetFolderByName(folder.Name, http.StatusOK) + folder, resp, err = httpdtest.GetFolderByName(f.Name, http.StatusOK) assert.NoError(t, err, string(resp)) - _, err = httpdtest.RemoveFolder(folder, http.StatusOK) + _, err = httpdtest.RemoveFolder(f, http.StatusOK) assert.NoError(t, err) + token, err := getJWTWebClientTokenFromTestServer(u.Username, defaultPassword) + assert.NoError(t, err) + assert.NotEmpty(t, token) + adminAPIToken, err := getJWTAPITokenFromTestServer(a.Username, defaultTokenAuthPass) + assert.NoError(t, err) + assert.NotEmpty(t, adminAPIToken) err = dataprovider.Close() assert.NoError(t, err) @@ -3821,7 +3833,7 @@ func TestSkipNaturalKeysValidation(t *testing.T) { csrfToken, err := getCSRFToken(httpBaseURL + webClientLoginPath) assert.NoError(t, err) - token, err := getJWTWebClientTokenFromTestServer(user.Username, defaultPassword) + token, err = getJWTWebClientTokenFromTestServer(user.Username, defaultPassword) assert.NoError(t, err) form := make(url.Values) form.Set(csrfFormToken, csrfToken) @@ -3854,7 +3866,7 @@ func TestSkipNaturalKeysValidation(t *testing.T) { assert.Equal(t, http.StatusOK, rr.Code) assert.Contains(t, rr.Body.String(), "Unable to set the new password") - adminAPIToken, err := getJWTAPITokenFromTestServer(admin.Username, defaultTokenAuthPass) + adminAPIToken, err = getJWTAPITokenFromTestServer(admin.Username, defaultTokenAuthPass) assert.NoError(t, err) userAPIToken, err := getJWTAPIUserTokenFromTestServer(user.Username, defaultPassword) assert.NoError(t, err) @@ -3961,7 +3973,7 @@ func TestSaveErrors(t *testing.T) { err = config.LoadConfig(configDir, "") assert.NoError(t, err) providerConf := config.GetProviderConf() - providerConf.SkipNaturalKeysValidation = true + providerConf.NamingRules = 1 err = dataprovider.Initialize(providerConf, configDir, true) assert.NoError(t, err) diff --git a/httpdtest/httpdtest.go b/httpdtest/httpdtest.go index 8b052010..f75145e7 100644 --- a/httpdtest/httpdtest.go +++ b/httpdtest/httpdtest.go @@ -1053,7 +1053,7 @@ func checkFolder(expected *vfs.BaseVirtualFolder, actual *vfs.BaseVirtualFolder) return errors.New("folder ID mismatch") } } - if expected.Name != actual.Name { + if dataprovider.ConvertName(expected.Name) != actual.Name { return errors.New("name mismatch") } if expected.MappedPath != actual.MappedPath { @@ -1145,7 +1145,7 @@ func checkAdmin(expected, actual *dataprovider.Admin) error { } func compareAdminEqualFields(expected *dataprovider.Admin, actual *dataprovider.Admin) error { - if expected.Username != actual.Username { + if dataprovider.ConvertName(expected.Username) != actual.Username { return errors.New("sername mismatch") } if expected.Email != actual.Email { @@ -1605,7 +1605,7 @@ func compareUserFilePatternsFilters(expected *dataprovider.User, actual *datapro } func compareEqualsUserFields(expected *dataprovider.User, actual *dataprovider.User) error { - if expected.Username != actual.Username { + if dataprovider.ConvertName(expected.Username) != actual.Username { return errors.New("username mismatch") } if expected.HomeDir != actual.HomeDir { diff --git a/sftpd/sftpd_test.go b/sftpd/sftpd_test.go index 29ce2eb3..5908d356 100644 --- a/sftpd/sftpd_test.go +++ b/sftpd/sftpd_test.go @@ -2398,6 +2398,49 @@ func TestInteractiveLoginWithPasscode(t *testing.T) { assert.NoError(t, err) } +func TestNamingRules(t *testing.T) { + err := dataprovider.Close() + assert.NoError(t, err) + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf := config.GetProviderConf() + providerConf.NamingRules = 7 + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) + usePubKey := true + u := getTestUser(usePubKey) + u.Username = "useR@user.com " + user, _, err := httpdtest.AddUser(u, http.StatusCreated) + assert.NoError(t, err) + assert.Equal(t, "user@user.com", user.Username) + conn, client, err := getSftpClient(u, usePubKey) + if assert.NoError(t, err) { + defer conn.Close() + defer client.Close() + assert.NoError(t, checkBasicSFTP(client)) + } + u.Password = defaultPassword + _, _, err = httpdtest.UpdateUser(u, http.StatusOK, "") + assert.NoError(t, err) + conn, client, err = getSftpClient(u, false) + if assert.NoError(t, err) { + defer conn.Close() + defer client.Close() + assert.NoError(t, checkBasicSFTP(client)) + } + _, err = httpdtest.RemoveUser(u, http.StatusOK) + assert.NoError(t, err) + err = os.RemoveAll(u.GetHomeDir()) + assert.NoError(t, err) + err = dataprovider.Close() + assert.NoError(t, err) + err = config.LoadConfig(configDir, "") + assert.NoError(t, err) + providerConf = config.GetProviderConf() + err = dataprovider.Initialize(providerConf, configDir, true) + assert.NoError(t, err) +} + func TestPreLoginScript(t *testing.T) { if runtime.GOOS == osWindows { t.Skip("this test is not available on Windows") diff --git a/sftpgo.json b/sftpgo.json index 15d15472..7e327c58 100644 --- a/sftpgo.json +++ b/sftpgo.json @@ -199,8 +199,8 @@ }, "password_caching": true, "update_mode": 0, - "skip_natural_keys_validation": false, "create_default_admin": false, + "naming_rules": 0, "is_shared": 0 }, "httpd": {