graph: Add unit test for LDAP identity backend

This reworks the LDAP backend a bit to allow for mocking the ldap.Client
interface. It also add a couple of unit test for the backend
This commit is contained in:
Ralf Haferkamp
2021-11-23 14:47:46 +01:00
parent b7ec12735b
commit d21ca0658f
4 changed files with 397 additions and 6 deletions

View File

@@ -9,7 +9,6 @@ import (
msgraph "github.com/yaegashi/msgraph.go/beta"
"github.com/owncloud/ocis/graph/pkg/config"
ldaputil "github.com/owncloud/ocis/graph/pkg/identity/ldap"
"github.com/owncloud/ocis/graph/pkg/service/v0/errorcode"
"github.com/owncloud/ocis/ocis-pkg/log"
)
@@ -26,7 +25,7 @@ type LDAP struct {
groupAttributeMap groupAttributeMap
logger *log.Logger
conn *ldaputil.ConnWithReconnect
conn ldap.Client
}
type userAttributeMap struct {
@@ -41,14 +40,21 @@ type groupAttributeMap struct {
id string
}
func NewLDAPBackend(config config.LDAP, logger *log.Logger) (*LDAP, error) {
conn := ldaputil.NewLDAPWithReconnect(logger, config.URI, config.BindDN, config.BindPassword)
func NewLDAPBackend(lc ldap.Client, config config.LDAP, logger *log.Logger) (*LDAP, error) {
if config.UserDisplayNameAttribute == "" || config.UserIDAttribute == "" ||
config.UserEmailAttribute == "" || config.UserNameAttribute == "" {
return nil, fmt.Errorf("Invalid user attribute mappings")
}
uam := userAttributeMap{
displayName: config.UserDisplayNameAttribute,
id: config.UserIDAttribute,
mail: config.UserEmailAttribute,
userName: config.UserNameAttribute,
}
if config.GroupNameAttribute == "" || config.GroupIDAttribute == "" {
return nil, fmt.Errorf("Invalid group attribute mappings")
}
gam := groupAttributeMap{
name: config.GroupNameAttribute,
id: config.GroupIDAttribute,
@@ -74,7 +80,7 @@ func NewLDAPBackend(config config.LDAP, logger *log.Logger) (*LDAP, error) {
groupScope: groupScope,
groupAttributeMap: gam,
logger: logger,
conn: &conn,
conn: lc,
}, nil
}
@@ -228,6 +234,9 @@ func (i *LDAP) GetGroups(ctx context.Context, queryParam url.Values) ([]*msgraph
}
func (i *LDAP) createUserModelFromLDAP(e *ldap.Entry) *msgraph.User {
if e == nil {
return nil
}
return &msgraph.User{
DisplayName: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.displayName)),
Mail: pointerOrNil(e.GetEqualFoldAttributeValue(i.userAttributeMap.mail)),

View File

@@ -4,7 +4,10 @@ package ldap
// https://gist.github.com/emsearcy/cba3295d1a06d4c432ab4f6173b65e4f#file-ldap_snippet-go
import (
"crypto/tls"
"errors"
"fmt"
"time"
"github.com/go-ldap/ldap/v3"
@@ -16,6 +19,7 @@ type ldapConnection struct {
Error error
}
// Implements the ldap.CLient interface
type ConnWithReconnect struct {
conn chan ldapConnection
reset chan *ldap.Conn
@@ -125,3 +129,67 @@ func (c ConnWithReconnect) reconnect(resetConn *ldap.Conn) (*ldap.Conn, error) {
result := <-c.conn
return result.Conn, result.Error
}
// Remaining methods to fulfill ldap.Client interface
func (c ConnWithReconnect) Start() {}
func (c ConnWithReconnect) StartTLS(*tls.Config) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) Close() {}
func (c ConnWithReconnect) IsClosing() bool {
return false
}
func (c ConnWithReconnect) SetTimeout(time.Duration) {}
func (c ConnWithReconnect) Bind(username, password string) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) UnauthenticatedBind(username string) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) SimpleBind(*ldap.SimpleBindRequest) (*ldap.SimpleBindResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) ExternalBind() error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) Add(*ldap.AddRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) Del(*ldap.DelRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) Modify(*ldap.ModifyRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) ModifyDN(*ldap.ModifyDNRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) ModifyWithResult(*ldap.ModifyRequest) (*ldap.ModifyResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) Compare(dn, attribute, value string) (bool, error) {
return false, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) PasswordModify(*ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ConnWithReconnect) SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}

View File

