LDAP: Search all DNs for users (#38891)

This commit is contained in:
Emil Tullstedt 2021-09-14 10:49:37 +02:00 committed by GitHub
parent 98cca6317d
commit ad971cc9be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 896 additions and 864 deletions

View File

@ -12,9 +12,10 @@ import (
"strings" "strings"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
"gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
"gopkg.in/ldap.v3"
) )
// IConnection is interface for LDAP connection manipulation // IConnection is interface for LDAP connection manipulation
@ -252,16 +253,11 @@ func (server *Server) Users(logins []string) (
[]*models.ExternalUserInfo, []*models.ExternalUserInfo,
error, error,
) { ) {
var users []*ldap.Entry var users [][]*ldap.Entry
err := getUsersIteration(logins, func(previous, current int) error { err := getUsersIteration(logins, func(previous, current int) error {
entries, err := server.users(logins[previous:current]) var err error
if err != nil { users, err = server.users(logins[previous:current])
return err return err
}
users = append(users, entries...)
return nil
}) })
if err != nil { if err != nil {
return nil, err return nil, err
@ -308,13 +304,15 @@ func getUsersIteration(logins []string, fn func(int, int) error) error {
// users is helper method for the Users() // users is helper method for the Users()
func (server *Server) users(logins []string) ( func (server *Server) users(logins []string) (
[]*ldap.Entry, [][]*ldap.Entry,
error, error,
) { ) {
var result *ldap.SearchResult var result *ldap.SearchResult
var Config = server.Config var Config = server.Config
var err error var err error
var entries = make([][]*ldap.Entry, 0, len(Config.SearchBaseDNs))
for _, base := range Config.SearchBaseDNs { for _, base := range Config.SearchBaseDNs {
result, err = server.Connection.Search( result, err = server.Connection.Search(
server.getSearchRequest(base, logins), server.getSearchRequest(base, logins),
@ -324,11 +322,11 @@ func (server *Server) users(logins []string) (
} }
if len(result.Entries) > 0 { if len(result.Entries) > 0 {
break entries = append(entries, result.Entries)
} }
} }
return result.Entries, nil return entries, nil
} }
// validateGrafanaUser validates user access. // validateGrafanaUser validates user access.
@ -557,17 +555,26 @@ func (server *Server) requestMemberOf(entry *ldap.Entry) ([]string, error) {
// serializeUsers serializes the users // serializeUsers serializes the users
// from LDAP result to ExternalInfo struct // from LDAP result to ExternalInfo struct
func (server *Server) serializeUsers( func (server *Server) serializeUsers(
entries []*ldap.Entry, entries [][]*ldap.Entry,
) ([]*models.ExternalUserInfo, error) { ) ([]*models.ExternalUserInfo, error) {
var serialized []*models.ExternalUserInfo var serialized []*models.ExternalUserInfo
var users = map[string]struct{}{}
for _, user := range entries { for _, dn := range entries {
extUser, err := server.buildGrafanaUser(user) for _, user := range dn {
if err != nil { extUser, err := server.buildGrafanaUser(user)
return nil, err if err != nil {
return nil, err
}
if _, exists := users[extUser.Login]; exists {
// ignore duplicates
continue
}
users[extUser.Login] = struct{}{}
serialized = append(serialized, extUser)
} }
serialized = append(serialized, extUser)
} }
return serialized, nil return serialized, nil

View File

@ -1,191 +1,141 @@
package ldap package ldap
import ( import (
"fmt"
"testing" "testing"
. "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/assert"
"gopkg.in/ldap.v3" "gopkg.in/ldap.v3"
) )
func TestLDAPHelpers(t *testing.T) { func TestIsMemberOf(t *testing.T) {
Convey("isMemberOf()", t, func() { tests := []struct {
Convey("Wildcard", func() { memberOf []string
result := isMemberOf([]string{}, "*") group string
So(result, ShouldBeTrue) expected bool
}) }{
{memberOf: []string{}, group: "*", expected: true},
{memberOf: []string{"one", "Two", "three"}, group: "two", expected: true},
{memberOf: []string{"one", "Two", "three"}, group: "twos", expected: false},
}
Convey("Should find one", func() { for _, tc := range tests {
result := isMemberOf([]string{"one", "Two", "three"}, "two") t.Run(fmt.Sprintf("isMemberOf(%v, \"%s\") = %v", tc.memberOf, tc.group, tc.expected), func(t *testing.T) {
So(result, ShouldBeTrue) assert.Equal(t, tc.expected, isMemberOf(tc.memberOf, tc.group))
}) })
}
}
Convey("Should not find one", func() { func TestGetUsersIteration(t *testing.T) {
result := isMemberOf([]string{"one", "Two", "three"}, "twos") const pageSize = UsersMaxRequest
So(result, ShouldBeFalse) iterations := map[int]int{
}) 0: 0,
}) 400: 1,
600: 2,
1500: 3,
}
for userCount, expectedIterations := range iterations {
t.Run(fmt.Sprintf("getUserIteration iterates %d times for %d users", expectedIterations, userCount), func(t *testing.T) {
logins := make([]string, userCount)
Convey("getUsersIteration()", t, func() {
Convey("it should execute twice for 600 users", func() {
logins := make([]string, 600)
i := 0 i := 0
_ = getUsersIteration(logins, func(first int, last int) error {
assert.Equal(t, pageSize*i, first)
result := getUsersIteration(logins, func(previous, current int) error { expectedLast := pageSize*i + pageSize
i++ if expectedLast > userCount {
expectedLast = userCount
if i == 1 {
So(previous, ShouldEqual, 0)
So(current, ShouldEqual, 500)
} else {
So(previous, ShouldEqual, 500)
So(current, ShouldEqual, 600)
} }
return nil assert.Equal(t, expectedLast, last)
})
So(i, ShouldEqual, 2)
So(result, ShouldBeNil)
})
Convey("it should execute three times for 1500 users", func() {
logins := make([]string, 1500)
i := 0
result := getUsersIteration(logins, func(previous, current int) error {
i++
switch i {
case 1:
So(previous, ShouldEqual, 0)
So(current, ShouldEqual, 500)
case 2:
So(previous, ShouldEqual, 500)
So(current, ShouldEqual, 1000)
default:
So(previous, ShouldEqual, 1000)
So(current, ShouldEqual, 1500)
}
return nil
})
So(i, ShouldEqual, 3)
So(result, ShouldBeNil)
})
Convey("it should execute once for 400 users", func() {
logins := make([]string, 400)
i := 0
result := getUsersIteration(logins, func(previous, current int) error {
i++
if i == 1 {
So(previous, ShouldEqual, 0)
So(current, ShouldEqual, 400)
}
return nil
})
So(i, ShouldEqual, 1)
So(result, ShouldBeNil)
})
Convey("it should not execute for 0 users", func() {
logins := make([]string, 0)
i := 0
result := getUsersIteration(logins, func(previous, current int) error {
i++ i++
return nil return nil
}) })
So(i, ShouldEqual, 0) assert.Equal(t, expectedIterations, i)
So(result, ShouldBeNil)
}) })
}
}
func TestGetAttribute(t *testing.T) {
t.Run("DN", func(t *testing.T) {
entry := &ldap.Entry{
DN: "test",
}
result := getAttribute("dn", entry)
assert.Equal(t, "test", result)
}) })
Convey("getAttribute()", t, func() { t.Run("username", func(t *testing.T) {
Convey("Should get DN", func() { value := "roelgerrits"
entry := &ldap.Entry{ entry := &ldap.Entry{
DN: "test", Attributes: []*ldap.EntryAttribute{
} {
Name: "username", Values: []string{value},
result := getAttribute("dn", entry)
So(result, ShouldEqual, "test")
})
Convey("Should get username", func() {
value := []string{"roelgerrits"}
entry := &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{
Name: "username", Values: value,
},
}, },
} },
}
result := getAttribute("username", entry) result := getAttribute("username", entry)
assert.Equal(t, value, result)
So(result, ShouldEqual, value[0])
})
Convey("Should not get anything", func() {
value := []string{"roelgerrits"}
entry := &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{
Name: "killa", Values: value,
},
},
}
result := getAttribute("username", entry)
So(result, ShouldEqual, "")
})
}) })
Convey("getArrayAttribute()", t, func() { t.Run("no result", func(t *testing.T) {
Convey("Should get DN", func() { value := []string{"roelgerrits"}
entry := &ldap.Entry{ entry := &ldap.Entry{
DN: "test", Attributes: []*ldap.EntryAttribute{
} {
Name: "killa", Values: value,
result := getArrayAttribute("dn", entry)
So(result, ShouldResemble, []string{"test"})
})
Convey("Should get username", func() {
value := []string{"roelgerrits"}
entry := &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{
Name: "username", Values: value,
},
}, },
} },
}
result := getArrayAttribute("username", entry) result := getAttribute("username", entry)
assert.Empty(t, result)
So(result, ShouldResemble, value) })
}) }
Convey("Should not get anything", func() { func TestGetArrayAttribute(t *testing.T) {
value := []string{"roelgerrits"} t.Run("DN", func(t *testing.T) {
entry := &ldap.Entry{ entry := &ldap.Entry{
Attributes: []*ldap.EntryAttribute{ DN: "test",
{ }
Name: "username", Values: value,
}, result := getArrayAttribute("dn", entry)
},
} assert.EqualValues(t, []string{"test"}, result)
})
result := getArrayAttribute("something", entry)
t.Run("username", func(t *testing.T) {
So(result, ShouldResemble, []string{}) value := []string{"roelgerrits"}
}) entry := &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{
Name: "username", Values: value,
},
},
}
result := getArrayAttribute("username", entry)
assert.EqualValues(t, value, result)
})
t.Run("no result", func(t *testing.T) {
value := []string{"roelgerrits"}
entry := &ldap.Entry{
Attributes: []*ldap.EntryAttribute{
{
Name: "username", Values: value,
},
},
}
result := getArrayAttribute("something", entry)
assert.Empty(t, result)
}) })
} }

