The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
grafana/pkg/api/user_test.go

1155 lines
40 KiB

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)
})
}