package api import ( "context" "encoding/json" "fmt" "net/http" "net/url" "strings" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "golang.org/x/oauth2" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/db/dbtest" "github.com/grafana/grafana/pkg/infra/remotecache" "github.com/grafana/grafana/pkg/infra/tracing" "github.com/grafana/grafana/pkg/login/social" "github.com/grafana/grafana/pkg/login/social/socialtest" "github.com/grafana/grafana/pkg/services/accesscontrol" "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/auth/idtest" "github.com/grafana/grafana/pkg/services/authz/zanzana" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/login" "github.com/grafana/grafana/pkg/services/login/authinfoimpl" "github.com/grafana/grafana/pkg/services/login/authinfotest" "github.com/grafana/grafana/pkg/services/notifications" "github.com/grafana/grafana/pkg/services/org/orgimpl" "github.com/grafana/grafana/pkg/services/quota/quotatest" "github.com/grafana/grafana/pkg/services/searchusers" "github.com/grafana/grafana/pkg/services/searchusers/filters" "github.com/grafana/grafana/pkg/services/secrets/database" "github.com/grafana/grafana/pkg/services/secrets/fakes" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/services/supportbundles/supportbundlestest" tempuser "github.com/grafana/grafana/pkg/services/temp_user" "github.com/grafana/grafana/pkg/services/temp_user/tempuserimpl" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/services/user/userimpl" "github.com/grafana/grafana/pkg/services/user/usertest" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/web/webtest" ) const newEmail = "newemail@localhost" func TestUserAPIEndpoint_userLoggedIn(t *testing.T) { settings := setting.NewCfg() sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: settings}) hs := &HTTPServer{ Cfg: settings, SQLStore: sqlStore, AccessControl: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), } mockResult := user.SearchUserQueryResult{ Users: []*user.UserSearchHitDTO{ {Name: "user1"}, {Name: "user2"}, }, TotalCount: 2, } mock := dbtest.NewFakeDB() userMock := usertest.NewUserServiceFake() loggedInUserScenario(t, "When calling GET on", "api/users/1", "api/users/:id", func(sc *scenarioContext) { fakeNow := time.Date(2019, 2, 11, 17, 30, 40, 0, time.UTC) secretsService := secretsManager.SetupTestService(t, database.ProvideSecretsStore(sqlStore)) authInfoStore := authinfoimpl.ProvideStore(sqlStore, secretsService) srv := authinfoimpl.ProvideService( authInfoStore, remotecache.NewFakeCacheStorage(), secretsService) hs.authInfoService = srv orgSvc, err := orgimpl.ProvideService(sqlStore, settings, quotatest.New(false, nil)) require.NoError(t, err) userSvc, err := userimpl.ProvideService( sqlStore, orgSvc, sc.cfg, nil, nil, tracing.InitializeTracerForTest(), quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), ) require.NoError(t, err) hs.userService = userSvc createUserCmd := user.CreateUserCommand{ Email: fmt.Sprint("user", "@test.com"), Name: "user", Login: "loginuser", IsAdmin: true, } usr, err := userSvc.Create(context.Background(), &createUserCmd) require.NoError(t, err) theUserUID := usr.UID sc.handlerFunc = hs.GetUserByID token := &oauth2.Token{ AccessToken: "testaccess", RefreshToken: "testrefresh", Expiry: time.Now(), TokenType: "Bearer", } idToken := "testidtoken" token = token.WithExtra(map[string]any{"id_token": idToken}) userlogin := "loginuser" query := &login.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test", UserLookupParams: login.UserLookupParams{Login: &userlogin}} cmd := &login.UpdateAuthInfoCommand{ UserId: usr.ID, AuthId: query.AuthId, AuthModule: query.AuthModule, OAuthToken: token, } err = srv.UpdateAuthInfo(context.Background(), cmd) require.NoError(t, err) avatarUrl := dtos.GetGravatarUrl(hs.Cfg, "@test.com") sc.fakeReqWithParams("GET", sc.url, map[string]string{"id": fmt.Sprintf("%v", usr.ID)}).exec() expected := user.UserProfileDTO{ ID: 1, UID: theUserUID, // from original request Email: "user@test.com", Name: "user", Login: "loginuser", OrgID: 1, IsGrafanaAdmin: true, AuthLabels: []string{}, CreatedAt: fakeNow, UpdatedAt: fakeNow, AvatarURL: avatarUrl, } var resp user.UserProfileDTO require.Equal(t, http.StatusOK, sc.resp.Code) err = json.Unmarshal(sc.resp.Body.Bytes(), &resp) require.NoError(t, err) resp.CreatedAt = fakeNow resp.UpdatedAt = fakeNow resp.AvatarURL = avatarUrl require.EqualValues(t, expected, resp) }, mock) loggedInUserScenario(t, "When calling GET on", "/api/users/lookup", "/api/users/lookup", func(sc *scenarioContext) { createUserCmd := user.CreateUserCommand{ Email: fmt.Sprint("admin", "@test.com"), Name: "admin", Login: "admin", IsAdmin: true, } orgSvc, err := orgimpl.ProvideService(sqlStore, sc.cfg, quotatest.New(false, nil)) require.NoError(t, err) userSvc, err := userimpl.ProvideService( sqlStore, orgSvc, sc.cfg, nil, nil, tracing.InitializeTracerForTest(), quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), ) require.NoError(t, err) _, err = userSvc.Create(context.Background(), &createUserCmd) require.Nil(t, err) sc.handlerFunc = hs.GetUserByLoginOrEmail userMock := usertest.NewUserServiceFake() userMock.ExpectedUser = &user.User{ID: 2} sc.userService = userMock hs.userService = userMock sc.fakeReqWithParams("GET", sc.url, map[string]string{"loginOrEmail": "admin@test.com"}).exec() var resp user.UserProfileDTO require.Equal(t, http.StatusOK, sc.resp.Code) err = json.Unmarshal(sc.resp.Body.Bytes(), &resp) require.NoError(t, err) }, mock) loggedInUserScenario(t, "When calling GET on", "/api/users", "/api/users", func(sc *scenarioContext) { userMock.ExpectedSearchUsers = mockResult searchUsersService := searchusers.ProvideUsersService(sc.cfg, filters.ProvideOSSSearchUserFilter(), userMock) sc.handlerFunc = searchUsersService.SearchUsers sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) assert.Equal(t, 2, len(respJSON.MustArray())) }, mock) loggedInUserScenario(t, "When calling GET with page and limit querystring parameters on", "/api/users", "/api/users", func(sc *scenarioContext) { userMock.ExpectedSearchUsers = mockResult searchUsersService := searchusers.ProvideUsersService(sc.cfg, filters.ProvideOSSSearchUserFilter(), userMock) sc.handlerFunc = searchUsersService.SearchUsers sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) assert.Equal(t, 2, len(respJSON.MustArray())) }, mock) loggedInUserScenario(t, "When calling GET on", "/api/users/search", "/api/users/search", func(sc *scenarioContext) { userMock.ExpectedSearchUsers = mockResult searchUsersService := searchusers.ProvideUsersService(sc.cfg, filters.ProvideOSSSearchUserFilter(), userMock) sc.handlerFunc = searchUsersService.SearchUsersWithPaging sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) assert.Equal(t, 1, respJSON.Get("page").MustInt()) assert.Equal(t, 1000, respJSON.Get("perPage").MustInt()) assert.Equal(t, 2, respJSON.Get("totalCount").MustInt()) assert.Equal(t, 2, len(respJSON.Get("users").MustArray())) }, mock) loggedInUserScenario(t, "When calling GET with page and perpage querystring parameters on", "/api/users/search", "/api/users/search", func(sc *scenarioContext) { userMock.ExpectedSearchUsers = mockResult searchUsersService := searchusers.ProvideUsersService(sc.cfg, filters.ProvideOSSSearchUserFilter(), userMock) sc.handlerFunc = searchUsersService.SearchUsersWithPaging sc.fakeReqWithParams("GET", sc.url, map[string]string{"perpage": "10", "page": "2"}).exec() respJSON, err := simplejson.NewJson(sc.resp.Body.Bytes()) require.NoError(t, err) assert.Equal(t, 2, respJSON.Get("page").MustInt()) assert.Equal(t, 10, respJSON.Get("perPage").MustInt()) }, mock) } func Test_GetUserByID(t *testing.T) { testcases := []struct { name string authModule string allowAssignGrafanaAdmin bool authEnabled bool skipOrgRoleSync bool expectedIsGrafanaAdminSynced bool }{ { name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if Grafana Admin role is not synced", authModule: login.GenericOAuthModule, authEnabled: true, allowAssignGrafanaAdmin: false, skipOrgRoleSync: false, expectedIsGrafanaAdminSynced: false, }, { name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if OAuth provider is not enabled", authModule: login.GenericOAuthModule, authEnabled: false, allowAssignGrafanaAdmin: true, skipOrgRoleSync: false, expectedIsGrafanaAdminSynced: false, }, { name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced OAuth user if org roles are not being synced", authModule: login.GenericOAuthModule, authEnabled: true, allowAssignGrafanaAdmin: true, skipOrgRoleSync: true, expectedIsGrafanaAdminSynced: false, }, { name: "Should return IsGrafanaAdminExternallySynced = true for an externally synced OAuth user", authModule: login.GenericOAuthModule, authEnabled: true, allowAssignGrafanaAdmin: true, skipOrgRoleSync: false, expectedIsGrafanaAdminSynced: true, }, { name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if Grafana Admin role is not synced", authModule: login.JWTModule, authEnabled: true, allowAssignGrafanaAdmin: false, skipOrgRoleSync: false, expectedIsGrafanaAdminSynced: false, }, { name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if JWT provider is not enabled", authModule: login.JWTModule, authEnabled: false, allowAssignGrafanaAdmin: true, skipOrgRoleSync: false, expectedIsGrafanaAdminSynced: false, }, { name: "Should return IsGrafanaAdminExternallySynced = false for an externally synced JWT user if org roles are not being synced", authModule: login.JWTModule, authEnabled: true, allowAssignGrafanaAdmin: true, skipOrgRoleSync: true, expectedIsGrafanaAdminSynced: false, }, { name: "Should return IsGrafanaAdminExternallySynced = true for an externally synced JWT user", authModule: login.JWTModule, authEnabled: true, allowAssignGrafanaAdmin: true, skipOrgRoleSync: false, expectedIsGrafanaAdminSynced: true, }, } for _, tc := range testcases { t.Run(tc.name, func(t *testing.T) { userAuth := &login.UserAuth{AuthModule: tc.authModule} authInfoService := &authinfotest.FakeService{ExpectedUserAuth: userAuth} socialService := &socialtest.FakeSocialService{} userService := &usertest.FakeUserService{ExpectedUserProfileDTO: &user.UserProfileDTO{}} cfg := setting.NewCfg() switch tc.authModule { case login.GenericOAuthModule: socialService.ExpectedAuthInfoProvider = &social.OAuthInfo{AllowAssignGrafanaAdmin: tc.allowAssignGrafanaAdmin, Enabled: tc.authEnabled, SkipOrgRoleSync: tc.skipOrgRoleSync} case login.JWTModule: cfg.JWTAuth.Enabled = tc.authEnabled cfg.JWTAuth.SkipOrgRoleSync = tc.skipOrgRoleSync cfg.JWTAuth.AllowAssignGrafanaAdmin = tc.allowAssignGrafanaAdmin } hs := &HTTPServer{ Cfg: cfg, authInfoService: authInfoService, SocialService: socialService, userService: userService, } sc := setupScenarioContext(t, "/api/users/1") sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { sc.context = c return hs.GetUserByID(c) }) sc.m.Get("/api/users/:id", sc.defaultHandler) sc.fakeReqWithParams("GET", sc.url, map[string]string{}).exec() var resp user.UserProfileDTO require.Equal(t, http.StatusOK, sc.resp.Code) err := json.Unmarshal(sc.resp.Body.Bytes(), &resp) require.NoError(t, err) assert.Equal(t, tc.expectedIsGrafanaAdminSynced, resp.IsGrafanaAdminExternallySynced) }) } } 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{ Email: fmt.Sprint("admin", "@test.com"), Name: "admin", Login: "admin", UserID: 1, } updateUserScenario(t, updateUserContext{ desc: "Should return 403 when the current User is an external user", url: "/api/users/1", routePattern: "/api/users/:id", cmd: updateUserCommand, fn: func(sc *scenarioContext) { 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) }, }, hs) } func setupUpdateEmailTests(t *testing.T, cfg *setting.Cfg) (*user.User, *HTTPServer, *notifications.NotificationServiceMock) { t.Helper() sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: cfg}) tempUserService := tempuserimpl.ProvideService(sqlStore, cfg) orgSvc, err := orgimpl.ProvideService(sqlStore, cfg, quotatest.New(false, nil)) require.NoError(t, err) userSvc, err := userimpl.ProvideService( sqlStore, orgSvc, cfg, nil, nil, tracing.InitializeTracerForTest(), quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), ) require.NoError(t, err) // Create test user createUserCmd := user.CreateUserCommand{ Email: "testuser@localhost", Name: "testuser", Login: "loginuser", Company: "testCompany", IsAdmin: true, } usr, err := userSvc.Create(context.Background(), &createUserCmd) require.NoError(t, err) nsMock := notifications.MockNotificationService() verifier := userimpl.ProvideVerifier(cfg, userSvc, tempUserService, nsMock, &idtest.MockService{}) hs := &HTTPServer{ Cfg: cfg, SQLStore: sqlStore, userService: userSvc, tempUserService: tempUserService, NotificationService: nsMock, userVerifier: verifier, } return usr, hs, nsMock } func TestUser_UpdateEmail(t *testing.T) { cases := []struct { Name string Field user.UpdateEmailActionType }{ { Name: "Updating Email field", Field: user.EmailUpdateAction, }, { Name: "Updating Login (username) field", Field: user.LoginUpdateAction, }, } for _, tt := range cases { t.Run(tt.Name, func(t *testing.T) { t.Run("With verification disabled should update without verifying", func(t *testing.T) { tests := []struct { name string smtpConfigured bool verifyEmailEnabled bool }{ { name: "SMTP not configured", smtpConfigured: false, verifyEmailEnabled: true, }, { name: "config verify_email_enabled = false", smtpConfigured: true, verifyEmailEnabled: false, }, { name: "config verify_email_enabled = false and SMTP not configured", smtpConfigured: false, verifyEmailEnabled: false, }, } for _, ttt := range tests { settings := setting.NewCfg() settings.Smtp.Enabled = ttt.smtpConfigured settings.VerifyEmailEnabled = ttt.verifyEmailEnabled usr, hs, nsMock := setupUpdateEmailTests(t, settings) updateUserCommand := user.UpdateUserCommand{ Email: usr.Email, Name: "newName", Login: usr.Login, UserID: usr.ID, } switch tt.Field { case user.LoginUpdateAction: updateUserCommand.Login = newEmail case user.EmailUpdateAction: updateUserCommand.Email = newEmail } fn := func(sc *scenarioContext) { // User is internal sc.authInfoService.ExpectedError = user.ErrUserNotFound sc.fakeReqWithParams("PUT", sc.url, nil).exec() assert.Equal(t, http.StatusOK, sc.resp.Code) // Verify that no email has been sent after update require.False(t, nsMock.EmailVerified) userQuery := user.GetUserByIDQuery{ID: usr.ID} updatedUsr, err := hs.userService.GetByID(context.Background(), &userQuery) require.NoError(t, err) // Verify fields have been updated require.NotEqual(t, usr.Name, updatedUsr.Name) require.Equal(t, updateUserCommand.Name, updatedUsr.Name) switch tt.Field { case user.LoginUpdateAction: require.Equal(t, usr.Email, updatedUsr.Email) require.NotEqual(t, usr.Login, updatedUsr.Login) require.Equal(t, updateUserCommand.Login, updatedUsr.Login) case user.EmailUpdateAction: require.Equal(t, usr.Login, updatedUsr.Login) require.NotEqual(t, usr.Email, updatedUsr.Email) require.Equal(t, updateUserCommand.Email, updatedUsr.Email) } // Verify other fields have been kept require.Equal(t, usr.Company, updatedUsr.Company) } updateUserScenario(t, updateUserContext{ desc: ttt.name, url: fmt.Sprintf("/api/users/%d", usr.ID), routePattern: "/api/users/:id", cmd: updateUserCommand, fn: fn, }, hs) updateSignedInUserScenario(t, updateUserContext{ desc: ttt.name, url: "/api/user", routePattern: "/api/user", cmd: updateUserCommand, fn: fn, }, hs) } }) }) } doReq := func(req *http.Request, usr *user.User) (*http.Response, error) { r := webtest.RequestWithSignedInUser( req, authedUserWithPermissions( usr.ID, usr.OrgID, []accesscontrol.Permission{ { Action: accesscontrol.ActionUsersWrite, Scope: accesscontrol.ScopeGlobalUsersAll, }, }, ), ) client := &http.Client{ CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse }} return client.Do(r) } sendUpdateReq := func(server *webtest.Server, usr *user.User, body string) { req := server.NewRequest( http.MethodPut, "/api/user", strings.NewReader(body), ) req.Header.Add("Content-Type", "application/json") res, err := doReq(req, usr) require.NoError(t, err) assert.Equal(t, http.StatusOK, res.StatusCode) require.NoError(t, res.Body.Close()) } sendVerificationReq := func(server *webtest.Server, usr *user.User, code string) { url := fmt.Sprintf("/user/email/update?code=%s", url.QueryEscape(code)) req := server.NewGetRequest(url) res, err := doReq(req, usr) require.NoError(t, err) assert.Equal(t, http.StatusFound, res.StatusCode) require.NoError(t, res.Body.Close()) } getVerificationTempUser := func(tempUserSvc tempuser.Service, code string) *tempuser.TempUserDTO { tmpUserQuery := tempuser.GetTempUserByCodeQuery{Code: code} tmpUser, err := tempUserSvc.GetTempUserByCode(context.Background(), &tmpUserQuery) require.NoError(t, err) return tmpUser } verifyEmailData := func(tempUserSvc tempuser.Service, nsMock *notifications.NotificationServiceMock, originalUsr *user.User, newEmail string) { verification := nsMock.EmailVerification tmpUsr := getVerificationTempUser(tempUserSvc, verification.Code) require.True(t, nsMock.EmailVerified) require.Equal(t, newEmail, verification.Email) require.Equal(t, originalUsr.ID, verification.User.ID) require.Equal(t, tmpUsr.Code, verification.Code) } verifyUserNotUpdated := func(userSvc user.Service, usr *user.User) { userQuery := user.GetUserByIDQuery{ID: usr.ID} checkUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.Equal(t, usr.Email, checkUsr.Email) require.Equal(t, usr.Login, checkUsr.Login) require.Equal(t, usr.Name, checkUsr.Name) } setupScenario := func(cfg *setting.Cfg) (*webtest.Server, user.Service, tempuser.Service, *notifications.NotificationServiceMock) { settings := setting.NewCfg() settings.Smtp.Enabled = true settings.VerificationEmailMaxLifetime = 1 * time.Hour settings.VerifyEmailEnabled = true if cfg != nil { settings = cfg } nsMock := notifications.MockNotificationService() sqlStore := db.InitTestDB(t, sqlstore.InitTestDBOpt{Cfg: settings}) tempUserSvc := tempuserimpl.ProvideService(sqlStore, settings) orgSvc, err := orgimpl.ProvideService(sqlStore, settings, quotatest.New(false, nil)) require.NoError(t, err) userSvc, err := userimpl.ProvideService( sqlStore, orgSvc, settings, nil, nil, tracing.InitializeTracerForTest(), quotatest.New(false, nil), supportbundlestest.NewFakeBundleService(), ) require.NoError(t, err) server := SetupAPITestServer(t, func(hs *HTTPServer) { hs.Cfg = settings hs.SQLStore = sqlStore hs.userService = userSvc hs.tempUserService = tempUserSvc hs.NotificationService = nsMock hs.SecretsService = fakes.NewFakeSecretsService() hs.userVerifier = userimpl.ProvideVerifier(settings, userSvc, tempUserSvc, nsMock, &idtest.MockService{}) // User is internal hs.authInfoService = &authinfotest.FakeService{ExpectedError: user.ErrUserNotFound} }) return server, userSvc, tempUserSvc, nsMock } createUser := func(userSvc user.Service, name string, email string, login string) *user.User { createUserCmd := user.CreateUserCommand{ Email: email, Name: name, Login: login, Company: "testCompany", IsAdmin: true, } usr, err := userSvc.Create(context.Background(), &createUserCmd) require.NoError(t, err) return usr } t.Run("Update Email and disregard other fields", func(t *testing.T) { server, userSvc, tempUserSvc, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update newName := "newname" body := fmt.Sprintf(`{"email": "%s", "name": "%s"}`, newEmail, newName) sendUpdateReq(server, originalUsr, body) // Verify email data verifyEmailData(tempUserSvc, nsMock, originalUsr, newEmail) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Second part of the verification flow, when user clicks email button code := nsMock.EmailVerification.Code sendVerificationReq(server, originalUsr, code) // Verify Email has been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Email, updatedUsr.Email) require.Equal(t, newEmail, updatedUsr.Email) // Fields unchanged require.Equal(t, originalUsr.Login, updatedUsr.Login) require.Equal(t, originalUsr.Name, updatedUsr.Name) require.NotEqual(t, newName, updatedUsr.Name) }) t.Run("Update Email when Login was also an email should update both", func(t *testing.T) { server, userSvc, tempUserSvc, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "email@localhost") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update body := fmt.Sprintf(`{"email": "%s"}`, newEmail) sendUpdateReq(server, originalUsr, body) // Verify email data verifyEmailData(tempUserSvc, nsMock, originalUsr, newEmail) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Second part of the verification flow, when user clicks email button code := nsMock.EmailVerification.Code sendVerificationReq(server, originalUsr, code) // Verify Email and Login have been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Email, updatedUsr.Email) require.Equal(t, newEmail, updatedUsr.Email) require.Equal(t, newEmail, updatedUsr.Login) // Fields unchanged require.Equal(t, originalUsr.Name, updatedUsr.Name) }) t.Run("Update Login with an email should update Email too", func(t *testing.T) { server, userSvc, tempUserSvc, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update body := fmt.Sprintf(`{"login": "%s"}`, newEmail) sendUpdateReq(server, originalUsr, body) // Verify email data verifyEmailData(tempUserSvc, nsMock, originalUsr, newEmail) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Second part of the verification flow, when user clicks email button code := nsMock.EmailVerification.Code sendVerificationReq(server, originalUsr, code) // Verify Email and Login have been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Email, updatedUsr.Email) require.NotEqual(t, originalUsr.Login, updatedUsr.Login) require.Equal(t, newEmail, updatedUsr.Email) require.Equal(t, newEmail, updatedUsr.Login) // Fields unchanged require.Equal(t, originalUsr.Name, updatedUsr.Name) }) t.Run("Update Login should not need verification if it is not an email", func(t *testing.T) { server, userSvc, _, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update newLogin := "newlogin" newName := "newname" body := fmt.Sprintf(`{"login": "%s", "name": "%s"}`, newLogin, newName) sendUpdateReq(server, originalUsr, body) // Verify that email has not been sent require.False(t, nsMock.EmailVerified) // Verify Login has been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Login, updatedUsr.Login) require.NotEqual(t, originalUsr.Name, updatedUsr.Name) require.Equal(t, newLogin, updatedUsr.Login) require.Equal(t, newName, updatedUsr.Name) // Fields unchanged require.Equal(t, originalUsr.Email, updatedUsr.Email) }) t.Run("Update Login should not need verification if it is being updated to the already configured email", func(t *testing.T) { server, userSvc, _, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update body := fmt.Sprintf(`{"login": "%s"}`, originalUsr.Email) sendUpdateReq(server, originalUsr, body) // Verify that email has not been sent require.False(t, nsMock.EmailVerified) // Verify Login has been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Login, updatedUsr.Login) require.Equal(t, originalUsr.Email, updatedUsr.Login) require.Equal(t, originalUsr.Email, updatedUsr.Email) }) t.Run("Update Login and Email with different email values at once should disregard the Login update", func(t *testing.T) { server, userSvc, tempUserSvc, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update newLogin := "newemail2@localhost" body := fmt.Sprintf(`{"email": "%s", "login": "%s"}`, newEmail, newLogin) sendUpdateReq(server, originalUsr, body) // Verify email data verifyEmailData(tempUserSvc, nsMock, originalUsr, newEmail) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Second part of the verification flow, when user clicks email button code := nsMock.EmailVerification.Code sendVerificationReq(server, originalUsr, code) // Verify only Email has been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Email, updatedUsr.Email) require.Equal(t, newEmail, updatedUsr.Email) // Fields unchanged require.NotEqual(t, newLogin, updatedUsr.Login) require.Equal(t, originalUsr.Login, updatedUsr.Login) require.Equal(t, originalUsr.Name, updatedUsr.Name) }) t.Run("Update Login and Email with different email values at once when Login was already an email should update both with Email", func(t *testing.T) { server, userSvc, tempUserSvc, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "email@localhost") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update newLogin := "newemail2@localhost" body := fmt.Sprintf(`{"email": "%s", "login": "%s"}`, newEmail, newLogin) sendUpdateReq(server, originalUsr, body) // Verify email data verifyEmailData(tempUserSvc, nsMock, originalUsr, newEmail) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Second part of the verification flow, when user clicks email button code := nsMock.EmailVerification.Code sendVerificationReq(server, originalUsr, code) // Verify only Email has been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Email, updatedUsr.Email) require.NotEqual(t, originalUsr.Login, updatedUsr.Login) require.NotEqual(t, newLogin, updatedUsr.Login) require.Equal(t, newEmail, updatedUsr.Email) require.Equal(t, newEmail, updatedUsr.Login) // Fields unchanged require.Equal(t, originalUsr.Name, updatedUsr.Name) }) t.Run("Email verification should expire", func(t *testing.T) { cfg := setting.NewCfg() cfg.Smtp.Enabled = true cfg.VerificationEmailMaxLifetime = 0 // Expire instantly cfg.VerifyEmailEnabled = true server, userSvc, tempUserSvc, nsMock := setupScenario(cfg) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update body := fmt.Sprintf(`{"email": "%s"}`, newEmail) sendUpdateReq(server, originalUsr, body) // Verify email data verifyEmailData(tempUserSvc, nsMock, originalUsr, newEmail) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Second part of the verification flow, when user clicks email button code := nsMock.EmailVerification.Code sendVerificationReq(server, originalUsr, code) // Verify user has not been updated userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, newEmail, updatedUsr.Email) require.Equal(t, originalUsr.Email, updatedUsr.Email) require.Equal(t, originalUsr.Login, updatedUsr.Login) }) t.Run("A new verification should revoke other pending verifications", func(t *testing.T) { server, userSvc, tempUserSvc, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // First email verification firstNewEmail := "newemail1@localhost" body := fmt.Sprintf(`{"email": "%s"}`, firstNewEmail) sendUpdateReq(server, originalUsr, body) verifyEmailData(tempUserSvc, nsMock, originalUsr, firstNewEmail) firstCode := nsMock.EmailVerification.Code // Second email verification secondNewEmail := "newemail2@localhost" body = fmt.Sprintf(`{"email": "%s"}`, secondNewEmail) sendUpdateReq(server, originalUsr, body) verifyEmailData(tempUserSvc, nsMock, originalUsr, secondNewEmail) secondCode := nsMock.EmailVerification.Code // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Try to follow through with the first verification unsuccessfully sendVerificationReq(server, originalUsr, firstCode) verifyUserNotUpdated(userSvc, originalUsr) // Follow through with second verification successfully sendVerificationReq(server, originalUsr, secondCode) userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.NotEqual(t, originalUsr.Email, updatedUsr.Email) require.Equal(t, secondNewEmail, updatedUsr.Email) // Fields unchanged require.Equal(t, originalUsr.Login, updatedUsr.Login) }) t.Run("Email verification should fail if code is not valid", func(t *testing.T) { server, userSvc, tempUserSvc, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Start email update body := fmt.Sprintf(`{"email": "%s"}`, newEmail) sendUpdateReq(server, originalUsr, body) // Verify email data verifyEmailData(tempUserSvc, nsMock, originalUsr, newEmail) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Second part of the verification flow should fail if using the wrong code sendVerificationReq(server, originalUsr, "notTheRightCode") verifyUserNotUpdated(userSvc, originalUsr) }) t.Run("Email verification code can only be used once", func(t *testing.T) { server, userSvc, _, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name", "email@localhost", "login") // Start email update require.NotEqual(t, originalUsr.Email, newEmail) body := fmt.Sprintf(`{"email": "%s"}`, newEmail) sendUpdateReq(server, originalUsr, body) // Verify user has not been updated yet verifyUserNotUpdated(userSvc, originalUsr) // Use code to verify successfully codeToReuse := nsMock.EmailVerification.Code sendVerificationReq(server, originalUsr, codeToReuse) // User should have an updated Email userQuery := user.GetUserByIDQuery{ID: originalUsr.ID} updatedUsr, err := userSvc.GetByID(context.Background(), &userQuery) require.NoError(t, err) require.Equal(t, newEmail, updatedUsr.Email) // Change email back to what it was body = fmt.Sprintf(`{"email": "%s"}`, originalUsr.Email) sendUpdateReq(server, originalUsr, body) sendVerificationReq(server, originalUsr, nsMock.EmailVerification.Code) verifyUserNotUpdated(userSvc, originalUsr) // Re-use code to verify new email again, unsuccessfully sendVerificationReq(server, originalUsr, codeToReuse) verifyUserNotUpdated(userSvc, originalUsr) }) t.Run("Update Email with an email that is already being used should fail", func(t *testing.T) { testCases := []struct { description string clashLogin bool }{ { description: "when Email clashes", clashLogin: false, }, { description: "when Login clashes", clashLogin: true, }, } for _, tt := range testCases { t.Run(tt.description, func(t *testing.T) { server, userSvc, _, nsMock := setupScenario(nil) originalUsr := createUser(userSvc, "name1", "email1@localhost", "login1@localhost") badUsr := createUser(userSvc, "name2", "email2@localhost", "login2") // Verify that no email has been sent yet require.False(t, nsMock.EmailVerified) // Update `badUsr` to use the same email as `originalUsr` body := fmt.Sprintf(`{"email": "%s"}`, originalUsr.Email) if tt.clashLogin { body = fmt.Sprintf(`{"login": "%s"}`, originalUsr.Login) } req := server.NewRequest( http.MethodPut, "/api/user", strings.NewReader(body), ) req.Header.Add("Content-Type", "application/json") res, err := doReq(req, badUsr) require.NoError(t, err) assert.Equal(t, http.StatusConflict, res.StatusCode) require.NoError(t, res.Body.Close()) // Verify that no email has been sent require.False(t, nsMock.EmailVerified) // Verify user has not been updated verifyUserNotUpdated(userSvc, badUsr) }) } }) } type updateUserContext struct { desc string url string routePattern string cmd user.UpdateUserCommand fn scenarioFunc } func updateUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPServer) { t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { sc := setupScenarioContext(t, ctx.url) sc.authInfoService = &authinfotest.FakeService{} hs.authInfoService = sc.authInfoService sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c sc.context.OrgID = testOrgID sc.context.UserID = testUserID return hs.UpdateUser(c) }) sc.m.Put(ctx.routePattern, sc.defaultHandler) ctx.fn(sc) }) } 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{ Email: fmt.Sprint("admin", "@test.com"), Name: "admin", Login: "admin", UserID: 1, } updateSignedInUserScenario(t, updateUserContext{ desc: "Should return 403 when the current User is an external user", url: "/api/users/", routePattern: "/api/users/", cmd: updateUserCommand, fn: func(sc *scenarioContext) { 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) }, }, hs) } func updateSignedInUserScenario(t *testing.T, ctx updateUserContext, hs *HTTPServer) { t.Run(fmt.Sprintf("%s %s", ctx.desc, ctx.url), func(t *testing.T) { sc := setupScenarioContext(t, ctx.url) sc.authInfoService = &authinfotest.FakeService{} hs.authInfoService = sc.authInfoService sc.defaultHandler = routing.Wrap(func(c *contextmodel.ReqContext) response.Response { c.Req.Body = mockRequestBody(ctx.cmd) c.Req.Header.Add("Content-Type", "application/json") sc.context = c sc.context.OrgID = testOrgID sc.context.UserID = testUserID return hs.UpdateSignedInUser(c) }) sc.m.Put(ctx.routePattern, sc.defaultHandler) ctx.fn(sc) }) }