View File

@ -4,231 +4,227 @@ import (
"errors" "errors"
"testing" "testing"
. "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/ldap.v3" "gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
) )
func TestLDAPLogin(t *testing.T) { var defaultLogin = &models.LoginUserQuery{
defaultLogin := &models.LoginUserQuery{ Username: "user",
Username: "user", Password: "pwd",
Password: "pwd", IpAddress: "192.168.1.1:56433",
IpAddress: "192.168.1.1:56433", }
func TestServer_Login_UserBind_Fail(t *testing.T) {
connection := &MockConnection{}
entry := ldap.Entry{}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
connection.setSearchResult(&result)
connection.BindProvider = func(username, password string) error {
return &ldap.Error{
ResultCode: 49,
}
}
server := &Server{
Config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
Connection: connection,
log: log.New("test-logger"),
} }
Convey("Login()", t, func() { _, err := server.Login(defaultLogin)
Convey("Should get invalid credentials when userBind fails", func() {
connection := &MockConnection{}
entry := ldap.Entry{}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
connection.setSearchResult(&result)
connection.BindProvider = func(username, password string) error { assert.ErrorIs(t, err, ErrInvalidCredentials)
return &ldap.Error{ }
ResultCode: 49,
} func TestServer_Login_Search_NoResult(t *testing.T) {
} connection := &MockConnection{}
server := &Server{ result := ldap.SearchResult{Entries: []*ldap.Entry{}}
Config: &ServerConfig{ connection.setSearchResult(&result)
SearchBaseDNs: []string{"BaseDNHere"},
}, connection.BindProvider = func(username, password string) error {
Connection: connection, return nil
log: log.New("test-logger"), }
} server := &Server{
Config: &ServerConfig{
_, err := server.Login(defaultLogin) SearchBaseDNs: []string{"BaseDNHere"},
},
So(err, ShouldEqual, ErrInvalidCredentials) Connection: connection,
}) log: log.New("test-logger"),
}
Convey("Returns an error when search didn't find anything", func() {
connection := &MockConnection{} _, err := server.Login(defaultLogin)
result := ldap.SearchResult{Entries: []*ldap.Entry{}} assert.ErrorIs(t, err, ErrCouldNotFindUser)
connection.setSearchResult(&result) }
connection.BindProvider = func(username, password string) error { func TestServer_Login_Search_Error(t *testing.T) {
return nil connection := &MockConnection{}
} expected := errors.New("Killa-gorilla")
server := &Server{ connection.setSearchError(expected)
Config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"}, connection.BindProvider = func(username, password string) error {
}, return nil
Connection: connection, }
log: log.New("test-logger"), server := &Server{
} Config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
_, err := server.Login(defaultLogin) },
Connection: connection,
So(err, ShouldEqual, ErrCouldNotFindUser) log: log.New("test-logger"),
}) }
Convey("When search returns an error", func() { _, err := server.Login(defaultLogin)
connection := &MockConnection{} assert.ErrorIs(t, err, expected)
expected := errors.New("Killa-gorilla") }
connection.setSearchError(expected)
func TestServer_Login_ValidCredentials(t *testing.T) {
connection.BindProvider = func(username, password string) error { connection := &MockConnection{}
return nil entry := ldap.Entry{
} DN: "dn", Attributes: []*ldap.EntryAttribute{
server := &Server{ {Name: "username", Values: []string{"markelog"}},
Config: &ServerConfig{ {Name: "surname", Values: []string{"Gaidarenko"}},
SearchBaseDNs: []string{"BaseDNHere"}, {Name: "email", Values: []string{"markelog@gmail.com"}},
}, {Name: "name", Values: []string{"Oleg"}},
Connection: connection, {Name: "memberof", Values: []string{"admins"}},
log: log.New("test-logger"), },
} }
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
_, err := server.Login(defaultLogin) connection.setSearchResult(&result)
So(err, ShouldEqual, expected) connection.BindProvider = func(username, password string) error {
}) return nil
}
Convey("When login with valid credentials", func() { server := &Server{
connection := &MockConnection{} Config: &ServerConfig{
entry := ldap.Entry{ Attr: AttributeMap{
DN: "dn", Attributes: []*ldap.EntryAttribute{ Username: "username",
{Name: "username", Values: []string{"markelog"}}, Name: "name",
{Name: "surname", Values: []string{"Gaidarenko"}}, MemberOf: "memberof",
{Name: "email", Values: []string{"markelog@gmail.com"}}, },
{Name: "name", Values: []string{"Oleg"}}, SearchBaseDNs: []string{"BaseDNHere"},
{Name: "memberof", Values: []string{"admins"}}, },
}, Connection: connection,
} log: log.New("test-logger"),
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} }
connection.setSearchResult(&result)
resp, err := server.Login(defaultLogin)
connection.BindProvider = func(username, password string) error { require.NoError(t, err)
return nil assert.Equal(t, "markelog", resp.Login)
} }
server := &Server{
Config: &ServerConfig{ // TestServer_Login_UnauthenticatedBind tests that unauthenticated bind
Attr: AttributeMap{ // is called when there is no admin password or user wildcard in the
Username: "username", // bind_dn.
Name: "name", func TestServer_Login_UnauthenticatedBind(t *testing.T) {
MemberOf: "memberof", connection := &MockConnection{}
}, entry := ldap.Entry{
SearchBaseDNs: []string{"BaseDNHere"}, DN: "test",
}, }
Connection: connection, result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
log: log.New("test-logger"), connection.setSearchResult(&result)
}
connection.UnauthenticatedBindProvider = func() error {
resp, err := server.Login(defaultLogin) return nil
}
So(err, ShouldBeNil) server := &Server{
So(resp.Login, ShouldEqual, "markelog") Config: &ServerConfig{
}) SearchBaseDNs: []string{"BaseDNHere"},
},
Convey("Should perform unauthenticated bind without admin", func() { Connection: connection,
connection := &MockConnection{} log: log.New("test-logger"),
entry := ldap.Entry{ }
DN: "test",
} user, err := server.Login(defaultLogin)
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} require.NoError(t, err)
connection.setSearchResult(&result) assert.Equal(t, "test", user.AuthId)
assert.True(t, connection.UnauthenticatedBindCalled)
connection.UnauthenticatedBindProvider = func() error { }
return nil
} func TestServer_Login_AuthenticatedBind(t *testing.T) {
server := &Server{ connection := &MockConnection{}
Config: &ServerConfig{ entry := ldap.Entry{
SearchBaseDNs: []string{"BaseDNHere"}, DN: "test",
}, }
Connection: connection, result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
log: log.New("test-logger"), connection.setSearchResult(&result)
}
adminUsername := ""
user, err := server.Login(defaultLogin) adminPassword := ""
username := ""
So(err, ShouldBeNil) password := ""
So(user.AuthId, ShouldEqual, "test")
So(connection.UnauthenticatedBindCalled, ShouldBeTrue) i := 0
}) connection.BindProvider = func(name, pass string) error {
i++
Convey("Should perform authenticated binds", func() { if i == 1 {
connection := &MockConnection{} adminUsername = name
entry := ldap.Entry{ adminPassword = pass
DN: "test", }
}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} if i == 2 {
connection.setSearchResult(&result) username = name
password = pass
adminUsername := "" }
adminPassword := ""
username := "" return nil
password := "" }
server := &Server{
i := 0 Config: &ServerConfig{
connection.BindProvider = func(name, pass string) error { BindDN: "killa",
i++ BindPassword: "gorilla",
if i == 1 { SearchBaseDNs: []string{"BaseDNHere"},
adminUsername = name },
adminPassword = pass Connection: connection,
} log: log.New("test-logger"),
}
if i == 2 {
username = name user, err := server.Login(defaultLogin)
password = pass require.NoError(t, err)
}
assert.Equal(t, "test", user.AuthId)
return nil assert.True(t, connection.BindCalled)
}
server := &Server{ assert.Equal(t, "killa", adminUsername)
Config: &ServerConfig{ assert.Equal(t, "gorilla", adminPassword)
BindDN: "killa",
BindPassword: "gorilla", assert.Equal(t, "test", username)
SearchBaseDNs: []string{"BaseDNHere"}, assert.Equal(t, "pwd", password)
}, }
Connection: connection,
log: log.New("test-logger"), func TestServer_Login_UserWildcardBind(t *testing.T) {
} connection := &MockConnection{}
entry := ldap.Entry{
user, err := server.Login(defaultLogin) DN: "test",
}
So(err, ShouldBeNil) connection.setSearchResult(&ldap.SearchResult{Entries: []*ldap.Entry{&entry}})
So(user.AuthId, ShouldEqual, "test") authBindUser := ""
So(connection.BindCalled, ShouldBeTrue) authBindPassword := ""
So(adminUsername, ShouldEqual, "killa") connection.BindProvider = func(name, pass string) error {
So(adminPassword, ShouldEqual, "gorilla") authBindUser = name
authBindPassword = pass
So(username, ShouldEqual, "test") return nil
So(password, ShouldEqual, "pwd") }
}) server := &Server{
Convey("Should bind with user if %s exists in the bind_dn", func() { Config: &ServerConfig{
connection := &MockConnection{} BindDN: "cn=%s,ou=users,dc=grafana,dc=org",
entry := ldap.Entry{ SearchBaseDNs: []string{"BaseDNHere"},
DN: "test", },
} Connection: connection,
connection.setSearchResult(&ldap.SearchResult{Entries: []*ldap.Entry{&entry}}) log: log.New("test-logger"),
}
authBindUser := ""
authBindPassword := "" _, err := server.Login(defaultLogin)
require.NoError(t, err)
connection.BindProvider = func(name, pass string) error {
authBindUser = name assert.Equal(t, "cn=user,ou=users,dc=grafana,dc=org", authBindUser)
authBindPassword = pass assert.Equal(t, "pwd", authBindPassword)
return nil assert.True(t, connection.BindCalled)
}
server := &Server{
Config: &ServerConfig{
BindDN: "cn=%s,ou=users,dc=grafana,dc=org",
SearchBaseDNs: []string{"BaseDNHere"},
},
Connection: connection,
log: log.New("test-logger"),
}
_, err := server.Login(defaultLogin)
So(err, ShouldBeNil)
So(authBindUser, ShouldEqual, "cn=user,ou=users,dc=grafana,dc=org")
So(authBindPassword, ShouldEqual, "pwd")
So(connection.BindCalled, ShouldBeTrue)
})
})
} }

