Pārlūkot izejas kodu

LDAP: refactoring (#17479)

* LDAP: use only one struct

* Use only models.ExternalUserInfo

* Add additional helper method :/

* Move all the helpers to one module

* LDAP: refactoring

* Rename some of the public methods and change their behaviour

* Remove outdated methods

* Simplify logic

* More tests
  There is no and never were tests for settings.go, added tests for helper
  methods (cover is now about 100% for them). Added tests for the main
  LDAP logic, but there is some stuff to add. Dial() is not tested and not
  decoupled. It might be a challenge to do it properly

* Restructure tests:
   * they wouldn't depend on external modules
   * more consistent naming
   * logical division

* More guards for erroneous paths

* Login: make login service an explicit dependency

* LDAP: remove no longer needed test helper fns

* LDAP: remove useless import

* LDAP: Use new interface in multildap module

* LDAP: corrections for the groups of multiple users

* In case there is several users their groups weren't detected correctly

* Simplify helpers module
Oleg Gaidarenko 6 gadi atpakaļ
vecāks
revīzija
1b1d951495

+ 2 - 0
pkg/api/http_server.go

@@ -25,6 +25,7 @@ import (
 	"github.com/grafana/grafana/pkg/registry"
 	"github.com/grafana/grafana/pkg/services/datasources"
 	"github.com/grafana/grafana/pkg/services/hooks"
+	"github.com/grafana/grafana/pkg/services/login"
 	"github.com/grafana/grafana/pkg/services/quota"
 	"github.com/grafana/grafana/pkg/services/rendering"
 	"github.com/grafana/grafana/pkg/setting"
@@ -66,6 +67,7 @@ type HTTPServer struct {
 	QuotaService        *quota.QuotaService      `inject:""`
 	RemoteCacheService  *remotecache.RemoteCache `inject:""`
 	ProvisioningService ProvisioningService      `inject:""`
+	Login               *login.LoginService      `inject:""`
 }
 
 func (hs *HTTPServer) Init() error {

+ 5 - 5
pkg/login/ldap_login.go

@@ -1,9 +1,9 @@
 package login
 
 import (
+	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/multildap"
-	"github.com/grafana/grafana/pkg/services/user"
 	"github.com/grafana/grafana/pkg/setting"
 	"github.com/grafana/grafana/pkg/util/errutil"
 )
@@ -36,15 +36,15 @@ var loginUsingLDAP = func(query *models.LoginUserQuery) (bool, error) {
 		return true, err
 	}
 
-	login, err := user.Upsert(&user.UpsertArgs{
+	upsert := &models.UpsertUserCommand{
 		ExternalUser:  externalUser,
 		SignupAllowed: setting.LDAPAllowSignup,
-	})
+	}
+	err = bus.Dispatch(upsert)
 	if err != nil {
 		return true, err
 	}
-
-	query.User = login
+	query.User = upsert.Result
 
 	return true, nil
 }

+ 8 - 7
pkg/middleware/auth_proxy/auth_proxy.go

@@ -13,7 +13,6 @@ import (
 	"github.com/grafana/grafana/pkg/models"
 	"github.com/grafana/grafana/pkg/services/ldap"
 	"github.com/grafana/grafana/pkg/services/multildap"
-	"github.com/grafana/grafana/pkg/services/user"
 	"github.com/grafana/grafana/pkg/setting"
 )
 
@@ -211,16 +210,17 @@ func (auth *AuthProxy) LoginViaLDAP() (int64, *Error) {
 	}
 
 	// Have to sync grafana and LDAP user during log in
-	user, err := user.Upsert(&user.UpsertArgs{
+	upsert := &models.UpsertUserCommand{
 		ReqContext:    auth.ctx,
 		SignupAllowed: auth.LDAPAllowSignup,
 		ExternalUser:  extUser,
-	})
+	}
+	err = bus.Dispatch(upsert)
 	if err != nil {
 		return 0, newError(err.Error(), nil)
 	}
 
-	return user.Id, nil
+	return upsert.Result.Id, nil
 }
 
 // LoginViaHeader logs in user from the header only
@@ -256,16 +256,17 @@ func (auth *AuthProxy) LoginViaHeader() (int64, error) {
 		}
 	}
 
-	result, err := user.Upsert(&user.UpsertArgs{
+	upsert := &models.UpsertUserCommand{
 		ReqContext:    auth.ctx,
 		SignupAllowed: true,
 		ExternalUser:  extUser,
-	})
+	}
+	err := bus.Dispatch(upsert)
 	if err != nil {
 		return 0, err
 	}
 
-	return result.Id, nil
+	return upsert.Result.Id, nil
 }
 
 // GetSignedUser get full signed user info

+ 4 - 1
pkg/middleware/middleware.go

@@ -26,7 +26,10 @@ var (
 	ReqOrgAdmin     = RoleAuth(m.ROLE_ADMIN)
 )
 
