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
This commit is contained in:
Oleg Gaidarenko 2019-06-13 17:47:52 +03:00 committed by Leonard Gram
parent c78b6e2a67
commit 1b1d951495
16 changed files with 579 additions and 801 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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