diff --git a/CHANGELOG.md b/CHANGELOG.md index 627e9bfe5af..2d30ed5d8bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -55,6 +55,8 @@ * **Permission list**: Improved ux [#10747](https://github.com/grafana/grafana/issues/10747) * **Dashboard**: Sizing and positioning of settings menu icons [#11572](https://github.com/grafana/grafana/pull/11572) * **Folders**: User with org viewer role should not be able to save/move dashboards in/to general folder [#11553](https://github.com/grafana/grafana/issues/11553) +* **Tech**: Backend code simplification [#11613](https://github.com/grafana/grafana/pull/11613), thx [@knweiss](https://github.com/knweiss) +* **Tech**: Add codespell to CI [#11602](https://github.com/grafana/grafana/pull/11602), thx [@mjtrangoni](https://github.com/mjtrangoni) ### Tech * Migrated JavaScript files to TypeScript diff --git a/pkg/api/login.go b/pkg/api/login.go index 671e5fb7ecd..9d0fa31946f 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -101,13 +101,14 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { return Error(401, "Login is disabled", nil) } - authQuery := login.LoginUserQuery{ - Username: cmd.User, - Password: cmd.Password, - IpAddress: c.Req.RemoteAddr, + authQuery := &m.LoginUserQuery{ + ReqContext: c, + Username: cmd.User, + Password: cmd.Password, + IpAddress: c.Req.RemoteAddr, } - if err := bus.Dispatch(&authQuery); err != nil { + if err := bus.Dispatch(authQuery); err != nil { if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts { return Error(401, "Invalid username or password", err) } diff --git a/pkg/api/login_oauth.go b/pkg/api/login_oauth.go index 1dba38e9cbd..c4a5f8fdacf 100644 --- a/pkg/api/login_oauth.go +++ b/pkg/api/login_oauth.go @@ -6,7 +6,6 @@ import ( "crypto/tls" "crypto/x509" "encoding/base64" - "errors" "fmt" "io/ioutil" "net/http" @@ -16,22 +15,15 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/quota" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/social" ) -var ( - ErrProviderDeniedRequest = errors.New("Login provider denied login request") - ErrEmailNotAllowed = errors.New("Required email domain not fulfilled") - ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter") - ErrUsersQuotaReached = errors.New("Users quota reached") - ErrNoEmail = errors.New("Login provider didn't return an email address") - oauthLogger = log.New("oauth") -) +var oauthLogger = log.New("oauth") func GenStateString() string { rnd := make([]byte, 32) @@ -56,7 +48,7 @@ func OAuthLogin(ctx *m.ReqContext) { if errorParam != "" { errorDesc := ctx.Query("error_description") oauthLogger.Error("failed to login ", "error", errorParam, "errorDesc", errorDesc) - redirectWithError(ctx, ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc) + redirectWithError(ctx, login.ErrProviderDeniedRequest, "error", errorParam, "errorDesc", errorDesc) return } @@ -149,54 +141,43 @@ func OAuthLogin(ctx *m.ReqContext) { // validate that we got at least an email address if userInfo.Email == "" { - redirectWithError(ctx, ErrNoEmail) + redirectWithError(ctx, login.ErrNoEmail) return } // validate that the email is allowed to login to grafana if !connect.IsEmailAllowed(userInfo.Email) { - redirectWithError(ctx, ErrEmailNotAllowed) + redirectWithError(ctx, login.ErrEmailNotAllowed) return } - userQuery := m.GetUserByEmailQuery{Email: userInfo.Email} - err = bus.Dispatch(&userQuery) - - // create account if missing - if err == m.ErrUserNotFound { - if !connect.IsSignupAllowed() { - redirectWithError(ctx, ErrSignUpNotAllowed) - return - } - limitReached, err := quota.QuotaReached(ctx, "user") - if err != nil { - ctx.Handle(500, "Failed to get user quota", err) - return - } - if limitReached { - redirectWithError(ctx, ErrUsersQuotaReached) - return - } - cmd := m.CreateUserCommand{ - Login: userInfo.Login, - Email: userInfo.Email, - Name: userInfo.Name, - Company: userInfo.Company, - DefaultOrgRole: userInfo.Role, - } + extUser := &m.ExternalUserInfo{ + AuthModule: "oauth_" + name, + AuthId: userInfo.Id, + Name: userInfo.Name, + Login: userInfo.Login, + Email: userInfo.Email, + OrgRoles: map[int64]m.RoleType{}, + } - if err = bus.Dispatch(&cmd); err != nil { - ctx.Handle(500, "Failed to create account", err) - return - } + if userInfo.Role != "" { + extUser.OrgRoles[1] = m.RoleType(userInfo.Role) + } - userQuery.Result = &cmd.Result - } else if err != nil { - ctx.Handle(500, "Unexpected error", err) + // add/update user in grafana + cmd := &m.UpsertUserCommand{ + ReqContext: ctx, + ExternalUser: extUser, + SignupAllowed: connect.IsSignupAllowed(), + } + err = bus.Dispatch(cmd) + if err != nil { + redirectWithError(ctx, err) + return } // login - loginUserWithUser(userQuery.Result, ctx) + loginUserWithUser(cmd.Result, ctx) metrics.M_Api_Login_OAuth.Inc() diff --git a/pkg/login/auth.go b/pkg/login/auth.go index 5527c7271d6..215a22cde33 100644 --- a/pkg/login/auth.go +++ b/pkg/login/auth.go @@ -8,23 +8,22 @@ import ( ) var ( - ErrInvalidCredentials = errors.New("Invalid Username or Password") - ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked") + ErrEmailNotAllowed = errors.New("Required email domain not fulfilled") + ErrInvalidCredentials = errors.New("Invalid Username or Password") + ErrNoEmail = errors.New("Login provider didn't return an email address") + ErrProviderDeniedRequest = errors.New("Login provider denied login request") + ErrSignUpNotAllowed = errors.New("Signup is not allowed for this adapter") + ErrTooManyLoginAttempts = errors.New("Too many consecutive incorrect login attempts for user. Login for user temporarily blocked") + ErrUsersQuotaReached = errors.New("Users quota reached") + ErrGettingUserQuota = errors.New("Error getting user quota") ) -type LoginUserQuery struct { - Username string - Password string - User *m.User - IpAddress string -} - func Init() { bus.AddHandler("auth", AuthenticateUser) loadLdapConfig() } -func AuthenticateUser(query *LoginUserQuery) error { +func AuthenticateUser(query *m.LoginUserQuery) error { if err := validateLoginAttempts(query.Username); err != nil { return err } diff --git a/pkg/login/auth_test.go b/pkg/login/auth_test.go index 59d3c8f2b33..932125c410e 100644 --- a/pkg/login/auth_test.go +++ b/pkg/login/auth_test.go @@ -151,7 +151,7 @@ func TestAuthenticateUser(t *testing.T) { } type authScenarioContext struct { - loginUserQuery *LoginUserQuery + loginUserQuery *m.LoginUserQuery grafanaLoginWasCalled bool ldapLoginWasCalled bool loginAttemptValidationWasCalled bool @@ -161,14 +161,14 @@ type authScenarioContext struct { type authScenarioFunc func(sc *authScenarioContext) func mockLoginUsingGrafanaDB(err error, sc *authScenarioContext) { - loginUsingGrafanaDB = func(query *LoginUserQuery) error { + loginUsingGrafanaDB = func(query *m.LoginUserQuery) error { sc.grafanaLoginWasCalled = true return err } } func mockLoginUsingLdap(enabled bool, err error, sc *authScenarioContext) { - loginUsingLdap = func(query *LoginUserQuery) (bool, error) { + loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) { sc.ldapLoginWasCalled = true return enabled, err } @@ -182,7 +182,7 @@ func mockLoginAttemptValidation(err error, sc *authScenarioContext) { } func mockSaveInvalidLoginAttempt(sc *authScenarioContext) { - saveInvalidLoginAttempt = func(query *LoginUserQuery) { + saveInvalidLoginAttempt = func(query *m.LoginUserQuery) { sc.saveInvalidLoginAttemptWasCalled = true } } @@ -195,7 +195,7 @@ func authScenario(desc string, fn authScenarioFunc) { origSaveInvalidLoginAttempt := saveInvalidLoginAttempt sc := &authScenarioContext{ - loginUserQuery: &LoginUserQuery{ + loginUserQuery: &m.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/brute_force_login_protection.go b/pkg/login/brute_force_login_protection.go index 2ea93979c7a..ca5e0a667ff 100644 --- a/pkg/login/brute_force_login_protection.go +++ b/pkg/login/brute_force_login_protection.go @@ -34,7 +34,7 @@ var validateLoginAttempts = func(username string) error { return nil } -var saveInvalidLoginAttempt = func(query *LoginUserQuery) { +var saveInvalidLoginAttempt = func(query *m.LoginUserQuery) { if setting.DisableBruteForceLoginProtection { return } diff --git a/pkg/login/brute_force_login_protection_test.go b/pkg/login/brute_force_login_protection_test.go index 5375134ba88..aca100760c7 100644 --- a/pkg/login/brute_force_login_protection_test.go +++ b/pkg/login/brute_force_login_protection_test.go @@ -50,7 +50,7 @@ func TestLoginAttemptsValidation(t *testing.T) { return nil }) - saveInvalidLoginAttempt(&LoginUserQuery{ + saveInvalidLoginAttempt(&m.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", @@ -103,7 +103,7 @@ func TestLoginAttemptsValidation(t *testing.T) { return nil }) - saveInvalidLoginAttempt(&LoginUserQuery{ + saveInvalidLoginAttempt(&m.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/ext_user.go b/pkg/login/ext_user.go new file mode 100644 index 00000000000..e1d5e3e3b48 --- /dev/null +++ b/pkg/login/ext_user.go @@ -0,0 +1,184 @@ +package login + +import ( + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/quota" +) + +func init() { + bus.AddHandler("auth", UpsertUser) +} + +func UpsertUser(cmd *m.UpsertUserCommand) error { + extUser := cmd.ExternalUser + + userQuery := &m.GetUserByAuthInfoQuery{ + AuthModule: extUser.AuthModule, + AuthId: extUser.AuthId, + UserId: extUser.UserId, + Email: extUser.Email, + Login: extUser.Login, + } + err := bus.Dispatch(userQuery) + if err != m.ErrUserNotFound && err != nil { + return err + } + + if err != nil { + if !cmd.SignupAllowed { + log.Warn("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule) + return ErrInvalidCredentials + } + + limitReached, err := quota.QuotaReached(cmd.ReqContext, "user") + if err != nil { + log.Warn("Error getting user quota", "err", err) + return ErrGettingUserQuota + } + if limitReached { + return ErrUsersQuotaReached + } + + cmd.Result, err = createUser(extUser) + if err != nil { + return err + } + + if extUser.AuthModule != "" && extUser.AuthId != "" { + cmd2 := &m.SetAuthInfoCommand{ + UserId: cmd.Result.Id, + AuthModule: extUser.AuthModule, + AuthId: extUser.AuthId, + } + if err := bus.Dispatch(cmd2); err != nil { + return err + } + } + + } else { + cmd.Result = userQuery.Result + + err = updateUser(cmd.Result, extUser) + if err != nil { + return err + } + } + + return syncOrgRoles(cmd.Result, extUser) +} + +func createUser(extUser *m.ExternalUserInfo) (*m.User, error) { + cmd := &m.CreateUserCommand{ + Login: extUser.Login, + Email: extUser.Email, + Name: extUser.Name, + SkipOrgSetup: len(extUser.OrgRoles) > 0, + } + if err := bus.Dispatch(cmd); err != nil { + return nil, err + } + + return &cmd.Result, nil +} + +func updateUser(user *m.User, extUser *m.ExternalUserInfo) error { + // sync user info + updateCmd := &m.UpdateUserCommand{ + UserId: user.Id, + } + + needsUpdate := false + if extUser.Login != "" && extUser.Login != user.Login { + updateCmd.Login = extUser.Login + user.Login = extUser.Login + needsUpdate = true + } + + if extUser.Email != "" && extUser.Email != user.Email { + updateCmd.Email = extUser.Email + user.Email = extUser.Email + needsUpdate = true + } + + if extUser.Name != "" && extUser.Name != user.Name { + updateCmd.Name = extUser.Name + user.Name = extUser.Name + needsUpdate = true + } + + if !needsUpdate { + return nil + } + + log.Debug("Syncing user info", "id", user.Id, "update", updateCmd) + return bus.Dispatch(updateCmd) +} + +func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error { + // don't sync org roles if none are specified + if len(extUser.OrgRoles) == 0 { + return nil + } + + orgsQuery := &m.GetUserOrgListQuery{UserId: user.Id} + if err := bus.Dispatch(orgsQuery); err != nil { + return err + } + + handledOrgIds := map[int64]bool{} + deleteOrgIds := []int64{} + + // update existing org roles + for _, org := range orgsQuery.Result { + handledOrgIds[org.OrgId] = true + + if extUser.OrgRoles[org.OrgId] == "" { + deleteOrgIds = append(deleteOrgIds, org.OrgId) + } else if extUser.OrgRoles[org.OrgId] != org.Role { + // update role + cmd := &m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: extUser.OrgRoles[org.OrgId]} + if err := bus.Dispatch(cmd); err != nil { + return err + } + } + } + + // add any new org roles + for orgId, orgRole := range extUser.OrgRoles { + if _, exists := handledOrgIds[orgId]; exists { + continue + } + + // add role + cmd := &m.AddOrgUserCommand{UserId: user.Id, Role: orgRole, OrgId: orgId} + err := bus.Dispatch(cmd) + if err != nil && err != m.ErrOrgNotFound { + return err + } + } + + // delete any removed org roles + for _, orgId := range deleteOrgIds { + cmd := &m.RemoveOrgUserCommand{OrgId: orgId, UserId: user.Id} + if err := bus.Dispatch(cmd); err != nil { + return err + } + } + + // update user's default org if needed + if _, ok := extUser.OrgRoles[user.OrgId]; !ok { + for orgId := range extUser.OrgRoles { + user.OrgId = orgId + break + } + + return bus.Dispatch(&m.SetUsingOrgCommand{ + UserId: user.Id, + OrgId: user.OrgId, + }) + } + + return nil +} diff --git a/pkg/login/grafana_login.go b/pkg/login/grafana_login.go index 677ba776e4f..e8594fdd190 100644 --- a/pkg/login/grafana_login.go +++ b/pkg/login/grafana_login.go @@ -17,7 +17,7 @@ var validatePassword = func(providedPassword string, userPassword string, userSa return nil } -var loginUsingGrafanaDB = func(query *LoginUserQuery) error { +var loginUsingGrafanaDB = func(query *m.LoginUserQuery) error { userQuery := m.GetUserByLoginQuery{LoginOrEmail: query.Username} if err := bus.Dispatch(&userQuery); err != nil { diff --git a/pkg/login/grafana_login_test.go b/pkg/login/grafana_login_test.go index 88e52224113..90422678fd2 100644 --- a/pkg/login/grafana_login_test.go +++ b/pkg/login/grafana_login_test.go @@ -66,7 +66,7 @@ func TestGrafanaLogin(t *testing.T) { } type grafanaLoginScenarioContext struct { - loginUserQuery *LoginUserQuery + loginUserQuery *m.LoginUserQuery validatePasswordCalled bool } @@ -77,7 +77,7 @@ func grafanaLoginScenario(desc string, fn grafanaLoginScenarioFunc) { origValidatePassword := validatePassword sc := &grafanaLoginScenarioContext{ - loginUserQuery: &LoginUserQuery{ + loginUserQuery: &m.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index 9f2338d653c..ccf44f77f23 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -24,10 +24,9 @@ type ILdapConn interface { } type ILdapAuther interface { - Login(query *LoginUserQuery) error - SyncSignedInUser(signedInUser *m.SignedInUser) error - GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) - SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error + Login(query *m.LoginUserQuery) error + SyncUser(query *m.LoginUserQuery) error + GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) } type ldapAuther struct { @@ -89,7 +88,8 @@ func (a *ldapAuther) Dial() error { return err } -func (a *ldapAuther) Login(query *LoginUserQuery) error { +func (a *ldapAuther) Login(query *m.LoginUserQuery) error { + // connect to ldap server if err := a.Dial(); err != nil { return err } @@ -101,206 +101,105 @@ func (a *ldapAuther) Login(query *LoginUserQuery) error { } // find user entry & attributes - if ldapUser, err := a.searchForUser(query.Username); err != nil { + ldapUser, err := a.searchForUser(query.Username) + if err != nil { return err - } else { - a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser)) + } - // check if a second user bind is needed - if a.requireSecondBind { - if err := a.secondBind(ldapUser, query.Password); err != nil { - return err - } - } + a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser)) - if grafanaUser, err := a.GetGrafanaUserFor(ldapUser); err != nil { + // check if a second user bind is needed + if a.requireSecondBind { + err = a.secondBind(ldapUser, query.Password) + if err != nil { return err - } else { - if syncErr := a.syncInfoAndOrgRoles(grafanaUser, ldapUser); syncErr != nil { - return syncErr - } - query.User = grafanaUser - return nil } } -} -func (a *ldapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error { - grafanaUser := m.User{ - Id: signedInUser.UserId, - Login: signedInUser.Login, - Email: signedInUser.Email, - Name: signedInUser.Name, + grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser) + if err != nil { + return err } - if err := a.Dial(); err != nil { + query.User = grafanaUser + return nil +} + +func (a *ldapAuther) SyncUser(query *m.LoginUserQuery) error { + // connect to ldap server + err := a.Dial() + if err != nil { return err } - defer a.conn.Close() - if err := a.serverBind(); err != nil { + + err = a.serverBind() + if err != nil { return err } - if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil { + // find user entry & attributes + ldapUser, err := a.searchForUser(query.Username) + if err != nil { a.log.Error("Failed searching for user in ldap", "error", err) - return err - } else { - if err := a.syncInfoAndOrgRoles(&grafanaUser, ldapUser); err != nil { - return err - } - - a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser)) } - return nil -} + a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser)) -// Sync info for ldap user and grafana user -func (a *ldapAuther) syncInfoAndOrgRoles(user *m.User, ldapUser *LdapUserInfo) error { - // sync user details - if err := a.syncUserInfo(user, ldapUser); err != nil { - return err - } - // sync org roles - if err := a.SyncOrgRoles(user, ldapUser); err != nil { + grafanaUser, err := a.GetGrafanaUserFor(query.ReqContext, ldapUser) + if err != nil { return err } + query.User = grafanaUser return nil } -func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) { - // validate that the user has access - // if there are no ldap group mappings access is true - // otherwise a single group must match - access := len(a.server.LdapGroups) == 0 - for _, ldapGroup := range a.server.LdapGroups { - if ldapUser.isMemberOf(ldapGroup.GroupDN) { - access = true - break - } - } - - if !access { - a.log.Info("Ldap Auth: user does not belong in any of the specified ldap groups", "username", ldapUser.Username, "groups", ldapUser.MemberOf) - return nil, ErrInvalidCredentials - } - - // get user from grafana db - userQuery := m.GetUserByLoginQuery{LoginOrEmail: ldapUser.Username} - if err := bus.Dispatch(&userQuery); err != nil { - if err == m.ErrUserNotFound && setting.LdapAllowSignup { - return a.createGrafanaUser(ldapUser) - } else if err == m.ErrUserNotFound { - a.log.Warn("Not allowing LDAP login, user not found in internal user database, and ldap allow signup = false") - return nil, ErrInvalidCredentials - } else { - return nil, err - } - } - - return userQuery.Result, nil - -} -func (a *ldapAuther) createGrafanaUser(ldapUser *LdapUserInfo) (*m.User, error) { - cmd := m.CreateUserCommand{ - Login: ldapUser.Username, - Email: ldapUser.Email, - Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName), - } - - if err := bus.Dispatch(&cmd); err != nil { - return nil, err - } - - return &cmd.Result, nil -} - -func (a *ldapAuther) syncUserInfo(user *m.User, ldapUser *LdapUserInfo) error { - var name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName) - if user.Email == ldapUser.Email && user.Name == name { - return nil - } - - a.log.Debug("Syncing user info", "username", ldapUser.Username) - updateCmd := m.UpdateUserCommand{} - updateCmd.UserId = user.Id - updateCmd.Login = user.Login - updateCmd.Email = ldapUser.Email - updateCmd.Name = fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName) - return bus.Dispatch(&updateCmd) -} - -func (a *ldapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error { - if len(a.server.LdapGroups) == 0 { - a.log.Warn("No group mappings defined") - return nil +func (a *ldapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) { + extUser := &m.ExternalUserInfo{ + AuthModule: "ldap", + AuthId: ldapUser.DN, + Name: fmt.Sprintf("%s %s", ldapUser.FirstName, ldapUser.LastName), + Login: ldapUser.Username, + Email: ldapUser.Email, + OrgRoles: map[int64]m.RoleType{}, } - orgsQuery := m.GetUserOrgListQuery{UserId: user.Id} - if err := bus.Dispatch(&orgsQuery); err != nil { - return err - } - - handledOrgIds := map[int64]bool{} - - // update or remove org roles - for _, org := range orgsQuery.Result { - match := false - handledOrgIds[org.OrgId] = true - - for _, group := range a.server.LdapGroups { - if org.OrgId != group.OrgId { - continue - } - - if ldapUser.isMemberOf(group.GroupDN) { - match = true - if org.Role != group.OrgRole { - // update role - cmd := m.UpdateOrgUserCommand{OrgId: org.OrgId, UserId: user.Id, Role: group.OrgRole} - if err := bus.Dispatch(&cmd); err != nil { - return err - } - } - // ignore subsequent ldap group mapping matches - break - } - } - - // remove role if no mappings match - if !match { - cmd := m.RemoveOrgUserCommand{OrgId: org.OrgId, UserId: user.Id} - if err := bus.Dispatch(&cmd); err != nil { - return err - } - } - } - - // add missing org roles for _, group := range a.server.LdapGroups { - if !ldapUser.isMemberOf(group.GroupDN) { + // only use the first match for each org + if extUser.OrgRoles[group.OrgId] != "" { continue } - if _, exists := handledOrgIds[group.OrgId]; exists { - continue + if ldapUser.isMemberOf(group.GroupDN) { + extUser.OrgRoles[group.OrgId] = group.OrgRole } + } - // add role - cmd := m.AddOrgUserCommand{UserId: user.Id, Role: group.OrgRole, OrgId: group.OrgId} - err := bus.Dispatch(&cmd) - if err != nil && err != m.ErrOrgNotFound { - return err - } + // validate that the user has access + // if there are no ldap group mappings access is true + // otherwise a single group must match + if len(a.server.LdapGroups) > 0 && len(extUser.OrgRoles) < 1 { + a.log.Info( + "Ldap Auth: user does not belong in any of the specified ldap groups", + "username", ldapUser.Username, + "groups", ldapUser.MemberOf) + return nil, ErrInvalidCredentials + } - // mark this group has handled so we do not process it again - handledOrgIds[group.OrgId] = true + // add/update user in grafana + userQuery := &m.UpsertUserCommand{ + ReqContext: ctx, + ExternalUser: extUser, + SignupAllowed: setting.LdapAllowSignup, + } + err := bus.Dispatch(userQuery) + if err != nil { + return nil, err } - return nil + return userQuery.Result, nil } func (a *ldapAuther) serverBind() error { @@ -469,7 +368,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string { } return []string{} } - -func createUserFromLdapInfo() error { - return nil -} diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go index b74b69db036..5974e19d691 100644 --- a/pkg/login/ldap_login.go +++ b/pkg/login/ldap_login.go @@ -1,10 +1,11 @@ package login import ( + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" ) -var loginUsingLdap = func(query *LoginUserQuery) (bool, error) { +var loginUsingLdap = func(query *m.LoginUserQuery) (bool, error) { if !setting.LdapEnabled { return false, nil } diff --git a/pkg/login/ldap_login_test.go b/pkg/login/ldap_login_test.go index 6af125566e8..6067a063795 100644 --- a/pkg/login/ldap_login_test.go +++ b/pkg/login/ldap_login_test.go @@ -79,7 +79,7 @@ func TestLdapLogin(t *testing.T) { ldapLoginScenario("When login", func(sc *ldapLoginScenarioContext) { sc.withLoginResult(false) - enabled, err := loginUsingLdap(&LoginUserQuery{ + enabled, err := loginUsingLdap(&m.LoginUserQuery{ Username: "user", Password: "pwd", }) @@ -117,7 +117,7 @@ type mockLdapAuther struct { loginCalled bool } -func (a *mockLdapAuther) Login(query *LoginUserQuery) error { +func (a *mockLdapAuther) Login(query *m.LoginUserQuery) error { a.loginCalled = true if !a.validLogin { @@ -127,20 +127,16 @@ func (a *mockLdapAuther) Login(query *LoginUserQuery) error { return nil } -func (a *mockLdapAuther) SyncSignedInUser(signedInUser *m.SignedInUser) error { +func (a *mockLdapAuther) SyncUser(query *m.LoginUserQuery) error { return nil } -func (a *mockLdapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error) { +func (a *mockLdapAuther) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error) { return nil, nil } -func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) error { - return nil -} - type ldapLoginScenarioContext struct { - loginUserQuery *LoginUserQuery + loginUserQuery *m.LoginUserQuery ldapAuthenticatorMock *mockLdapAuther } @@ -151,7 +147,7 @@ func ldapLoginScenario(desc string, fn ldapLoginScenarioFunc) { origNewLdapAuthenticator := NewLdapAuthenticator sc := &ldapLoginScenarioContext{ - loginUserQuery: &LoginUserQuery{ + loginUserQuery: &m.LoginUserQuery{ Username: "user", Password: "pwd", IpAddress: "192.168.1.1:56433", diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index 8677bbeae42..6085fffb638 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -18,7 +18,7 @@ func TestLdapAuther(t *testing.T) { ldapAuther := NewLdapAuthenticator(&LdapServerConf{ LdapGroups: []*LdapGroupToOrgRole{{}}, }) - _, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{}) + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{}) So(err, ShouldEqual, ErrInvalidCredentials) }) @@ -34,7 +34,7 @@ func TestLdapAuther(t *testing.T) { sc.userQueryReturns(user1) - result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{}) + result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{}) So(err, ShouldBeNil) So(result, ShouldEqual, user1) }) @@ -48,7 +48,7 @@ func TestLdapAuther(t *testing.T) { sc.userQueryReturns(user1) - result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{MemberOf: []string{"cn=users"}}) + result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{MemberOf: []string{"cn=users"}}) So(err, ShouldBeNil) So(result, ShouldEqual, user1) }) @@ -64,7 +64,8 @@ func TestLdapAuther(t *testing.T) { sc.userQueryReturns(nil) - result, err := ldapAuther.GetGrafanaUserFor(&LdapUserInfo{ + result, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + DN: "torkelo", Username: "torkelo", Email: "my@email.com", MemberOf: []string{"cn=editor"}, @@ -72,11 +73,6 @@ func TestLdapAuther(t *testing.T) { So(err, ShouldBeNil) - Convey("Should create new user", func() { - So(sc.createUserCmd.Login, ShouldEqual, "torkelo") - So(sc.createUserCmd.Email, ShouldEqual, "my@email.com") - }) - Convey("Should return new user", func() { So(result.Login, ShouldEqual, "torkelo") }) @@ -95,7 +91,7 @@ func TestLdapAuther(t *testing.T) { }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) - err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{ + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ MemberOf: []string{"cn=users"}, }) @@ -114,7 +110,7 @@ func TestLdapAuther(t *testing.T) { }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) - err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{ + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ MemberOf: []string{"cn=users"}, }) @@ -122,24 +118,29 @@ func TestLdapAuther(t *testing.T) { So(err, ShouldBeNil) So(sc.updateOrgUserCmd, ShouldNotBeNil) So(sc.updateOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN) + So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1) }) }) ldapAutherScenario("given current org role is removed in ldap", func(sc *scenarioContext) { ldapAuther := NewLdapAuthenticator(&LdapServerConf{ LdapGroups: []*LdapGroupToOrgRole{ - {GroupDN: "cn=users", OrgId: 1, OrgRole: "Admin"}, + {GroupDN: "cn=users", OrgId: 2, OrgRole: "Admin"}, }, }) - sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) - err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{ - MemberOf: []string{"cn=other"}, + sc.userOrgsQueryReturns([]*m.UserOrgDTO{ + {OrgId: 1, Role: m.ROLE_EDITOR}, + {OrgId: 2, Role: m.ROLE_EDITOR}, + }) + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ + MemberOf: []string{"cn=users"}, }) Convey("Should remove org role", func() { So(err, ShouldBeNil) So(sc.removeOrgUserCmd, ShouldNotBeNil) + So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 2) }) }) @@ -152,7 +153,7 @@ func TestLdapAuther(t *testing.T) { }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_EDITOR}}) - err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{ + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ MemberOf: []string{"cn=users"}, }) @@ -160,6 +161,7 @@ func TestLdapAuther(t *testing.T) { So(err, ShouldBeNil) So(sc.removeOrgUserCmd, ShouldBeNil) So(sc.updateOrgUserCmd, ShouldNotBeNil) + So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1) }) }) @@ -172,13 +174,14 @@ func TestLdapAuther(t *testing.T) { }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{{OrgId: 1, Role: m.ROLE_ADMIN}}) - err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{ + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ MemberOf: []string{"cn=admins"}, }) Convey("Should take first match, and ignore subsequent matches", func() { So(err, ShouldBeNil) So(sc.updateOrgUserCmd, ShouldBeNil) + So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1) }) }) @@ -191,19 +194,20 @@ func TestLdapAuther(t *testing.T) { }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) - err := ldapAuther.SyncOrgRoles(&m.User{}, &LdapUserInfo{ + _, err := ldapAuther.GetGrafanaUserFor(nil, &LdapUserInfo{ MemberOf: []string{"cn=admins"}, }) Convey("Should take first match, and ignore subsequent matches", func() { So(err, ShouldBeNil) So(sc.addOrgUserCmd.Role, ShouldEqual, m.ROLE_ADMIN) + So(sc.setUsingOrgCmd.OrgId, ShouldEqual, 1) }) }) }) - Convey("When calling SyncSignedInUser", t, func() { + Convey("When calling SyncUser", t, func() { mockLdapConnection := &mockLdapConn{} ldapAuther := NewLdapAuthenticator( @@ -243,17 +247,20 @@ func TestLdapAuther(t *testing.T) { ldapAutherScenario("When ldapUser found call syncInfo and orgRoles", func(sc *scenarioContext) { // arrange - signedInUser := &m.SignedInUser{ - Email: "roel@test.net", - UserId: 1, - Name: "Roel Gerrits", - Login: "roelgerrits", + query := &m.LoginUserQuery{ + Username: "roelgerrits", } + sc.userQueryReturns(&m.User{ + Id: 1, + Email: "roel@test.net", + Name: "Roel Gerrits", + Login: "roelgerrits", + }) sc.userOrgsQueryReturns([]*m.UserOrgDTO{}) // act - syncErrResult := ldapAuther.SyncSignedInUser(signedInUser) + syncErrResult := ldapAuther.SyncUser(query) // assert So(dialCalled, ShouldBeTrue) @@ -299,6 +306,19 @@ func ldapAutherScenario(desc string, fn scenarioFunc) { sc := &scenarioContext{} + bus.AddHandler("test", UpsertUser) + + bus.AddHandler("test", func(cmd *m.GetUserByAuthInfoQuery) error { + sc.getUserByAuthInfoQuery = cmd + sc.getUserByAuthInfoQuery.Result = &m.User{Login: cmd.Login} + return nil + }) + + bus.AddHandler("test", func(cmd *m.GetUserOrgListQuery) error { + sc.getUserOrgListQuery = cmd + return nil + }) + bus.AddHandler("test", func(cmd *m.CreateUserCommand) error { sc.createUserCmd = cmd sc.createUserCmd.Result = m.User{Login: cmd.Login} @@ -325,20 +345,28 @@ func ldapAutherScenario(desc string, fn scenarioFunc) { return nil }) + bus.AddHandler("test", func(cmd *m.SetUsingOrgCommand) error { + sc.setUsingOrgCmd = cmd + return nil + }) + fn(sc) }) } type scenarioContext struct { - createUserCmd *m.CreateUserCommand - addOrgUserCmd *m.AddOrgUserCommand - updateOrgUserCmd *m.UpdateOrgUserCommand - removeOrgUserCmd *m.RemoveOrgUserCommand - updateUserCmd *m.UpdateUserCommand + getUserByAuthInfoQuery *m.GetUserByAuthInfoQuery + getUserOrgListQuery *m.GetUserOrgListQuery + createUserCmd *m.CreateUserCommand + addOrgUserCmd *m.AddOrgUserCommand + updateOrgUserCmd *m.UpdateOrgUserCommand + removeOrgUserCmd *m.RemoveOrgUserCommand + updateUserCmd *m.UpdateUserCommand + setUsingOrgCmd *m.SetUsingOrgCommand } func (sc *scenarioContext) userQueryReturns(user *m.User) { - bus.AddHandler("test", func(query *m.GetUserByLoginQuery) error { + bus.AddHandler("test", func(query *m.GetUserByAuthInfoQuery) error { if user == nil { return m.ErrUserNotFound } else { @@ -346,6 +374,9 @@ func (sc *scenarioContext) userQueryReturns(user *m.User) { return nil } }) + bus.AddHandler("test", func(query *m.SetAuthInfoCommand) error { + return nil + }) } func (sc *scenarioContext) userOrgsQueryReturns(orgs []*m.UserOrgDTO) { diff --git a/pkg/middleware/auth_proxy.go b/pkg/middleware/auth_proxy.go index adf0b7b53d5..36b059e4ae7 100644 --- a/pkg/middleware/auth_proxy.go +++ b/pkg/middleware/auth_proxy.go @@ -3,6 +3,7 @@ package middleware import ( "fmt" "net" + "net/mail" "strings" "time" @@ -14,6 +15,8 @@ import ( "github.com/grafana/grafana/pkg/setting" ) +var AUTH_PROXY_SESSION_VAR = "authProxyHeaderValue" + func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { if !setting.AuthProxyEnabled { return false @@ -30,38 +33,102 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { return true } - query := getSignedInUserQueryForProxyAuth(proxyHeaderValue) - query.OrgId = orgID - if err := bus.Dispatch(query); err != nil { - if err != m.ErrUserNotFound { - ctx.Handle(500, "Failed to find user specified in auth proxy header", err) - return true + // initialize session + if err := ctx.Session.Start(ctx.Context); err != nil { + log.Error(3, "Failed to start session", err) + return false + } + + query := &m.GetSignedInUserQuery{OrgId: orgID} + + // if this session has already been authenticated by authProxy just load the user + sessProxyValue := ctx.Session.Get(AUTH_PROXY_SESSION_VAR) + if sessProxyValue != nil && sessProxyValue.(string) == proxyHeaderValue && getRequestUserId(ctx) > 0 { + // if we're using ldap, sync user periodically + if setting.LdapEnabled { + syncQuery := &m.LoginUserQuery{ + ReqContext: ctx, + Username: proxyHeaderValue, + } + + if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil { + if err == login.ErrInvalidCredentials { + ctx.Handle(500, "Unable to authenticate user", err) + return false + } + + ctx.Handle(500, "Failed to sync user", err) + return false + } + } + + query.UserId = getRequestUserId(ctx) + // if we're using ldap, pass authproxy login name to ldap user sync + } else if setting.LdapEnabled { + ctx.Session.Delete(session.SESS_KEY_LASTLDAPSYNC) + + syncQuery := &m.LoginUserQuery{ + ReqContext: ctx, + Username: proxyHeaderValue, } - if !setting.AuthProxyAutoSignUp { + if err := syncGrafanaUserWithLdapUser(syncQuery); err != nil { + if err == login.ErrInvalidCredentials { + ctx.Handle(500, "Unable to authenticate user", err) + return false + } + + ctx.Handle(500, "Failed to sync user", err) return false } - cmd := getCreateUserCommandForProxyAuth(proxyHeaderValue) - if setting.LdapEnabled { - cmd.SkipOrgSetup = true + if syncQuery.User == nil { + ctx.Handle(500, "Failed to sync user", nil) + return false + } + + query.UserId = syncQuery.User.Id + // no ldap, just use the info we have + } else { + extUser := &m.ExternalUserInfo{ + AuthModule: "authproxy", + AuthId: proxyHeaderValue, } - if err := bus.Dispatch(cmd); err != nil { - ctx.Handle(500, "Failed to create user specified in auth proxy header", err) + if setting.AuthProxyHeaderProperty == "username" { + extUser.Login = proxyHeaderValue + + // only set Email if it can be parsed as an email address + emailAddr, emailErr := mail.ParseAddress(proxyHeaderValue) + if emailErr == nil { + extUser.Email = emailAddr.Address + } + } else if setting.AuthProxyHeaderProperty == "email" { + extUser.Email = proxyHeaderValue + extUser.Login = proxyHeaderValue + } else { + ctx.Handle(500, "Auth proxy header property invalid", nil) return true } - query = &m.GetSignedInUserQuery{UserId: cmd.Result.Id, OrgId: orgID} - if err := bus.Dispatch(query); err != nil { - ctx.Handle(500, "Failed find user after creation", err) + + // add/update user in grafana + cmd := &m.UpsertUserCommand{ + ReqContext: ctx, + ExternalUser: extUser, + SignupAllowed: setting.AuthProxyAutoSignUp, + } + err := bus.Dispatch(cmd) + if err != nil { + ctx.Handle(500, "Failed to login as user specified in auth proxy header", err) return true } + + query.UserId = cmd.Result.Id } - // initialize session - if err := ctx.Session.Start(ctx.Context); err != nil { - log.Error(3, "Failed to start session", err) - return false + if err := bus.Dispatch(query); err != nil { + ctx.Handle(500, "Failed to find user", err) + return true } // Make sure that we cannot share a session between different users! @@ -77,16 +144,7 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { } } - // When ldap is enabled, sync userinfo and org roles - if err := syncGrafanaUserWithLdapUser(ctx, query); err != nil { - if err == login.ErrInvalidCredentials { - ctx.Handle(500, "Unable to authenticate user", err) - return false - } - - ctx.Handle(500, "Failed to sync user", err) - return false - } + ctx.Session.Set(AUTH_PROXY_SESSION_VAR, proxyHeaderValue) ctx.SignedInUser = query.Result ctx.IsSignedIn = true @@ -95,29 +153,29 @@ func initContextWithAuthProxy(ctx *m.ReqContext, orgID int64) bool { return true } -var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error { - if !setting.LdapEnabled { - return nil - } - +var syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error { expireEpoch := time.Now().Add(time.Duration(-setting.AuthProxyLdapSyncTtl) * time.Minute).Unix() var lastLdapSync int64 - if lastLdapSyncInSession := ctx.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil { + if lastLdapSyncInSession := query.ReqContext.Session.Get(session.SESS_KEY_LASTLDAPSYNC); lastLdapSyncInSession != nil { lastLdapSync = lastLdapSyncInSession.(int64) } if lastLdapSync < expireEpoch { ldapCfg := login.LdapCfg + if len(ldapCfg.Servers) < 1 { + return fmt.Errorf("No LDAP servers available") + } + for _, server := range ldapCfg.Servers { author := login.NewLdapAuthenticator(server) - if err := author.SyncSignedInUser(query.Result); err != nil { + if err := author.SyncUser(query); err != nil { return err } } - ctx.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix()) + query.ReqContext.Session.Set(session.SESS_KEY_LASTLDAPSYNC, time.Now().Unix()) } return nil @@ -143,29 +201,3 @@ func checkAuthenticationProxy(remoteAddr string, proxyHeaderValue string) error return fmt.Errorf("Request for user (%s) from %s is not from the authentication proxy", proxyHeaderValue, sourceIP) } - -func getSignedInUserQueryForProxyAuth(headerVal string) *m.GetSignedInUserQuery { - query := m.GetSignedInUserQuery{} - if setting.AuthProxyHeaderProperty == "username" { - query.Login = headerVal - } else if setting.AuthProxyHeaderProperty == "email" { - query.Email = headerVal - } else { - panic("Auth proxy header property invalid") - } - return &query -} - -func getCreateUserCommandForProxyAuth(headerVal string) *m.CreateUserCommand { - cmd := m.CreateUserCommand{} - if setting.AuthProxyHeaderProperty == "username" { - cmd.Login = headerVal - cmd.Email = headerVal - } else if setting.AuthProxyHeaderProperty == "email" { - cmd.Email = headerVal - cmd.Login = headerVal - } else { - panic("Auth proxy header property invalid") - } - return &cmd -} diff --git a/pkg/middleware/auth_proxy_test.go b/pkg/middleware/auth_proxy_test.go index b3c011bd870..47ed2f71a79 100644 --- a/pkg/middleware/auth_proxy_test.go +++ b/pkg/middleware/auth_proxy_test.go @@ -26,57 +26,71 @@ func TestAuthProxyWithLdapEnabled(t *testing.T) { return &mockLdapAuther } - signedInUser := m.SignedInUser{} - query := m.GetSignedInUserQuery{Result: &signedInUser} - - Convey("When session variable lastLdapSync not set, call syncSignedInUser and set lastLdapSync", func() { + Convey("When user logs in, call SyncUser", func() { // arrange - sess := mockSession{} + sess := newMockSession() ctx := m.ReqContext{Session: &sess} So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeNil) // act - syncGrafanaUserWithLdapUser(&ctx, &query) + syncGrafanaUserWithLdapUser(&m.LoginUserQuery{ + ReqContext: &ctx, + Username: "test", + }) // assert - So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue) + So(mockLdapAuther.syncUserCalled, ShouldBeTrue) So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, 0) }) Convey("When session variable not expired, don't sync and don't change session var", func() { // arrange - sess := mockSession{} + sess := newMockSession() ctx := m.ReqContext{Session: &sess} now := time.Now().Unix() sess.Set(session.SESS_KEY_LASTLDAPSYNC, now) + sess.Set(AUTH_PROXY_SESSION_VAR, "test") // act - syncGrafanaUserWithLdapUser(&ctx, &query) + syncGrafanaUserWithLdapUser(&m.LoginUserQuery{ + ReqContext: &ctx, + Username: "test", + }) // assert So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldEqual, now) - So(mockLdapAuther.syncSignedInUserCalled, ShouldBeFalse) + So(mockLdapAuther.syncUserCalled, ShouldBeFalse) }) Convey("When lastldapsync is expired, session variable should be updated", func() { // arrange - sess := mockSession{} + sess := newMockSession() ctx := m.ReqContext{Session: &sess} expiredTime := time.Now().Add(time.Duration(-120) * time.Minute).Unix() sess.Set(session.SESS_KEY_LASTLDAPSYNC, expiredTime) + sess.Set(AUTH_PROXY_SESSION_VAR, "test") // act - syncGrafanaUserWithLdapUser(&ctx, &query) + syncGrafanaUserWithLdapUser(&m.LoginUserQuery{ + ReqContext: &ctx, + Username: "test", + }) // assert So(sess.Get(session.SESS_KEY_LASTLDAPSYNC), ShouldBeGreaterThan, expiredTime) - So(mockLdapAuther.syncSignedInUserCalled, ShouldBeTrue) + So(mockLdapAuther.syncUserCalled, ShouldBeTrue) }) }) } type mockSession struct { - value interface{} + value map[interface{}]interface{} +} + +func newMockSession() mockSession { + session := mockSession{} + session.value = make(map[interface{}]interface{}) + return session } func (s *mockSession) Start(c *macaron.Context) error { @@ -84,15 +98,16 @@ func (s *mockSession) Start(c *macaron.Context) error { } func (s *mockSession) Set(k interface{}, v interface{}) error { - s.value = v + s.value[k] = v return nil } func (s *mockSession) Get(k interface{}) interface{} { - return s.value + return s.value[k] } func (s *mockSession) Delete(k interface{}) interface{} { + delete(s.value, k) return nil } @@ -113,21 +128,18 @@ func (s *mockSession) RegenerateId(c *macaron.Context) error { } type mockLdapAuthenticator struct { - syncSignedInUserCalled bool + syncUserCalled bool } -func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error { +func (a *mockLdapAuthenticator) Login(query *m.LoginUserQuery) error { return nil } -func (a *mockLdapAuthenticator) SyncSignedInUser(signedInUser *m.SignedInUser) error { - a.syncSignedInUserCalled = true +func (a *mockLdapAuthenticator) SyncUser(query *m.LoginUserQuery) error { + a.syncUserCalled = true return nil } -func (a *mockLdapAuthenticator) GetGrafanaUserFor(ldapUser *login.LdapUserInfo) (*m.User, error) { +func (a *mockLdapAuthenticator) GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *login.LdapUserInfo) (*m.User, error) { return nil, nil } -func (a *mockLdapAuthenticator) SyncOrgRoles(user *m.User, ldapUser *login.LdapUserInfo) error { - return nil -} diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index b5b244d5bff..93db49ed880 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -8,7 +8,6 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/log" - l "github.com/grafana/grafana/pkg/login" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" @@ -165,7 +164,7 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool { user := loginQuery.Result - loginUserQuery := l.LoginUserQuery{Username: username, Password: password, User: user} + loginUserQuery := m.LoginUserQuery{Username: username, Password: password, User: user} if err := bus.Dispatch(&loginUserQuery); err != nil { ctx.JsonApiErr(401, "Invalid username or password", err) return true diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index f80a30de02f..072cb793d3c 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -9,7 +9,6 @@ import ( ms "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" - l "github.com/grafana/grafana/pkg/login" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" @@ -72,7 +71,7 @@ func TestMiddlewareContext(t *testing.T) { return nil }) - bus.AddHandler("test", func(loginUserQuery *l.LoginUserQuery) error { + bus.AddHandler("test", func(loginUserQuery *m.LoginUserQuery) error { return nil }) @@ -177,12 +176,18 @@ func TestMiddlewareContext(t *testing.T) { setting.AuthProxyEnabled = true setting.AuthProxyHeaderName = "X-WEBAUTH-USER" setting.AuthProxyHeaderProperty = "username" + setting.LdapEnabled = false bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} return nil }) + bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { + cmd.Result = &m.User{Id: 12} + return nil + }) + sc.fakeReq("GET", "/") sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") sc.exec() @@ -199,6 +204,7 @@ func TestMiddlewareContext(t *testing.T) { setting.AuthProxyHeaderName = "X-WEBAUTH-USER" setting.AuthProxyHeaderProperty = "username" setting.AuthProxyAutoSignUp = true + setting.LdapEnabled = false bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { if query.UserId > 0 { @@ -209,8 +215,8 @@ func TestMiddlewareContext(t *testing.T) { } }) - bus.AddHandler("test", func(cmd *m.CreateUserCommand) error { - cmd.Result = m.User{Id: 33} + bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { + cmd.Result = &m.User{Id: 33} return nil }) @@ -271,6 +277,11 @@ func TestMiddlewareContext(t *testing.T) { return nil }) + bus.AddHandler("test", func(cmd *m.UpsertUserCommand) error { + cmd.Result = &m.User{Id: 33} + return nil + }) + sc.fakeReq("GET", "/") sc.req.Header.Add("X-WEBAUTH-USER", "torkelo") sc.req.RemoteAddr = "[2001::23]:12345" @@ -289,6 +300,11 @@ func TestMiddlewareContext(t *testing.T) { setting.AuthProxyHeaderProperty = "username" setting.AuthProxyWhitelist = "" + bus.AddHandler("test", func(query *m.UpsertUserCommand) error { + query.Result = &m.User{Id: 32} + return nil + }) + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { query.Result = &m.SignedInUser{OrgId: 4, UserId: 32} return nil @@ -319,11 +335,17 @@ func TestMiddlewareContext(t *testing.T) { setting.LdapEnabled = true called := false - syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUserQuery) error { + syncGrafanaUserWithLdapUser = func(query *m.LoginUserQuery) error { called = true + query.User = &m.User{Id: 32} return nil } + bus.AddHandler("test", func(query *m.UpsertUserCommand) error { + query.Result = &m.User{Id: 32} + return nil + }) + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { query.Result = &m.SignedInUser{OrgId: 4, UserId: 32} return nil diff --git a/pkg/models/user_auth.go b/pkg/models/user_auth.go new file mode 100644 index 00000000000..0ecd144d52c --- /dev/null +++ b/pkg/models/user_auth.go @@ -0,0 +1,72 @@ +package models + +import ( + "time" +) + +type UserAuth struct { + Id int64 + UserId int64 + AuthModule string + AuthId string + Created time.Time +} + +type ExternalUserInfo struct { + AuthModule string + AuthId string + UserId int64 + Email string + Login string + Name string + OrgRoles map[int64]RoleType +} + +// --------------------- +// COMMANDS + +type UpsertUserCommand struct { + ReqContext *ReqContext + ExternalUser *ExternalUserInfo + SignupAllowed bool + + Result *User +} + +type SetAuthInfoCommand struct { + AuthModule string + AuthId string + UserId int64 +} + +type DeleteAuthInfoCommand struct { + UserAuth *UserAuth +} + +// ---------------------- +// QUERIES + +type LoginUserQuery struct { + ReqContext *ReqContext + Username string + Password string + User *User + IpAddress string +} + +type GetUserByAuthInfoQuery struct { + AuthModule string + AuthId string + UserId int64 + Email string + Login string + + Result *User +} + +type GetAuthInfoQuery struct { + AuthModule string + AuthId string + + Result *UserAuth +} diff --git a/pkg/services/sqlstore/migrations/migrations.go b/pkg/services/sqlstore/migrations/migrations.go index 282f98e7318..58ac6256f41 100644 --- a/pkg/services/sqlstore/migrations/migrations.go +++ b/pkg/services/sqlstore/migrations/migrations.go @@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) { addDashboardAclMigrations(mg) addTagMigration(mg) addLoginAttemptMigrations(mg) + addUserAuthMigrations(mg) } func addMigrationLogMigrations(mg *Migrator) { diff --git a/pkg/services/sqlstore/migrations/user_auth_mig.go b/pkg/services/sqlstore/migrations/user_auth_mig.go new file mode 100644 index 00000000000..4d8a18ce33e --- /dev/null +++ b/pkg/services/sqlstore/migrations/user_auth_mig.go @@ -0,0 +1,24 @@ +package migrations + +import . "github.com/grafana/grafana/pkg/services/sqlstore/migrator" + +func addUserAuthMigrations(mg *Migrator) { + userAuthV1 := Table{ + Name: "user_auth", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "user_id", Type: DB_BigInt, Nullable: false}, + {Name: "auth_module", Type: DB_NVarchar, Length: 190, Nullable: false}, + {Name: "auth_id", Type: DB_NVarchar, Length: 100, Nullable: false}, + {Name: "created", Type: DB_DateTime, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"auth_module", "auth_id"}}, + }, + } + + // create table + mg.AddMigration("create user auth table", NewAddTableMigration(userAuthV1)) + // add indices + addTableIndicesMigrations(mg, "v1", userAuthV1) +} diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 6d8bd0c5279..1546fb83b21 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -289,11 +289,12 @@ func SetUsingOrg(cmd *m.SetUsingOrgCommand) error { } return inTransaction(func(sess *DBSession) error { - user := m.User{} - sess.Id(cmd.UserId).Get(&user) + user := m.User{ + Id: cmd.UserId, + OrgId: cmd.OrgId, + } - user.OrgId = cmd.OrgId - _, err := sess.Id(user.Id).Update(&user) + _, err := sess.Id(cmd.UserId).Update(&user) return err }) } @@ -439,6 +440,7 @@ func DeleteUser(cmd *m.DeleteUserCommand) error { "DELETE FROM dashboard_acl WHERE user_id = ?", "DELETE FROM preferences WHERE user_id = ?", "DELETE FROM team_member WHERE user_id = ?", + "DELETE FROM user_auth WHERE user_id = ?", } for _, sql := range deletes { diff --git a/pkg/services/sqlstore/user_auth.go b/pkg/services/sqlstore/user_auth.go new file mode 100644 index 00000000000..aec828451a4 --- /dev/null +++ b/pkg/services/sqlstore/user_auth.go @@ -0,0 +1,148 @@ +package sqlstore + +import ( + "time" + + "github.com/grafana/grafana/pkg/bus" + m "github.com/grafana/grafana/pkg/models" +) + +func init() { + bus.AddHandler("sql", GetUserByAuthInfo) + bus.AddHandler("sql", GetAuthInfo) + bus.AddHandler("sql", SetAuthInfo) + bus.AddHandler("sql", DeleteAuthInfo) +} + +func GetUserByAuthInfo(query *m.GetUserByAuthInfoQuery) error { + user := &m.User{} + has := false + var err error + authQuery := &m.GetAuthInfoQuery{} + + // Try to find the user by auth module and id first + if query.AuthModule != "" && query.AuthId != "" { + authQuery.AuthModule = query.AuthModule + authQuery.AuthId = query.AuthId + + err = GetAuthInfo(authQuery) + if err != m.ErrUserNotFound { + if err != nil { + return err + } + + // if user id was specified and doesn't match the user_auth entry, remove it + if query.UserId != 0 && query.UserId != authQuery.Result.UserId { + err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{ + UserAuth: authQuery.Result, + }) + if err != nil { + sqlog.Error("Error removing user_auth entry", "error", err) + } + + authQuery.Result = nil + } else { + has, err = x.Id(authQuery.Result.UserId).Get(user) + if err != nil { + return err + } + + if !has { + // if the user has been deleted then remove the entry + err = DeleteAuthInfo(&m.DeleteAuthInfoCommand{ + UserAuth: authQuery.Result, + }) + if err != nil { + sqlog.Error("Error removing user_auth entry", "error", err) + } + + authQuery.Result = nil + } + } + } + } + + // If not found, try to find the user by id + if !has && query.UserId != 0 { + has, err = x.Id(query.UserId).Get(user) + if err != nil { + return err + } + } + + // If not found, try to find the user by email address + if !has && query.Email != "" { + user = &m.User{Email: query.Email} + has, err = x.Get(user) + if err != nil { + return err + } + } + + // If not found, try to find the user by login + if !has && query.Login != "" { + user = &m.User{Login: query.Login} + has, err = x.Get(user) + if err != nil { + return err + } + } + + // No user found + if !has { + return m.ErrUserNotFound + } + + // create authInfo record to link accounts + if authQuery.Result == nil && query.AuthModule != "" && query.AuthId != "" { + cmd2 := &m.SetAuthInfoCommand{ + UserId: user.Id, + AuthModule: query.AuthModule, + AuthId: query.AuthId, + } + if err := SetAuthInfo(cmd2); err != nil { + return err + } + } + + query.Result = user + return nil +} + +func GetAuthInfo(query *m.GetAuthInfoQuery) error { + userAuth := &m.UserAuth{ + AuthModule: query.AuthModule, + AuthId: query.AuthId, + } + has, err := x.Get(userAuth) + if err != nil { + return err + } + if !has { + return m.ErrUserNotFound + } + + query.Result = userAuth + return nil +} + +func SetAuthInfo(cmd *m.SetAuthInfoCommand) error { + return inTransaction(func(sess *DBSession) error { + authUser := &m.UserAuth{ + UserId: cmd.UserId, + AuthModule: cmd.AuthModule, + AuthId: cmd.AuthId, + Created: time.Now(), + } + + _, err := sess.Insert(authUser) + return err + }) +} + +func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error { + return inTransaction(func(sess *DBSession) error { + _, err := sess.Delete(cmd.UserAuth) + return err + }) +} diff --git a/pkg/services/sqlstore/user_auth_test.go b/pkg/services/sqlstore/user_auth_test.go new file mode 100644 index 00000000000..279fd7aa0f5 --- /dev/null +++ b/pkg/services/sqlstore/user_auth_test.go @@ -0,0 +1,131 @@ +package sqlstore + +import ( + "fmt" + "testing" + + . "github.com/smartystreets/goconvey/convey" + + m "github.com/grafana/grafana/pkg/models" +) + +func TestUserAuth(t *testing.T) { + InitTestDB(t) + + Convey("Given 5 users", t, func() { + var err error + var cmd *m.CreateUserCommand + users := []m.User{} + for i := 0; i < 5; i++ { + cmd = &m.CreateUserCommand{ + Email: fmt.Sprint("user", i, "@test.com"), + Name: fmt.Sprint("user", i), + Login: fmt.Sprint("loginuser", i), + } + err = CreateUser(cmd) + So(err, ShouldBeNil) + users = append(users, cmd.Result) + } + + Reset(func() { + _, err := x.Exec("DELETE FROM org_user WHERE 1=1") + So(err, ShouldBeNil) + _, err = x.Exec("DELETE FROM org WHERE 1=1") + So(err, ShouldBeNil) + _, err = x.Exec("DELETE FROM user WHERE 1=1") + So(err, ShouldBeNil) + _, err = x.Exec("DELETE FROM user_auth WHERE 1=1") + So(err, ShouldBeNil) + }) + + Convey("Can find existing user", func() { + // By Login + login := "loginuser0" + + query := &m.GetUserByAuthInfoQuery{Login: login} + err = GetUserByAuthInfo(query) + + So(err, ShouldBeNil) + So(query.Result.Login, ShouldEqual, login) + + // By ID + id := query.Result.Id + + query = &m.GetUserByAuthInfoQuery{UserId: id} + err = GetUserByAuthInfo(query) + + So(err, ShouldBeNil) + So(query.Result.Id, ShouldEqual, id) + + // By Email + email := "user1@test.com" + + query = &m.GetUserByAuthInfoQuery{Email: email} + err = GetUserByAuthInfo(query) + + So(err, ShouldBeNil) + So(query.Result.Email, ShouldEqual, email) + + // Don't find nonexistent user + email = "nonexistent@test.com" + + query = &m.GetUserByAuthInfoQuery{Email: email} + err = GetUserByAuthInfo(query) + + So(err, ShouldEqual, m.ErrUserNotFound) + So(query.Result, ShouldBeNil) + }) + + Convey("Can set & locate by AuthModule and AuthId", func() { + // get nonexistent user_auth entry + query := &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + err = GetUserByAuthInfo(query) + + So(err, ShouldEqual, m.ErrUserNotFound) + So(query.Result, ShouldBeNil) + + // create user_auth entry + login := "loginuser0" + + query.Login = login + err = GetUserByAuthInfo(query) + + So(err, ShouldBeNil) + So(query.Result.Login, ShouldEqual, login) + + // get via user_auth + query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + err = GetUserByAuthInfo(query) + + So(err, ShouldBeNil) + So(query.Result.Login, ShouldEqual, login) + + // get with non-matching id + id := query.Result.Id + + query.UserId = id + 1 + err = GetUserByAuthInfo(query) + + So(err, ShouldBeNil) + So(query.Result.Login, ShouldEqual, "loginuser1") + + // get via user_auth + query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + err = GetUserByAuthInfo(query) + + So(err, ShouldBeNil) + So(query.Result.Login, ShouldEqual, "loginuser1") + + // remove user + _, err = x.Exec("DELETE FROM user WHERE id=?", query.Result.Id) + So(err, ShouldBeNil) + + // get via user_auth for deleted user + query = &m.GetUserByAuthInfoQuery{AuthModule: "test", AuthId: "test"} + err = GetUserByAuthInfo(query) + + So(err, ShouldEqual, m.ErrUserNotFound) + So(query.Result, ShouldBeNil) + }) + }) +} diff --git a/pkg/social/grafana_com_oauth.go b/pkg/social/grafana_com_oauth.go index d3614520d61..87601788c3f 100644 --- a/pkg/social/grafana_com_oauth.go +++ b/pkg/social/grafana_com_oauth.go @@ -51,6 +51,7 @@ func (s *SocialGrafanaCom) IsOrganizationMember(organizations []OrgRecord) bool func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { var data struct { + Id int `json:"id"` Name string `json:"name"` Login string `json:"username"` Email string `json:"email"` @@ -69,6 +70,7 @@ func (s *SocialGrafanaCom) UserInfo(client *http.Client, token *oauth2.Token) (* } userInfo := &BasicUserInfo{ + Id: fmt.Sprintf("%d", data.Id), Name: data.Name, Login: data.Login, Email: data.Email, diff --git a/pkg/social/social.go b/pkg/social/social.go index b763e2d71b2..8f0618b7f74 100644 --- a/pkg/social/social.go +++ b/pkg/social/social.go @@ -14,6 +14,7 @@ import ( ) type BasicUserInfo struct { + Id string Name string Email string Login string