From 1b1d95149504027adfb37ac1cd87c3657d356690 Mon Sep 17 00:00:00 2001 From: Oleg Gaidarenko Date: Thu, 13 Jun 2019 17:47:52 +0300 Subject: [PATCH] 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 --- pkg/api/http_server.go | 2 + pkg/login/ldap_login.go | 10 +- pkg/middleware/auth_proxy/auth_proxy.go | 15 +- pkg/middleware/middleware.go | 5 +- pkg/services/ldap/helpers.go | 49 ++++ pkg/services/ldap/ldap.go | 256 +++++------------- pkg/services/ldap/ldap_helpers_test.go | 175 +++++------- pkg/services/ldap/ldap_login_test.go | 238 ++++------------ pkg/services/ldap/ldap_private_test.go | 146 ++++++++++ pkg/services/ldap/ldap_test.go | 94 +++++-- pkg/services/ldap/settings.go | 24 +- pkg/services/ldap/test.go | 214 --------------- pkg/services/ldap/test_test.go | 84 ++++++ pkg/services/ldap/user.go | 27 -- .../multildap/{test.go => test_test.go} | 2 +- pkg/services/user/user.go | 39 --- 16 files changed, 579 insertions(+), 801 deletions(-) create mode 100644 pkg/services/ldap/helpers.go create mode 100644 pkg/services/ldap/ldap_private_test.go delete mode 100644 pkg/services/ldap/test.go create mode 100644 pkg/services/ldap/test_test.go delete mode 100644 pkg/services/ldap/user.go rename pkg/services/multildap/{test.go => test_test.go} (95%) delete mode 100644 pkg/services/user/user.go diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 1832e29ed85..590d2225a05 100644 --- a/pkg/api/http_server.go +++ b/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 { diff --git a/pkg/login/ldap_login.go b/pkg/login/ldap_login.go index 33693212ac7..4312355f924 100644 --- a/pkg/login/ldap_login.go +++ b/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 } diff --git a/pkg/middleware/auth_proxy/auth_proxy.go b/pkg/middleware/auth_proxy/auth_proxy.go index ccc7089bf19..94cb954291b 100644 --- a/pkg/middleware/auth_proxy/auth_proxy.go +++ b/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 diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index e28054fe250..f180a355d1e 100644 --- a/pkg/middleware/middleware.go +++ b/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, diff --git a/pkg/services/ldap/helpers.go b/pkg/services/ldap/helpers.go new file mode 100644 index 00000000000..4cee20cf311 --- /dev/null +++ b/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{} +} diff --git a/pkg/services/ldap/ldap.go b/pkg/services/ldap/ldap.go index 418673be446..b2a17f420f8 100644 --- a/pkg/services/ldap/ldap.go +++ b/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 + return extUser, nil } -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 +// shouldBindAdmin checks if we should use +// admin username & password for LDAP bind +func (server *Server) shouldBindAdmin() bool { + return server.Config.BindPassword != "" } -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) +// 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 - if ldapErr, ok := err.(*ldap.Error); ok { - if ldapErr.ResultCode == 49 { - return ErrInvalidCredentials - } - } - return err - } - - return nil -} - -// 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 - } - - 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{} -} diff --git a/pkg/services/ldap/ldap_helpers_test.go b/pkg/services/ldap/ldap_helpers_test.go index 48e6bce8b5b..0f30f228fc4 100644 --- a/pkg/services/ldap/ldap_helpers_test.go +++ b/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", - }, - 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("isMemberOf()", t, func() { + Convey("Wildcard", func() { + result := isMemberOf([]string{}, "*") + So(result, ShouldBeTrue) }) - 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"), - } + Convey("Should find one", func() { + result := isMemberOf([]string{"one", "Two", "three"}, "two") + So(result, ShouldBeTrue) + }) - 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("Should not find one", func() { + result := isMemberOf([]string{"one", "Two", "three"}, "twos") + So(result, ShouldBeFalse) }) }) - 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("getAttribute()", 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 := getAttribute("username", entry) + + So(result, ShouldEqual, value[0]) }) - 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: "killa", Values: value, + }, }, } - err := server.serverBind() - So(err, ShouldBeNil) - So(unauthenticatedBindWasCalled, ShouldBeTrue) - So(actualUsername, ShouldEqual, "o=users,dc=grafana,dc=org") + + result := getAttribute("username", entry) + + So(result, ShouldEqual, "") + }) + }) + + Convey("getArrayAttribute()", t, func() { + Convey("Should get username", func() { + value := []string{"roelgerrits"} + entry := &ldap.Entry{ + Attributes: []*ldap.EntryAttribute{ + { + Name: "username", Values: value, + }, + }, + } + + result := getArrayAttribute("username", entry) + + So(result, ShouldResemble, value) }) - 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 + Convey("Should not get anything", func() { + value := []string{"roelgerrits"} + entry := &ldap.Entry{ + Attributes: []*ldap.EntryAttribute{ + { + Name: "username", Values: value, + }, + }, } - 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{}) }) }) } diff --git a/pkg/services/ldap/ldap_login_test.go b/pkg/services/ldap/ldap_login_test.go index 573a9a560e8..162cba53c91 100644 --- a/pkg/services/ldap/ldap_login_test.go +++ b/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) { + defaultLogin := &models.LoginUserQuery{ + Username: "user", + Password: "pwd", + IpAddress: "192.168.1.1:56433", + } + Convey("Login()", t, func() { - serverScenario("When user is log in and updated", func(sc *scenarioContext) { - // arrange - mockConnection := &MockConnection{} - - 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("Should get invalid credentials when auth fails", func() { connection := &MockConnection{} entry := ldap.Entry{} result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} @@ -95,25 +31,60 @@ 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{} + result := ldap.SearchResult{Entries: []*ldap.Entry{}} + connection.setSearchResult(&result) + + connection.bindProvider = func(username, password string) error { + return nil + } + server := &Server{ + Config: &ServerConfig{ + SearchBaseDNs: []string{"BaseDNHere"}, + }, + Connection: connection, + log: log.New("test-logger"), + } + + _, err := server.Login(defaultLogin) + + So(err, ShouldEqual, ErrInvalidCredentials) + }) + + Convey("When search returns an error", func() { + connection := &MockConnection{} + expected := errors.New("Killa-gorilla") + connection.setSearchError(expected) + + connection.bindProvider = func(username, password string) error { + return nil + } + server := &Server{ + Config: &ServerConfig{ + SearchBaseDNs: []string{"BaseDNHere"}, + }, + Connection: connection, + log: log.New("test-logger"), + } + + _, err := server.Login(defaultLogin) + + So(err, ShouldEqual, expected) + }) + + Convey("When login with valid credentials", func() { connection := &MockConnection{} entry := ldap.Entry{ DN: "dn", Attributes: []*ldap.EntryAttribute{ @@ -143,107 +114,10 @@ func TestLDAPLogin(t *testing.T) { log: log.New("test-logger"), } - resp, err := server.Login(scenario.loginUserQuery) + resp, err := server.Login(defaultLogin) So(err, ShouldBeNil) So(resp.Login, ShouldEqual, "markelog") }) - - serverScenario("When user not found in LDAP, but exist in Grafana", func(scenario *scenarioContext) { - connection := &MockConnection{} - result := ldap.SearchResult{Entries: []*ldap.Entry{}} - connection.setSearchResult(&result) - - externalUser := &models.ExternalUserInfo{UserId: 42, IsDisabled: false} - scenario.getExternalUserInfoByLoginQueryReturns(externalUser) - - connection.bindProvider = func(username, password string) error { - return nil - } - server := &Server{ - Config: &ServerConfig{ - SearchBaseDNs: []string{"BaseDNHere"}, - }, - Connection: connection, - 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) - }) - - Convey("it should return invalid credentials error", func() { - So(err, ShouldEqual, ErrInvalidCredentials) - }) - }) - - serverScenario("When user not found in LDAP, and disabled in Grafana already", func(scenario *scenarioContext) { - 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"}, - }, - 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{ - 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) - }) - }) }) } diff --git a/pkg/services/ldap/ldap_private_test.go b/pkg/services/ldap/ldap_private_test.go new file mode 100644 index 00000000000..83de153ceab --- /dev/null +++ b/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) + }) + }) +} diff --git a/pkg/services/ldap/ldap_test.go b/pkg/services/ldap/ldap_test.go index 98b15ec4457..3aca78f2e9e 100644 --- a/pkg/services/ldap/ldap_test.go +++ b/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) }) }) } diff --git a/pkg/services/ldap/settings.go b/pkg/services/ldap/settings.go index df63f10a6fa..beba4c76c19 100644 --- a/pkg/services/ldap/settings.go +++ b/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 } } } diff --git a/pkg/services/ldap/test.go b/pkg/services/ldap/test.go deleted file mode 100644 index 6319cddd280..00000000000 --- a/pkg/services/ldap/test.go +++ /dev/null @@ -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) diff --git a/pkg/services/ldap/test_test.go b/pkg/services/ldap/test_test.go new file mode 100644 index 00000000000..fda9fd95a8c --- /dev/null +++ b/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 +} diff --git a/pkg/services/ldap/user.go b/pkg/services/ldap/user.go deleted file mode 100644 index 0e4a0063da2..00000000000 --- a/pkg/services/ldap/user.go +++ /dev/null @@ -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 -} diff --git a/pkg/services/multildap/test.go b/pkg/services/multildap/test_test.go similarity index 95% rename from pkg/services/multildap/test.go rename to pkg/services/multildap/test_test.go index 183f50d0fea..3802f5971b4 100644 --- a/pkg/services/multildap/test.go +++ b/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 { diff --git a/pkg/services/user/user.go b/pkg/services/user/user.go deleted file mode 100644 index 94762c811b0..00000000000 --- a/pkg/services/user/user.go +++ /dev/null @@ -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 -}