@@ -0,0 +1,307 @@
package identity
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net/url"
"testing"
"time"
"github.com/go-ldap/ldap/v3"
"github.com/owncloud/ocis/graph/pkg/config"
"github.com/owncloud/ocis/ocis-pkg/log"
)
// ldapMock implements the ldap.Client interfac
type ldapMock struct {
SearchFunc *searchFunc
}
type searchFunc func(*ldap.SearchRequest) (*ldap.SearchResult, error)
func getMockedBackend(sf *searchFunc, lc config.LDAP, logger *log.Logger) (*LDAP, error) {
// Mock a Sizelimit Error
lm := ldapMock{SearchFunc: sf}
return NewLDAPBackend(lm, lconfig, logger)
}
var lconfig = config.LDAP{
UserBaseDN: "dc=test",
UserSearchScope: "sub",
UserFilter: "filter",
UserDisplayNameAttribute: "displayname",
UserIDAttribute: "entryUUID",
UserEmailAttribute: "mail",
UserNameAttribute: "uid",
GroupBaseDN: "dc=test",
GroupSearchScope: "sub",
GroupFilter: "filter",
GroupNameAttribute: "cn",
GroupIDAttribute: "entryUUID",
}
var userEntry = ldap.NewEntry("uid=user",
map[string][]string{
"uid": {"user"},
"displayname": {"DisplayName"},
"mail": {"user@example"},
"entryuuid": {"abcd-defg"},
})
var groupEntry = ldap.NewEntry("cn=group",
map[string][]string{
"cn": {"group"},
"entryuuid": {"abcd-defg"},
})
var logger = log.NewLogger(log.Level("debug"))
func TestNewLDAPBackend(t *testing.T) {
l := ldapMock{}
tc := lconfig
tc.UserDisplayNameAttribute = ""
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
t.Error("Should fail with incomplete user attr config")
}
tc = lconfig
tc.GroupIDAttribute = ""
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
t.Errorf("Should fail with incomplete group config")
}
tc = lconfig
tc.UserSearchScope = ""
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
t.Errorf("Should fail with invalid user search scope")
}
tc = lconfig
tc.GroupSearchScope = ""
if _, err := NewLDAPBackend(l, tc, &logger); err == nil {
t.Errorf("Should fail with invalid group search scope")
}
if _, err := NewLDAPBackend(l, lconfig, &logger); err != nil {
t.Errorf("Should fail with invalid group search scope")
}
}
func TestCreateUserModelFromLDAP(t *testing.T) {
l := ldapMock{}
logger := log.NewLogger(log.Level("debug"))
b, _ := NewLDAPBackend(l, lconfig, &logger)
if user := b.createUserModelFromLDAP(nil); user != nil {
t.Errorf("createUserModelFromLDAP should return on nil Entry")
}
user := b.createUserModelFromLDAP(userEntry)
if user == nil {
t.Error("Converting a valid LDAP Entry should succeed")
} else {
if *user.OnPremisesSamAccountName != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.userName) {
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.OnPremisesSamAccountName, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.userName))
}
if *user.Mail != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.mail) {
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.Mail, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.mail))
}
if *user.DisplayName != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.displayName) {
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.DisplayName, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.displayName))
}
if *user.ID != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
t.Errorf("Error creating msGraph User from LDAP Entry: %s != %s", *user.ID, userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id))
}
}
}
func TestGetUser(t *testing.T) {
// Mock a Sizelimit Error
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock"))
}
b, _ := getMockedBackend(&sf, lconfig, &logger)
_, err := b.GetUser(context.Background(), "fred")
if err == nil || err.Error() != "itemNotFound" {
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
}
// Mock an empty Search Result
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return &ldap.SearchResult{}, nil
}
b, _ = getMockedBackend(&sf, lconfig, &logger)
_, err = b.GetUser(context.Background(), "fred")
if err == nil || err.Error() != "itemNotFound" {
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
}
// Mock a valid Search Result
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return &ldap.SearchResult{
Entries: []*ldap.Entry{userEntry},
}, nil
}
b, _ = getMockedBackend(&sf, lconfig, &logger)
u, err := b.GetUser(context.Background(), "user")
if err != nil {
t.Errorf("Expected GetUser to succeed. Got %s", err.Error())
} else if *u.ID != userEntry.GetEqualFoldAttributeValue(b.userAttributeMap.id) {
t.Errorf("Expected GetUser to return a valid user")
}
}
func TestGetUsers(t *testing.T) {
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock"))
}
b, _ := getMockedBackend(&sf, lconfig, &logger)
_, err := b.GetUsers(context.Background(), url.Values{})
if err == nil || err.Error() != "itemNotFound" {
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
}
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return &ldap.SearchResult{}, nil
}
b, _ = getMockedBackend(&sf, lconfig, &logger)
g, err := b.GetUsers(context.Background(), url.Values{})
if err != nil {
t.Errorf("Expected success, got '%s'", err.Error())
} else if g == nil || len(g) != 0 {
t.Errorf("Expected zero length user slice")
}
}
func TestGetGroup(t *testing.T) {
// Mock a Sizelimit Error
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultSizeLimitExceeded, errors.New("mock"))
}
b, _ := getMockedBackend(&sf, lconfig, &logger)
_, err := b.GetGroup(context.Background(), "group")
if err == nil || err.Error() != "itemNotFound" {
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
}
// Mock an empty Search Result
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return &ldap.SearchResult{}, nil
}
b, _ = getMockedBackend(&sf, lconfig, &logger)
_, err = b.GetGroup(context.Background(), "group")
if err == nil || err.Error() != "itemNotFound" {
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
}
// Mock a valid Search Result
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return &ldap.SearchResult{
Entries: []*ldap.Entry{groupEntry},
}, nil
}
b, _ = getMockedBackend(&sf, lconfig, &logger)
g, err := b.GetGroup(context.Background(), "group")
if err != nil {
t.Errorf("Expected GetGroup to succeed. Got %s", err.Error())
} else if *g.ID != groupEntry.GetEqualFoldAttributeValue(b.groupAttributeMap.id) {
t.Errorf("Expected GetGroup to return a valid group")
}
}
func TestGetGroups(t *testing.T) {
var sf searchFunc = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultOperationsError, errors.New("mock"))
}
b, _ := getMockedBackend(&sf, lconfig, &logger)
_, err := b.GetGroups(context.Background(), url.Values{})
if err == nil || err.Error() != "itemNotFound" {
t.Errorf("Expected 'itemNotFound' got '%s'", err.Error())
}
sf = func(*ldap.SearchRequest) (*ldap.SearchResult, error) {
return &ldap.SearchResult{}, nil
}
b, _ = getMockedBackend(&sf, lconfig, &logger)
g, err := b.GetGroups(context.Background(), url.Values{})
if err != nil {
t.Errorf("Expected success, got '%s'", err.Error())
} else if g == nil || len(g) != 0 {
t.Errorf("Expected zero length user slice")
}
}
// below here ldap.Client interface method for ldapMock
func (c ldapMock) Start() {}
func (c ldapMock) StartTLS(*tls.Config) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) Close() {}
func (c ldapMock) IsClosing() bool {
return false
}
func (c ldapMock) SetTimeout(time.Duration) {}
func (c ldapMock) Bind(username, password string) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) UnauthenticatedBind(username string) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) SimpleBind(*ldap.SimpleBindRequest) (*ldap.SimpleBindResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) ExternalBind() error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) Add(*ldap.AddRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) Del(*ldap.DelRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) Modify(*ldap.ModifyRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) ModifyDN(*ldap.ModifyDNRequest) error {
return ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) ModifyWithResult(*ldap.ModifyRequest) (*ldap.ModifyResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) Compare(dn, attribute, value string) (bool, error) {
return false, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) PasswordModify(*ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) Search(searchRequest *ldap.SearchRequest) (*ldap.SearchResult, error) {
if c.SearchFunc != nil {
return (*c.SearchFunc)(searchRequest)
}
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}
func (c ldapMock) SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) {
return nil, ldap.NewError(ldap.LDAPResultNotSupported, fmt.Errorf("not implemented"))
}

View File

@@ -5,7 +5,9 @@ import (
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/owncloud/ocis/graph/pkg/identity"
"github.com/owncloud/ocis/graph/pkg/identity/ldap"
"github.com/owncloud/ocis/ocis-pkg/account"
opkgm "github.com/owncloud/ocis/ocis-pkg/middleware"
)
@@ -34,7 +36,12 @@ func NewService(opts ...Option) Service {
}
case "ldap":
var err error
if backend, err = identity.NewLDAPBackend(options.Config.Identity.LDAP, &options.Logger); err != nil {
conn := ldap.NewLDAPWithReconnect(&options.Logger,
options.Config.Identity.LDAP.URI,
options.Config.Identity.LDAP.BindDN,
options.Config.Identity.LDAP.BindPassword,
)
if backend, err = identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger); err != nil {
options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err)
return nil
}