Commit de92c360 by Oleg Gaidarenko Committed by GitHub

LDAP: reduce API and allow its extension (#17209)

* Removes Add/Remove methods

* Publicise necessary fields and methods so we could extend it

* Publicise mock API

* More comments and additional simplifications

* Sync with master

Still having low coverage :/ - should be addressed in #17208
parent 5884e235
......@@ -13,7 +13,7 @@ func TestLDAPHelpers(t *testing.T) {
Convey("serializeUsers()", t, func() {
Convey("simple case", func() {
server := &Server{
config: &ServerConfig{
Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
......@@ -22,7 +22,7 @@ func TestLDAPHelpers(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: &mockConnection{},
Connection: &MockConnection{},
log: log.New("test-logger"),
}
......@@ -46,7 +46,7 @@ func TestLDAPHelpers(t *testing.T) {
Convey("without lastname", func() {
server := &Server{
config: &ServerConfig{
Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
......@@ -55,7 +55,7 @@ func TestLDAPHelpers(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: &mockConnection{},
Connection: &MockConnection{},
log: log.New("test-logger"),
}
......@@ -75,74 +75,9 @@ func TestLDAPHelpers(t *testing.T) {
})
})
Convey("initialBind", 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: "cn=%s,o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := server.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(server.requireSecondBind, ShouldBeTrue)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn 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: "cn=%s,o=users,dc=grafana,dc=org",
},
}
err := server.initialBind("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() {
connection := &mockConnection{}
unauthenticatedBindWasCalled := false
var actualUsername string
connection.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
server := &Server{
connection: connection,
config: &ServerConfig{},
}
err := server.initialBind("user", "pwd")
So(err, ShouldBeNil)
So(server.requireSecondBind, ShouldBeTrue)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
Convey("serverBind()", t, func() {
Convey("Given bind dn and password configured", func() {
connection := &mockConnection{}
connection := &MockConnection{}
var actualUsername, actualPassword string
connection.bindProvider = func(username, password string) error {
actualUsername = username
......@@ -150,8 +85,8 @@ func TestLDAPHelpers(t *testing.T) {
return nil
}
server := &Server{
connection: connection,
config: &ServerConfig{
Connection: connection,
Config: &ServerConfig{
BindDN: "o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
......@@ -163,7 +98,7 @@ func TestLDAPHelpers(t *testing.T) {
})
Convey("Given bind dn configured", func() {
connection := &mockConnection{}
connection := &MockConnection{}
unauthenticatedBindWasCalled := false
var actualUsername string
connection.unauthenticatedBindProvider = func(username string) error {
......@@ -172,8 +107,8 @@ func TestLDAPHelpers(t *testing.T) {
return nil
}
server := &Server{
connection: connection,
config: &ServerConfig{
Connection: connection,
Config: &ServerConfig{
BindDN: "o=users,dc=grafana,dc=org",
},
}
......@@ -184,7 +119,7 @@ func TestLDAPHelpers(t *testing.T) {
})
Convey("Given empty bind dn and password", func() {
connection := &mockConnection{}
connection := &MockConnection{}
unauthenticatedBindWasCalled := false
var actualUsername string
connection.unauthenticatedBindProvider = func(username string) error {
......@@ -193,8 +128,8 @@ func TestLDAPHelpers(t *testing.T) {
return nil
}
server := &Server{
connection: connection,
config: &ServerConfig{},
Connection: connection,
Config: &ServerConfig{},
}
err := server.serverBind()
So(err, ShouldBeNil)
......
......@@ -13,12 +13,12 @@ import (
func TestLDAPLogin(t *testing.T) {
Convey("Login()", t, func() {
authScenario("When user is log in and updated", func(sc *scenarioContext) {
serverScenario("When user is log in and updated", func(sc *scenarioContext) {
// arrange
mockConnection := &mockConnection{}
mockConnection := &MockConnection{}
auth := &Server{
config: &ServerConfig{
server := &Server{
Config: &ServerConfig{
Host: "",
RootCACert: "",
Groups: []*GroupToOrgRole{
......@@ -33,7 +33,7 @@ func TestLDAPLogin(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: mockConnection,
Connection: mockConnection,
log: log.New("test-logger"),
}
......@@ -61,7 +61,7 @@ func TestLDAPLogin(t *testing.T) {
sc.userOrgsQueryReturns([]*models.UserOrgDTO{})
// act
extUser, _ := auth.Login(query)
extUser, _ := server.Login(query)
userInfo, err := user.Upsert(&user.UpsertArgs{
SignupAllowed: true,
ExternalUser: extUser,
......@@ -73,7 +73,7 @@ func TestLDAPLogin(t *testing.T) {
So(err, ShouldBeNil)
// User should be searched in ldap
So(mockConnection.searchCalled, ShouldBeTrue)
So(mockConnection.SearchCalled, ShouldBeTrue)
// Info should be updated (email differs)
So(userInfo.Email, ShouldEqual, "roel@test.com")
......@@ -82,8 +82,8 @@ func TestLDAPLogin(t *testing.T) {
So(sc.addOrgUserCmd.Role, ShouldEqual, "Admin")
})
authScenario("When login with invalid credentials", func(scenario *scenarioContext) {
connection := &mockConnection{}
serverScenario("When login with invalid credentials", func(scenario *scenarioContext) {
connection := &MockConnection{}
entry := ldap.Entry{}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
connection.setSearchResult(&result)
......@@ -93,8 +93,8 @@ func TestLDAPLogin(t *testing.T) {
ResultCode: 49,
}
}
auth := &Server{
config: &ServerConfig{
server := &Server{
Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
......@@ -102,19 +102,19 @@ func TestLDAPLogin(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
Connection: connection,
log: log.New("test-logger"),
}
_, err := auth.Login(scenario.loginUserQuery)
_, err := server.Login(scenario.loginUserQuery)
Convey("it should return invalid credentials error", func() {
So(err, ShouldEqual, ErrInvalidCredentials)
})
})
authScenario("When login with valid credentials", func(scenario *scenarioContext) {
connection := &mockConnection{}
serverScenario("When login with valid credentials", func(scenario *scenarioContext) {
connection := &MockConnection{}
entry := ldap.Entry{
DN: "dn", Attributes: []*ldap.EntryAttribute{
{Name: "username", Values: []string{"markelog"}},
......@@ -130,8 +130,8 @@ func TestLDAPLogin(t *testing.T) {
connection.bindProvider = func(username, password string) error {
return nil
}
auth := &Server{
config: &ServerConfig{
server := &Server{
Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
......@@ -139,18 +139,18 @@ func TestLDAPLogin(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
Connection: connection,
log: log.New("test-logger"),
}
resp, err := auth.Login(scenario.loginUserQuery)
resp, err := server.Login(scenario.loginUserQuery)
So(err, ShouldBeNil)
So(resp.Login, ShouldEqual, "markelog")
})
authScenario("When user not found in LDAP, but exist in Grafana", func(scenario *scenarioContext) {
connection := &mockConnection{}
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)
......@@ -160,15 +160,15 @@ func TestLDAPLogin(t *testing.T) {
connection.bindProvider = func(username, password string) error {
return nil
}
auth := &Server{
config: &ServerConfig{
server := &Server{
Config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
Connection: connection,
log: log.New("test-logger"),
}
_, err := auth.Login(scenario.loginUserQuery)
_, err := server.Login(scenario.loginUserQuery)
Convey("it should disable user", func() {
So(scenario.disableExternalUserCalled, ShouldBeTrue)
......@@ -181,8 +181,8 @@ func TestLDAPLogin(t *testing.T) {
})
})
authScenario("When user not found in LDAP, and disabled in Grafana already", func(scenario *scenarioContext) {
connection := &mockConnection{}
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)
......@@ -192,15 +192,15 @@ func TestLDAPLogin(t *testing.T) {
connection.bindProvider = func(username, password string) error {
return nil
}
auth := &Server{
config: &ServerConfig{
server := &Server{
Config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
Connection: connection,
log: log.New("test-logger"),
}
_, err := auth.Login(scenario.loginUserQuery)
_, err := server.Login(scenario.loginUserQuery)
Convey("it should't call disable function", func() {
So(scenario.disableExternalUserCalled, ShouldBeFalse)
......@@ -211,8 +211,8 @@ func TestLDAPLogin(t *testing.T) {
})
})
authScenario("When user found in LDAP, and disabled in Grafana", func(scenario *scenarioContext) {
connection := &mockConnection{}
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)
......@@ -221,15 +221,15 @@ func TestLDAPLogin(t *testing.T) {
connection.bindProvider = func(username, password string) error {
return nil
}
auth := &Server{
config: &ServerConfig{
server := &Server{
Config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
Connection: connection,
log: log.New("test-logger"),
}
extUser, _ := auth.Login(scenario.loginUserQuery)
extUser, _ := server.Login(scenario.loginUserQuery)
_, err := user.Upsert(&user.UpsertArgs{
SignupAllowed: true,
ExternalUser: extUser,
......
......@@ -9,114 +9,10 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
)
func TestAuth(t *testing.T) {
Convey("Add()", t, func() {
connection := &mockConnection{}
auth := &Server{
config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
log: log.New("test-logger"),
}
Convey("Adds user", func() {
err := auth.Add(
"cn=ldap-tuz,ou=users,dc=grafana,dc=org",
map[string][]string{
"mail": {"ldap-viewer@grafana.com"},
"userPassword": {"grafana"},
"objectClass": {
"person",
"top",
"inetOrgPerson",
"organizationalPerson",
},
"sn": {"ldap-tuz"},
"cn": {"ldap-tuz"},
},
)
hasMail := false
hasUserPassword := false
hasObjectClass := false
hasSN := false
hasCN := false
So(err, ShouldBeNil)
So(connection.addParams.Controls, ShouldBeNil)
So(connection.addCalled, ShouldBeTrue)
So(
connection.addParams.DN,
ShouldEqual,
"cn=ldap-tuz,ou=users,dc=grafana,dc=org",
)
attrs := connection.addParams.Attributes
for _, value := range attrs {
if value.Type == "mail" {
So(value.Vals, ShouldContain, "ldap-viewer@grafana.com")
hasMail = true
}
if value.Type == "userPassword" {
hasUserPassword = true
So(value.Vals, ShouldContain, "grafana")
}
if value.Type == "objectClass" {
hasObjectClass = true
So(value.Vals, ShouldContain, "person")
So(value.Vals, ShouldContain, "top")
So(value.Vals, ShouldContain, "inetOrgPerson")
So(value.Vals, ShouldContain, "organizationalPerson")
}
if value.Type == "sn" {
hasSN = true
So(value.Vals, ShouldContain, "ldap-tuz")
}
if value.Type == "cn" {
hasCN = true
So(value.Vals, ShouldContain, "ldap-tuz")
}
}
So(hasMail, ShouldBeTrue)
So(hasUserPassword, ShouldBeTrue)
So(hasObjectClass, ShouldBeTrue)
So(hasSN, ShouldBeTrue)
So(hasCN, ShouldBeTrue)
})
})
Convey("Remove()", t, func() {
connection := &mockConnection{}
auth := &Server{
config: &ServerConfig{
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: connection,
log: log.New("test-logger"),
}
Convey("Removes the user", func() {
dn := "cn=ldap-tuz,ou=users,dc=grafana,dc=org"
err := auth.Remove(dn)
So(err, ShouldBeNil)
So(connection.delCalled, ShouldBeTrue)
So(connection.delParams.Controls, ShouldBeNil)
So(connection.delParams.DN, ShouldEqual, dn)
})
})
func TestPublicAPI(t *testing.T) {
Convey("Users()", t, func() {
Convey("find one user", func() {
mockConnection := &mockConnection{}
MockConnection := &MockConnection{}
entry := ldap.Entry{
DN: "dn", Attributes: []*ldap.EntryAttribute{
{Name: "username", Values: []string{"roelgerrits"}},
......@@ -126,11 +22,11 @@ func TestAuth(t *testing.T) {
{Name: "memberof", Values: []string{"admins"}},
}}
result := ldap.SearchResult{Entries: []*ldap.Entry{&entry}}
mockConnection.setSearchResult(&result)
MockConnection.setSearchResult(&result)
// Set up attribute map without surname and email
server := &Server{
config: &ServerConfig{
Config: &ServerConfig{
Attr: AttributeMap{
Username: "username",
Name: "name",
......@@ -138,7 +34,7 @@ func TestAuth(t *testing.T) {
},
SearchBaseDNs: []string{"BaseDNHere"},
},
connection: mockConnection,
Connection: MockConnection,
log: log.New("test-logger"),
}
......@@ -148,10 +44,75 @@ func TestAuth(t *testing.T) {
So(searchResult, ShouldNotBeNil)
// User should be searched in ldap
So(mockConnection.searchCalled, ShouldBeTrue)
So(MockConnection.SearchCalled, ShouldBeTrue)
// No empty attributes should be added to the search request
So(len(mockConnection.searchAttributes), ShouldEqual, 3)
So(len(MockConnection.SearchAttributes), ShouldEqual, 3)
})
})
Convey("InitialBind", 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: "cn=%s,o=users,dc=grafana,dc=org",
BindPassword: "bindpwd",
},
}
err := server.InitialBind("user", "pwd")
So(err, ShouldBeNil)
So(server.requireSecondBind, ShouldBeTrue)
So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org")
So(actualPassword, ShouldEqual, "bindpwd")
})
Convey("Given bind dn 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: "cn=%s,o=users,dc=grafana,dc=org",
},
}
err := server.InitialBind("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() {
connection := &MockConnection{}
unauthenticatedBindWasCalled := false
var actualUsername string
connection.unauthenticatedBindProvider = func(username string) error {
unauthenticatedBindWasCalled = true
actualUsername = username
return nil
}
server := &Server{
Connection: connection,
Config: &ServerConfig{},
}
err := server.InitialBind("user", "pwd")
So(err, ShouldBeNil)
So(server.requireSecondBind, ShouldBeTrue)
So(unauthenticatedBindWasCalled, ShouldBeTrue)
So(actualUsername, ShouldBeEmpty)
})
})
}
......@@ -12,22 +12,24 @@ import (
"github.com/grafana/grafana/pkg/services/login"
)
type mockConnection struct {
searchResult *ldap.SearchResult
searchCalled bool
searchAttributes []string
// MockConnection struct for testing
type MockConnection struct {
SearchResult *ldap.SearchResult
SearchCalled bool
SearchAttributes []string
addParams *ldap.AddRequest
addCalled bool
AddParams *ldap.AddRequest
AddCalled bool
delParams *ldap.DelRequest
delCalled bool
DelParams *ldap.DelRequest
DelCalled bool
bindProvider func(username, password string) error
unauthenticatedBindProvider func(username string) error
}
func (c *mockConnection) Bind(username, password string) error {
// Bind mocks Bind connection function
func (c *MockConnection) Bind(username, password string) error {
if c.bindProvider != nil {
return c.bindProvider(username, password)
}
......@@ -35,7 +37,8 @@ func (c *mockConnection) Bind(username, password string) error {
return nil
}
func (c *mockConnection) UnauthenticatedBind(username string) error {
// UnauthenticatedBind mocks UnauthenticatedBind connection function
func (c *MockConnection) UnauthenticatedBind(username string) error {
if c.unauthenticatedBindProvider != nil {
return c.unauthenticatedBindProvider(username)
}
......@@ -43,35 +46,40 @@ func (c *mockConnection) UnauthenticatedBind(username string) error {
return nil
}
func (c *mockConnection) Close() {}
// Close mocks Close connection function
func (c *MockConnection) Close() {}
func (c *mockConnection) setSearchResult(result *ldap.SearchResult) {
c.searchResult = result
func (c *MockConnection) setSearchResult(result *ldap.SearchResult) {
c.SearchResult = result
}
func (c *mockConnection) Search(sr *ldap.SearchRequest) (*ldap.SearchResult, error) {
c.searchCalled = true
c.searchAttributes = sr.Attributes
return c.searchResult, nil
// 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
}
func (c *mockConnection) Add(request *ldap.AddRequest) error {
c.addCalled = true
c.addParams = request
// Add mocks Add connection function
func (c *MockConnection) Add(request *ldap.AddRequest) error {
c.AddCalled = true
c.AddParams = request
return nil
}
func (c *mockConnection) Del(request *ldap.DelRequest) error {
c.delCalled = true
c.delParams = request
// Del mocks Del connection function
func (c *MockConnection) Del(request *ldap.DelRequest) error {
c.DelCalled = true
c.DelParams = request
return nil
}
func (c *mockConnection) StartTLS(*tls.Config) error {
// StartTLS mocks StartTLS connection function
func (c *MockConnection) StartTLS(*tls.Config) error {
return nil
}
func authScenario(desc string, fn scenarioFunc) {
func serverScenario(desc string, fn scenarioFunc) {
Convey(desc, func() {
defer bus.ClearBusHandlers()
......
......@@ -35,9 +35,6 @@ type IMultiLDAP interface {
User(login string) (
*models.ExternalUserInfo, error,
)
Add(dn string, values map[string][]string) error
Remove(dn string) error
}
// MultiLDAP is basic struct of LDAP authorization
......@@ -52,55 +49,6 @@ func New(configs []*ldap.ServerConfig) IMultiLDAP {
}
}
// Add adds user to the *first* defined LDAP
func (multiples *MultiLDAP) Add(
dn string,
values map[string][]string,
) error {
if len(multiples.configs) == 0 {
return ErrNoLDAPServers
}
config := multiples.configs[0]
ldap := ldap.New(config)
if err := ldap.Dial(); err != nil {
return err
}
defer ldap.Close()
err := ldap.Add(dn, values)
if err != nil {
return err
}
return nil
}
// Remove removes user from the *first* defined LDAP
func (multiples *MultiLDAP) Remove(dn string) error {
if len(multiples.configs) == 0 {
return ErrNoLDAPServers
}
config := multiples.configs[0]
ldap := ldap.New(config)
if err := ldap.Dial(); err != nil {
return err
}
defer ldap.Close()
err := ldap.Remove(dn)
if err != nil {
return err
}
return nil
}
// Login tries to log in the user in multiples LDAP
func (multiples *MultiLDAP) Login(query *models.LoginUserQuery) (
*models.ExternalUserInfo, error,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment