Merge pull request #11354 from grafana/external-auth

External auth refactor
pull/11646/head
Carl Bergquist 7 years ago committed by GitHub
commit 0ad6336634
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 11
      pkg/api/login.go
  3. 73
      pkg/api/login_oauth.go
  4. 19
      pkg/login/auth.go
  5. 10
      pkg/login/auth_test.go
  6. 2
      pkg/login/brute_force_login_protection.go
  7. 4
      pkg/login/brute_force_login_protection_test.go
  8. 184
      pkg/login/ext_user.go
  9. 2
      pkg/login/grafana_login.go
  10. 4
      pkg/login/grafana_login_test.go
  11. 239
      pkg/login/ldap.go
  12. 3
      pkg/login/ldap_login.go
  13. 16
      pkg/login/ldap_login_test.go
  14. 93
      pkg/login/ldap_test.go
  15. 158
      pkg/middleware/auth_proxy.go
  16. 60
      pkg/middleware/auth_proxy_test.go
  17. 3
      pkg/middleware/middleware.go
  18. 32
      pkg/middleware/middleware_test.go
  19. 72
      pkg/models/user_auth.go
  20. 1
      pkg/services/sqlstore/migrations/migrations.go
  21. 24
      pkg/services/sqlstore/migrations/user_auth_mig.go
  22. 10
      pkg/services/sqlstore/user.go
  23. 148
      pkg/services/sqlstore/user_auth.go
  24. 131
      pkg/services/sqlstore/user_auth_test.go
  25. 2
      pkg/social/grafana_com_oauth.go
  26. 1
      pkg/social/social.go

@ -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

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

@ -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()

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

@ -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",

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

@ -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",

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

@ -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 {

@ -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",

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

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

@ -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",

@ -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) {

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

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

@ -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

@ -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

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

@ -30,6 +30,7 @@ func AddMigrations(mg *Migrator) {
addDashboardAclMigrations(mg)
addTagMigration(mg)
addLoginAttemptMigrations(mg)
addUserAuthMigrations(mg)
}
func addMigrationLogMigrations(mg *Migrator) {

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

@ -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 {

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

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

@ -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,

@ -14,6 +14,7 @@ import (
)
type BasicUserInfo struct {
Id string
Name string
Email string
Login string

Loading…
Cancel
Save