From 5ce9688780e7cc44836c4b750bb21c19c9bec9d6 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Sun, 26 Oct 2025 09:44:32 +0100 Subject: [PATCH] enforce group-level password strength for users and shares Signed-off-by: Nicola Murino --- internal/dataprovider/dataprovider.go | 16 ++++++-- internal/dataprovider/share.go | 2 +- internal/httpd/httpd_test.go | 54 +++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 4 deletions(-) diff --git a/internal/dataprovider/dataprovider.go b/internal/dataprovider/dataprovider.go index f4bb7211..a3baf9eb 100644 --- a/internal/dataprovider/dataprovider.go +++ b/internal/dataprovider/dataprovider.go @@ -2144,9 +2144,6 @@ func UpdateUserPassword(username, plainPwd, executor, ipAddress, role string) er return err } userCopy := user.getACopy() - if err := userCopy.LoadAndApplyGroupSettings(); err != nil { - return err - } userCopy.Password = plainPwd if err := createUserPasswordHash(&userCopy); err != nil { return err @@ -3336,6 +3333,19 @@ func hashPlainPassword(plainPwd string) (string, error) { func createUserPasswordHash(user *User) error { 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 err := passwordvalidator.Validate(user.Password, minEntropy); err != nil { return util.NewI18nError(util.NewValidationError(err.Error()), util.I18nErrorPasswordComplexity) diff --git a/internal/dataprovider/share.go b/internal/dataprovider/share.go index 06f6c915..78854331 100644 --- a/internal/dataprovider/share.go +++ b/internal/dataprovider/share.go @@ -146,7 +146,7 @@ func (s *Share) HasRedactedPassword() bool { func (s *Share) hashPassword() error { if s.Password != "" && !util.IsStringPrefixInSlice(s.Password, internalHashPwdPrefixes) { - user, err := UserExists(s.Username, "") + user, err := GetUserWithGroupSettings(s.Username, "") if err != nil { return util.NewGenericError(fmt.Sprintf("unable to validate user: %v", err)) } diff --git a/internal/httpd/httpd_test.go b/internal/httpd/httpd_test.go index 95f2c461..0d55d28a 100644 --- a/internal/httpd/httpd_test.go +++ b/internal/httpd/httpd_test.go @@ -1842,6 +1842,19 @@ func TestGroupSettingsOverride(t *testing.T) { assert.Contains(t, user.Filters.WebClient, sdk.WebClientInfoChangeDisabled) 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()) assert.NoError(t, err) @@ -15109,6 +15122,47 @@ func TestShareUsage(t *testing.T) { 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) { u := getTestUser() u.Filters.MaxSharesExpiration = 5