Browse Source

shared library for managing external user accounts

Dan Cech 7 years ago
parent
commit
1c5afa731f

+ 2 - 2
pkg/api/login.go

@@ -101,13 +101,13 @@ func LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response {
 		return Error(401, "Login is disabled", nil)
 	}
 
-	authQuery := login.LoginUserQuery{
+	authQuery := m.LoginUserQuery{
 		Username:  cmd.User,
 		Password:  cmd.Password,
 		IpAddress: c.Req.RemoteAddr,
 	}
 
-	if err := bus.Dispatch(&authQuery); err != nil {
+	if err := login.AuthenticateUser(c, &authQuery); err != nil {
 		if err == login.ErrInvalidCredentials || err == login.ErrTooManyLoginAttempts {
 			return Error(401, "Invalid username or password", err)
 		}

+ 26 - 47
pkg/api/login_oauth.go

@@ -6,7 +6,6 @@ import (
 	"crypto/tls"
 	"crypto/x509"
 	"encoding/base64"
-	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
@@ -14,24 +13,16 @@ import (
 
 	"golang.org/x/oauth2"
 
-	"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 +47,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 +140,42 @@ 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
+	userQuery := &m.UpsertUserCommand{
+		ExternalUser:  &extUser,
+		SignupAllowed: connect.IsSignupAllowed(),
+	}
+	err = login.UpsertUser(ctx, userQuery)
+	if err != nil {
+		redirectWithError(ctx, err)
+		return
 	}
 
 	// login
-	loginUserWithUser(userQuery.Result, ctx)
+	loginUserWithUser(userQuery.User, ctx)
 
 	metrics.M_Api_Login_OAuth.Inc()
 

+ 10 - 11
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(ctx *m.ReqContext, query *m.LoginUserQuery) error {
 	if err := validateLoginAttempts(query.Username); err != nil {
 		return err
 	}
@@ -34,7 +33,7 @@ func AuthenticateUser(query *LoginUserQuery) error {
 		return err
 	}
 
-	ldapEnabled, ldapErr := loginUsingLdap(query)
+	ldapEnabled, ldapErr := loginUsingLdap(ctx, query)
 	if ldapEnabled {
 		if ldapErr == nil || ldapErr != ErrInvalidCredentials {
 			return ldapErr

+ 5 - 5
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",

+ 1 - 1
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
 	}

+ 2 - 2
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",

+ 157 - 0
pkg/login/ext_user.go

@@ -0,0 +1,157 @@
+package login
+
+import (
+	"fmt"
+
+	"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 UpsertUser(ctx *m.ReqContext, 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 != nil {
+		if err != m.ErrUserNotFound {
+			return err
+		}
+
+		if !cmd.SignupAllowed {
+			log.Warn(fmt.Sprintf("Not allowing %s login, user not found in internal user database and allow signup = false", extUser.AuthModule))
+			return ErrInvalidCredentials
+		}
+
+		limitReached, err := quota.QuotaReached(ctx, "user")
+		if err != nil {
+			log.Warn("Error getting user quota", "err", err)
+			return ErrGettingUserQuota
+		}
+		if limitReached {
+			return ErrUsersQuotaReached
+		}
+
+		cmd.User, err = createUser(extUser)
+		if err != nil {
+			return err
+		}
+	} else {
+		cmd.User = userQuery.User
+
+		// sync user info
+		err = updateUser(cmd.User, extUser)
+		if err != nil {
+			return err
+		}
+	}
+
+	err = syncOrgRoles(cmd.User, extUser)
+	if err != nil {
+		return err
+	}
+
+	return nil
+}
+
+func createUser(extUser *m.ExternalUserInfo) (*m.User, error) {
+	cmd := m.CreateUserCommand{
+		Login: extUser.Login,
+		Email: extUser.Email,
+		Name:  extUser.Name,
+	}
+	if err := bus.Dispatch(&cmd); err != nil {
+		return nil, err
+	}
+
+	cmd2 := m.SetAuthInfoCommand{
+		UserId:     cmd.Result.Id,
+		AuthModule: extUser.AuthModule,
+		AuthId:     extUser.AuthId,
+	}
+	if err := bus.Dispatch(&cmd2); err != nil {
+		return nil, err
+	}
+
+	return &cmd.Result, nil
+}
+
+func updateUser(user *m.User, extUser *m.ExternalUserInfo) error {
+	// sync user info
+	if user.Login != extUser.Login || user.Email != extUser.Email || user.Name != extUser.Name {
+		log.Debug("Syncing user info", "id", user.Id, "login", extUser.Login, "email", extUser.Email)
+		updateCmd := m.UpdateUserCommand{
+			UserId: user.Id,
+			Login:  extUser.Login,
+			Email:  extUser.Email,
+			Name:   extUser.Name,
+		}
+		err := bus.Dispatch(&updateCmd)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+func syncOrgRoles(user *m.User, extUser *m.ExternalUserInfo) error {
+	if len(extUser.OrgRoles) == 0 {
+		// log.Warn("No group mappings defined")
+		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
+		}
+	}
+
+	return nil
+}

+ 1 - 1
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 {

+ 2 - 2
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",

+ 67 - 164
pkg/login/ldap.go

@@ -10,7 +10,6 @@ import (
 
 	"github.com/davecgh/go-spew/spew"
 	"github.com/go-ldap/ldap"
-	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/log"
 	m "github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/setting"
@@ -24,10 +23,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(ctx *m.ReqContext, query *m.LoginUserQuery) error
+	SyncSignedInUser(ctx *m.ReqContext, signedInUser *m.SignedInUser) error
+	GetGrafanaUserFor(ctx *m.ReqContext, ldapUser *LdapUserInfo) (*m.User, error)
 }
 
 type ldapAuther struct {
@@ -89,89 +87,36 @@ func (a *ldapAuther) Dial() error {
 	return err
 }
 
-func (a *ldapAuther) Login(query *LoginUserQuery) error {
-	if err := a.Dial(); err != nil {
+func (a *ldapAuther) Login(ctx *m.ReqContext, query *m.LoginUserQuery) error {
+	// connect to ldap server
+	err := a.Dial()
+	if err != nil {
 		return err
 	}
 	defer a.conn.Close()
 
 	// perform initial authentication
-	if err := a.initialBind(query.Username, query.Password); err != nil {
+	err = a.initialBind(query.Username, query.Password)
+	if err != nil {
 		return err
 	}
 
 	// find user entry & attributes
-	if ldapUser, err := a.searchForUser(query.Username); 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
-			}
-		}
-
-		if grafanaUser, err := a.GetGrafanaUserFor(ldapUser); 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,
-	}
-
-	if err := a.Dial(); err != nil {
+	ldapUser, err := a.searchForUser(query.Username)
+	if err != nil {
 		return err
 	}
 
-	defer a.conn.Close()
-	if err := a.serverBind(); err != nil {
-		return err
-	}
+	a.log.Debug("Ldap User found", "info", spew.Sdump(ldapUser))
 
-	if ldapUser, err := a.searchForUser(signedInUser.Login); err != nil {
-		a.log.Error("Failed searching for user in ldap", "error", err)
-
-		return err
-	} else {
-		if err := a.syncInfoAndOrgRoles(&grafanaUser, 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
 		}
-
-		a.log.Debug("Got Ldap User Info", "user", spew.Sdump(ldapUser))
 	}
 
-	return nil
-}
-
-// 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 {
-		return err
-	}
-
-	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
@@ -184,123 +129,85 @@ func (a *ldapAuther) GetGrafanaUserFor(ldapUser *LdapUserInfo) (*m.User, error)
 	}
 
 	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
+		a.log.Info(
+			"Ldap Auth: user does not belong in any of the specified ldap groups",
+			"username", ldapUser.Username,
+			"groups", ldapUser.MemberOf)
+		return 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
-		}
+	grafanaUser, err := a.GetGrafanaUserFor(ctx, ldapUser)
+	if err != nil {
+		return err
 	}
 
-	return userQuery.Result, nil
-
+	query.User = grafanaUser
+	return 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
+func (a *ldapAuther) SyncSignedInUser(ctx *m.ReqContext, signedInUser *m.SignedInUser) error {
+	err := a.Dial()
+	if err != nil {
+		return err
 	}
 
-	return &cmd.Result, nil
-}
+	defer a.conn.Close()
 
-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
+	err = a.serverBind()
+	if err != nil {
+		return err
 	}
 
-	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
+	ldapUser, err := a.searchForUser(signedInUser.Login)
+	if err != nil {
+		a.log.Error("Failed searching for user in ldap", "error", err)
+		return err
 	}
 
-	orgsQuery := m.GetUserOrgListQuery{UserId: user.Id}
-	if err := bus.Dispatch(&orgsQuery); err != nil {
+	grafanaUser, err := a.GetGrafanaUserFor(ctx, ldapUser)
+	if err != nil {
 		return err
 	}
 
-	handledOrgIds := map[int64]bool{}
+	signedInUser.Login = grafanaUser.Login
+	signedInUser.Email = grafanaUser.Email
+	signedInUser.Name = grafanaUser.Name
 
-	// 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
-			}
-		}
+	return nil
+}
 
-		// 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
-			}
-		}
+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{},
 	}
 
-	// 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
-		}
-
-		// 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
+		if ldapUser.isMemberOf(group.GroupDN) {
+			extUser.OrgRoles[group.OrgId] = group.OrgRole
 		}
+	}
 