-func GetContextHandler(ats m.UserTokenService, remoteCache *remotecache.RemoteCache) macaron.Handler {
+func GetContextHandler(
+	ats m.UserTokenService,
+	remoteCache *remotecache.RemoteCache,
+) macaron.Handler {
 	return func(c *macaron.Context) {
 		ctx := &m.ReqContext{
 			Context:        c,

+ 49 - 0
pkg/services/ldap/helpers.go

@@ -0,0 +1,49 @@
+package ldap
+
+import (
+	"strings"
+
+	"gopkg.in/ldap.v3"
+)
+
+func isMemberOf(memberOf []string, group string) bool {
+	if group == "*" {
+		return true
+	}
+
+	for _, member := range memberOf {
+		if strings.EqualFold(member, group) {
+			return true
+		}
+	}
+	return false
+}
+
+func appendIfNotEmpty(slice []string, values ...string) []string {
+	for _, v := range values {
+		if v != "" {
+			slice = append(slice, v)
+		}
+	}
+	return slice
+}
+
+func getAttribute(name string, entry *ldap.Entry) string {
+	for _, attr := range entry.Attributes {
+		if attr.Name == name {
+			if len(attr.Values) > 0 {
+				return attr.Values[0]
+			}
+		}
+	}
+	return ""
+}
+
+func getArrayAttribute(name string, entry *ldap.Entry) []string {
+	for _, attr := range entry.Attributes {
+		if attr.Name == name && len(attr.Values) > 0 {
+			return attr.Values
+		}
+	}
+	return []string{}
+}

+ 73 - 183
pkg/services/ldap/ldap.go

@@ -10,6 +10,7 @@ import (
 
 	"gopkg.in/ldap.v3"
 
+	"github.com/davecgh/go-spew/spew"
 	"github.com/grafana/grafana/pkg/bus"
 	"github.com/grafana/grafana/pkg/infra/log"
 	"github.com/grafana/grafana/pkg/models"
@@ -30,17 +31,16 @@ type IConnection interface {
 type IServer interface {
 	Login(*models.LoginUserQuery) (*models.ExternalUserInfo, error)
 	Users([]string) ([]*models.ExternalUserInfo, error)
-	InitialBind(string, string) error
+	Auth(string, string) error
 	Dial() error
 	Close()
 }
 
 // Server is basic struct of LDAP authorization
 type Server struct {
-	Config            *ServerConfig
-	Connection        IConnection
-	requireSecondBind bool
-	log               log.Logger
+	Config     *ServerConfig
+	Connection IConnection
+	log        log.Logger
 }
 
 var (
@@ -49,10 +49,6 @@ var (
 	ErrInvalidCredentials = errors.New("Invalid Username or Password")
 )
 
-var dial = func(network, addr string) (IConnection, error) {
-	return ldap.Dial(network, addr)
-}
-
 // New creates the new LDAP auth
 func New(config *ServerConfig) IServer {
 	return &Server{
@@ -96,7 +92,7 @@ func (server *Server) Dial() error {
 				tlsCfg.Certificates = append(tlsCfg.Certificates, clientCert)
 			}
 			if server.Config.StartTLS {
-				server.Connection, err = dial("tcp", address)
+				server.Connection, err = ldap.Dial("tcp", address)
 				if err == nil {
 					if err = server.Connection.StartTLS(tlsCfg); err == nil {
 						return nil
@@ -106,7 +102,7 @@ func (server *Server) Dial() error {
 				server.Connection, err = ldap.DialTLS("tcp", address, tlsCfg)
 			}
 		} else {
-			server.Connection, err = dial("tcp", address)
+			server.Connection, err = ldap.Dial("tcp", address)
 		}
 
 		if err == nil {
@@ -125,9 +121,8 @@ func (server *Server) Close() {
 func (server *Server) Login(query *models.LoginUserQuery) (
 	*models.ExternalUserInfo, error,
 ) {
-
-	// Perform initial authentication
-	err := server.InitialBind(query.Username, query.Password)
+	// Authentication
+	err := server.Auth(query.Username, query.Password)
 	if err != nil {
 		return nil, err
 	}
@@ -145,20 +140,11 @@ func (server *Server) Login(query *models.LoginUserQuery) (
 		return nil, ErrInvalidCredentials
 	}
 
-	// Check if a second user bind is needed
 	user := users[0]
-
 	if err := server.validateGrafanaUser(user); err != nil {
 		return nil, err
 	}
 
-	if server.requireSecondBind {
-		err = server.secondBind(user, query.Password)
-		if err != nil {
-			return nil, err
-		}
-	}
-
 	return user, nil
 }
 
@@ -168,8 +154,8 @@ func (server *Server) Users(logins []string) (
 	error,
 ) {
 	var result *ldap.SearchResult
-	var err error
 	var Config = server.Config
+	var err error
 
 	for _, base := range Config.SearchBaseDNs {
 		result, err = server.Connection.Search(
@@ -184,11 +170,17 @@ func (server *Server) Users(logins []string) (
 		}
 	}
 
+	if len(result.Entries) == 0 {
+		return []*models.ExternalUserInfo{}, nil
+	}
+
 	serializedUsers, err := server.serializeUsers(result)
 	if err != nil {
 		return nil, err
 	}
 
+	server.log.Debug("LDAP users found", "users", spew.Sdump(serializedUsers))
+
 	return serializedUsers, nil
 }
 
@@ -276,108 +268,71 @@ func (server *Server) getSearchRequest(
 }
 
 // buildGrafanaUser extracts info from UserInfo model to ExternalUserInfo
-func (server *Server) buildGrafanaUser(user *UserInfo) *models.ExternalUserInfo {
+func (server *Server) buildGrafanaUser(user *ldap.Entry) (*models.ExternalUserInfo, error) {
+	memberOf, err := server.getMemberOf(user)
+	if err != nil {
+		return nil, err
+	}
+
+	attrs := server.Config.Attr
 	extUser := &models.ExternalUserInfo{
 		AuthModule: models.AuthModuleLDAP,
 		AuthId:     user.DN,
 		Name: strings.TrimSpace(
-			fmt.Sprintf("%s %s", user.FirstName, user.LastName),
+			fmt.Sprintf(
+				"%s %s",
+				getAttribute(attrs.Name, user),
+				getAttribute(attrs.Surname, user),
+			),
 		),
-		Login:    user.Username,
-		Email:    user.Email,
-		Groups:   user.MemberOf,
+		Login:    getAttribute(attrs.Username, user),
+		Email:    getAttribute(attrs.Email, user),
+		Groups:   memberOf,
 		OrgRoles: map[int64]models.RoleType{},
 	}
 
 	for _, group := range server.Config.Groups {
 		// only use the first match for each org
-		if extUser.OrgRoles[group.OrgId] != "" {
+		if extUser.OrgRoles[group.OrgID] != "" {
 			continue
 		}
 
-		if user.isMemberOf(group.GroupDN) {
-			extUser.OrgRoles[group.OrgId] = group.OrgRole
+		if isMemberOf(memberOf, group.GroupDN) {
+			extUser.OrgRoles[group.OrgID] = group.OrgRole
 			if extUser.IsGrafanaAdmin == nil || !*extUser.IsGrafanaAdmin {
 				extUser.IsGrafanaAdmin = group.IsGrafanaAdmin
 			}
 		}
 	}
 
-	return extUser
-}
-
-func (server *Server) serverBind() error {
-	bindFn := func() error {
-		return server.Connection.Bind(
-			server.Config.BindDN,
-			server.Config.BindPassword,
-		)
-	}
-
-	if server.Config.BindPassword == "" {
-		bindFn = func() error {
-			return server.Connection.UnauthenticatedBind(server.Config.BindDN)
-		}
-	}
-
-	// bind_dn and bind_password to bind
-	if err := bindFn(); err != nil {
-		server.log.Info("LDAP initial bind failed, %v", err)
-
-		if ldapErr, ok := err.(*ldap.Error); ok {
-			if ldapErr.ResultCode == 49 {
-				return ErrInvalidCredentials
-			}
-		}
-		return err
-	}
-
-	return nil
+	return extUser, nil
 }
 
-func (server *Server) secondBind(
-	user *models.ExternalUserInfo,
-	userPassword string,
-) error {
-	err := server.Connection.Bind(user.AuthId, userPassword)
-	if err != nil {
-		server.log.Info("Second bind failed", "error", err)
-
-		if ldapErr, ok := err.(*ldap.Error); ok {
-			if ldapErr.ResultCode == 49 {
-				return ErrInvalidCredentials
-			}
-		}
-		return err
-	}
-
-	return nil
+// shouldBindAdmin checks if we should use
+// admin username & password for LDAP bind
+func (server *Server) shouldBindAdmin() bool {
+	return server.Config.BindPassword != ""
 }
 
-// InitialBind intiates first bind to LDAP server
-func (server *Server) InitialBind(username, userPassword string) error {
-	if server.Config.BindPassword != "" || server.Config.BindDN == "" {
-		userPassword = server.Config.BindPassword
-		server.requireSecondBind = true
-	}
+// Auth authentificates user in LDAP.
+// It might not use passed password and username,
+// since they can be overwritten with admin config values -
+// see "bind_dn" and "bind_password" options in LDAP config
+func (server *Server) Auth(username, password string) error {
+	path := server.Config.BindDN
 
-	bindPath := server.Config.BindDN
-	if strings.Contains(bindPath, "%s") {
-		bindPath = fmt.Sprintf(server.Config.BindDN, username)
+	if server.shouldBindAdmin() {
+		password = server.Config.BindPassword
+	} else {
+		path = fmt.Sprintf(path, username)
 	}
 
 	bindFn := func() error {
-		return server.Connection.Bind(bindPath, userPassword)
-	}
-
-	if userPassword == "" {
-		bindFn = func() error {
-			return server.Connection.UnauthenticatedBind(bindPath)
-		}
+		return server.Connection.Bind(path, password)
 	}
 
 	if err := bindFn(); err != nil {
-		server.log.Info("Initial bind failed", "error", err)
+		server.log.Error("Cannot authentificate in LDAP", "err", err)
 
 		if ldapErr, ok := err.(*ldap.Error); ok {
 			if ldapErr.ResultCode == 49 {
@@ -391,19 +346,23 @@ func (server *Server) InitialBind(username, userPassword string) error {
 }
 
 // requestMemberOf use this function when POSIX LDAP schema does not support memberOf, so it manually search the groups
-func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string, error) {
+func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) {
 	var memberOf []string
+	var config = server.Config
 
-	for _, groupSearchBase := range server.Config.GroupSearchBaseDNs {
+	for _, groupSearchBase := range config.GroupSearchBaseDNs {
 		var filterReplace string
-		if server.Config.GroupSearchFilterUserAttribute == "" {
-			filterReplace = getLDAPAttr(server.Config.Attr.Username, searchResult)
+		if config.GroupSearchFilterUserAttribute == "" {
+			filterReplace = getAttribute(config.Attr.Username, entry)
 		} else {
-			filterReplace = getLDAPAttr(server.Config.GroupSearchFilterUserAttribute, searchResult)
+			filterReplace = getAttribute(
+				config.GroupSearchFilterUserAttribute,
+				entry,
+			)
 		}
 
 		filter := strings.Replace(
-			server.Config.GroupSearchFilter, "%s",
+			config.GroupSearchFilter, "%s",
 			ldap.EscapeFilter(filterReplace),
 			-1,
 		)
@@ -411,7 +370,7 @@ func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string
 		server.log.Info("Searching for user's groups", "filter", filter)
 
 		// support old way of reading settings
-		groupIDAttribute := server.Config.Attr.MemberOf
+		groupIDAttribute := config.Attr.MemberOf
 		// but prefer dn attribute if default settings are used
 		if groupIDAttribute == "" || groupIDAttribute == "memberOf" {
 			groupIDAttribute = "dn"
@@ -431,8 +390,11 @@ func (server *Server) requestMemberOf(searchResult *ldap.SearchResult) ([]string
 		}
 
 		if len(groupSearchResult.Entries) > 0 {
-			for i := range groupSearchResult.Entries {
-				memberOf = append(memberOf, getLDAPAttrN(groupIDAttribute, groupSearchResult, i))
+			for _, group := range groupSearchResult.Entries {
+				memberOf = append(
+					memberOf,
+					getAttribute(groupIDAttribute, group),
+				)
 			}
 			break
 		}
@@ -448,104 +410,32 @@ func (server *Server) serializeUsers(
 ) ([]*models.ExternalUserInfo, error) {
 	var serialized []*models.ExternalUserInfo
 
-	for index := range users.Entries {
-		memberOf, err := server.getMemberOf(users)
+	for _, user := range users.Entries {
+		extUser, err := server.buildGrafanaUser(user)
 		if err != nil {
 			return nil, err
 		}
 
-		userInfo := &UserInfo{
-			DN: getLDAPAttrN(
-				"dn",
-				users,
-				index,
-			),
-			LastName: getLDAPAttrN(
-				server.Config.Attr.Surname,
-				users,
-				index,
-			),
-			FirstName: getLDAPAttrN(
-				server.Config.Attr.Name,
-				users,
-				index,
-			),
-			Username: getLDAPAttrN(
-				server.Config.Attr.Username,
-				users,
-				index,
-			),
-			Email: getLDAPAttrN(
-				server.Config.Attr.Email,
-				users,
-				index,
-			),
-			MemberOf: memberOf,
-		}
-
-		serialized = append(
-			serialized,
-			server.buildGrafanaUser(userInfo),
-		)
+		serialized = append(serialized, extUser)
 	}
 
 	return serialized, nil
 }
 
 // getMemberOf finds memberOf property or request it
-func (server *Server) getMemberOf(search *ldap.SearchResult) (
+func (server *Server) getMemberOf(result *ldap.Entry) (
 	[]string, error,
 ) {
 	if server.Config.GroupSearchFilter == "" {
-		memberOf := getLDAPAttrArray(server.Config.Attr.MemberOf, search)
+		memberOf := getArrayAttribute(server.Config.Attr.MemberOf, result)
 
 		return memberOf, nil
 	}
 
-	memberOf, err := server.requestMemberOf(search)
+	memberOf, err := server.requestMemberOf(result)
 	if err != nil {
 		return nil, err
 	}
 
 	return memberOf, nil
 }
-
-func appendIfNotEmpty(slice []string, values ...string) []string {
-	for _, v := range values {
-		if v != "" {
-			slice = append(slice, v)
-		}
-	}
-	return slice
-}
-
-func getLDAPAttr(name string, result *ldap.SearchResult) string {
-	return getLDAPAttrN(name, result, 0)
-}
-
-func getLDAPAttrN(name string, result *ldap.SearchResult, n int) string {
-	if strings.ToLower(name) == "dn" {
-		return result.Entries[n].DN
-	}
-	for _, attr := range result.Entries[n].Attributes {
-		if attr.Name == name {
-			if len(attr.Values) > 0 {
-				return attr.Values[0]
-			}
-		}
-	}
-	return ""
-}
-
-func getLDAPAttrArray(name string, result *ldap.SearchResult) []string {
-	return getLDAPAttrArrayN(name, result, 0)
-}
-
-func getLDAPAttrArrayN(name string, result *ldap.SearchResult, n int) []string {
-	for _, attr := range result.Entries[n].Attributes {
-		if attr.Name == name {
-			return attr.Values
-		}
-	}
-	return []string{}
-}

+ 57 - 106
pkg/services/ldap/ldap_helpers_test.go

@@ -5,136 +5,87 @@ import (
 
 	. "github.com/smartystreets/goconvey/convey"
 	"gopkg.in/ldap.v3"
-
-	"github.com/grafana/grafana/pkg/infra/log"
 )
 
 func TestLDAPHelpers(t *testing.T) {
-	Convey("serializeUsers()", t, func() {
-		Convey("simple case", func() {
-			server := &Server{
-				Config: &ServerConfig{
-					Attr: AttributeMap{
-						Username: "username",
-						Name:     "name",
-						MemberOf: "memberof",
-						Email:    "email",
+	Convey("isMemberOf()", t, func() {
+		Convey("Wildcard", func() {
+			result := isMemberOf([]string{}, "*")
+			So(result, ShouldBeTrue)
+		})
+
+		Convey("Should find one", func() {
+			result := isMemberOf([]string{"one", "Two", "three"}, "two")
+			So(result, ShouldBeTrue)
+		})
+
+		Convey("Should not find one", func() {
+			result := isMemberOf([]string{"one", "Two", "three"}, "twos")
+			So(result, ShouldBeFalse)
+		})
+	})
+
+	Convey("getAttribute()", t, func() {
+		Convey("Should get username", func() {
+			value := []string{"roelgerrits"}
+			entry := &ldap.Entry{
+				Attributes: []*ldap.EntryAttribute{
+					{
+						Name: "username", Values: value,
 					},
-					SearchBaseDNs: []string{"BaseDNHere"},
 				},
-				Connection: &MockConnection{},
-				log:        log.New("test-logger"),
 			}
 
-			entry := ldap.Entry{
-				DN: "dn", Attributes: []*ldap.EntryAttribute{
-					{Name: "username", Values: []string{"roelgerrits"}},
-					{Name: "surname", Values: []string{"Gerrits"}},
-					{Name: "email", Values: []string{"roel@test.com"}},
-					{Name: "name", Values: []string{"Roel"}},
-					{Name: "memberof", Values: []string{"admins"}},
-				}}
-			users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
-
-			result, err := server.serializeUsers(users)
-
-			So(err, ShouldBeNil)
-			So(result[0].Login, ShouldEqual, "roelgerrits")
-			So(result[0].Email, ShouldEqual, "roel@test.com")
-			So(result[0].Groups, ShouldContain, "admins")
+			result := getAttribute("username", entry)
+
+			So(result, ShouldEqual, value[0])
 		})
 
-		Convey("without lastname", func() {
-			server := &Server{
-				Config: &ServerConfig{
-					Attr: AttributeMap{
-						Username: "username",
-						Name:     "name",
-						MemberOf: "memberof",
-						Email:    "email",
+		Convey("Should not get anything", func() {
+			value := []string{"roelgerrits"}
+			entry := &ldap.Entry{
+				Attributes: []*ldap.EntryAttribute{
+					{
+						Name: "killa", Values: value,
 					},
-					SearchBaseDNs: []string{"BaseDNHere"},
 				},
-				Connection: &MockConnection{},
-				log:        log.New("test-logger"),
 			}
 
-			entry := ldap.Entry{
-				DN: "dn", Attributes: []*ldap.EntryAttribute{
-					{Name: "username", Values: []string{"roelgerrits"}},
-					{Name: "email", Values: []string{"roel@test.com"}},
-					{Name: "name", Values: []string{"Roel"}},
-					{Name: "memberof", Values: []string{"admins"}},
-				}}
-			users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
-
-			result, err := server.serializeUsers(users)
+			result := getAttribute("username", entry)
 
-			So(err, ShouldBeNil)
-			So(result[0].Name, ShouldEqual, "Roel")
+			So(result, ShouldEqual, "")
 		})
 	})
 
-	Convey("serverBind()", t, func() {
-		Convey("Given bind dn and password configured", func() {
-			connection := &MockConnection{}
-			var actualUsername, actualPassword string
-			connection.bindProvider = func(username, password string) error {
-				actualUsername = username
-				actualPassword = password
-				return nil
-			}
-			server := &Server{
-				Connection: connection,
-				Config: &ServerConfig{
-					BindDN:       "o=users,dc=grafana,dc=org",
-					BindPassword: "bindpwd",
+	Convey("getArrayAttribute()", t, func() {
+		Convey("Should get username", func() {
+			value := []string{"roelgerrits"}
+			entry := &ldap.Entry{
+				Attributes: []*ldap.EntryAttribute{
+					{
+						Name: "username", Values: value,
+					},
 				},
 			}
-			err := server.serverBind()
-			So(err, ShouldBeNil)
-			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
-			So(actualPassword, ShouldEqual, "bindpwd")
+
+			result := getArrayAttribute("username", entry)
+
+			So(result, ShouldResemble, value)
 		})
 
-		Convey("Given bind dn configured", func() {
-			connection := &MockConnection{}
-			unauthenticatedBindWasCalled := false
-			var actualUsername string
-			connection.unauthenticatedBindProvider = func(username string) error {
-				unauthenticatedBindWasCalled = true
-				actualUsername = username
-				return nil
-			}
-			server := &Server{
-				Connection: connection,
-				Config: &ServerConfig{
-					BindDN: "o=users,dc=grafana,dc=org",
+		Convey("Should not get anything", func() {
+			value := []string{"roelgerrits"}
+			entry := &ldap.Entry{
+				Attributes: []*ldap.EntryAttribute{
+					{
+						Name: "username", Values: value,
+					},
 				},
 			}
-			err := server.serverBind()
-			So(err, ShouldBeNil)
-			So(unauthenticatedBindWasCalled, ShouldBeTrue)
-			So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org")
-		})
 
-		Convey("Given empty bind dn and password", func() {
-			connection := &MockConnection{}
-			unauthenticatedBindWasCalled := false
-			var actualUsername string
-			connection.unauthenticatedBindProvider = func(username string) error {
-				unauthenticatedBindWasCalled = true
-				actualUsername = username
-				return nil
-			}
-			server := &Server{
-				Connection: connection,
-				Config:     &ServerConfig{},
-			}
-			err := server.serverBind()
-			So(err, ShouldBeNil)
-			So(unauthenticatedBindWasCalled, ShouldBeTrue)
-			So(actualUsername, ShouldBeEmpty)
+			result := getArrayAttribute("something", entry)
+
+			So(result, ShouldResemble, []string{})
 		})
 	})
 }

+ 38 - 164
pkg/services/ldap/ldap_login_test.go

@@ -1,88 +1,24 @@
 package ldap
 
 import (
+	"errors"
 	"testing"
 
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ldap.v3"
-
 	"github.com/grafana/grafana/pkg/infra/log"
 	"github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/user"
+	. "github.com/smartystreets/goconvey/convey"
+	"gopkg.in/ldap.v3"
 )
 
 func TestLDAPLogin(t *testing.T) {
-	Convey("Login()", t, func() {
-		serverScenario("When user is log in and updated", func(sc *scenarioContext) {
-			// arrange
-			mockConnection := &MockConnection{}
+	defaultLogin := &models.LoginUserQuery{
+		Username:  "user",
+		Password:  "pwd",
+		IpAddress: "192.168.1.1:56433",
+	}
 
-			server := &Server{
-				Config: &ServerConfig{
-					Host:       "",
-					RootCACert: "",
-					Groups: []*GroupToOrgRole{
-						{GroupDN: "*", OrgRole: "Admin"},
-					},
-					Attr: AttributeMap{
-						Username: "username",
-						Surname:  "surname",
-						Email:    "email",
-						Name:     "name",
-						MemberOf: "memberof",
-					},
-					SearchBaseDNs: []string{"BaseDNHere"},
-				},
-				Connection: mockConnection,
-				log:        log.New("test-logger"),
-			}
-
-			entry := ldap.Entry{
-				DN: "dn", Attributes: []*ldap.EntryAttribute{
-					{Name: "username", Values: []string{"roelgerrits"}},
-					{Name: "surname", Values: []string{"Gerrits"}},
-					{Name: "email", Values: []string{"roel@test.com"}},
-					{Name: "name", Values: []string{"Roel"}},
-					{Name: "memberof", Values: []string{"admins"}},
-				}}
-			result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
-			mockConnection.setSearchResult(&result)
-
-			query := &models.LoginUserQuery{
-				Username: "roelgerrits",
-			}
-
-			sc.userQueryReturns(&models.User{
-				Id:    1,
-				Email: "roel@test.net",
-				Name:  "Roel Gerrits",
-				Login: "roelgerrits",
-			})
-			sc.userOrgsQueryReturns([]*models.UserOrgDTO{})
-
-			// act
-			extUser, _ := server.Login(query)
-			userInfo, err := user.Upsert(&user.UpsertArgs{
-				SignupAllowed: true,
-				ExternalUser:  extUser,
-			})
-
-			// assert
-
-			// Check absence of the error
-			So(err, ShouldBeNil)
-
-			// User should be searched in ldap
-			So(mockConnection.SearchCalled, ShouldBeTrue)
-
-			// Info should be updated (email differs)
-			So(userInfo.Email, ShouldEqual, "roel@test.com")
-
-			// User should have admin privileges
-			So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
-		})
-
-		serverScenario("When login with invalid credentials", func(scenario *scenarioContext) {
+	Convey("Login()", t, func() {
+		Convey("Should get invalid credentials when auth fails", func() {
 			connection := &MockConnection{}
 			entry := ldap.Entry{}
 			result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
@@ -95,36 +31,20 @@ func TestLDAPLogin(t *testing.T) {
 			}
 			server := &Server{
 				Config: &ServerConfig{
-					Attr: AttributeMap{
-						Username: "username",
-						Name:     "name",
-						MemberOf: "memberof",
-					},
 					SearchBaseDNs: []string{"BaseDNHere"},
 				},
 				Connection: connection,
 				log:        log.New("test-logger"),
 			}
 
-			_, err := server.Login(scenario.loginUserQuery)
+			_, err := server.Login(defaultLogin)
 
-			Convey("it should return invalid credentials error", func() {
-				So(err, ShouldEqual, ErrInvalidCredentials)
-			})
+			So(err, ShouldEqual, ErrInvalidCredentials)
 		})
 
-		serverScenario("When login with valid credentials", func(scenario *scenarioContext) {
+		Convey("Returns an error when search hasn't find anything", func() {
 			connection := &MockConnection{}
-			entry := ldap.Entry{
-				DN: "dn", Attributes: []*ldap.EntryAttribute{
-					{Name: "username", Values: []string{"markelog"}},
-					{Name: "surname", Values: []string{"Gaidarenko"}},
-					{Name: "email", Values: []string{"markelog@gmail.com"}},
-					{Name: "name", Values: []string{"Oleg"}},
-					{Name: "memberof", Values: []string{"admins"}},
-				},
-			}
-			result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+			result := ldap.SearchResult{Entries: []*ldap.Entry{}}
 			connection.setSearchResult(&result)
 
 			connection.bindProvider = func(username, password string) error {
@@ -132,30 +52,21 @@ func TestLDAPLogin(t *testing.T) {
 			}
 			server := &Server{
 				Config: &ServerConfig{
-					Attr: AttributeMap{
-						Username: "username",
-						Name:     "name",
-						MemberOf: "memberof",
-					},
 					SearchBaseDNs: []string{"BaseDNHere"},
 				},
 				Connection: connection,
 				log:        log.New("test-logger"),
 			}
 
-			resp, err := server.Login(scenario.loginUserQuery)
+			_, err := server.Login(defaultLogin)
 
-			So(err, ShouldBeNil)
-			So(resp.Login, ShouldEqual, "markelog")
+			So(err, ShouldEqual, ErrInvalidCredentials)
 		})
 
-		serverScenario("When user not found in LDAP, but exist in Grafana", func(scenario *scenarioContext) {
+		Convey("When search returns an error", func() {
 			connection := &MockConnection{}
-			result := ldap.SearchResult{Entries: []*ldap.Entry{}}
-			connection.setSearchResult(&result)
-
-			externalUser := &models.ExternalUserInfo{UserId: 42, IsDisabled: false}
-			scenario.getExternalUserInfoByLoginQueryReturns(externalUser)
+			expected := errors.New("Killa-gorilla")
+			connection.setSearchError(expected)
 
 			connection.bindProvider = func(username, password string) error {
 				return nil
@@ -168,82 +79,45 @@ func TestLDAPLogin(t *testing.T) {
 				log:        log.New("test-logger"),
 			}
 
-			_, err := server.Login(scenario.loginUserQuery)
-
-			Convey("it should disable user", func() {
-				So(scenario.disableExternalUserCalled, ShouldBeTrue)
-				So(scenario.disableUserCmd.IsDisabled, ShouldBeTrue)
-				So(scenario.disableUserCmd.UserId, ShouldEqual, 42)
-			})
+			_, err := server.Login(defaultLogin)
 
-			Convey("it should return invalid credentials error", func() {
-				So(err, ShouldEqual, ErrInvalidCredentials)
-			})
+			So(err, ShouldEqual, expected)
 		})
 
-		serverScenario("When user not found in LDAP, and disabled in Grafana already", func(scenario *scenarioContext) {
+		Convey("When login with valid credentials", func() {
 			connection := &MockConnection{}
-			result := ldap.SearchResult{Entries: []*ldap.Entry{}}
-			connection.setSearchResult(&result)
-
-			externalUser := &models.ExternalUserInfo{UserId: 42, IsDisabled: true}
-			scenario.getExternalUserInfoByLoginQueryReturns(externalUser)
-
-			connection.bindProvider = func(username, password string) error {
-				return nil
-			}
-			server := &Server{
-				Config: &ServerConfig{
-					SearchBaseDNs: []string{"BaseDNHere"},
+			entry := ldap.Entry{
+				DN: "dn", Attributes: []*ldap.EntryAttribute{
+					{Name: "username", Values: []string{"markelog"}},
+					{Name: "surname", Values: []string{"Gaidarenko"}},
+					{Name: "email", Values: []string{"markelog@gmail.com"}},
+					{Name: "name", Values: []string{"Oleg"}},
+					{Name: "memberof", Values: []string{"admins"}},
 				},
-				Connection: connection,
-				log:        log.New("test-logger"),
 			}
-
-			_, err := server.Login(scenario.loginUserQuery)
-
-			Convey("it should't call disable function", func() {
-				So(scenario.disableExternalUserCalled, ShouldBeFalse)
-			})
-
-			Convey("it should return invalid credentials error", func() {
-				So(err, ShouldEqual, ErrInvalidCredentials)
-			})
-		})
-
-		serverScenario("When user found in LDAP, and disabled in Grafana", func(scenario *scenarioContext) {
-			connection := &MockConnection{}
-			entry := ldap.Entry{}
 			result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
 			connection.setSearchResult(&result)
-			scenario.userQueryReturns(&models.User{Id: 42, IsDisabled: true})
 
 			connection.bindProvider = func(username, password string) error {
 				return nil
 			}
 			server := &Server{
 				Config: &ServerConfig{
+					Attr: AttributeMap{
+						Username: "username",
+						Name:     "name",
+						MemberOf: "memberof",
+					},
 					SearchBaseDNs: []string{"BaseDNHere"},
 				},
 				Connection: connection,
 				log:        log.New("test-logger"),
 			}
 
-			extUser, _ := server.Login(scenario.loginUserQuery)
-			_, err := user.Upsert(&user.UpsertArgs{
-				SignupAllowed: true,
-				ExternalUser:  extUser,
-			})
-
-			Convey("it should re-enable user", func() {
-				So(scenario.disableExternalUserCalled, ShouldBeTrue)
-				So(scenario.disableUserCmd.IsDisabled, ShouldBeFalse)
-				So(scenario.disableUserCmd.UserId, ShouldEqual, 42)
-			})
-
-			Convey("it should not return error", func() {
-				So(err, ShouldBeNil)
-			})
+			resp, err := server.Login(defaultLogin)
+
+			So(err, ShouldBeNil)
+			So(resp.Login, ShouldEqual, "markelog")
 		})
 	})
 }

+ 146 - 0
pkg/services/ldap/ldap_private_test.go

@@ -0,0 +1,146 @@
+package ldap
+
+import (
+	"testing"
+
+	. "github.com/smartystreets/goconvey/convey"
+	"gopkg.in/ldap.v3"
+
+	"github.com/grafana/grafana/pkg/infra/log"
+	"github.com/grafana/grafana/pkg/models"
+)
+
+func TestLDAPPrivateMethods(t *testing.T) {
+	Convey("serializeUsers()", t, func() {
+		Convey("simple case", func() {
+			server := &Server{
+				Config: &ServerConfig{
+					Attr: AttributeMap{
+						Username: "username",
+						Name:     "name",
+						MemberOf: "memberof",
+						Email:    "email",
+					},
+					SearchBaseDNs: []string{"BaseDNHere"},
+				},
+				Connection: &MockConnection{},
+				log:        log.New("test-logger"),
+			}
+
+			entry := ldap.Entry{
+				DN: "dn",
+				Attributes: []*ldap.EntryAttribute{
+					{Name: "username", Values: []string{"roelgerrits"}},
+					{Name: "surname", Values: []string{"Gerrits"}},
+					{Name: "email", Values: []string{"roel@test.com"}},
+					{Name: "name", Values: []string{"Roel"}},
+					{Name: "memberof", Values: []string{"admins"}},
+				},
+			}
+			users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+
+			result, err := server.serializeUsers(users)
+
+			So(err, ShouldBeNil)
+			So(result[0].Login, ShouldEqual, "roelgerrits")
+			So(result[0].Email, ShouldEqual, "roel@test.com")
+			So(result[0].Groups, ShouldContain, "admins")
+		})
+
+		Convey("without lastname", func() {
+			server := &Server{
+				Config: &ServerConfig{
+					Attr: AttributeMap{
+						Username: "username",
+						Name:     "name",
+						MemberOf: "memberof",
+						Email:    "email",
+					},
+					SearchBaseDNs: []string{"BaseDNHere"},
+				},
+				Connection: &MockConnection{},
+				log:        log.New("test-logger"),
+			}
+
+			entry := ldap.Entry{
+				DN: "dn",
+				Attributes: []*ldap.EntryAttribute{
+					{Name: "username", Values: []string{"roelgerrits"}},
+					{Name: "email", Values: []string{"roel@test.com"}},
+					{Name: "name", Values: []string{"Roel"}},
+					{Name: "memberof", Values: []string{"admins"}},
+				},
+			}
+			users := &ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
+
+			result, err := server.serializeUsers(users)
+
+			So(err, ShouldBeNil)
+			So(result[0].Name, ShouldEqual, "Roel")
+		})
+	})
+
+	Convey("validateGrafanaUser()", t, func() {
+		Convey("Returns error when user does not belong in any of the specified LDAP groups", func() {
+			server := &Server{
+				Config: &ServerConfig{
+					Groups: []*GroupToOrgRole{
+						{
+							OrgID: 1,
+						},
+					},
+				},
+				log: logger.New("test"),
+			}
+
+			user := &models.ExternalUserInfo{
+				Login: "markelog",
+			}
+
+			result := server.validateGrafanaUser(user)
+
+			So(result, ShouldEqual, ErrInvalidCredentials)
+		})
+
+		Convey("Does not return error when group config is empty", func() {
+			server := &Server{
+				Config: &ServerConfig{
+					Groups: []*GroupToOrgRole{},
+				},
+				log: logger.New("test"),
+			}
+
+			user := &models.ExternalUserInfo{
+				Login: "markelog",
+			}
+
+			result := server.validateGrafanaUser(user)
+
+			So(result, ShouldBeNil)
+		})
+
+		Convey("Does not return error when groups are there", func() {
+			server := &Server{
+				Config: &ServerConfig{
+					Groups: []*GroupToOrgRole{
+						{
+							OrgID: 1,
+						},
+					},
+				},
+				log: logger.New("test"),
+			}
+
+			user := &models.ExternalUserInfo{
+				Login: "markelog",
+				OrgRoles: map[int64]models.RoleType{
+					1: "test",
+				},
+			}
+
+			result := server.validateGrafanaUser(user)
+
+			So(result, ShouldBeNil)
+		})
+	})
+}

+ 71 - 23
pkg/services/ldap/ldap_test.go

@@ -1,17 +1,29 @@
 package ldap
 
 import (
+	"errors"
 	"testing"
 
 	. "github.com/smartystreets/goconvey/convey"
-	ldap "gopkg.in/ldap.v3"
+	"gopkg.in/ldap.v3"
 
 	"github.com/grafana/grafana/pkg/infra/log"
 )
 
 func TestPublicAPI(t *testing.T) {
+	Convey("New()", t, func() {
+		Convey("Should return ", func() {
+			result := New(&ServerConfig{
+				Attr:          AttributeMap{},
+				SearchBaseDNs: []string{"BaseDNHere"},
+			})
+
+			So(result, ShouldImplement, (*IServer)(nil))
+		})
+	})
+
 	Convey("Users()", t, func() {
-		Convey("find one user", func() {
+		Convey("Finds one user", func() {
 			MockConnection := &MockConnection{}
 			entry := ldap.Entry{
 				DN: "dn", Attributes: []*ldap.EntryAttribute{
@@ -49,10 +61,49 @@ func TestPublicAPI(t *testing.T) {
 			// No empty attributes should be added to the search request
 			So(len(MockConnection.SearchAttributes), ShouldEqual, 3)
 		})
+
+		Convey("Handles a error", func() {
+			expected := errors.New("Killa-gorilla")
+			MockConnection := &MockConnection{}
+			MockConnection.setSearchError(expected)
+
+			// Set up attribute map without surname and email
+			server := &Server{
+				Config: &ServerConfig{
+					SearchBaseDNs: []string{"BaseDNHere"},
+				},
+				Connection: MockConnection,
+				log:        log.New("test-logger"),
+			}
+
+			_, err := server.Users([]string{"roelgerrits"})
+
+			So(err, ShouldEqual, expected)
+		})
+
+		Convey("Should return empty slice if none were found", func() {
+			MockConnection := &MockConnection{}
+			result := ldap.SearchResult{Entries: []*ldap.Entry{}}
+			MockConnection.setSearchResult(&result)
+
+			// Set up attribute map without surname and email
+			server := &Server{
+				Config: &ServerConfig{
+					SearchBaseDNs: []string{"BaseDNHere"},
+				},
+				Connection: MockConnection,
+				log:        log.New("test-logger"),
+			}
+
+			searchResult, err := server.Users([]string{"roelgerrits"})
+
+			So(err, ShouldBeNil)
+			So(searchResult, ShouldBeEmpty)
+		})
 	})
 
-	Convey("InitialBind", t, func() {
-		Convey("Given bind dn and password configured", func() {
+	Convey("Auth()", t, func() {
+		Convey("Should ignore passsed username and password", func() {
 			connection := &MockConnection{}
 			var actualUsername, actualPassword string
 			connection.bindProvider = func(username, password string) error {
@@ -63,14 +114,13 @@ func TestPublicAPI(t *testing.T) {
 			server := &Server{
 				Connection: connection,
 				Config: &ServerConfig{
-					BindDN:       "cn=%s,o=users,dc=grafana,dc=org",
+					BindDN:       "cn=admin,dc=grafana,dc=org",
 					BindPassword: "bindpwd",
 				},
 			}
-			err := server.InitialBind("user", "pwd")
+			err := server.Auth("user", "pwd")
 			So(err, ShouldBeNil)
-			So(server.requireSecondBind, ShouldBeTrue)
-			So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
+			So(actualUsername, ShouldEqual, "cn=admin,dc=grafana,dc=org")
 			So(actualPassword, ShouldEqual, "bindpwd")
 		})
 
@@ -88,31 +138,29 @@ func TestPublicAPI(t *testing.T) {
 					BindDN: "cn=%s,o=users,dc=grafana,dc=org",
 				},
 			}
-			err := server.InitialBind("user", "pwd")
+			err := server.Auth("user", "pwd")
 			So(err, ShouldBeNil)
-			So(server.requireSecondBind, ShouldBeFalse)
 			So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
 			So(actualPassword, ShouldEqual, "pwd")
 		})
 
-		Convey("Given empty bind dn and password", func() {
+		Convey("Should handle an error", func() {
 			connection := &MockConnection{}
-			unauthenticatedBindWasCalled := false
-			var actualUsername string
-			connection.unauthenticatedBindProvider = func(username string) error {
-				unauthenticatedBindWasCalled = true
-				actualUsername = username
-				return nil
+			expected := &ldap.Error{
+				ResultCode: uint16(25),
+			}
+			connection.bindProvider = func(username, password string) error {
+				return expected
 			}
 			server := &Server{
 				Connection: connection,
-				Config:     &ServerConfig{},
+				Config: &ServerConfig{
+					BindDN: "cn=%s,o=users,dc=grafana,dc=org",
+				},
+				log: log.New("test-logger"),
 			}
-			err := server.InitialBind("user", "pwd")
-			So(err, ShouldBeNil)
-			So(server.requireSecondBind, ShouldBeTrue)
-			So(unauthenticatedBindWasCalled, ShouldBeTrue)
-			So(actualUsername, ShouldBeEmpty)
+			err := server.Auth("user", "pwd")
+			So(err, ShouldEqual, expected)
 		})
 	})
 }

+ 17 - 7
pkg/services/ldap/settings.go

@@ -42,6 +42,7 @@ type ServerConfig struct {
 	Groups []*GroupToOrgRole `toml:"group_mappings"`
 }
 
+// AttributeMap is a struct representation for LDAP "attributes" setting
 type AttributeMap struct {
 	Username string `toml:"username"`
 	Name     string `toml:"name"`
@@ -50,14 +51,19 @@ type AttributeMap struct {
 	MemberOf string `toml:"member_of"`
 }
 
+// GroupToOrgRole is a struct representation of LDAP
+// config "group_mappings" setting
 type GroupToOrgRole struct {
-	GroupDN        string     `toml:"group_dn"`
-	OrgId          int64      `toml:"org_id"`
-	IsGrafanaAdmin *bool      `toml:"grafana_admin"` // This is a pointer to know if it was set or not (for backwards compatibility)
-	OrgRole        m.RoleType `toml:"org_role"`
+	GroupDN string `toml:"group_dn"`
+	OrgID   int64  `toml:"org_id"`
+
+	// This pointer specifies if setting was set (for backwards compatibility)
+	IsGrafanaAdmin *bool `toml:"grafana_admin"`
+
+	OrgRole m.RoleType `toml:"org_role"`
 }
 
-var config *Config
+// logger for all LDAP stuff
 var logger = log.New("ldap")
 
 // loadingMutex locks the reading of the config so multiple requests for reloading are sequential.
@@ -82,6 +88,10 @@ func ReloadConfig() error {
 	return err
 }
 
+// We need to define in this space so `GetConfig` fn
+// could be defined as singleton
+var config *Config
+
 // GetConfig returns the LDAP config if LDAP is enabled otherwise it returns nil. It returns either cached value of
 // the config or it reads it and caches it first.
 func GetConfig() (*Config, error) {
@@ -129,8 +139,8 @@ func readConfig(configFile string) (*Config, error) {
 		}
 
 		for _, groupMap := range server.Groups {
-			if groupMap.OrgId == 0 {
-				groupMap.OrgId = 1
+			if groupMap.OrgID == 0 {
+				groupMap.OrgID = 1
 			}
 		}
 	}

+ 0 - 214
pkg/services/ldap/test.go

@@ -1,214 +0,0 @@
-package ldap
-
-import (
-	"context"
-	"crypto/tls"
-
-	. "github.com/smartystreets/goconvey/convey"
-	"gopkg.in/ldap.v3"
-
-	"github.com/grafana/grafana/pkg/bus"
-	"github.com/grafana/grafana/pkg/models"
-	"github.com/grafana/grafana/pkg/services/login"
-)
-
-// MockConnection struct for testing
-type MockConnection struct {
-	SearchResult     *ldap.SearchResult
-	SearchCalled     bool
-	SearchAttributes []string
-
-	AddParams *ldap.AddRequest
-	AddCalled bool
-
-	DelParams *ldap.DelRequest
-	DelCalled bool
-
-	bindProvider                func(username, password string) error
-	unauthenticatedBindProvider func(username string) error
-}
-
-// Bind mocks Bind connection function
-func (c *MockConnection) Bind(username, password string) error {
-	if c.bindProvider != nil {
-		return c.bindProvider(username, password)
-	}
-
-	return nil
-}
-
-// UnauthenticatedBind mocks UnauthenticatedBind connection function
-func (c *MockConnection) UnauthenticatedBind(username string) error {
-	if c.unauthenticatedBindProvider != nil {
-		return c.unauthenticatedBindProvider(username)
-	}
-
-	return nil
-}
-
-// Close mocks Close connection function
-func (c *MockConnection) Close() {}
-
-func (c *MockConnection) setSearchResult(result *ldap.SearchResult) {
-	c.SearchResult = result
-}
-
-// Search mocks Search connection function
-func (c *MockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
-	c.SearchCalled = true
-	c.SearchAttributes = sr.Attributes
-	return c.SearchResult, nil
-}
-
-// Add mocks Add connection function
-func (c *MockConnection) Add(request *ldap.AddRequest) error {
-	c.AddCalled = true
-	c.AddParams = request
-	return nil
-}
-
-// Del mocks Del connection function
-func (c *MockConnection) Del(request *ldap.DelRequest) error {
-	c.DelCalled = true
-	c.DelParams = request
-	return nil
-}
-
-// StartTLS mocks StartTLS connection function
-func (c *MockConnection) StartTLS(*tls.Config) error {
-	return nil
-}
-
-func serverScenario(desc string, fn scenarioFunc) {
-	Convey(desc, func() {
-		defer bus.ClearBusHandlers()
-
-		sc := &scenarioContext{
-			loginUserQuery: &models.LoginUserQuery{
-				Username:  "user",
-				Password:  "pwd",
-				IpAddress: "192.168.1.1:56433",
-			},
-		}
-
-		loginService := &login.LoginService{
-			Bus: bus.GetBus(),
-		}
-
-		bus.AddHandler("test", loginService.UpsertUser)
-
-		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.SyncTeamsCommand) error {
-			return nil
-		})
-
-		bus.AddHandlerCtx("test", func(ctx context.Context, cmd *models.UpdateUserPermissionsCommand) error {
-			sc.updateUserPermissionsCmd = cmd
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.GetUserByAuthInfoQuery) error {
-			sc.getUserByAuthInfoQuery = cmd
-			sc.getUserByAuthInfoQuery.Result = &models.User{Login: cmd.Login}
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.GetUserOrgListQuery) error {
-			sc.getUserOrgListQuery = cmd
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.CreateUserCommand) error {
-			sc.createUserCmd = cmd
-			sc.createUserCmd.Result = models.User{Login: cmd.Login}
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.GetExternalUserInfoByLoginQuery) error {
-			sc.getExternalUserInfoByLoginQuery = cmd
-			sc.getExternalUserInfoByLoginQuery.Result = &models.ExternalUserInfo{UserId: 42, IsDisabled: false}
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.DisableUserCommand) error {
-			sc.disableExternalUserCalled = true
-			sc.disableUserCmd = cmd
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.AddOrgUserCommand) error {
-			sc.addOrgUserCmd = cmd
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.UpdateOrgUserCommand) error {
-			sc.updateOrgUserCmd = cmd
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.RemoveOrgUserCommand) error {
-			sc.removeOrgUserCmd = cmd
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.UpdateUserCommand) error {
-			sc.updateUserCmd = cmd
-			return nil
-		})
-
-		bus.AddHandler("test", func(cmd *models.SetUsingOrgCommand) error {
-			sc.setUsingOrgCmd = cmd
-			return nil
-		})
-
-		fn(sc)
-	})
-}
-
-type scenarioContext struct {
-	loginUserQuery                  *models.LoginUserQuery
-	getUserByAuthInfoQuery          *models.GetUserByAuthInfoQuery
-	getExternalUserInfoByLoginQuery *models.GetExternalUserInfoByLoginQuery
-	getUserOrgListQuery             *models.GetUserOrgListQuery
-	createUserCmd                   *models.CreateUserCommand
-	disableUserCmd                  *models.DisableUserCommand
-	addOrgUserCmd                   *models.AddOrgUserCommand
-	updateOrgUserCmd                *models.UpdateOrgUserCommand
-	removeOrgUserCmd                *models.RemoveOrgUserCommand
-	updateUserCmd                   *models.UpdateUserCommand
-	setUsingOrgCmd                  *models.SetUsingOrgCommand
-	updateUserPermissionsCmd        *models.UpdateUserPermissionsCommand
-	disableExternalUserCalled       bool
-}
-
-func (sc *scenarioContext) userQueryReturns(user *models.User) {
-	bus.AddHandler("test", func(query *models.GetUserByAuthInfoQuery) error {
-		if user == nil {
-			return models.ErrUserNotFound
-		}
-		query.Result = user
-		return nil
-	})
-	bus.AddHandler("test", func(query *models.SetAuthInfoCommand) error {
-		return nil
-	})
-}
-
-func (sc *scenarioContext) userOrgsQueryReturns(orgs []*models.UserOrgDTO) {
-	bus.AddHandler("test", func(query *models.GetUserOrgListQuery) error {
-		query.Result = orgs
-		return nil
-	})
-}
-
-func (sc *scenarioContext) getExternalUserInfoByLoginQueryReturns(externalUser *models.ExternalUserInfo) {
-	bus.AddHandler("test", func(cmd *models.GetExternalUserInfoByLoginQuery) error {
-		sc.getExternalUserInfoByLoginQuery = cmd
-		sc.getExternalUserInfoByLoginQuery.Result = &models.ExternalUserInfo{
-			UserId:     externalUser.UserId,
-			IsDisabled: externalUser.IsDisabled,
-		}
-		return nil
-	})
-}
-
-type scenarioFunc func(c *scenarioContext)

+ 84 - 0
pkg/services/ldap/test_test.go

@@ -0,0 +1,84 @@
+package ldap
+
+import (
+	"crypto/tls"
+
+	"gopkg.in/ldap.v3"
+)
+
+// MockConnection struct for testing
+type MockConnection struct {
+	SearchResult     *ldap.SearchResult
+	SearchError      error
+	SearchCalled     bool
+	SearchAttributes []string
+
+	AddParams *ldap.AddRequest
+	AddCalled bool
+
+	DelParams *ldap.DelRequest
+	DelCalled bool
+
+	bindProvider                func(username, password string) error
+	unauthenticatedBindProvider func(username string) error
+}
+
+// Bind mocks Bind connection function
+func (c *MockConnection) Bind(username, password string) error {
+	if c.bindProvider != nil {
+		return c.bindProvider(username, password)
+	}
+
+	return nil
+}
+
+// UnauthenticatedBind mocks UnauthenticatedBind connection function
+func (c *MockConnection) UnauthenticatedBind(username string) error {
+	if c.unauthenticatedBindProvider != nil {
+		return c.unauthenticatedBindProvider(username)
+	}
+
+	return nil
+}
+
+// Close mocks Close connection function
+func (c *MockConnection) Close() {}
+
+func (c *MockConnection) setSearchResult(result *ldap.SearchResult) {
+	c.SearchResult = result
+}
+
+func (c *MockConnection) setSearchError(err error) {
+	c.SearchError = err
+}
+
+// Search mocks Search connection function
+func (c *MockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
+	c.SearchCalled = true
+	c.SearchAttributes = sr.Attributes
+
+	if c.SearchError != nil {
+		return nil, c.SearchError
+	}
+
+	return c.SearchResult, nil
+}
+
+// Add mocks Add connection function
+func (c *MockConnection) Add(request *ldap.AddRequest) error {
+	c.AddCalled = true
+	c.AddParams = request
+	return nil
+}
+
+// Del mocks Del connection function
+func (c *MockConnection) Del(request *ldap.DelRequest) error {
+	c.DelCalled = true
+	c.DelParams = request
+	return nil
+}
+
+// StartTLS mocks StartTLS connection function
+func (c *MockConnection) StartTLS(*tls.Config) error {
+	return nil
+}

+ 0 - 27
pkg/services/ldap/user.go

@@ -1,27 +0,0 @@
-package ldap
-
-import (
-	"strings"
-)
-
-type UserInfo struct {
-	DN        string
-	FirstName string
-	LastName  string
-	Username  string
-	Email     string
-	MemberOf  []string
-}
-
-func (u *UserInfo) isMemberOf(group string) bool {
-	if group == "*" {
-		return true
-	}
-
-	for _, member := range u.MemberOf {
-		if strings.EqualFold(member, group) {
-			return true
-		}
-	}
-	return false
-}

+ 1 - 1
pkg/services/multildap/test.go → pkg/services/multildap/test_test.go

@@ -35,7 +35,7 @@ func (mock *mockLDAP) Users([]string) ([]*models.ExternalUserInfo, error) {
 
 	return mock.usersRestReturn, mock.usersErrReturn
 }
-func (mock *mockLDAP) InitialBind(string, string) error {
+func (mock *mockLDAP) Auth(string, string) error {
 	return nil
 }
 func (mock *mockLDAP) Dial() error {

+ 0 - 39
pkg/services/user/user.go

@@ -1,39 +0,0 @@
-package user
-
-import (
-	"github.com/grafana/grafana/pkg/bus"
-	"github.com/grafana/grafana/pkg/models"
-)
-
-// UpsertArgs are object for Upsert method
-type UpsertArgs struct {
-	ReqContext    *models.ReqContext
-	ExternalUser  *models.ExternalUserInfo
-	SignupAllowed bool
-}
-
-// Upsert add/update grafana user
-func Upsert(args *UpsertArgs) (*models.User, error) {
-	query := &models.UpsertUserCommand{
-		ReqContext:    args.ReqContext,
-		ExternalUser:  args.ExternalUser,
-		SignupAllowed: args.SignupAllowed,
-	}
-	err := bus.Dispatch(query)
-	if err != nil {
-		return nil, err
-	}
-
-	return query.Result, nil
-}
-
-// Get the users
-func Get(
-	query *models.SearchUsersQuery,
-) ([]*models.UserSearchHitDTO, error) {
-	if err := bus.Dispatch(query); err != nil {
-		return nil, err
-	}
-
-	return query.Result.Users, nil
-}