View File

@ -3,271 +3,252 @@ package ldap
import ( import (
"testing" "testing"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/assert"
"gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
"gopkg.in/ldap.v3"
) )
func TestLDAPPrivateMethods(t *testing.T) { func TestServer_getSearchRequest(t *testing.T) {
Convey("getSearchRequest()", t, func() { expected := &ldap.SearchRequest{
Convey("with enabled GroupSearchFilterUserAttribute setting", func() { BaseDN: "killa",
server := &Server{ Scope: 2,
Config: &ServerConfig{ DerefAliases: 0,
Attr: AttributeMap{ SizeLimit: 0,
Username: "username", TimeLimit: 0,
Name: "name", TypesOnly: false,
MemberOf: "memberof", Filter: "(|)",
Email: "email", Attributes: []string{
}, "username",
GroupSearchFilterUserAttribute: "gansta", "email",
SearchBaseDNs: []string{"BaseDNHere"}, "name",
}, "memberof",
log: log.New("test-logger"), "gansta",
} },
Controls: nil,
}
result := server.getSearchRequest("killa", []string{"gorilla"}) server := &Server{
Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
MemberOf: "memberof",
Email: "email",
},
GroupSearchFilterUserAttribute: "gansta",
SearchBaseDNs: []string{"BaseDNHere"},
},
log: log.New("test-logger"),
}
So(result, ShouldResemble, &ldap.SearchRequest{ result := server.getSearchRequest("killa", []string{"gorilla"})
BaseDN: "killa",
Scope: 2, assert.EqualValues(t, expected, result)
DerefAliases: 0, }
SizeLimit: 0,
TimeLimit: 0, func TestSerializeUsers(t *testing.T) {
TypesOnly: false, t.Run("simple case", func(t *testing.T) {
Filter: "(|)", server := &Server{
Attributes: []string{ Config: &ServerConfig{
"username", Attr: AttributeMap{
"email", Username: "username",
"name", Name: "name",
"memberof", MemberOf: "memberof",
"gansta", Email: "email",
}, },
Controls: nil, 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.Entry{{&entry}}
result, err := server.serializeUsers(users)
require.NoError(t, err)
assert.Equal(t, "roelgerrits", result[0].Login)
assert.Equal(t, "roel@test.com", result[0].Email)
assert.Contains(t, result[0].Groups, "admins")
}) })
Convey("serializeUsers()", t, func() { t.Run("without lastname", func(t *testing.T) {
Convey("simple case", func() { server := &Server{
server := &Server{ Config: &ServerConfig{
Config: &ServerConfig{ Attr: AttributeMap{
Attr: AttributeMap{ Username: "username",
Username: "username", Name: "name",
Name: "name", MemberOf: "memberof",
MemberOf: "memberof", Email: "email",
Email: "email",
},
SearchBaseDNs: []string{"BaseDNHere"},
}, },
Connection: &MockConnection{}, SearchBaseDNs: []string{"BaseDNHere"},
log: log.New("test-logger"), },
} Connection: &MockConnection{},
log: log.New("test-logger"),
}
entry := ldap.Entry{ entry := ldap.Entry{
DN: "dn", DN: "dn",
Attributes: []*ldap.EntryAttribute{ Attributes: []*ldap.EntryAttribute{
{Name: "username", Values: []string{"roelgerrits"}}, {Name: "username", Values: []string{"roelgerrits"}},
{Name: "surname", Values: []string{"Gerrits"}}, {Name: "email", Values: []string{"roel@test.com"}},
{Name: "email", Values: []string{"roel@test.com"}}, {Name: "name", Values: []string{"Roel"}},
{Name: "name", Values: []string{"Roel"}}, {Name: "memberof", Values: []string{"admins"}},
{Name: "memberof", Values: []string{"admins"}}, },
}, }
} users := [][]*ldap.Entry{{&entry}}
users := []*ldap.Entry{&entry}
result, err := server.serializeUsers(users) result, err := server.serializeUsers(users)
require.NoError(t, err)
So(err, ShouldBeNil) assert.False(t, result[0].IsDisabled)
So(result[0].Login, ShouldEqual, "roelgerrits") assert.Equal(t, "Roel", result[0].Name)
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.Entry{&entry}
result, err := server.serializeUsers(users)
So(err, ShouldBeNil)
So(result[0].IsDisabled, ShouldBeFalse)
So(result[0].Name, ShouldEqual, "Roel")
})
Convey("a user without matching groups should be marked as disabled", func() {
server := &Server{
Config: &ServerConfig{
Groups: []*GroupToOrgRole{{
GroupDN: "foo",
OrgId: 1,
OrgRole: models.ROLE_EDITOR,
}},
},
Connection: &MockConnection{},
log: log.New("test-logger"),
}
entry := ldap.Entry{
DN: "dn",
Attributes: []*ldap.EntryAttribute{
{Name: "memberof", Values: []string{"admins"}},
},
}
users := []*ldap.Entry{&entry}
result, err := server.serializeUsers(users)
So(err, ShouldBeNil)
So(len(result), ShouldEqual, 1)
So(result[0].IsDisabled, ShouldBeTrue)
})
}) })
Convey("validateGrafanaUser()", t, func() { t.Run("mark user without matching group as disabled", func(t *testing.T) {
Convey("Returns error when user does not belong in any of the specified LDAP groups", func() { server := &Server{
server := &Server{ Config: &ServerConfig{
Config: &ServerConfig{ Groups: []*GroupToOrgRole{{
Groups: []*GroupToOrgRole{ GroupDN: "foo",
{ OrgId: 1,
OrgId: 1, OrgRole: models.ROLE_EDITOR,
}, }},
}, },
}, Connection: &MockConnection{},
log: logger.New("test"), log: log.New("test-logger"),
} }
user := &models.ExternalUserInfo{ entry := ldap.Entry{
Login: "markelog", DN: "dn",
} Attributes: []*ldap.EntryAttribute{
{Name: "memberof", Values: []string{"admins"}},
},
}
users := [][]*ldap.Entry{{&entry}}
result := server.validateGrafanaUser(user) result, err := server.serializeUsers(users)
require.NoError(t, err)
So(result, ShouldEqual, ErrInvalidCredentials) assert.Len(t, result, 1)
}) assert.True(t, result[0].IsDisabled)
})
Convey("Does not return error when group config is empty", func() { }
server := &Server{
Config: &ServerConfig{ func TestServer_validateGrafanaUser(t *testing.T) {
Groups: []*GroupToOrgRole{}, t.Run("no group config", func(t *testing.T) {
}, server := &Server{
log: logger.New("test"), Config: &ServerConfig{
} Groups: []*GroupToOrgRole{},
},
user := &models.ExternalUserInfo{ log: logger.New("test"),
Login: "markelog", }
}
user := &models.ExternalUserInfo{
result := server.validateGrafanaUser(user) Login: "markelog",
}
So(result, ShouldBeNil)
}) err := server.validateGrafanaUser(user)
require.NoError(t, err)
Convey("Does not return error when groups are there", func() { })
server := &Server{
Config: &ServerConfig{ t.Run("user in group", func(t *testing.T) {
Groups: []*GroupToOrgRole{ server := &Server{
{ Config: &ServerConfig{
OrgId: 1, Groups: []*GroupToOrgRole{
}, {
}, OrgId: 1,
}, },
log: logger.New("test"), },
} },
log: logger.New("test"),
user := &models.ExternalUserInfo{ }
Login: "markelog",
OrgRoles: map[int64]models.RoleType{ user := &models.ExternalUserInfo{
1: "test", Login: "markelog",
}, OrgRoles: map[int64]models.RoleType{
} 1: "test",
},
result := server.validateGrafanaUser(user) }
So(result, ShouldBeNil) err := server.validateGrafanaUser(user)
}) require.NoError(t, err)
}) })
Convey("shouldAdminBind()", t, func() { t.Run("user not in group", func(t *testing.T) {
Convey("it should require admin userBind", func() { server := &Server{
server := &Server{ Config: &ServerConfig{
Config: &ServerConfig{ Groups: []*GroupToOrgRole{
BindPassword: "test", {
}, OrgId: 1,
} },
},
result := server.shouldAdminBind() },
So(result, ShouldBeTrue) log: logger.New("test"),
}) }
Convey("it should not require admin userBind", func() { user := &models.ExternalUserInfo{
server := &Server{ Login: "markelog",
Config: &ServerConfig{ }
BindPassword: "",
}, err := server.validateGrafanaUser(user)
} require.ErrorIs(t, err, ErrInvalidCredentials)
})
result := server.shouldAdminBind() }
So(result, ShouldBeFalse)
}) func TestServer_binds(t *testing.T) {
}) t.Run("single bind with cn wildcard", func(t *testing.T) {
server := &Server{
Convey("shouldSingleBind()", t, func() { Config: &ServerConfig{
Convey("it should allow single bind", func() { BindDN: "cn=%s,dc=grafana,dc=org",
server := &Server{ },
Config: &ServerConfig{ }
BindDN: "cn=%s,dc=grafana,dc=org",
}, assert.True(t, server.shouldSingleBind())
} assert.Equal(t, "cn=test,dc=grafana,dc=org", server.singleBindDN("test"))
})
result := server.shouldSingleBind()
So(result, ShouldBeTrue) t.Run("don't single bind", func(t *testing.T) {
}) server := &Server{
Config: &ServerConfig{
Convey("it should not allow single bind", func() { BindDN: "cn=admin,dc=grafana,dc=org",
server := &Server{ },
Config: &ServerConfig{ }
BindDN: "cn=admin,dc=grafana,dc=org",
}, assert.False(t, server.shouldSingleBind())
} })
result := server.shouldSingleBind() t.Run("admin user bind", func(t *testing.T) {
So(result, ShouldBeFalse) server := &Server{
}) Config: &ServerConfig{
}) BindPassword: "test",
},
Convey("singleBindDN()", t, func() { }
Convey("it should allow single bind", func() {
server := &Server{ assert.True(t, server.shouldAdminBind())
Config: &ServerConfig{ })
BindDN: "cn=%s,dc=grafana,dc=org",
}, t.Run("don't admin user bind", func(t *testing.T) {
} server := &Server{
Config: &ServerConfig{
result := server.singleBindDN("test") BindPassword: "",
So(result, ShouldEqual, "cn=test,dc=grafana,dc=org") },
}) }
assert.False(t, server.shouldAdminBind())
}) })
} }

View File

@ -2,226 +2,319 @@ package ldap
import ( import (
"errors" "errors"
"fmt"
"testing" "testing"
"github.com/grafana/grafana/pkg/infra/log" "github.com/stretchr/testify/assert"
. "github.com/smartystreets/goconvey/convey" "github.com/stretchr/testify/require"
"gopkg.in/ldap.v3" "gopkg.in/ldap.v3"
"github.com/grafana/grafana/pkg/infra/log"
) )
func TestPublicAPI(t *testing.T) { func TestNew(t *testing.T) {
Convey("New()", t, func() { result := New(&ServerConfig{
Convey("Should return ", func() { Attr: AttributeMap{},
result := New(&ServerConfig{ SearchBaseDNs: []string{"BaseDNHere"},
})
assert.Implements(t, (*IServer)(nil), result)
}
func TestServer_Close(t *testing.T) {
t.Run("close the connection", func(t *testing.T) {
connection := &MockConnection{}
server := &Server{
Config: &ServerConfig{
Attr: AttributeMap{}, Attr: AttributeMap{},
SearchBaseDNs: []string{"BaseDNHere"}, SearchBaseDNs: []string{"BaseDNHere"},
}) },
Connection: connection,
}
So(result, ShouldImplement, (*IServer)(nil)) assert.NotPanics(t, server.Close)
}) assert.True(t, connection.CloseCalled)
}) })
Convey("Close()", t, func() { t.Run("panic if no connection", func(t *testing.T) {
Convey("Should close the connection", func() { server := &Server{
connection := &MockConnection{} Config: &ServerConfig{
Attr: AttributeMap{},
SearchBaseDNs: []string{"BaseDNHere"},
},
Connection: nil,
}
server := &Server{ assert.Panics(t, server.Close)
Config: &ServerConfig{ })
Attr: AttributeMap{}, }
SearchBaseDNs: []string{"BaseDNHere"},
}, func TestServer_Users(t *testing.T) {
Connection: connection, t.Run("one user", func(t *testing.T) {
} conn := &MockConnection{}
entry := ldap.Entry{
So(server.Close, ShouldNotPanic) DN: "dn", Attributes: []*ldap.EntryAttribute{
So(connection.CloseCalled, ShouldBeTrue) {Name: "username", Values: []string{"roelgerrits"}},
}) {Name: "surname", Values: []string{"Gerrits"}},
{Name: "email", Values: []string{"roel@test.com"}},
Convey("Should panic if no connection is established", func() { {Name: "name", Values: []string{"Roel"}},
server := &Server{ {Name: "memberof", Values: []string{"admins"}},
Config: &ServerConfig{ }}
Attr: AttributeMap{}, result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
SearchBaseDNs: []string{"BaseDNHere"}, conn.setSearchResult(&result)
},
Connection: nil, // Set up attribute map without surname and email
} server := &Server{
Config: &ServerConfig{
So(server.Close, ShouldPanic) Attr: AttributeMap{
}) Username: "username",
}) Name: "name",
Convey("Users()", t, func() { MemberOf: "memberof",
Convey("Finds one user", func() { },
MockConnection := &MockConnection{} SearchBaseDNs: []string{"BaseDNHere"},
entry := ldap.Entry{ },
DN: "dn", Attributes: []*ldap.EntryAttribute{ Connection: conn,
{Name: "username", Values: []string{"roelgerrits"}}, log: log.New("test-logger"),
{Name: "surname", Values: []string{"Gerrits"}}, }
{Name: "email", Values: []string{"roel@test.com"}},
{Name: "name", Values: []string{"Roel"}}, searchResult, err := server.Users([]string{"roelgerrits"})
{Name: "memberof", Values: []string{"admins"}},
}} require.NoError(t, err)
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}} assert.NotNil(t, searchResult)
MockConnection.setSearchResult(&result)
// User should be searched in ldap
// Set up attribute map without surname and email assert.True(t, conn.SearchCalled)
server := &Server{ // No empty attributes should be added to the search request
Config: &ServerConfig{ assert.Len(t, conn.SearchAttributes, 3)
Attr: AttributeMap{ })
Username: "username",
Name: "name", t.Run("error", func(t *testing.T) {
MemberOf: "memberof", expected := errors.New("Killa-gorilla")
}, conn := &MockConnection{}
SearchBaseDNs: []string{"BaseDNHere"}, conn.setSearchError(expected)
},
Connection: MockConnection, // Set up attribute map without surname and email
log: log.New("test-logger"), server := &Server{
} Config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
searchResult, err := server.Users([]string{"roelgerrits"}) },
Connection: conn,
So(err, ShouldBeNil) log: log.New("test-logger"),
So(searchResult, ShouldNotBeNil) }
// User should be searched in ldap _, err := server.Users([]string{"roelgerrits"})
So(MockConnection.SearchCalled, ShouldBeTrue)
assert.ErrorIs(t, err, expected)
// No empty attributes should be added to the search request })
So(len(MockConnection.SearchAttributes), ShouldEqual, 3)
}) t.Run("no user", func(t *testing.T) {
conn := &MockConnection{}
Convey("Handles a error", func() { result := ldap.SearchResult{Entries: []*ldap.Entry{}}
expected := errors.New("Killa-gorilla") conn.setSearchResult(&result)
MockConnection := &MockConnection{}
MockConnection.setSearchError(expected) // Set up attribute map without surname and email
server := &Server{
// Set up attribute map without surname and email Config: &ServerConfig{
server := &Server{ SearchBaseDNs: []string{"BaseDNHere"},
Config: &ServerConfig{ },
SearchBaseDNs: []string{"BaseDNHere"}, Connection: conn,
}, log: log.New("test-logger"),
Connection: MockConnection, }
log: log.New("test-logger"),
} searchResult, err := server.Users([]string{"roelgerrits"})
_, err := server.Users([]string{"roelgerrits"}) require.NoError(t, err)
assert.Empty(t, searchResult)
So(err, ShouldEqual, expected) })
})
t.Run("multiple DNs", func(t *testing.T) {
Convey("Should return empty slice if none were found", func() { conn := &MockConnection{}
MockConnection := &MockConnection{} serviceDN := "dc=svc,dc=example,dc=org"
result := ldap.SearchResult{Entries: []*ldap.Entry{}} serviceEntry := ldap.Entry{
MockConnection.setSearchResult(&result) DN: "dn", Attributes: []*ldap.EntryAttribute{
{Name: "username", Values: []string{"imgrenderer"}},
// Set up attribute map without surname and email {Name: "name", Values: []string{"Image renderer"}},
server := &Server{ }}
Config: &ServerConfig{ services := ldap.SearchResult{Entries: []*ldap.Entry{&serviceEntry}}
SearchBaseDNs: []string{"BaseDNHere"},
}, userDN := "dc=users,dc=example,dc=org"
Connection: MockConnection, userEntry := ldap.Entry{
log: log.New("test-logger"), DN: "dn", Attributes: []*ldap.EntryAttribute{
} {Name: "username", Values: []string{"grot"}},
{Name: "name", Values: []string{"Grot"}},
searchResult, err := server.Users([]string{"roelgerrits"}) }}
users := ldap.SearchResult{Entries: []*ldap.Entry{&userEntry}}
So(err, ShouldBeNil)
So(searchResult, ShouldBeEmpty) conn.setSearchFunc(func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
}) switch request.BaseDN {
}) case userDN:
return &users, nil
Convey("UserBind()", t, func() { case serviceDN:
Convey("Should use provided DN and password", func() { return &services, nil
connection := &MockConnection{} default:
var actualUsername, actualPassword string return nil, fmt.Errorf("test case not defined for baseDN: '%s'", request.BaseDN)
connection.BindProvider = func(username, password string) error { }
actualUsername = username })
actualPassword = password
return nil server := &Server{
} Config: &ServerConfig{
server := &Server{ Attr: AttributeMap{
Connection: connection, Username: "username",
Config: &ServerConfig{ Name: "name",
BindDN: "cn=admin,dc=grafana,dc=org", },
}, SearchBaseDNs: []string{serviceDN, userDN},
} },
Connection: conn,
dn := "cn=user,ou=users,dc=grafana,dc=org" log: log.New("test-logger"),
err := server.UserBind(dn, "pwd") }
So(err, ShouldBeNil) searchResult, err := server.Users([]string{"imgrenderer", "grot"})
So(actualUsername, ShouldEqual, dn) require.NoError(t, err)
So(actualPassword, ShouldEqual, "pwd")
}) assert.Len(t, searchResult, 2)
})
Convey("Should handle an error", func() {
connection := &MockConnection{} t.Run("same user in multiple DNs", func(t *testing.T) {
expected := &ldap.Error{ conn := &MockConnection{}
ResultCode: uint16(25), firstDN := "dc=users1,dc=example,dc=org"
} firstEntry := ldap.Entry{
connection.BindProvider = func(username, password string) error { DN: "dn", Attributes: []*ldap.EntryAttribute{
return expected {Name: "username", Values: []string{"grot"}},
} {Name: "name", Values: []string{"Grot the First"}},
server := &Server{ }}
Connection: connection, firsts := ldap.SearchResult{Entries: []*ldap.Entry{&firstEntry}}
Config: &ServerConfig{
BindDN: "cn=%s,ou=users,dc=grafana,dc=org", secondDN := "dc=users2,dc=example,dc=org"
}, secondEntry := ldap.Entry{
log: log.New("test-logger"), DN: "dn", Attributes: []*ldap.EntryAttribute{
} {Name: "username", Values: []string{"grot"}},
err := server.UserBind("user", "pwd") {Name: "name", Values: []string{"Grot the Second"}},
So(err, ShouldEqual, expected) }}
}) seconds := ldap.SearchResult{Entries: []*ldap.Entry{&secondEntry}}
})
conn.setSearchFunc(func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
Convey("AdminBind()", t, func() { switch request.BaseDN {
Convey("Should use admin DN and password", func() { case secondDN:
connection := &MockConnection{} return &seconds, nil
var actualUsername, actualPassword string case firstDN:
connection.BindProvider = func(username, password string) error { return &firsts, nil
actualUsername = username default:
actualPassword = password return nil, fmt.Errorf("test case not defined for baseDN: '%s'", request.BaseDN)
return nil }
} })
dn := "cn=admin,dc=grafana,dc=org" server := &Server{
Config: &ServerConfig{
server := &Server{ Attr: AttributeMap{
Connection: connection, Username: "username",
Config: &ServerConfig{ Name: "name",
BindPassword: "pwd", },
BindDN: dn, SearchBaseDNs: []string{firstDN, secondDN},
}, },
} Connection: conn,
log: log.New("test-logger"),
err := server.AdminBind() }
So(err, ShouldBeNil) res, err := server.Users([]string{"grot"})
So(actualUsername, ShouldEqual, dn) require.NoError(t, err)
So(actualPassword, ShouldEqual, "pwd") require.Len(t, res, 1)
}) assert.Equal(t, "Grot the First", res[0].Name)
})
Convey("Should handle an error", func() { }
connection := &MockConnection{}
expected := &ldap.Error{ func TestServer_UserBind(t *testing.T) {
ResultCode: uint16(25), t.Run("use provided DN and password", func(t *testing.T) {
} connection := &MockConnection{}
connection.BindProvider = func(username, password string) error { var actualUsername, actualPassword string
return expected connection.BindProvider = func(username, password string) error {
} actualUsername = username
actualPassword = password
dn := "cn=admin,dc=grafana,dc=org" return nil
}
server := &Server{ server := &Server{
Connection: connection, Connection: connection,
Config: &ServerConfig{ Config: &ServerConfig{
BindPassword: "pwd", BindDN: "cn=admin,dc=grafana,dc=org",
BindDN: dn, },
}, }
log: log.New("test-logger"),
} dn := "cn=user,ou=users,dc=grafana,dc=org"
err := server.UserBind(dn, "pwd")
err := server.AdminBind()
So(err, ShouldEqual, expected) require.NoError(t, err)
}) assert.Equal(t, dn, actualUsername)
assert.Equal(t, "pwd", actualPassword)
})
t.Run("error", func(t *testing.T) {
connection := &MockConnection{}
expected := &ldap.Error{
ResultCode: uint16(25),
}
connection.BindProvider = func(username, password string) error {
return expected
}
server := &Server{
Connection: connection,
Config: &ServerConfig{
BindDN: "cn=%s,ou=users,dc=grafana,dc=org",
},
log: log.New("test-logger"),
}
err := server.UserBind("user", "pwd")
assert.ErrorIs(t, err, expected)
})
}
func TestServer_AdminBind(t *testing.T) {
t.Run("use admin DN and password", func(t *testing.T) {
connection := &MockConnection{}
var actualUsername, actualPassword string
connection.BindProvider = func(username, password string) error {
actualUsername = username
actualPassword = password
return nil
}
dn := "cn=admin,dc=grafana,dc=org"
server := &Server{
Connection: connection,
Config: &ServerConfig{
BindPassword: "pwd",
BindDN: dn,
},
}
err := server.AdminBind()
require.NoError(t, err)
assert.Equal(t, dn, actualUsername)
assert.Equal(t, "pwd", actualPassword)
})
t.Run("error", func(t *testing.T) {
connection := &MockConnection{}
expected := &ldap.Error{
ResultCode: uint16(25),
}
connection.BindProvider = func(username, password string) error {
return expected
}
dn := "cn=admin,dc=grafana,dc=org"
server := &Server{
Connection: connection,
Config: &ServerConfig{
BindPassword: "pwd",
BindDN: dn,
},
log: log.New("test-logger"),
}
err := server.AdminBind()
assert.ErrorIs(t, err, expected)
}) })
} }