-		// mark this group has handled so we do not process it again
-		handledOrgIds[group.OrgId] = true
+	// add/update user in grafana
+	userQuery := m.UpsertUserCommand{
+		ExternalUser:  &extUser,
+		SignupAllowed: setting.LdapAllowSignup,
+	}
+	err := UpsertUser(ctx, &userQuery)
+	if err != nil {
+		return nil, err
 	}
 
-	return nil
+	return userQuery.User, nil
 }
 
 func (a *ldapAuther) serverBind() error {
@@ -470,7 +377,3 @@ func getLdapAttrArray(name string, result *ldap.SearchResult) []string {
 	}
 	return []string{}
 }
-
-func createUserFromLdapInfo() error {
-	return nil
-}

+ 3 - 2
pkg/login/ldap_login.go

@@ -1,17 +1,18 @@
 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(ctx *m.ReqContext, query *m.LoginUserQuery) (bool, error) {
 	if !setting.LdapEnabled {
 		return false, nil
 	}
 
 	for _, server := range LdapCfg.Servers {
 		author := NewLdapAuthenticator(server)
-		err := author.Login(query)
+		err := author.Login(ctx, query)
 		if err == nil || err != ErrInvalidCredentials {
 			return true, err
 		}

+ 4 - 4
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 {
@@ -140,7 +140,7 @@ func (a *mockLdapAuther) SyncOrgRoles(user *m.User, ldapUser *LdapUserInfo) erro
 }
 
 type ldapLoginScenarioContext struct {
-	loginUserQuery        *LoginUserQuery
+	loginUserQuery        *m.LoginUserQuery
 	ldapAuthenticatorMock *mockLdapAuther
 }
 
@@ -151,7 +151,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",

+ 1 - 1
pkg/middleware/auth_proxy.go

@@ -112,7 +112,7 @@ var syncGrafanaUserWithLdapUser = func(ctx *m.ReqContext, query *m.GetSignedInUs
 
 		for _, server := range ldapCfg.Servers {
 			author := login.NewLdapAuthenticator(server)
-			if err := author.SyncSignedInUser(query.Result); err != nil {
+			if err := author.SyncSignedInUser(ctx, query.Result); err != nil {
 				return err
 			}
 		}

+ 1 - 1
pkg/middleware/auth_proxy_test.go

@@ -116,7 +116,7 @@ type mockLdapAuthenticator struct {
 	syncSignedInUserCalled bool
 }
 
-func (a *mockLdapAuthenticator) Login(query *login.LoginUserQuery) error {
+func (a *mockLdapAuthenticator) Login(query *m.LoginUserQuery) error {
 	return nil
 }
 

+ 1 - 2
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

+ 1 - 2
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
 			})
 

+ 66 - 0
pkg/models/user_auth.go

@@ -0,0 +1,66 @@
+package models
+
+type UserAuth struct {
+	Id         int64
+	UserId     int64
+	AuthModule string
+	AuthId     string
+}
+
+type ExternalUserInfo struct {
+	AuthModule string
+	AuthId     string
+	UserId     int64
+	Email      string
+	Login      string
+	Name       string
+	OrgRoles   map[int64]RoleType
+}
+
+// ---------------------
+// COMMANDS
+
+type UpsertUserCommand struct {
+	ExternalUser  *ExternalUserInfo
+	SignupAllowed bool
+
+	User *User
+}
+
+type SetAuthInfoCommand struct {
+	AuthModule string
+	AuthId     string
+	UserId     int64
+}
+
+type DeleteAuthInfoCommand struct {
+	UserAuth *UserAuth
+}
+
+// ----------------------
+// QUERIES
+
+type LoginUserQuery struct {
+	Username  string
+	Password  string
+	User      *User
+	IpAddress string
+}
+
+type GetUserByAuthInfoQuery struct {
+	AuthModule string
+	AuthId     string
+	UserId     int64
+	Email      string
+	Login      string
+
+	User     *User
+	UserAuth *UserAuth
+}
+
+type GetAuthInfoQuery struct {
+	AuthModule string
+	AuthId     string
+
+	UserAuth *UserAuth
+}

+ 1 - 0
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) {

+ 24 - 0
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: 30, 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)
+}

+ 1 - 0
pkg/services/sqlstore/user.go

@@ -445,6 +445,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 {

+ 130 - 0
pkg/services/sqlstore/user_auth.go

@@ -0,0 +1,130 @@
+package sqlstore
+
+import (
+	"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 := new(m.User)
+	has := false
+	var err error
+
+	// Try to find the user by auth module and id first
+	if query.AuthModule != "" && query.AuthId != "" {
+		authQuery := &m.GetAuthInfoQuery{
+			AuthModule: query.AuthModule,
+			AuthId:     query.AuthId,
+		}
+
+		err = GetAuthInfo(authQuery)
+		// if user id was specified and doesn't match the user_auth entry, remove it
+		if err == nil && query.UserId != 0 && query.UserId != authQuery.UserAuth.UserId {
+			DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+				UserAuth: authQuery.UserAuth,
+			})
+		} else if err == nil {
+			has, err = x.Id(authQuery.UserAuth.UserId).Get(user)
+			if err != nil {
+				return err
+			}
+
+			if has {
+				query.UserAuth = authQuery.UserAuth
+			} else {
+				// if the user has been deleted then remove the entry
+				DeleteAuthInfo(&m.DeleteAuthInfoCommand{
+					UserAuth: authQuery.UserAuth,
+				})
+			}
+		} else if err != m.ErrUserNotFound {
+			return err
+		}
+	}
+
+	// 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
+	}
+
+	query.User = 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.UserAuth = 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,
+		}
+
+		_, err := sess.Insert(&authUser)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	})
+}
+
+func DeleteAuthInfo(cmd *m.DeleteAuthInfoCommand) error {
+	return inTransaction(func(sess *DBSession) error {
+		_, err := sess.Delete(cmd.UserAuth)
+		if err != nil {
+			return err
+		}
+
+		return nil
+	})
+}

+ 2 - 0
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,

+ 1 - 0
pkg/social/social.go

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