mirror of
https://github.com/drakkan/sftpgo.git
synced 2025-12-06 22:30:56 +03:00
enforce group-level password strength for users and shares
Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
@@ -2144,9 +2144,6 @@ func UpdateUserPassword(username, plainPwd, executor, ipAddress, role string) er
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
userCopy := user.getACopy()
|
userCopy := user.getACopy()
|
||||||
if err := userCopy.LoadAndApplyGroupSettings(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
userCopy.Password = plainPwd
|
userCopy.Password = plainPwd
|
||||||
if err := createUserPasswordHash(&userCopy); err != nil {
|
if err := createUserPasswordHash(&userCopy); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -3336,6 +3333,19 @@ func hashPlainPassword(plainPwd string) (string, error) {
|
|||||||
|
|
||||||
func createUserPasswordHash(user *User) error {
|
func createUserPasswordHash(user *User) error {
|
||||||
if user.Password != "" && !user.IsPasswordHashed() {
|
if user.Password != "" && !user.IsPasswordHashed() {
|
||||||
|
for _, g := range user.Groups {
|
||||||
|
if g.Type == sdk.GroupTypePrimary {
|
||||||
|
group, err := GroupExists(g.Name)
|
||||||
|
if err != nil {
|
||||||
|
return errors.New("unable to load group password policies")
|
||||||
|
}
|
||||||
|
if minEntropy := group.UserSettings.Filters.PasswordStrength; minEntropy > 0 {
|
||||||
|
if err := passwordvalidator.Validate(user.Password, float64(minEntropy)); err != nil {
|
||||||
|
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
|
if minEntropy := user.getMinPasswordEntropy(); minEntropy > 0 {
|
||||||
if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil {
|
if err := passwordvalidator.Validate(user.Password, minEntropy); err != nil {
|
||||||
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
|
return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity)
|
||||||
|
|||||||
@@ -146,7 +146,7 @@ func (s *Share) HasRedactedPassword() bool {
|
|||||||
|
|
||||||
func (s *Share) hashPassword() error {
|
func (s *Share) hashPassword() error {
|
||||||
if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
|
if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) {
|
||||||
user, err := UserExists(s.Username, "")
|
user, err := GetUserWithGroupSettings(s.Username, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err))
|
return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1842,6 +1842,19 @@ func TestGroupSettingsOverride(t *testing.T) {
|
|||||||
assert.Contains(t, user.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
|
assert.Contains(t, user.Filters.WebClient, sdk.WebClientInfoChangeDisabled)
|
||||||
assert.Contains(t, user.Filters.WebClient, sdk.WebClientMFADisabled)
|
assert.Contains(t, user.Filters.WebClient, sdk.WebClientMFADisabled)
|
||||||
}
|
}
|
||||||
|
// Attempt to create a user with a weak password and group1 as the primary group: this should fail
|
||||||
|
u = getTestUser()
|
||||||
|
u.Username = rand.Text()
|
||||||
|
u.Password = defaultPassword
|
||||||
|
u.Groups = []sdk.GroupMapping{
|
||||||
|
{
|
||||||
|
Name: group1.Name,
|
||||||
|
Type: sdk.GroupTypePrimary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, resp, err = httpdtest.AddUser(u, http.StatusBadRequest)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.Contains(t, string(resp), "insecure password")
|
||||||
|
|
||||||
err = os.RemoveAll(user.GetHomeDir())
|
err = os.RemoveAll(user.GetHomeDir())
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
@@ -15109,6 +15122,47 @@ func TestShareUsage(t *testing.T) {
|
|||||||
executeRequest(req)
|
executeRequest(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSharePasswordPolicy(t *testing.T) {
|
||||||
|
g := getTestGroup()
|
||||||
|
g.UserSettings.Filters.PasswordStrength = 70
|
||||||
|
group, _, err := httpdtest.AddGroup(g, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
u := getTestUser()
|
||||||
|
u.Groups = []sdk.GroupMapping{
|
||||||
|
{
|
||||||
|
Name: g.Name,
|
||||||
|
Type: sdk.GroupTypePrimary,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
u.Password = rand.Text()
|
||||||
|
user, _, err := httpdtest.AddUser(u, http.StatusCreated)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
webAPIToken, err := getJWTAPIUserTokenFromTestServer(defaultUsername, u.Password)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
share := dataprovider.Share{
|
||||||
|
Name: util.GenerateUniqueID(),
|
||||||
|
Scope: dataprovider.ShareScopeRead,
|
||||||
|
Paths: []string{"/"},
|
||||||
|
Password: defaultPassword,
|
||||||
|
}
|
||||||
|
asJSON, err := json.Marshal(share)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
req, err := http.NewRequest(http.MethodPost, userSharesPath, bytes.NewBuffer(asJSON))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
setBearerForReq(req, webAPIToken)
|
||||||
|
rr := executeRequest(req)
|
||||||
|
checkResponseCode(t, http.StatusBadRequest, rr)
|
||||||
|
assert.Contains(t, rr.Body.String(), "insecure password")
|
||||||
|
|
||||||
|
_, err = httpdtest.RemoveUser(user, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
_, err = httpdtest.RemoveGroup(group, http.StatusOK)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
func TestShareMaxExpiration(t *testing.T) {
|
func TestShareMaxExpiration(t *testing.T) {
|
||||||
u := getTestUser()
|
u := getTestUser()
|
||||||
u.Filters.MaxSharesExpiration = 5
|
u.Filters.MaxSharesExpiration = 5
|
||||||
|
|||||||
Reference in New Issue
Block a user