allow to set password strength at user/group level
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
parent
662164c7ff
commit
4ba3ae876d
11 changed files with 150 additions and 31 deletions
|
@ -277,7 +277,7 @@ The configuration file contains the following sections:
|
|||
- `admins`, struct. It defines the password validation rules for SFTPGo admins.
|
||||
- `min_entropy`, float. Defines the minimum password entropy. Take a looke [here](https://github.com/wagslane/go-password-validator#what-entropy-value-should-i-use) for more details. `0` means disabled, any password will be accepted. Default: `0`.
|
||||
- `users`, struct. It defines the password validation rules for SFTPGo protocol users.
|
||||
- `min_entropy`, float. Default: `0`.
|
||||
- `min_entropy`, float. This value is used as fallback if no more specific password strength is set at user/group level. 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.
|
||||
- `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`.
|
||||
|
|
|
@ -16,7 +16,7 @@ The following settings are inherited from the primary group:
|
|||
|
||||
- home dir, if set for the group will replace the one defined for the user. The `%username%` placeholder is replaced with the username
|
||||
- filesystem config, if the provider set for the group is different from the "local provider" will replace the one defined for the user. The `%username%` placeholder is replaced with the username within the defined "prefix", for any vfs, and the "username" for the SFTP filesystem config
|
||||
- max sessions, quota size/files, upload/download bandwidth, upload/download/total data transfer, max upload size, external auth cache time, ftp_security, default share expiration, password expiration: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`
|
||||
- max sessions, quota size/files, upload/download bandwidth, upload/download/total data transfer, max upload size, external auth cache time, ftp_security, default share expiration, password expiration, password strength: if they are set to `0` for the user they are replaced with the value set for the group, if different from `0`. The password strength defined at group level is only enforce when users change their password
|
||||
- expires_in, if defined and the user does not have an expiration date set, defines the expiration of the account in number of days from the creation date
|
||||
- TLS username, check password hook disabled, pre-login hook disabled, external auth hook disabled, filesystem checks disabled, allow API key authentication, anonymous user: if they are not set for the user they are replaced with the value set for the group
|
||||
- starting directory, if the user does not have a starting directory set, the value set for the group is used, if any. The `%username%` placeholder is replaced with the username
|
||||
|
|
2
go.mod
2
go.mod
|
@ -52,7 +52,7 @@ require (
|
|||
github.com/rs/cors v1.8.3
|
||||
github.com/rs/xid v1.4.0
|
||||
github.com/rs/zerolog v1.29.0
|
||||
github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810
|
||||
github.com/sftpgo/sdk v0.1.3-0.20230302063609-7677616c090b
|
||||
github.com/shirou/gopsutil/v3 v3.23.2
|
||||
github.com/spf13/afero v1.9.4
|
||||
github.com/spf13/cobra v1.6.1
|
||||
|
|
4
go.sum
4
go.sum
|
@ -1802,8 +1802,8 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg
|
|||
github.com/seccomp/libseccomp-golang v0.9.1/go.mod h1:GbW5+tmTXfcxTToHLXlScSlAvWlF4P2Ca7zGrPiEpWo=
|
||||
github.com/seccomp/libseccomp-golang v0.9.2-0.20210429002308-3879420cc921/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg=
|
||||
github.com/secsy/goftp v0.0.0-20200609142545-aa2de14babf4 h1:PT+ElG/UUFMfqy5HrxJxNzj3QBOf7dZwupeVC+mG1Lo=
|
||||
github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810 h1:9K/1RGoZcWiv2ue1JvAnKwerOzJsCAUqCR2/BnibT8s=
|
||||
github.com/sftpgo/sdk v0.1.3-0.20230213182959-2d89540f8810/go.mod h1:B1lPGb05WtvvrX5IuhHrSjWdRT867qBaoxlS2Q9+1bA=
|
||||
github.com/sftpgo/sdk v0.1.3-0.20230302063609-7677616c090b h1:OpQr1PQ1repUl1HYFEG6aDp9ljbovP9ccAfQmNih914=
|
||||
github.com/sftpgo/sdk v0.1.3-0.20230302063609-7677616c090b/go.mod h1:+STA4nxcXm/uLW3CGXwgnyo0hCeocbEjyznlFxZhtnw=
|
||||
github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU=
|
||||
github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M=
|
||||
github.com/shoenig/test v0.4.3/go.mod h1:xYtyGBC5Q3kzCNyJg/SjgNpfAa2kvmgA0i5+lQso8x0=
|
||||
|
|
|
@ -2039,7 +2039,16 @@ func UpdateUserPassword(username, plainPwd, executor, ipAddress, role string) er
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
user.Password = plainPwd
|
||||
userCopy := user.getACopy()
|
||||
if err := userCopy.LoadAndApplyGroupSettings(); err != nil {
|
||||
return err
|
||||
}
|
||||
userCopy.Password = plainPwd
|
||||
if err := createUserPasswordHash(&userCopy); err != nil {
|
||||
return err
|
||||
}
|
||||
user.LastPasswordChange = userCopy.LastPasswordChange
|
||||
user.Password = userCopy.Password
|
||||
user.Filters.RequirePasswordChange = false
|
||||
// the last password change is set when validating the user
|
||||
if err := provider.updateUser(&user); err != nil {
|
||||
|
@ -2438,6 +2447,7 @@ func copyBaseUserFilters(in sdk.BaseUserFilters) sdk.BaseUserFilters {
|
|||
filters.ExternalAuthCacheTime = in.ExternalAuthCacheTime
|
||||
filters.DefaultSharesExpiration = in.DefaultSharesExpiration
|
||||
filters.PasswordExpiration = in.PasswordExpiration
|
||||
filters.PasswordStrength = in.PasswordStrength
|
||||
filters.WebClient = make([]string, len(in.WebClient))
|
||||
copy(filters.WebClient, in.WebClient)
|
||||
filters.BandwidthLimits = make([]sdk.BandwidthLimit, 0, len(in.BandwidthLimits))
|
||||
|
@ -2963,8 +2973,8 @@ func hashPlainPassword(plainPwd string) (string, error) {
|
|||
|
||||
func createUserPasswordHash(user *User) error {
|
||||
if user.Password != "" && !user.IsPasswordHashed() {
|
||||
if config.PasswordValidation.Users.MinEntropy > 0 {
|
||||
if err := passwordvalidator.Validate(user.Password, config.PasswordValidation.Users.MinEntropy); err != nil {
|
||||
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
|
||||
if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil {
|
||||
return util.NewValidationError(err.Error())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1013,6 +1013,13 @@ func (u *User) isDirHidden(virtualPath string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (u *User) getMinPasswordEntropy() float64 {
|
||||
if u.Filters.PasswordStrength > 0 {
|
||||
return float64(u.Filters.PasswordStrength)
|
||||
}
|
||||
return config.PasswordValidation.Users.MinEntropy
|
||||
}
|
||||
|
||||
// IsFileAllowed returns true if the specified file is allowed by the file restrictions filters.
|
||||
// The second parameter returned is the deny policy
|
||||
func (u *User) IsFileAllowed(virtualPath string) (bool, int) {
|
||||
|
@ -1748,7 +1755,7 @@ func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters, replacer *s
|
|||
if u.Filters.MaxUploadFileSize == 0 {
|
||||
u.Filters.MaxUploadFileSize = filters.MaxUploadFileSize
|
||||
}
|
||||
if u.Filters.TLSUsername == "" || u.Filters.TLSUsername == sdk.TLSUsernameNone {
|
||||
if !u.IsTLSUsernameVerificationEnabled() {
|
||||
u.Filters.TLSUsername = filters.TLSUsername
|
||||
}
|
||||
if !u.Filters.Hooks.CheckPasswordDisabled {
|
||||
|
@ -1784,6 +1791,9 @@ func (u *User) mergePrimaryGroupFilters(filters sdk.BaseUserFilters, replacer *s
|
|||
if u.Filters.PasswordExpiration == 0 {
|
||||
u.Filters.PasswordExpiration = filters.PasswordExpiration
|
||||
}
|
||||
if u.Filters.PasswordStrength == 0 {
|
||||
u.Filters.PasswordStrength = filters.PasswordStrength
|
||||
}
|
||||
}
|
||||
|
||||
func (u *User) mergeAdditiveProperties(group Group, groupType int, replacer *strings.Replacer) {
|
||||
|
|
|
@ -1217,6 +1217,7 @@ func TestGroupSettingsOverride(t *testing.T) {
|
|||
}
|
||||
group2.UserSettings.DownloadBandwidth = 128
|
||||
group2.UserSettings.UploadBandwidth = 256
|
||||
group2.UserSettings.Filters.PasswordStrength = 70
|
||||
group2.UserSettings.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled, sdk.WebClientMFADisabled}
|
||||
_, _, err = httpdtest.UpdateGroup(group2, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
|
@ -1226,6 +1227,7 @@ func TestGroupSettingsOverride(t *testing.T) {
|
|||
assert.Equal(t, sdk.LocalFilesystemProvider, user.FsConfig.Provider)
|
||||
assert.Equal(t, int64(0), user.DownloadBandwidth)
|
||||
assert.Equal(t, int64(0), user.UploadBandwidth)
|
||||
assert.Equal(t, 0, user.Filters.PasswordStrength)
|
||||
assert.Equal(t, []string{dataprovider.PermAny}, user.GetPermissionsForPath("/"))
|
||||
assert.Equal(t, []string{dataprovider.PermListItems}, user.GetPermissionsForPath("/"+defaultUsername))
|
||||
assert.Len(t, user.Filters.WebClient, 2)
|
||||
|
@ -1249,6 +1251,7 @@ func TestGroupSettingsOverride(t *testing.T) {
|
|||
group1.UserSettings.ExpiresIn = 15
|
||||
group1.UserSettings.Filters.MaxUploadFileSize = 1024 * 1024
|
||||
group1.UserSettings.Filters.StartDirectory = "/startdir/%username%"
|
||||
group1.UserSettings.Filters.PasswordStrength = 70
|
||||
group1.UserSettings.Filters.WebClient = []string{sdk.WebClientInfoChangeDisabled}
|
||||
group1.UserSettings.Permissions = map[string][]string{
|
||||
"/": {dataprovider.PermListItems, dataprovider.PermUpload},
|
||||
|
@ -1268,6 +1271,7 @@ func TestGroupSettingsOverride(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
assert.Len(t, user.VirtualFolders, 3)
|
||||
assert.Equal(t, user.CreatedAt+int64(group1.UserSettings.ExpiresIn)*86400000, user.ExpirationDate)
|
||||
assert.Equal(t, group1.UserSettings.Filters.PasswordStrength, user.Filters.PasswordStrength)
|
||||
assert.Equal(t, sdk.SFTPFilesystemProvider, user.FsConfig.Provider)
|
||||
assert.Equal(t, altAdminUsername, user.FsConfig.SFTPConfig.Username)
|
||||
assert.Equal(t, "/dirs/"+defaultUsername, user.FsConfig.SFTPConfig.Prefix)
|
||||
|
@ -2958,6 +2962,45 @@ func TestPermMFADisabled(t *testing.T) {
|
|||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestUpdateUserPassword(t *testing.T) {
|
||||
g := getTestGroup()
|
||||
g.UserSettings.Filters.PasswordStrength = 20
|
||||
g.UserSettings.MaxSessions = 10
|
||||
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
u := getTestUser()
|
||||
u.Filters.RequirePasswordChange = true
|
||||
u.Groups = []sdk.GroupMapping{
|
||||
{
|
||||
Name: group.Name,
|
||||
Type: sdk.GroupTypePrimary,
|
||||
},
|
||||
}
|
||||
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||
assert.NoError(t, err)
|
||||
lastPwdChange := user.LastPasswordChange
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
newPwd := "uaCooGh3pheiShooghah"
|
||||
err = dataprovider.UpdateUserPassword(user.Username, newPwd, "", "", "")
|
||||
assert.NoError(t, err)
|
||||
_, err = dataprovider.CheckUserAndPass(user.Username, newPwd, "", common.ProtocolHTTP)
|
||||
assert.NoError(t, err)
|
||||
user, _, err = httpdtest.GetUserByUsername(user.Username, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, user.Filters.RequirePasswordChange)
|
||||
assert.NotEqual(t, lastPwdChange, user.LastPasswordChange)
|
||||
// check that we don't save group overrides
|
||||
assert.Equal(t, 0, user.MaxSessions)
|
||||
assert.Equal(t, 0, user.Filters.PasswordStrength)
|
||||
|
||||
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
err = os.RemoveAll(user.GetHomeDir())
|
||||
assert.NoError(t, err)
|
||||
_, err = httpdtest.RemoveGroup(group, http.StatusOK)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestMustChangePasswordRequirement(t *testing.T) {
|
||||
u := getTestUser()
|
||||
u.Filters.RequirePasswordChange = true
|
||||
|
@ -18887,6 +18930,16 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "invalid password expiration")
|
||||
form.Set("password_expiration", "90")
|
||||
// test invalid password strength
|
||||
form.Set("password_strength", "a")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, webToken)
|
||||
req.Header.Set("Content-Type", contentType)
|
||||
rr = executeRequest(req)
|
||||
checkResponseCode(t, http.StatusOK, rr)
|
||||
assert.Contains(t, rr.Body.String(), "invalid password strength")
|
||||
form.Set("password_strength", "60")
|
||||
// test invalid tls username
|
||||
form.Set("tls_username", "username")
|
||||
b, contentType, _ = getMultipartFormData(form, "", "")
|
||||
|
@ -19036,6 +19089,7 @@ func TestWebUserAddMock(t *testing.T) {
|
|||
assert.Equal(t, 0, newUser.Filters.FTPSecurity)
|
||||
assert.Equal(t, 10, newUser.Filters.DefaultSharesExpiration)
|
||||
assert.Equal(t, 90, newUser.Filters.PasswordExpiration)
|
||||
assert.Equal(t, 60, newUser.Filters.PasswordStrength)
|
||||
assert.Greater(t, newUser.LastPasswordChange, int64(0))
|
||||
assert.True(t, newUser.Filters.RequirePasswordChange)
|
||||
assert.True(t, util.Contains(newUser.PublicKeys, testPubKey))
|
||||
|
@ -19244,6 +19298,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "100")
|
||||
form.Set("default_shares_expiration", "30")
|
||||
form.Set("password_expiration", "60")
|
||||
form.Set("password_strength", "40")
|
||||
form.Set("disconnect", "1")
|
||||
form.Set("additional_info", user.AdditionalInfo)
|
||||
form.Set("description", user.Description)
|
||||
|
@ -19324,6 +19379,7 @@ func TestWebUserUpdateMock(t *testing.T) {
|
|||
assert.Equal(t, int64(0), updateUser.Filters.ExternalAuthCacheTime)
|
||||
assert.Equal(t, 30, updateUser.Filters.DefaultSharesExpiration)
|
||||
assert.Equal(t, 60, updateUser.Filters.PasswordExpiration)
|
||||
assert.Equal(t, 40, updateUser.Filters.PasswordStrength)
|
||||
assert.True(t, updateUser.Filters.RequirePasswordChange)
|
||||
if val, ok := updateUser.Permissions["/otherdir"]; ok {
|
||||
assert.True(t, util.Contains(val, dataprovider.PermListItems))
|
||||
|
@ -19437,6 +19493,7 @@ func TestUserTemplateWithFoldersMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
form.Set("ftp_security", "1")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("description", "desc %username% %password%")
|
||||
|
@ -19542,6 +19599,7 @@ func TestUserSaveFromTemplateMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Add("tpl_username", user1)
|
||||
form.Add("tpl_password", "password1")
|
||||
|
@ -19632,6 +19690,7 @@ func TestUserTemplateMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
form.Add("hooks", "external_auth_disabled")
|
||||
form.Add("hooks", "check_password_disabled")
|
||||
form.Set("disable_fs_checks", "checked")
|
||||
|
@ -19764,6 +19823,7 @@ func TestUserPlaceholders(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ := http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
setJWTCookieForReq(req, token)
|
||||
|
@ -20109,6 +20169,7 @@ func TestWebUserS3Mock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
form.Set("ftp_security", "1")
|
||||
form.Set("s3_force_path_style", "checked")
|
||||
form.Set("description", user.Description)
|
||||
|
@ -20325,6 +20386,7 @@ func TestWebUserGCSMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
form.Set("ftp_security", "1")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
|
@ -20453,6 +20515,7 @@ func TestWebUserHTTPFsMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
form.Set("http_equality_check_mode", "true")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
|
@ -20579,6 +20642,7 @@ func TestWebUserAzureBlobMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
// test invalid az_upload_part_size
|
||||
form.Set("az_upload_part_size", "a")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
|
@ -20760,6 +20824,7 @@ func TestWebUserCryptMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
// passphrase cannot be empty
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
|
@ -20869,6 +20934,7 @@ func TestWebUserSFTPFsMock(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
// empty sftpconfig
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, _ = http.NewRequest(http.MethodPost, path.Join(webUserPath, user.Username), &b)
|
||||
|
@ -20995,6 +21061,7 @@ func TestWebUserRole(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "10")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
b, contentType, _ := getMultipartFormData(form, "", "")
|
||||
req, err := http.NewRequest(http.MethodPost, webUserPath, &b)
|
||||
assert.NoError(t, err)
|
||||
|
@ -22153,6 +22220,7 @@ func TestAddWebGroup(t *testing.T) {
|
|||
form.Set("max_upload_file_size", "0")
|
||||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
b, contentType, err = getMultipartFormData(form, "", "")
|
||||
assert.NoError(t, err)
|
||||
req, err = http.NewRequest(http.MethodPost, webGroupPath, &b)
|
||||
|
@ -22587,6 +22655,7 @@ func TestUpdateWebGroupMock(t *testing.T) {
|
|||
form.Set("default_shares_expiration", "0")
|
||||
form.Set("expires_in", "0")
|
||||
form.Set("password_expiration", "0")
|
||||
form.Set("password_strength", "0")
|
||||
form.Set("external_auth_cache_time", "0")
|
||||
form.Set("fs_provider", strconv.FormatInt(int64(group.UserSettings.FsConfig.Provider), 10))
|
||||
form.Set("sftp_endpoint", group.UserSettings.FsConfig.SFTPConfig.Endpoint)
|
||||
|
|
|
@ -1542,6 +1542,10 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
if err != nil {
|
||||
return filters, fmt.Errorf("invalid password expiration: %w", err)
|
||||
}
|
||||
passwordStrength, err := strconv.ParseInt(r.Form.Get("password_strength"), 10, 64)
|
||||
if err != nil {
|
||||
return filters, fmt.Errorf("invalid password strength: %w", err)
|
||||
}
|
||||
if r.Form.Get("ftp_security") == "1" {
|
||||
filters.FTPSecurity = 1
|
||||
}
|
||||
|
@ -1557,6 +1561,7 @@ func getFiltersFromUserPostFields(r *http.Request) (sdk.BaseUserFilters, error)
|
|||
filters.WebClient = r.Form["web_client_options"]
|
||||
filters.DefaultSharesExpiration = int(defaultSharesExpiration)
|
||||
filters.PasswordExpiration = int(passwordExpiration)
|
||||
filters.PasswordStrength = int(passwordStrength)
|
||||
hooks := r.Form["hooks"]
|
||||
if util.Contains(hooks, "external_auth_disabled") {
|
||||
filters.Hooks.ExternalAuthDisabled = true
|
||||
|
|
|
@ -2450,6 +2450,9 @@ func compareBaseUserFilters(expected sdk.BaseUserFilters, actual sdk.BaseUserFil
|
|||
if expected.PasswordExpiration != actual.PasswordExpiration {
|
||||
return errors.New("password_expiration mismatch")
|
||||
}
|
||||
if expected.PasswordStrength != actual.PasswordStrength {
|
||||
return errors.New("password_strength mismatch")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -717,6 +717,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idPasswordStrength" class="col-sm-2 col-form-label">Password strength</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idPasswordStrength" name="password_strength"
|
||||
value="{{.Group.UserSettings.Filters.PasswordStrength}}" min="0" max="100" aria-describedby="passwordStrengthHelpBlock">
|
||||
<small id="passwordStrengthHelpBlock" class="form-text text-muted">
|
||||
Values in the 50-70 range are suggested for common use cases. 0 means disabled, any password will be accepted. Applied when users change their password
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idPasswordExpiration" class="col-sm-2 col-form-label">Password expiration</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
|
@ -360,6 +360,39 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idPasswordStrength" class="col-sm-2 col-form-label">Password strength</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idPasswordStrength" name="password_strength"
|
||||
value="{{.User.Filters.PasswordStrength}}" min="0" max="100" aria-describedby="passwordStrengthHelpBlock">
|
||||
<small id="passwordStrengthHelpBlock" class="form-text text-muted">
|
||||
Values in the 50-70 range are suggested for common use cases. 0 means disabled, any password will be accepted
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idPasswordExpiration" class="col-sm-2 col-form-label">Password expiration</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idPasswordExpiration" name="password_expiration"
|
||||
value="{{.User.Filters.PasswordExpiration}}" min="0" aria-describedby="passwordExpirationHelpBlock">
|
||||
<small id="passwordExpirationHelpBlock" class="form-text text-muted">
|
||||
Password expiration as number of days. 0 means no expiration
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDefaultSharesExpiration" class="col-sm-2 col-form-label">Default shares expiration</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idDefaultSharesExpiration" name="default_shares_expiration"
|
||||
value="{{.User.Filters.DefaultSharesExpiration}}" min="0" aria-describedby="defaultSharesExpirationHelpBlock">
|
||||
<small id="defaultSharesExpirationHelpBlock" class="form-text text-muted">
|
||||
Default expiration for newly created shares as number of days
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDescription" class="col-sm-2 col-form-label">Description</label>
|
||||
<div class="col-sm-10">
|
||||
|
@ -953,28 +986,6 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idPasswordExpiration" class="col-sm-2 col-form-label">Password expiration</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idPasswordExpiration" name="password_expiration"
|
||||
value="{{.User.Filters.PasswordExpiration}}" min="0" aria-describedby="passwordExpirationHelpBlock">
|
||||
<small id="passwordExpirationHelpBlock" class="form-text text-muted">
|
||||
Password expiration as number of days. 0 means no expiration
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idDefaultSharesExpiration" class="col-sm-2 col-form-label">Default shares expiration</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="number" class="form-control" id="idDefaultSharesExpiration" name="default_shares_expiration"
|
||||
value="{{.User.Filters.DefaultSharesExpiration}}" min="0" aria-describedby="defaultSharesExpirationHelpBlock">
|
||||
<small id="defaultSharesExpirationHelpBlock" class="form-text text-muted">
|
||||
Default expiration for newly created shares as number of days
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group row">
|
||||
<label for="idHooks" class="col-sm-2 col-form-label">Hooks</label>
|
||||
<div class="col-sm-10">
|
||||
|
|
Loading…
Reference in a new issue