User: use update function for password updates (#86419)

* Update password through Update function instead

* Remove duplicated to lower

* Refactor password code
This commit is contained in:
Karl Persson
2024-04-17 15:24:36 +02:00
committed by GitHub
parent f99d5a1c1a
commit 1a6777cb93
21 changed files with 182 additions and 205 deletions

View File

@@ -5,7 +5,6 @@ import (
"fmt"
"net/http"
"strconv"
"strings"
"golang.org/x/sync/errgroup"
@@ -19,7 +18,6 @@ import (
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -46,9 +44,6 @@ func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Respo
return response.Error(http.StatusBadRequest, "bad request data", err)
}
form.Email = strings.TrimSpace(form.Email)
form.Login = strings.TrimSpace(form.Login)
cmd := user.CreateUserCommand{
Login: form.Login,
Email: form.Email,
@@ -57,10 +52,6 @@ func (hs *HTTPServer) AdminCreateUser(c *contextmodel.ReqContext) response.Respo
OrgID: form.OrgId,
}
if len(cmd.Password) < 4 {
return response.Error(http.StatusBadRequest, "Password is missing or too short", nil)
}
usr, err := hs.userService.Create(c.Req.Context(), &cmd)
if err != nil {
if errors.Is(err, org.ErrOrgNotFound) {
@@ -115,39 +106,19 @@ func (hs *HTTPServer) AdminUpdateUserPassword(c *contextmodel.ReqContext) respon
return response.Error(http.StatusBadRequest, "id is invalid", err)
}
if err := form.Password.Validate(hs.Cfg); err != nil {
return response.Err(err)
if response := hs.errOnExternalUser(c.Req.Context(), userID); response != nil {
return response
}
userQuery := user.GetUserByIDQuery{ID: userID}
if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userID, Password: &form.Password}); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to update user password", err)
}
usr, err := hs.userService.GetByID(c.Req.Context(), &userQuery)
usr, err := hs.userService.GetByID(c.Req.Context(), &user.GetUserByIDQuery{ID: userID})
if err != nil {
return response.Error(http.StatusInternalServerError, "Could not read user from database", err)
}
getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Error(http.StatusBadRequest, "Cannot update external user password", err)
}
}
passwordHashed, err := util.EncodePassword(string(form.Password), usr.Salt)
if err != nil {
return response.Error(http.StatusInternalServerError, "Could not encode password", err)
}
cmd := user.ChangeUserPasswordCommand{
UserID: userID,
NewPassword: user.Password(passwordHashed),
}
if err := hs.userService.ChangePassword(c.Req.Context(), &cmd); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to update user password", err)
}
if err := hs.loginAttemptService.Reset(c.Req.Context(),
usr.Login); err != nil {
c.Logger.Warn("could not reset login attempts", "err", err, "username", usr.Login)

View File

@@ -355,8 +355,9 @@ func putAdminScenario(t *testing.T, desc string, url string, routePattern string
hs := &HTTPServer{
Cfg: setting.NewCfg(),
SQLStore: sqlStore,
authInfoService: &authinfotest.FakeService{},
authInfoService: &authinfotest.FakeService{ExpectedError: user.ErrUserNotFound},
userService: userSvc,
SocialService: &mockSocialService{},
}
sc := setupScenarioContext(t, url)

View File

@@ -270,10 +270,6 @@ func (hs *HTTPServer) CompleteInvite(c *contextmodel.ReqContext) response.Respon
}
}
if err := completeInvite.Password.Validate(hs.Cfg); err != nil {
return response.Err(err)
}
cmd := user.CreateUserCommand{
Email: completeInvite.Email,
Name: completeInvite.Name,

View File

@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/notifications"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/web"
)
@@ -65,6 +64,11 @@ func (hs *HTTPServer) ResetPassword(c *contextmodel.ReqContext) response.Respons
if err := web.Bind(c.Req, &form); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
if form.NewPassword != form.ConfirmPassword {
return response.Error(http.StatusBadRequest, "Passwords do not match", nil)
}
query := notifications.ValidateResetPasswordCodeQuery{Code: form.Code}
// For now the only way to know the username to clear login attempts for is
@@ -85,33 +89,12 @@ func (hs *HTTPServer) ResetPassword(c *contextmodel.ReqContext) response.Respons
return response.Error(http.StatusInternalServerError, "Unknown error validating email code", err)
}
getAuthQuery := login.GetAuthInfoQuery{UserId: userResult.ID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Error(http.StatusBadRequest, "Cannot update external user password", err)
}
if response := hs.errOnExternalUser(c.Req.Context(), userResult.ID); response != nil {
return response
}
if form.NewPassword != form.ConfirmPassword {
return response.Error(http.StatusBadRequest, "Passwords do not match", nil)
}
if err := form.NewPassword.Validate(hs.Cfg); err != nil {
c.Logger.Warn("the new password doesn't meet the password policy criteria", "err", err)
return response.Err(err)
}
cmd := user.ChangeUserPasswordCommand{}
cmd.UserID = userResult.ID
encodedPassword, err := util.EncodePassword(string(form.NewPassword), userResult.Salt)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to encode password", err)
}
cmd.NewPassword = user.Password(encodedPassword)
if err := hs.userService.ChangePassword(c.Req.Context(), &cmd); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to change user password", err)
if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userResult.ID, Password: &form.ConfirmPassword}); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to change user password", err)
}
if err := hs.loginAttemptService.Reset(c.Req.Context(), username); err != nil {

View File

@@ -4,7 +4,6 @@ import (
"context"
"errors"
"net/http"
"strings"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
@@ -89,9 +88,6 @@ func (hs *HTTPServer) SignUpStep2(c *contextmodel.ReqContext) response.Response
return response.Error(http.StatusUnauthorized, "User signup is disabled", nil)
}
form.Email = strings.TrimSpace(form.Email)
form.Username = strings.TrimSpace(form.Username)
createUserCmd := user.CreateUserCommand{
Email: form.Email,
Login: form.Username,

View File

@@ -226,13 +226,8 @@ func (hs *HTTPServer) UpdateUserActiveOrg(c *contextmodel.ReqContext) response.R
func (hs *HTTPServer) handleUpdateUser(ctx context.Context, cmd user.UpdateUserCommand) response.Response {
// external user -> user data cannot be updated
isExternal, err := hs.isExternalUser(ctx, cmd.UserID)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to validate User", err)
}
if isExternal {
return response.Error(http.StatusForbidden, "User info cannot be updated for external Users", nil)
if response := hs.errOnExternalUser(ctx, cmd.UserID); response != nil {
return response
}
if len(cmd.Login) == 0 {
@@ -344,20 +339,6 @@ func (hs *HTTPServer) UpdateUserEmail(c *contextmodel.ReqContext) response.Respo
return response.Redirect(hs.Cfg.AppSubURL + "/profile")
}
func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, error) {
getAuthQuery := login.GetAuthInfoQuery{UserId: userID}
var err error
if _, err = hs.authInfoService.GetAuthInfo(ctx, &getAuthQuery); err == nil {
return true, nil
}
if errors.Is(err, user.ErrUserNotFound) {
return false, nil
}
return false, err
}
// swagger:route GET /user/orgs signed_in_user getSignedInUserOrgList
//
// Organizations of the actual User.
@@ -571,8 +552,8 @@ func (hs *HTTPServer) ChangeActiveOrgAndRedirectToHome(c *contextmodel.ReqContex
// 403: forbiddenError
// 500: internalServerError
func (hs *HTTPServer) ChangeUserPassword(c *contextmodel.ReqContext) response.Response {
cmd := user.ChangeUserPasswordCommand{}
if err := web.Bind(c.Req, &cmd); err != nil {
form := user.ChangeUserPasswordCommand{}
if err := web.Bind(c.Req, &form); err != nil {
return response.Error(http.StatusBadRequest, "bad request data", err)
}
@@ -581,43 +562,12 @@ func (hs *HTTPServer) ChangeUserPassword(c *contextmodel.ReqContext) response.Re
return errResponse
}
userQuery := user.GetUserByIDQuery{ID: userID}
usr, err := hs.userService.GetByID(c.Req.Context(), &userQuery)
if err != nil {
return response.Error(http.StatusInternalServerError, "Could not read user from database", err)
if response := hs.errOnExternalUser(c.Req.Context(), userID); response != nil {
return response
}
getAuthQuery := login.GetAuthInfoQuery{UserId: usr.ID}
if authInfo, err := hs.authInfoService.GetAuthInfo(c.Req.Context(), &getAuthQuery); err == nil {
oauthInfo := hs.SocialService.GetOAuthInfoProvider(authInfo.AuthModule)
if login.IsProviderEnabled(hs.Cfg, authInfo.AuthModule, oauthInfo) {
return response.Error(http.StatusBadRequest, "Cannot update external user password", err)
}
}
passwordHashed, err := util.EncodePassword(string(cmd.OldPassword), usr.Salt)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to encode password", err)
}
if user.Password(passwordHashed) != usr.Password {
return response.Error(http.StatusUnauthorized, "Invalid old password", nil)
}
if err := cmd.NewPassword.Validate(hs.Cfg); err != nil {
c.Logger.Warn("the new password doesn't meet the password policy criteria", "err", err)
return response.Err(err)
}
cmd.UserID = userID
encodedPassword, err := util.EncodePassword(string(cmd.NewPassword), usr.Salt)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to encode password", err)
}
cmd.NewPassword = user.Password(encodedPassword)
if err := hs.userService.ChangePassword(c.Req.Context(), &cmd); err != nil {
return response.Error(http.StatusInternalServerError, "Failed to change user password", err)
if err := hs.userService.Update(c.Req.Context(), &user.UpdateUserCommand{UserID: userID, Password: &form.NewPassword, OldPassword: &form.OldPassword}); err != nil {
return response.ErrOrFallback(http.StatusInternalServerError, "Failed to change user password", err)
}
return response.Success("User password changed")

View File

@@ -345,12 +345,14 @@ func Test_GetUserByID(t *testing.T) {
func TestHTTPServer_UpdateUser(t *testing.T) {
settings := setting.NewCfg()
settings.SAMLAuthEnabled = true
sqlStore := db.InitTestDB(t)
hs := &HTTPServer{
Cfg: settings,
SQLStore: sqlStore,
AccessControl: acmock.New(),
SocialService: &socialtest.FakeSocialService{ExpectedAuthInfoProvider: &social.OAuthInfo{Enabled: true}},
}
updateUserCommand := user.UpdateUserCommand{
@@ -366,7 +368,8 @@ func TestHTTPServer_UpdateUser(t *testing.T) {
routePattern: "/api/users/:id",
cmd: updateUserCommand,
fn: func(sc *scenarioContext) {
sc.authInfoService.ExpectedUserAuth = &login.UserAuth{}
sc.authInfoService.ExpectedUserAuth = &login.UserAuth{AuthModule: login.SAMLAuthModule}
sc.fakeReqWithParams("PUT", sc.url, map[string]string{"id": "1"}).exec()
assert.Equal(t, 403, sc.resp.Code)
},
@@ -1086,11 +1089,13 @@ func updateUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPServer) {
func TestHTTPServer_UpdateSignedInUser(t *testing.T) {
settings := setting.NewCfg()
sqlStore := db.InitTestDB(t)
settings.SAMLAuthEnabled = true
hs := &HTTPServer{
Cfg: settings,
SQLStore: sqlStore,
AccessControl: acmock.New(),
SocialService: &socialtest.FakeSocialService{},
}
updateUserCommand := user.UpdateUserCommand{
@@ -1106,7 +1111,7 @@ func TestHTTPServer_UpdateSignedInUser(t *testing.T) {
routePattern: "/api/users/",
cmd: updateUserCommand,
fn: func(sc *scenarioContext) {
sc.authInfoService.ExpectedUserAuth = &login.UserAuth{}
sc.authInfoService.ExpectedUserAuth = &login.UserAuth{AuthModule: login.SAMLAuthModule}
sc.fakeReqWithParams("PUT", sc.url, map[string]string{"id": "1"}).exec()
assert.Equal(t, 403, sc.resp.Code)
},

View File

@@ -1,10 +1,16 @@
package api
import (
"context"
"errors"
"net/http"
"net/mail"
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/middleware/cookies"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/login"
"github.com/grafana/grafana/pkg/services/user"
)
func (hs *HTTPServer) GetRedirectURL(c *contextmodel.ReqContext) string {
@@ -20,6 +26,31 @@ func (hs *HTTPServer) GetRedirectURL(c *contextmodel.ReqContext) string {
return redirectURL
}
func (hs *HTTPServer) errOnExternalUser(ctx context.Context, userID int64) response.Response {
isExternal, err := hs.isExternalUser(ctx, userID)
if err != nil {
return response.Error(http.StatusInternalServerError, "Failed to validate User", err)
}
if isExternal {
return response.Error(http.StatusForbidden, "Cannot update external User", nil)
}
return nil
}
func (hs *HTTPServer) isExternalUser(ctx context.Context, userID int64) (bool, error) {
info, err := hs.authInfoService.GetAuthInfo(ctx, &login.GetAuthInfoQuery{UserId: userID})
if errors.Is(err, user.ErrUserNotFound) {
return false, nil
}
if err != nil {
return true, err
}
return login.IsProviderEnabled(hs.Cfg, info.AuthModule, hs.SocialService.GetOAuthInfoProvider(info.AuthModule)), nil
}
func ValidateAndNormalizeEmail(email string) (string, error) {
if email == "" {
return "", nil