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 <nicola.murino@gmail.com>
This commit is contained in:
parent
fb2d59ec92
commit
02db00d008
13 changed files with 137 additions and 30 deletions
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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{}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue