mirror of https://github.com/grafana/grafana.git
				
				
				
			
		
			
				
	
	
		
			487 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			487 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
| package ldap
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"testing"
 | |
| 
 | |
| 	"github.com/go-ldap/ldap/v3"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/stretchr/testify/require"
 | |
| 
 | |
| 	"github.com/grafana/grafana/pkg/infra/log"
 | |
| 	"github.com/grafana/grafana/pkg/models/roletype"
 | |
| 	"github.com/grafana/grafana/pkg/setting"
 | |
| )
 | |
| 
 | |
| func TestNew(t *testing.T) {
 | |
| 	result := New(&ServerConfig{
 | |
| 		Attr:          AttributeMap{},
 | |
| 		SearchBaseDNs: []string{"BaseDNHere"},
 | |
| 	}, &setting.Cfg{})
 | |
| 
 | |
| 	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{},
 | |
| 				SearchBaseDNs: []string{"BaseDNHere"},
 | |
| 			},
 | |
| 			Connection: connection,
 | |
| 		}
 | |
| 
 | |
| 		assert.NotPanics(t, server.Close)
 | |
| 		assert.True(t, connection.CloseCalled)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("panic if no connection", func(t *testing.T) {
 | |
| 		server := &Server{
 | |
| 			Config: &ServerConfig{
 | |
| 				Attr:          AttributeMap{},
 | |
| 				SearchBaseDNs: []string{"BaseDNHere"},
 | |
| 			},
 | |
| 			Connection: nil,
 | |
| 		}
 | |
| 
 | |
| 		assert.Panics(t, server.Close)
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestServer_Users(t *testing.T) {
 | |
| 	t.Run("one user", func(t *testing.T) {
 | |
| 		conn := &MockConnection{}
 | |
| 		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}}
 | |
| 		conn.setSearchResult(&result)
 | |
| 
 | |
| 		// Set up attribute map without surname and email
 | |
| 		cfg := setting.NewCfg()
 | |
| 		cfg.LDAPAuthEnabled = true
 | |
| 
 | |
| 		server := &Server{
 | |
| 			cfg: cfg,
 | |
| 			Config: &ServerConfig{
 | |
| 				Attr: AttributeMap{
 | |
| 					Username: "username",
 | |
| 					Name:     "name",
 | |
| 					MemberOf: "memberof",
 | |
| 				},
 | |
| 				SearchBaseDNs: []string{"BaseDNHere"},
 | |
| 			},
 | |
| 			Connection: conn,
 | |
| 			log:        log.New("test-logger"),
 | |
| 		}
 | |
| 
 | |
| 		searchResult, err := server.Users([]string{"roelgerrits"})
 | |
| 
 | |
| 		require.NoError(t, err)
 | |
| 		assert.NotNil(t, searchResult)
 | |
| 
 | |
| 		// User should be searched in ldap
 | |
| 		assert.True(t, conn.SearchCalled)
 | |
| 		// No empty attributes should be added to the search request
 | |
| 		assert.Len(t, conn.SearchAttributes, 3)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("error", func(t *testing.T) {
 | |
| 		expected := errors.New("Killa-gorilla")
 | |
| 		conn := &MockConnection{}
 | |
| 		conn.setSearchError(expected)
 | |
| 
 | |
| 		// Set up attribute map without surname and email
 | |
| 		server := &Server{
 | |
| 			Config: &ServerConfig{
 | |
| 				SearchBaseDNs: []string{"BaseDNHere"},
 | |
| 			},
 | |
| 			Connection: conn,
 | |
| 			log:        log.New("test-logger"),
 | |
| 		}
 | |
| 
 | |
| 		_, err := server.Users([]string{"roelgerrits"})
 | |
| 
 | |
| 		assert.ErrorIs(t, err, expected)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("no user", func(t *testing.T) {
 | |
| 		conn := &MockConnection{}
 | |
| 		result := ldap.SearchResult{Entries: []*ldap.Entry{}}
 | |
| 		conn.setSearchResult(&result)
 | |
| 
 | |
| 		// Set up attribute map without surname and email
 | |
| 		server := &Server{
 | |
| 			Config: &ServerConfig{
 | |
| 				SearchBaseDNs: []string{"BaseDNHere"},
 | |
| 			},
 | |
| 			Connection: conn,
 | |
| 			log:        log.New("test-logger"),
 | |
| 		}
 | |
| 
 | |
| 		searchResult, err := server.Users([]string{"roelgerrits"})
 | |
| 
 | |
| 		require.NoError(t, err)
 | |
| 		assert.Empty(t, searchResult)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("multiple DNs", func(t *testing.T) {
 | |
| 		conn := &MockConnection{}
 | |
| 		serviceDN := "dc=svc,dc=example,dc=org"
 | |
| 		serviceEntry := ldap.Entry{
 | |
| 			DN: "dn", Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "username", Values: []string{"imgrenderer"}},
 | |
| 				{Name: "name", Values: []string{"Image renderer"}},
 | |
| 			}}
 | |
| 		services := ldap.SearchResult{Entries: []*ldap.Entry{&serviceEntry}}
 | |
| 
 | |
| 		userDN := "dc=users,dc=example,dc=org"
 | |
| 		userEntry := ldap.Entry{
 | |
| 			DN: "dn", Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "username", Values: []string{"grot"}},
 | |
| 				{Name: "name", Values: []string{"Grot"}},
 | |
| 			}}
 | |
| 		users := ldap.SearchResult{Entries: []*ldap.Entry{&userEntry}}
 | |
| 
 | |
| 		conn.setSearchFunc(func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
 | |
| 			switch request.BaseDN {
 | |
| 			case userDN:
 | |
| 				return &users, nil
 | |
| 			case serviceDN:
 | |
| 				return &services, nil
 | |
| 			default:
 | |
| 				return nil, fmt.Errorf("test case not defined for baseDN: '%s'", request.BaseDN)
 | |
| 			}
 | |
| 		})
 | |
| 
 | |
| 		server := &Server{
 | |
| 			cfg: setting.NewCfg(),
 | |
| 			Config: &ServerConfig{
 | |
| 				Attr: AttributeMap{
 | |
| 					Username: "username",
 | |
| 					Name:     "name",
 | |
| 				},
 | |
| 				SearchBaseDNs: []string{serviceDN, userDN},
 | |
| 			},
 | |
| 			Connection: conn,
 | |
| 			log:        log.New("test-logger"),
 | |
| 		}
 | |
| 
 | |
| 		searchResult, err := server.Users([]string{"imgrenderer", "grot"})
 | |
| 		require.NoError(t, err)
 | |
| 
 | |
| 		assert.Len(t, searchResult, 2)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("same user in multiple DNs", func(t *testing.T) {
 | |
| 		conn := &MockConnection{}
 | |
| 		firstDN := "dc=users1,dc=example,dc=org"
 | |
| 		firstEntry := ldap.Entry{
 | |
| 			DN: "dn", Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "username", Values: []string{"grot"}},
 | |
| 				{Name: "name", Values: []string{"Grot the First"}},
 | |
| 			}}
 | |
| 		firsts := ldap.SearchResult{Entries: []*ldap.Entry{&firstEntry}}
 | |
| 
 | |
| 		secondDN := "dc=users2,dc=example,dc=org"
 | |
| 		secondEntry := ldap.Entry{
 | |
| 			DN: "dn", Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "username", Values: []string{"grot"}},
 | |
| 				{Name: "name", Values: []string{"Grot the Second"}},
 | |
| 			}}
 | |
| 		seconds := ldap.SearchResult{Entries: []*ldap.Entry{&secondEntry}}
 | |
| 
 | |
| 		conn.setSearchFunc(func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
 | |
| 			switch request.BaseDN {
 | |
| 			case secondDN:
 | |
| 				return &seconds, nil
 | |
| 			case firstDN:
 | |
| 				return &firsts, nil
 | |
| 			default:
 | |
| 				return nil, fmt.Errorf("test case not defined for baseDN: '%s'", request.BaseDN)
 | |
| 			}
 | |
| 		})
 | |
| 
 | |
| 		cfg := setting.NewCfg()
 | |
| 		cfg.LDAPAuthEnabled = true
 | |
| 
 | |
| 		server := &Server{
 | |
| 			cfg: cfg,
 | |
| 			Config: &ServerConfig{
 | |
| 				Attr: AttributeMap{
 | |
| 					Username: "username",
 | |
| 					Name:     "name",
 | |
| 				},
 | |
| 				SearchBaseDNs: []string{firstDN, secondDN},
 | |
| 			},
 | |
| 			Connection: conn,
 | |
| 			log:        log.New("test-logger"),
 | |
| 		}
 | |
| 
 | |
| 		res, err := server.Users([]string{"grot"})
 | |
| 		require.NoError(t, err)
 | |
| 		require.Len(t, res, 1)
 | |
| 		assert.Equal(t, "Grot the First", res[0].Name)
 | |
| 	})
 | |
| 
 | |
| 	t.Run("org role mapping", func(t *testing.T) {
 | |
| 		conn := &MockConnection{}
 | |
| 
 | |
| 		usersOU := "ou=users,dc=example,dc=org"
 | |
| 		grootDN := "dn=groot," + usersOU
 | |
| 		grootSearch := ldap.SearchResult{Entries: []*ldap.Entry{{DN: grootDN,
 | |
| 			Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "username", Values: []string{"groot"}},
 | |
| 				{Name: "name", Values: []string{"I am Groot"}},
 | |
| 			}}}}
 | |
| 		babyGrootDN := "dn=babygroot," + usersOU
 | |
| 		babyGrootSearch := ldap.SearchResult{Entries: []*ldap.Entry{{DN: grootDN,
 | |
| 			Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "username", Values: []string{"babygroot"}},
 | |
| 				{Name: "name", Values: []string{"I am baby Groot"}},
 | |
| 			}}}}
 | |
| 		peterDN := "dn=peter," + usersOU
 | |
| 		peterSearch := ldap.SearchResult{Entries: []*ldap.Entry{{DN: peterDN,
 | |
| 			Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "username", Values: []string{"peter"}},
 | |
| 				{Name: "name", Values: []string{"Peter"}},
 | |
| 			}}}}
 | |
| 		groupsOU := "ou=groups,dc=example,dc=org"
 | |
| 		creaturesDN := "dn=creatures," + groupsOU
 | |
| 		grootGroups := ldap.SearchResult{Entries: []*ldap.Entry{{DN: creaturesDN,
 | |
| 			Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "member", Values: []string{grootDN}},
 | |
| 			}}},
 | |
| 		}
 | |
| 		babyCreaturesDN := "dn=babycreatures," + groupsOU
 | |
| 		babyGrootGroups := ldap.SearchResult{Entries: []*ldap.Entry{{DN: babyCreaturesDN,
 | |
| 			Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "member", Values: []string{babyGrootDN}},
 | |
| 			}}},
 | |
| 		}
 | |
| 		humansDN := "dn=humans," + groupsOU
 | |
| 		peterGroups := ldap.SearchResult{Entries: []*ldap.Entry{{DN: humansDN,
 | |
| 			Attributes: []*ldap.EntryAttribute{
 | |
| 				{Name: "member", Values: []string{peterDN}},
 | |
| 			}}},
 | |
| 		}
 | |
| 
 | |
| 		conn.setSearchFunc(func(request *ldap.SearchRequest) (*ldap.SearchResult, error) {
 | |
| 			switch request.BaseDN {
 | |
| 			case usersOU:
 | |
| 				switch request.Filter {
 | |
| 				case "(|(username=groot))":
 | |
| 					return &grootSearch, nil
 | |
| 				case "(|(username=babygroot))":
 | |
| 					return &babyGrootSearch, nil
 | |
| 				case "(|(username=peter))":
 | |
| 					return &peterSearch, nil
 | |
| 				default:
 | |
| 					return nil, fmt.Errorf("test case not defined for user filter: '%s'", request.Filter)
 | |
| 				}
 | |
| 			case groupsOU:
 | |
| 				switch request.Filter {
 | |
| 				case "(member=groot)":
 | |
| 					return &grootGroups, nil
 | |
| 				case "(member=babygroot)":
 | |
| 					return &babyGrootGroups, nil
 | |
| 				case "(member=peter)":
 | |
| 					return &peterGroups, nil
 | |
| 				default:
 | |
| 					return nil, fmt.Errorf("test case not defined for group filter: '%s'", request.Filter)
 | |
| 				}
 | |
| 			default:
 | |
| 				return nil, fmt.Errorf("test case not defined for baseDN: '%s'", request.BaseDN)
 | |
| 			}
 | |
| 		})
 | |
| 
 | |
| 		isGrafanaAdmin := true
 | |
| 		cfg := setting.NewCfg()
 | |
| 		cfg.LDAPAuthEnabled = true
 | |
| 
 | |
| 		server := &Server{
 | |
| 			cfg: cfg,
 | |
| 			Config: &ServerConfig{
 | |
| 				Attr: AttributeMap{
 | |
| 					Username: "username",
 | |
| 					Name:     "name",
 | |
| 				},
 | |
| 				SearchBaseDNs:      []string{usersOU},
 | |
| 				SearchFilter:       "(username=%s)",
 | |
| 				GroupSearchFilter:  "(member=%s)",
 | |
| 				GroupSearchBaseDNs: []string{groupsOU},
 | |
| 				Groups: []*GroupToOrgRole{
 | |
| 					{
 | |
| 						GroupDN:        creaturesDN,
 | |
| 						OrgId:          2,
 | |
| 						IsGrafanaAdmin: &isGrafanaAdmin,
 | |
| 						OrgRole:        "Admin",
 | |
| 					},
 | |
| 					{
 | |
| 						GroupDN: babyCreaturesDN,
 | |
| 						OrgId:   2,
 | |
| 						OrgRole: "Editor",
 | |
| 					},
 | |
| 				},
 | |
| 			},
 | |
| 			Connection: conn,
 | |
| 			log:        log.New("test-logger"),
 | |
| 		}
 | |
| 
 | |
| 		t.Run("disable user with no mapping", func(t *testing.T) {
 | |
| 			res, err := server.Users([]string{"peter"})
 | |
| 			require.NoError(t, err)
 | |
| 			require.Len(t, res, 1)
 | |
| 			require.Equal(t, "Peter", res[0].Name)
 | |
| 			require.ElementsMatch(t, res[0].Groups, []string{humansDN})
 | |
| 			require.Empty(t, res[0].OrgRoles)
 | |
| 			require.True(t, res[0].IsDisabled)
 | |
| 		})
 | |
| 		t.Run("skip org role sync", func(t *testing.T) {
 | |
| 			server.cfg.LDAPSkipOrgRoleSync = true
 | |
| 
 | |
| 			res, err := server.Users([]string{"groot"})
 | |
| 			require.NoError(t, err)
 | |
| 			require.Len(t, res, 1)
 | |
| 			require.Equal(t, "I am Groot", res[0].Name)
 | |
| 			require.ElementsMatch(t, res[0].Groups, []string{creaturesDN})
 | |
| 			require.Empty(t, res[0].OrgRoles)
 | |
| 			require.False(t, res[0].IsDisabled)
 | |
| 		})
 | |
| 		t.Run("sync org role", func(t *testing.T) {
 | |
| 			server.cfg.LDAPSkipOrgRoleSync = false
 | |
| 			res, err := server.Users([]string{"groot"})
 | |
| 			require.NoError(t, err)
 | |
| 			require.Len(t, res, 1)
 | |
| 			require.Equal(t, "I am Groot", res[0].Name)
 | |
| 			require.ElementsMatch(t, res[0].Groups, []string{creaturesDN})
 | |
| 			require.Len(t, res[0].OrgRoles, 1)
 | |
| 			role, mappingExist := res[0].OrgRoles[2]
 | |
| 			require.True(t, mappingExist)
 | |
| 			require.Equal(t, roletype.RoleAdmin, role)
 | |
| 			require.False(t, res[0].IsDisabled)
 | |
| 			require.NotNil(t, res[0].IsGrafanaAdmin)
 | |
| 			assert.True(t, *res[0].IsGrafanaAdmin)
 | |
| 		})
 | |
| 		t.Run("set Grafana Admin to false by default", func(t *testing.T) {
 | |
| 			res, err := server.Users([]string{"babygroot"})
 | |
| 			require.NoError(t, err)
 | |
| 			require.Len(t, res, 1)
 | |
| 			require.Equal(t, "I am baby Groot", res[0].Name)
 | |
| 			require.ElementsMatch(t, res[0].Groups, []string{babyCreaturesDN})
 | |
| 			require.Len(t, res[0].OrgRoles, 1)
 | |
| 			role, mappingExist := res[0].OrgRoles[2]
 | |
| 			require.True(t, mappingExist)
 | |
| 			require.Equal(t, roletype.RoleEditor, role)
 | |
| 			require.False(t, res[0].IsDisabled)
 | |
| 			require.NotNil(t, res[0].IsGrafanaAdmin)
 | |
| 			assert.False(t, *res[0].IsGrafanaAdmin)
 | |
| 		})
 | |
| 	})
 | |
| }
 | |
| 
 | |
| func TestServer_UserBind(t *testing.T) {
 | |
| 	t.Run("use provided 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
 | |
| 		}
 | |
| 		server := &Server{
 | |
| 			Connection: connection,
 | |
| 			Config: &ServerConfig{
 | |
| 				BindDN: "cn=admin,dc=grafana,dc=org",
 | |
| 			},
 | |
| 		}
 | |
| 
 | |
| 		dn := "cn=user,ou=users,dc=grafana,dc=org"
 | |
| 		err := server.UserBind(dn, "pwd")
 | |
| 
 | |
| 		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)
 | |
| 	})
 | |
| }
 |