authelia/internal/authentication/ldap_user_provider_test.go

389 lines
11 KiB
Go
Raw Normal View History

2019-12-06 08:15:54 +00:00
package authentication
import (
"testing"
"github.com/go-ldap/ldap/v3"
[FEATURE] Automatic Profile Refresh - LDAP (#912) * [FIX] LDAP Not Checking for Updated Groups * refactor handlers verifyFromSessionCookie * refactor authorizer selectMatchingObjectRules * refactor authorizer isDomainMatching * add authorizer URLHasGroupSubjects method * add user provider ProviderType method * update tests * check for new LDAP groups and update session when: * user provider type is LDAP * authorization is forbidden * URL has rule with group subjects * Implement Refresh Interval * add default values for LDAP user provider * add default for refresh interval * add schema validator for refresh interval * add various tests * rename hasUserBeenInactiveLongEnough to hasUserBeenInactiveTooLong * use Authelia ctx clock * add check to determine if user is deleted, if so destroy the * make ldap user not found error a const * implement GetRefreshSettings in mock * Use user not found const with FileProvider * comment exports * use ctx.Clock instead of time pkg * add debug logging * use ptr to reference userSession so we don't have to retrieve it again * add documenation * add check for 0 refresh interval to reduce CPU cost * remove badly copied debug msg * add group change delta message * add SliceStringDelta * refactor ldap refresh to use the new func * improve delta add/remove log message * fix incorrect logic in SliceStringDelta * add tests to SliceStringDelta * add always config option * add tests for always config option * update docs * apply suggestions from code review Co-Authored-By: Amir Zarrinkafsh <nightah@me.com> * complete mocks and fix an old one * show warning when LDAP details failed to update for an unknown reason * golint fix * actually fix existing mocks * use mocks for LDAP refresh testing * use mocks for LDAP refresh testing for both added and removed groups * use test mock to verify disabled refresh behaviour * add information to threat model * add time const for default Unix() value * misc adjustments to mocks * Suggestions from code review * requested changes * update emails * docs updates * test updates * misc * golint fix * set debug for dev testing * misc docs and logging updates * misc grammar/spelling * use built function for VerifyGet * fix reviewdog suggestions * requested changes * Apply suggestions from code review Co-authored-by: Amir Zarrinkafsh <nightah@me.com> Co-authored-by: Clément Michaud <clement.michaud34@gmail.com>
2020-05-04 19:39:25 +00:00
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
2019-12-06 08:15:54 +00:00
"github.com/stretchr/testify/require"
"github.com/authelia/authelia/internal/configuration/schema"
2019-12-06 08:15:54 +00:00
)
func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
}, mockFactory)
mockFactory.EXPECT().
Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
_, err := ldap.connect("cn=admin,dc=example,dc=com", "password")
require.NoError(t, err)
}
func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
}, mockFactory)
mockFactory.EXPECT().
DialTLS(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389"), gomock.Any()).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
_, err := ldap.connect("cn=admin,dc=example,dc=com", "password")
require.NoError(t, err)
}
func TestEscapeSpecialCharsFromUserInput(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
}, mockFactory)
// No escape
assert.Equal(t, "xyz", ldap.ldapEscape("xyz"))
// Escape
assert.Equal(t, "test\\,abc", ldap.ldapEscape("test,abc"))
assert.Equal(t, "test\\5cabc", ldap.ldapEscape("test\\abc"))
assert.Equal(t, "test\\2aabc", ldap.ldapEscape("test*abc"))
assert.Equal(t, "test \\28abc\\29", ldap.ldapEscape("test (abc)"))
assert.Equal(t, "test\\#abc", ldap.ldapEscape("test#abc"))
assert.Equal(t, "test\\+abc", ldap.ldapEscape("test+abc"))
assert.Equal(t, "test\\<abc", ldap.ldapEscape("test<abc"))
assert.Equal(t, "test\\>abc", ldap.ldapEscape("test>abc"))
assert.Equal(t, "test\\;abc", ldap.ldapEscape("test;abc"))
assert.Equal(t, "test\\\"abc", ldap.ldapEscape("test\"abc"))
assert.Equal(t, "test\\=abc", ldap.ldapEscape("test=abc"))
assert.Equal(t, "test\\,\\5c\\28abc\\29", ldap.ldapEscape("test,\\(abc)"))
}
func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
ldap := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldaps://127.0.0.1:389",
GroupsFilter: "(|(member={dn})(uid={username})(uid={input}))",
}, mockFactory)
profile := ldapUserProfile{
DN: "cn=john (external),dc=example,dc=com",
Username: "john",
DisplayName: "John Doe",
Emails: []string{"john.doe@authelia.com"},
}
filter, _ := ldap.resolveGroupsFilter("john", &profile)
assert.Equal(t, "(|(member=cn=john \\28external\\29,dc=example,dc=com)(uid=john)(uid=john))", filter)
filter, _ = ldap.resolveGroupsFilter("john#=(abc,def)", &profile)
assert.Equal(t, "(|(member=cn=john \\28external\\29,dc=example,dc=com)(uid=john)(uid=john\\#\\=\\28abc\\,def\\29))", filter)
}
type SearchRequestMatcher struct {
expected string
}
func NewSearchRequestMatcher(expected string) *SearchRequestMatcher {
return &SearchRequestMatcher{expected}
}
func (srm *SearchRequestMatcher) Matches(x interface{}) bool {
sr := x.(*ldap.SearchRequest)
return sr.Filter == srm.expected
}
func (srm *SearchRequestMatcher) String() string {
return ""
}
func TestShouldEscapeUserInput(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsersFilter: "(|({username_attribute}={input})({mail_attribute}={input}))",
UsernameAttribute: "uid",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
Password: "password",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
mockConn.EXPECT().
// Here we ensure that the input has been correctly escaped.
Search(NewSearchRequestMatcher("(|(uid=john\\=abc)(mail=john\\=abc))")).
Return(&ldap.SearchResult{}, nil)
ldapClient.getUserProfile(mockConn, "john=abc") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
}
func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
UsernameAttribute: "uid",
UsersFilter: "(&({username_attribute}={input})(&(objectCategory=person)(objectClass=user)))",
Password: "password",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
}, mockFactory)
mockConn.EXPECT().
Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")).
Return(&ldap.SearchResult{}, nil)
ldapClient.getUserProfile(mockConn, "john") //nolint:errcheck // TODO: Legacy code, consider refactoring time permitting.
}
func createSearchResultWithAttributes(attributes ...*ldap.EntryAttribute) *ldap.SearchResult {
return &ldap.SearchResult{
Entries: []*ldap.Entry{
{Attributes: attributes},
},
}
}
func createSearchResultWithAttributeValues(values ...string) *ldap.SearchResult {
return createSearchResultWithAttributes(&ldap.EntryAttribute{
Values: values,
})
}
func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
UsernameAttribute: "uid",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
mockFactory.EXPECT().
Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
mockConn.EXPECT().
Close()
searchGroups := mockConn.EXPECT().
Search(gomock.Any()).
Return(createSearchResultWithAttributes(), nil)
searchProfile := mockConn.EXPECT().
Search(gomock.Any()).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=test,dc=example,dc=com",
Attributes: []*ldap.EntryAttribute{
{
Name: "displayname",
Values: []string{"John Doe"},
},
{
Name: "mail",
Values: []string{"test@example.com"},
},
{
Name: "uid",
Values: []string{"john"},
},
},
},
},
}, nil)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)
assert.ElementsMatch(t, details.Groups, []string{})
assert.ElementsMatch(t, details.Emails, []string{"test@example.com"})
assert.Equal(t, details.DisplayName, "John Doe")
assert.Equal(t, details.Username, "john")
}
func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
UsernameAttribute: "uid",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
mockFactory.EXPECT().
Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
mockConn.EXPECT().
Close()
searchGroups := mockConn.EXPECT().
Search(gomock.Any()).
Return(createSearchResultWithAttributeValues("group1", "group2"), nil)
searchProfile := mockConn.EXPECT().
Search(gomock.Any()).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=test,dc=example,dc=com",
Attributes: []*ldap.EntryAttribute{
{
Name: "uid",
Values: []string{"john"},
},
},
},
},
}, nil)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)
assert.ElementsMatch(t, details.Groups, []string{"group1", "group2"})
assert.ElementsMatch(t, details.Emails, []string{})
assert.Equal(t, details.Username, "john")
}
func TestShouldReturnUsernameFromLDAP(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockFactory := NewMockLDAPConnectionFactory(ctrl)
mockConn := NewMockLDAPConnection(ctrl)
ldapClient := NewLDAPUserProviderWithFactory(schema.LDAPAuthenticationBackendConfiguration{
URL: "ldap://127.0.0.1:389",
User: "cn=admin,dc=example,dc=com",
Password: "password",
UsernameAttribute: "uid",
MailAttribute: "mail",
DisplayNameAttribute: "displayname",
UsersFilter: "uid={input}",
AdditionalUsersDN: "ou=users",
BaseDN: "dc=example,dc=com",
}, mockFactory)
mockFactory.EXPECT().
Dial(gomock.Eq("tcp"), gomock.Eq("127.0.0.1:389")).
Return(mockConn, nil)
mockConn.EXPECT().
Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")).
Return(nil)
mockConn.EXPECT().
Close()
searchGroups := mockConn.EXPECT().
Search(gomock.Any()).
Return(createSearchResultWithAttributeValues("group1", "group2"), nil)
searchProfile := mockConn.EXPECT().
Search(gomock.Any()).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: "uid=test,dc=example,dc=com",
Attributes: []*ldap.EntryAttribute{
{
Name: "displayname",
Values: []string{"John Doe"},
},
{
Name: "mail",
Values: []string{"test@example.com"},
},
{
Name: "uid",
Values: []string{"John"},
},
},
},
},
}, nil)
gomock.InOrder(searchProfile, searchGroups)
details, err := ldapClient.GetDetails("john")
require.NoError(t, err)
assert.ElementsMatch(t, details.Groups, []string{"group1", "group2"})
assert.ElementsMatch(t, details.Emails, []string{"test@example.com"})
assert.Equal(t, details.DisplayName, "John Doe")
assert.Equal(t, details.Username, "John")
}