View File

@ -6,10 +6,11 @@ import (
"gopkg.in/ldap.v3" "gopkg.in/ldap.v3"
) )
type searchFunc = func(request *ldap.SearchRequest) (*ldap.SearchResult, error)
// MockConnection struct for testing // MockConnection struct for testing
type MockConnection struct { type MockConnection struct {
SearchResult *ldap.SearchResult SearchFunc searchFunc
SearchError error
SearchCalled bool SearchCalled bool
SearchAttributes []string SearchAttributes []string
@ -56,11 +57,19 @@ func (c *MockConnection) Close() {
} }
func (c *MockConnection) setSearchResult(result *ldap.SearchResult) { func (c *MockConnection) setSearchResult(result *ldap.SearchResult) {
c.SearchResult = result c.SearchFunc = func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
return result, nil
}
} }
func (c *MockConnection) setSearchError(err error) { func (c *MockConnection) setSearchError(err error) {
c.SearchError = err c.SearchFunc = func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
return nil, err
}
}
func (c *MockConnection) setSearchFunc(fn searchFunc) {
c.SearchFunc = fn
} }
// Search mocks Search connection function // Search mocks Search connection function
@ -68,11 +77,7 @@ func (c *MockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, err
c.SearchCalled = true c.SearchCalled = true
c.SearchAttributes = sr.Attributes c.SearchAttributes = sr.Attributes
if c.SearchError != nil { return c.SearchFunc(sr)
return nil, c.SearchError
}
return c.SearchResult, nil
} }
// Add mocks Add connection function // Add mocks Add connection function