From 150e54c3aee4bb478218085f1e04f84638b0be33 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 10 May 2022 14:38:36 +1000 Subject: [PATCH] fix(authentication): utilize msad password history control (#3256) This fixes an issue where the Microsoft Active Directory Server Policy Hints control was not being used to prevent avoidance of the PSO / FGPP applicable to the user. --- internal/authentication/const.go | 45 +- internal/authentication/gen.go | 4 +- .../authentication/ldap_client_factory.go | 18 + .../ldap_client_factory_mock.go | 55 + ...connection_mock.go => ldap_client_mock.go} | 60 +- .../authentication/ldap_connection_factory.go | 18 - .../ldap_connection_factory_mock.go | 55 - internal/authentication/ldap_control_types.go | 36 + internal/authentication/ldap_user_provider.go | 226 +- .../ldap_user_provider_startup.go | 83 +- .../authentication/ldap_user_provider_test.go | 2705 ++++++++++++++++- internal/authentication/ldap_util.go | 47 + internal/authentication/ldap_util_test.go | 320 ++ internal/authentication/types.go | 37 +- 14 files changed, 3318 insertions(+), 391 deletions(-) create mode 100644 internal/authentication/ldap_client_factory.go create mode 100644 internal/authentication/ldap_client_factory_mock.go rename internal/authentication/{ldap_connection_mock.go => ldap_client_mock.go} (54%) delete mode 100644 internal/authentication/ldap_connection_factory.go delete mode 100644 internal/authentication/ldap_connection_factory_mock.go create mode 100644 internal/authentication/ldap_control_types.go create mode 100644 internal/authentication/ldap_util_test.go diff --git a/internal/authentication/const.go b/internal/authentication/const.go index 1100dd56..8ee6568a 100644 --- a/internal/authentication/const.go +++ b/internal/authentication/const.go @@ -18,7 +18,46 @@ const ( const ( ldapSupportedExtensionAttribute = "supportedExtension" - ldapOIDPasswdModifyExtension = "1.3.6.1.4.1.4203.1.11.1" // http://oidref.com/1.3.6.1.4.1.4203.1.11.1 + + // LDAP Extension OID: Password Modify Extended Operation. + // + // RFC3062: https://datatracker.ietf.org/doc/html/rfc3062 + // + // OID Reference: http://oidref.com/1.3.6.1.4.1.4203.1.11.1 + // + // See the linked documents for more information. + ldapOIDExtensionPwdModifyExOp = "1.3.6.1.4.1.4203.1.11.1" + + // LDAP Extension OID: Transport Layer Security. + // + // RFC2830: https://datatracker.ietf.org/doc/html/rfc2830 + // + // OID Reference: https://oidref.com/1.3.6.1.4.1.1466.20037 + // + // See the linked documents for more information. + ldapOIDExtensionTLS = "1.3.6.1.4.1.1466.20037" +) + +const ( + ldapSupportedControlAttribute = "supportedControl" + + // LDAP Control OID: Microsoft Password Policy Hints. + // + // MS ADTS: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/4add7bce-e502-4e0f-9d69-1a3f153713e2 + // + // OID Reference: https://oidref.com/1.2.840.113556.1.4.2239 + // + // See the linked documents for more information. + ldapOIDControlMsftServerPolicyHints = "1.2.840.113556.1.4.2239" + + // LDAP Control OID: Microsoft Password Policy Hints (deprecated). + // + // MS ADTS: https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/49751d58-8115-4277-8faf-64c83a5f658f + // + // OID Reference: https://oidref.com/1.2.840.113556.1.4.2066 + // + // See the linked documents for more information. + ldapOIDControlMsftServerPolicyHintsDeprecated = "1.2.840.113556.1.4.2066" ) const ( @@ -32,6 +71,10 @@ const ( ldapPlaceholderUsername = "{username}" ) +const ( + none = "none" +) + // CryptAlgo the crypt representation of an algorithm used in the prefix of the hash. type CryptAlgo string diff --git a/internal/authentication/gen.go b/internal/authentication/gen.go index 04eaad7a..18547d60 100644 --- a/internal/authentication/gen.go +++ b/internal/authentication/gen.go @@ -3,5 +3,5 @@ package authentication // This file is used to generate mocks. You can generate all mocks using the // command `go generate github.com/authelia/authelia/v4/internal/authentication`. -//go:generate mockgen -package authentication -destination ldap_connection_mock.go -mock_names LDAPConnection=MockLDAPConnection github.com/authelia/authelia/v4/internal/authentication LDAPConnection -//go:generate mockgen -package authentication -destination ldap_connection_factory_mock.go -mock_names LDAPConnectionFactory=MockLDAPConnectionFactory github.com/authelia/authelia/v4/internal/authentication LDAPConnectionFactory +//go:generate mockgen -package authentication -destination ldap_client_mock.go -mock_names LDAPClient=MockLDAPClient github.com/authelia/authelia/v4/internal/authentication LDAPClient +//go:generate mockgen -package authentication -destination ldap_client_factory_mock.go -mock_names LDAPClientFactory=MockLDAPClientFactory github.com/authelia/authelia/v4/internal/authentication LDAPClientFactory diff --git a/internal/authentication/ldap_client_factory.go b/internal/authentication/ldap_client_factory.go new file mode 100644 index 00000000..d360f05b --- /dev/null +++ b/internal/authentication/ldap_client_factory.go @@ -0,0 +1,18 @@ +package authentication + +import ( + "github.com/go-ldap/ldap/v3" +) + +// ProductionLDAPClientFactory the production implementation of an ldap connection factory. +type ProductionLDAPClientFactory struct{} + +// NewProductionLDAPClientFactory create a concrete ldap connection factory. +func NewProductionLDAPClientFactory() *ProductionLDAPClientFactory { + return &ProductionLDAPClientFactory{} +} + +// DialURL creates a client from an LDAP URL when successful. +func (f *ProductionLDAPClientFactory) DialURL(addr string, opts ...ldap.DialOpt) (client LDAPClient, err error) { + return ldap.DialURL(addr, opts...) +} diff --git a/internal/authentication/ldap_client_factory_mock.go b/internal/authentication/ldap_client_factory_mock.go new file mode 100644 index 00000000..d6cb4122 --- /dev/null +++ b/internal/authentication/ldap_client_factory_mock.go @@ -0,0 +1,55 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: github.com/authelia/authelia/v4/internal/authentication (interfaces: LDAPClientFactory) + +// Package authentication is a generated GoMock package. +package authentication + +import ( + reflect "reflect" + + v3 "github.com/go-ldap/ldap/v3" + gomock "github.com/golang/mock/gomock" +) + +// MockLDAPClientFactory is a mock of LDAPClientFactory interface. +type MockLDAPClientFactory struct { + ctrl *gomock.Controller + recorder *MockLDAPClientFactoryMockRecorder +} + +// MockLDAPClientFactoryMockRecorder is the mock recorder for MockLDAPClientFactory. +type MockLDAPClientFactoryMockRecorder struct { + mock *MockLDAPClientFactory +} + +// NewMockLDAPClientFactory creates a new mock instance. +func NewMockLDAPClientFactory(ctrl *gomock.Controller) *MockLDAPClientFactory { + mock := &MockLDAPClientFactory{ctrl: ctrl} + mock.recorder = &MockLDAPClientFactoryMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use. +func (m *MockLDAPClientFactory) EXPECT() *MockLDAPClientFactoryMockRecorder { + return m.recorder +} + +// DialURL mocks base method. +func (m *MockLDAPClientFactory) DialURL(arg0 string, arg1 ...v3.DialOpt) (LDAPClient, error) { + m.ctrl.T.Helper() + varargs := []interface{}{arg0} + for _, a := range arg1 { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "DialURL", varargs...) + ret0, _ := ret[0].(LDAPClient) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DialURL indicates an expected call of DialURL. +func (mr *MockLDAPClientFactoryMockRecorder) DialURL(arg0 interface{}, arg1 ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{arg0}, arg1...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialURL", reflect.TypeOf((*MockLDAPClientFactory)(nil).DialURL), varargs...) +} diff --git a/internal/authentication/ldap_connection_mock.go b/internal/authentication/ldap_client_mock.go similarity index 54% rename from internal/authentication/ldap_connection_mock.go rename to internal/authentication/ldap_client_mock.go index b234da16..ebfd8591 100644 --- a/internal/authentication/ldap_connection_mock.go +++ b/internal/authentication/ldap_client_mock.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: github.com/authelia/authelia/v4/internal/authentication (interfaces: LDAPConnection) +// Source: github.com/authelia/authelia/v4/internal/authentication (interfaces: LDAPClient) // Package authentication is a generated GoMock package. package authentication @@ -12,31 +12,31 @@ import ( gomock "github.com/golang/mock/gomock" ) -// MockLDAPConnection is a mock of LDAPConnection interface. -type MockLDAPConnection struct { +// MockLDAPClient is a mock of LDAPClient interface. +type MockLDAPClient struct { ctrl *gomock.Controller - recorder *MockLDAPConnectionMockRecorder + recorder *MockLDAPClientMockRecorder } -// MockLDAPConnectionMockRecorder is the mock recorder for MockLDAPConnection. -type MockLDAPConnectionMockRecorder struct { - mock *MockLDAPConnection +// MockLDAPClientMockRecorder is the mock recorder for MockLDAPClient. +type MockLDAPClientMockRecorder struct { + mock *MockLDAPClient } -// NewMockLDAPConnection creates a new mock instance. -func NewMockLDAPConnection(ctrl *gomock.Controller) *MockLDAPConnection { - mock := &MockLDAPConnection{ctrl: ctrl} - mock.recorder = &MockLDAPConnectionMockRecorder{mock} +// NewMockLDAPClient creates a new mock instance. +func NewMockLDAPClient(ctrl *gomock.Controller) *MockLDAPClient { + mock := &MockLDAPClient{ctrl: ctrl} + mock.recorder = &MockLDAPClientMockRecorder{mock} return mock } // EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockLDAPConnection) EXPECT() *MockLDAPConnectionMockRecorder { +func (m *MockLDAPClient) EXPECT() *MockLDAPClientMockRecorder { return m.recorder } // Bind mocks base method. -func (m *MockLDAPConnection) Bind(arg0, arg1 string) error { +func (m *MockLDAPClient) Bind(arg0, arg1 string) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Bind", arg0, arg1) ret0, _ := ret[0].(error) @@ -44,25 +44,25 @@ func (m *MockLDAPConnection) Bind(arg0, arg1 string) error { } // Bind indicates an expected call of Bind. -func (mr *MockLDAPConnectionMockRecorder) Bind(arg0, arg1 interface{}) *gomock.Call { +func (mr *MockLDAPClientMockRecorder) Bind(arg0, arg1 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bind", reflect.TypeOf((*MockLDAPConnection)(nil).Bind), arg0, arg1) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bind", reflect.TypeOf((*MockLDAPClient)(nil).Bind), arg0, arg1) } // Close mocks base method. -func (m *MockLDAPConnection) Close() { +func (m *MockLDAPClient) Close() { m.ctrl.T.Helper() m.ctrl.Call(m, "Close") } // Close indicates an expected call of Close. -func (mr *MockLDAPConnectionMockRecorder) Close() *gomock.Call { +func (mr *MockLDAPClientMockRecorder) Close() *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockLDAPConnection)(nil).Close)) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockLDAPClient)(nil).Close)) } // Modify mocks base method. -func (m *MockLDAPConnection) Modify(arg0 *ldap.ModifyRequest) error { +func (m *MockLDAPClient) Modify(arg0 *ldap.ModifyRequest) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Modify", arg0) ret0, _ := ret[0].(error) @@ -70,13 +70,13 @@ func (m *MockLDAPConnection) Modify(arg0 *ldap.ModifyRequest) error { } // Modify indicates an expected call of Modify. -func (mr *MockLDAPConnectionMockRecorder) Modify(arg0 interface{}) *gomock.Call { +func (mr *MockLDAPClientMockRecorder) Modify(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockLDAPConnection)(nil).Modify), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Modify", reflect.TypeOf((*MockLDAPClient)(nil).Modify), arg0) } // PasswordModify mocks base method. -func (m *MockLDAPConnection) PasswordModify(arg0 *ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) { +func (m *MockLDAPClient) PasswordModify(arg0 *ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "PasswordModify", arg0) ret0, _ := ret[0].(*ldap.PasswordModifyResult) @@ -85,13 +85,13 @@ func (m *MockLDAPConnection) PasswordModify(arg0 *ldap.PasswordModifyRequest) (* } // PasswordModify indicates an expected call of PasswordModify. -func (mr *MockLDAPConnectionMockRecorder) PasswordModify(arg0 interface{}) *gomock.Call { +func (mr *MockLDAPClientMockRecorder) PasswordModify(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordModify", reflect.TypeOf((*MockLDAPConnection)(nil).PasswordModify), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PasswordModify", reflect.TypeOf((*MockLDAPClient)(nil).PasswordModify), arg0) } // Search mocks base method. -func (m *MockLDAPConnection) Search(arg0 *ldap.SearchRequest) (*ldap.SearchResult, error) { +func (m *MockLDAPClient) Search(arg0 *ldap.SearchRequest) (*ldap.SearchResult, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "Search", arg0) ret0, _ := ret[0].(*ldap.SearchResult) @@ -100,13 +100,13 @@ func (m *MockLDAPConnection) Search(arg0 *ldap.SearchRequest) (*ldap.SearchResul } // Search indicates an expected call of Search. -func (mr *MockLDAPConnectionMockRecorder) Search(arg0 interface{}) *gomock.Call { +func (mr *MockLDAPClientMockRecorder) Search(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockLDAPConnection)(nil).Search), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Search", reflect.TypeOf((*MockLDAPClient)(nil).Search), arg0) } // StartTLS mocks base method. -func (m *MockLDAPConnection) StartTLS(arg0 *tls.Config) error { +func (m *MockLDAPClient) StartTLS(arg0 *tls.Config) error { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "StartTLS", arg0) ret0, _ := ret[0].(error) @@ -114,7 +114,7 @@ func (m *MockLDAPConnection) StartTLS(arg0 *tls.Config) error { } // StartTLS indicates an expected call of StartTLS. -func (mr *MockLDAPConnectionMockRecorder) StartTLS(arg0 interface{}) *gomock.Call { +func (mr *MockLDAPClientMockRecorder) StartTLS(arg0 interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTLS", reflect.TypeOf((*MockLDAPConnection)(nil).StartTLS), arg0) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StartTLS", reflect.TypeOf((*MockLDAPClient)(nil).StartTLS), arg0) } diff --git a/internal/authentication/ldap_connection_factory.go b/internal/authentication/ldap_connection_factory.go deleted file mode 100644 index ab5a8640..00000000 --- a/internal/authentication/ldap_connection_factory.go +++ /dev/null @@ -1,18 +0,0 @@ -package authentication - -import ( - "github.com/go-ldap/ldap/v3" -) - -// ProductionLDAPConnectionFactory the production implementation of an ldap connection factory. -type ProductionLDAPConnectionFactory struct{} - -// NewProductionLDAPConnectionFactory create a concrete ldap connection factory. -func NewProductionLDAPConnectionFactory() *ProductionLDAPConnectionFactory { - return &ProductionLDAPConnectionFactory{} -} - -// DialURL creates a connection from an LDAP URL when successful. -func (f *ProductionLDAPConnectionFactory) DialURL(addr string, opts ...ldap.DialOpt) (conn LDAPConnection, err error) { - return ldap.DialURL(addr, opts...) -} diff --git a/internal/authentication/ldap_connection_factory_mock.go b/internal/authentication/ldap_connection_factory_mock.go deleted file mode 100644 index 08b04115..00000000 --- a/internal/authentication/ldap_connection_factory_mock.go +++ /dev/null @@ -1,55 +0,0 @@ -// Code generated by MockGen. DO NOT EDIT. -// Source: github.com/authelia/authelia/v4/internal/authentication (interfaces: LDAPConnectionFactory) - -// Package authentication is a generated GoMock package. -package authentication - -import ( - reflect "reflect" - - v3 "github.com/go-ldap/ldap/v3" - gomock "github.com/golang/mock/gomock" -) - -// MockLDAPConnectionFactory is a mock of LDAPConnectionFactory interface. -type MockLDAPConnectionFactory struct { - ctrl *gomock.Controller - recorder *MockLDAPConnectionFactoryMockRecorder -} - -// MockLDAPConnectionFactoryMockRecorder is the mock recorder for MockLDAPConnectionFactory. -type MockLDAPConnectionFactoryMockRecorder struct { - mock *MockLDAPConnectionFactory -} - -// NewMockLDAPConnectionFactory creates a new mock instance. -func NewMockLDAPConnectionFactory(ctrl *gomock.Controller) *MockLDAPConnectionFactory { - mock := &MockLDAPConnectionFactory{ctrl: ctrl} - mock.recorder = &MockLDAPConnectionFactoryMockRecorder{mock} - return mock -} - -// EXPECT returns an object that allows the caller to indicate expected use. -func (m *MockLDAPConnectionFactory) EXPECT() *MockLDAPConnectionFactoryMockRecorder { - return m.recorder -} - -// DialURL mocks base method. -func (m *MockLDAPConnectionFactory) DialURL(arg0 string, arg1 ...v3.DialOpt) (LDAPConnection, error) { - m.ctrl.T.Helper() - varargs := []interface{}{arg0} - for _, a := range arg1 { - varargs = append(varargs, a) - } - ret := m.ctrl.Call(m, "DialURL", varargs...) - ret0, _ := ret[0].(LDAPConnection) - ret1, _ := ret[1].(error) - return ret0, ret1 -} - -// DialURL indicates an expected call of DialURL. -func (mr *MockLDAPConnectionFactoryMockRecorder) DialURL(arg0 interface{}, arg1 ...interface{}) *gomock.Call { - mr.mock.ctrl.T.Helper() - varargs := append([]interface{}{arg0}, arg1...) - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DialURL", reflect.TypeOf((*MockLDAPConnectionFactory)(nil).DialURL), varargs...) -} diff --git a/internal/authentication/ldap_control_types.go b/internal/authentication/ldap_control_types.go new file mode 100644 index 00000000..c7f4bd5c --- /dev/null +++ b/internal/authentication/ldap_control_types.go @@ -0,0 +1,36 @@ +package authentication + +import ( + ber "github.com/go-asn1-ber/asn1-ber" +) + +type controlMsftServerPolicyHints struct { + oid string +} + +// GetControlType implements ldap.Control. +func (c *controlMsftServerPolicyHints) GetControlType() string { + return c.oid +} + +// Encode implements ldap.Control. +func (c *controlMsftServerPolicyHints) Encode() (packet *ber.Packet) { + seq := ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "PolicyHintsRequestValue") + seq.AppendChild(ber.NewInteger(ber.ClassUniversal, ber.TypePrimitive, ber.TagInteger, 1, "Flags")) + + controlValue := ber.Encode(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, nil, "Control Value (Policy Hints)") + controlValue.AppendChild(seq) + + packet = ber.Encode(ber.ClassUniversal, ber.TypeConstructed, ber.TagSequence, nil, "Control") + packet.AppendChild(ber.NewString(ber.ClassUniversal, ber.TypePrimitive, ber.TagOctetString, c.GetControlType(), "Control Type (LDAP_SERVER_POLICY_HINTS_OID)")) + packet.AppendChild(ber.NewBoolean(ber.ClassUniversal, ber.TypePrimitive, ber.TagBoolean, true, "Criticality")) + + packet.AppendChild(controlValue) + + return packet +} + +// String implements ldap.Control. +func (c *controlMsftServerPolicyHints) String() string { + return "Enforce the password history length constraint (MS-SAMR section 3.1.1.7.1) during password set: " + c.GetControlType() +} diff --git a/internal/authentication/ldap_user_provider.go b/internal/authentication/ldap_user_provider.go index 4bc9c87c..23d5518e 100644 --- a/internal/authentication/ldap_user_provider.go +++ b/internal/authentication/ldap_user_provider.go @@ -21,12 +21,12 @@ type LDAPUserProvider struct { tlsConfig *tls.Config dialOpts []ldap.DialOpt log *logrus.Logger - factory LDAPConnectionFactory + factory LDAPClientFactory disableResetPassword bool - // Automatically detected ldap features. - supportExtensionPasswdModify bool + // Automatically detected LDAP features. + features LDAPSupportedFeatures // Dynamically generated users values. usersBaseDN string @@ -48,7 +48,7 @@ func NewLDAPUserProvider(config schema.AuthenticationBackendConfiguration, certP return provider } -func newLDAPUserProvider(config schema.LDAPAuthenticationBackendConfiguration, disableResetPassword bool, certPool *x509.CertPool, factory LDAPConnectionFactory) (provider *LDAPUserProvider) { +func newLDAPUserProvider(config schema.LDAPAuthenticationBackendConfiguration, disableResetPassword bool, certPool *x509.CertPool, factory LDAPClientFactory) (provider *LDAPUserProvider) { if config.TLS == nil { config.TLS = schema.DefaultLDAPAuthenticationBackendConfiguration.TLS } @@ -64,7 +64,7 @@ func newLDAPUserProvider(config schema.LDAPAuthenticationBackendConfiguration, d } if factory == nil { - factory = NewProductionLDAPConnectionFactory() + factory = NewProductionLDAPClientFactory() } provider = &LDAPUserProvider{ @@ -83,27 +83,27 @@ func newLDAPUserProvider(config schema.LDAPAuthenticationBackendConfiguration, d } // CheckUserPassword checks if provided password matches for the given user. -func (p *LDAPUserProvider) CheckUserPassword(inputUsername string, password string) (valid bool, err error) { +func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (valid bool, err error) { var ( - conn, connUser LDAPConnection - profile *ldapUserProfile + client, clientUser LDAPClient + profile *ldapUserProfile ) - if conn, err = p.connect(); err != nil { + if client, err = p.connect(); err != nil { return false, err } - defer conn.Close() + defer client.Close() - if profile, err = p.getUserProfile(conn, inputUsername); err != nil { + if profile, err = p.getUserProfile(client, username); err != nil { return false, err } - if connUser, err = p.connectCustom(p.config.URL, profile.DN, password, p.config.StartTLS, p.dialOpts...); err != nil { + if clientUser, err = p.connectCustom(p.config.URL, profile.DN, password, p.config.StartTLS, p.dialOpts...); err != nil { return false, fmt.Errorf("authentication failed. Cause: %w", err) } - defer connUser.Close() + defer clientUser.Close() return true, nil } @@ -111,17 +111,17 @@ func (p *LDAPUserProvider) CheckUserPassword(inputUsername string, password stri // GetDetails retrieve the groups a user belongs to. func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, err error) { var ( - conn LDAPConnection + client LDAPClient profile *ldapUserProfile ) - if conn, err = p.connect(); err != nil { + if client, err = p.connect(); err != nil { return nil, err } - defer conn.Close() + defer client.Close() - if profile, err = p.getUserProfile(conn, username); err != nil { + if profile, err = p.getUserProfile(client, username); err != nil { return nil, err } @@ -141,7 +141,7 @@ func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, er 0, 0, false, filter, p.groupsAttributes, nil, ) - if searchResult, err = p.search(conn, searchRequest); err != nil { + if searchResult, err = p.search(client, searchRequest); err != nil { return nil, fmt.Errorf("unable to retrieve groups of user '%s'. Cause: %w", username, err) } @@ -168,31 +168,38 @@ func (p *LDAPUserProvider) GetDetails(username string) (details *UserDetails, er // UpdatePassword update the password of the given user. func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error) { var ( - conn LDAPConnection + client LDAPClient profile *ldapUserProfile ) - if conn, err = p.connect(); err != nil { + if client, err = p.connect(); err != nil { return fmt.Errorf("unable to update password. Cause: %w", err) } - defer conn.Close() + defer client.Close() - if profile, err = p.getUserProfile(conn, username); err != nil { + if profile, err = p.getUserProfile(client, username); err != nil { return fmt.Errorf("unable to update password. Cause: %w", err) } var controls []ldap.Control switch { - case p.supportExtensionPasswdModify: + case p.features.ControlTypes.MsftPwdPolHints: + controls = append(controls, &controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}) + case p.features.ControlTypes.MsftPwdPolHintsDeprecated: + controls = append(controls, &controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHintsDeprecated}) + } + + switch { + case p.features.Extensions.PwdModifyExOp: pwdModifyRequest := ldap.NewPasswordModifyRequest( profile.DN, "", password, ) - err = p.pwdModify(conn, pwdModifyRequest) + err = p.pwdModify(client, pwdModifyRequest) case p.config.Implementation == schema.LDAPImplementationActiveDirectory: modifyRequest := ldap.NewModifyRequest(profile.DN, controls) // The password needs to be enclosed in quotes @@ -200,12 +207,12 @@ func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error) pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", password)) modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) - err = p.modify(conn, modifyRequest) + err = p.modify(client, modifyRequest) default: modifyRequest := ldap.NewModifyRequest(profile.DN, controls) modifyRequest.Replace(ldapAttributeUserPassword, []string{password}) - err = p.modify(conn, modifyRequest) + err = p.modify(client, modifyRequest) } if err != nil { @@ -215,73 +222,74 @@ func (p *LDAPUserProvider) UpdatePassword(username, password string) (err error) return nil } -func (p *LDAPUserProvider) connect() (LDAPConnection, error) { +func (p *LDAPUserProvider) connect() (client LDAPClient, err error) { return p.connectCustom(p.config.URL, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...) } -func (p *LDAPUserProvider) connectCustom(url, userDN, password string, startTLS bool, opts ...ldap.DialOpt) (conn LDAPConnection, err error) { - if conn, err = p.factory.DialURL(url, opts...); err != nil { +func (p *LDAPUserProvider) connectCustom(url, userDN, password string, startTLS bool, opts ...ldap.DialOpt) (client LDAPClient, err error) { + if client, err = p.factory.DialURL(url, opts...); err != nil { return nil, fmt.Errorf("dial failed with error: %w", err) } if startTLS { - if err = conn.StartTLS(p.tlsConfig); err != nil { + if err = client.StartTLS(p.tlsConfig); err != nil { + client.Close() + return nil, fmt.Errorf("starttls failed with error: %w", err) } } - if err = conn.Bind(userDN, password); err != nil { + if err = client.Bind(userDN, password); err != nil { + client.Close() + return nil, fmt.Errorf("bind failed with error: %w", err) } - return conn, nil + return client, nil } -func (p *LDAPUserProvider) search(conn LDAPConnection, searchRequest *ldap.SearchRequest) (searchResult *ldap.SearchResult, err error) { - searchResult, err = conn.Search(searchRequest) - if err != nil { +func (p *LDAPUserProvider) search(client LDAPClient, searchRequest *ldap.SearchRequest) (searchResult *ldap.SearchResult, err error) { + if searchResult, err = client.Search(searchRequest); err != nil { if referral, ok := p.getReferral(err); ok { - if errReferral := p.searchReferral(referral, searchRequest, searchResult); errReferral != nil { - return nil, err + if searchResult == nil { + searchResult = &ldap.SearchResult{ + Referrals: []string{referral}, + } + } else { + searchResult.Referrals = append(searchResult.Referrals, referral) } - - return searchResult, nil } - - return nil, err } if !p.config.PermitReferrals || len(searchResult.Referrals) == 0 { + if err != nil { + return nil, err + } + return searchResult, nil } - p.searchReferrals(searchRequest, searchResult) + if err = p.searchReferrals(searchRequest, searchResult); err != nil { + return nil, err + } return searchResult, nil } func (p *LDAPUserProvider) searchReferral(referral string, searchRequest *ldap.SearchRequest, searchResult *ldap.SearchResult) (err error) { var ( - conn LDAPConnection + client LDAPClient result *ldap.SearchResult ) - if conn, err = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); err != nil { - p.log.Errorf("Failed to connect during referred search request (referred to %s): %v", referral, err) - - return err + if client, err = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); err != nil { + return fmt.Errorf("error occurred connecting to referred LDAP server '%s': %w", referral, err) } - defer conn.Close() + defer client.Close() - if result, err = conn.Search(searchRequest); err != nil { - p.log.Errorf("Failed to perform search operation during referred search request (referred to %s): %v", referral, err) - - return err - } - - if len(result.Entries) == 0 { - return err + if result, err = client.Search(searchRequest); err != nil { + return fmt.Errorf("error occurred performing search on referred LDAP server '%s': %w", referral, err) } for i := 0; i < len(result.Entries); i++ { @@ -293,14 +301,18 @@ func (p *LDAPUserProvider) searchReferral(referral string, searchRequest *ldap.S return nil } -func (p *LDAPUserProvider) searchReferrals(searchRequest *ldap.SearchRequest, searchResult *ldap.SearchResult) { +func (p *LDAPUserProvider) searchReferrals(searchRequest *ldap.SearchRequest, searchResult *ldap.SearchResult) (err error) { for i := 0; i < len(searchResult.Referrals); i++ { - _ = p.searchReferral(searchResult.Referrals[i], searchRequest, searchResult) + if err = p.searchReferral(searchResult.Referrals[i], searchRequest, searchResult); err != nil { + return err + } } + + return nil } -func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername string) (profile *ldapUserProfile, err error) { - userFilter := p.resolveUsersFilter(inputUsername) +func (p *LDAPUserProvider) getUserProfile(client LDAPClient, username string) (profile *ldapUserProfile, err error) { + userFilter := p.resolveUsersFilter(username) // Search for the given username. searchRequest := ldap.NewSearchRequest( @@ -310,8 +322,8 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str var searchResult *ldap.SearchResult - if searchResult, err = p.search(conn, searchRequest); err != nil { - return nil, fmt.Errorf("cannot find user DN of user '%s'. Cause: %w", inputUsername, err) + if searchResult, err = p.search(client, searchRequest); err != nil { + return nil, fmt.Errorf("cannot find user DN of user '%s'. Cause: %w", username, err) } if len(searchResult.Entries) == 0 { @@ -319,7 +331,7 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str } if len(searchResult.Entries) > 1 { - return nil, fmt.Errorf("multiple users %s found", inputUsername) + return nil, fmt.Errorf("there were %d users found when searching for '%s' but there should only be 1", len(searchResult.Entries), username) } userProfile := ldapUserProfile{ @@ -327,37 +339,45 @@ func (p *LDAPUserProvider) getUserProfile(conn LDAPConnection, inputUsername str } for _, attr := range searchResult.Entries[0].Attributes { - if attr.Name == p.config.DisplayNameAttribute { + switch attr.Name { + case p.config.DisplayNameAttribute: userProfile.DisplayName = attr.Values[0] - } - - if attr.Name == p.config.MailAttribute { + case p.config.MailAttribute: userProfile.Emails = attr.Values - } + case p.config.UsernameAttribute: + attrs := len(attr.Values) - if attr.Name == p.config.UsernameAttribute { - if len(attr.Values) != 1 { - return nil, fmt.Errorf("user '%s' cannot have multiple value for attribute '%s'", - inputUsername, p.config.UsernameAttribute) + switch attrs { + case 1: + userProfile.Username = attr.Values[0] + case 0: + return nil, fmt.Errorf("user '%s' must have value for attribute '%s'", + username, p.config.UsernameAttribute) + default: + return nil, fmt.Errorf("user '%s' has %d values for for attribute '%s' but the attribute must be a single value attribute", + username, attrs, p.config.UsernameAttribute) } - - userProfile.Username = attr.Values[0] } } + if userProfile.Username == "" { + return nil, fmt.Errorf("user '%s' must have value for attribute '%s'", + username, p.config.UsernameAttribute) + } + if userProfile.DN == "" { - return nil, fmt.Errorf("no DN has been found for user %s", inputUsername) + return nil, fmt.Errorf("user '%s' must have a distinguished name but the result returned an empty distinguished name", username) } return &userProfile, nil } -func (p *LDAPUserProvider) resolveUsersFilter(inputUsername string) (filter string) { +func (p *LDAPUserProvider) resolveUsersFilter(username string) (filter string) { filter = p.config.UsersFilter if p.usersFilterReplacementInput { // The {input} placeholder is replaced by the username input. - filter = strings.ReplaceAll(filter, ldapPlaceholderInput, ldapEscape(inputUsername)) + filter = strings.ReplaceAll(filter, ldapPlaceholderInput, ldapEscape(username)) } p.log.Tracef("Detected user filter is %s", filter) @@ -365,12 +385,12 @@ func (p *LDAPUserProvider) resolveUsersFilter(inputUsername string) (filter stri return filter } -func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ldapUserProfile) (filter string, err error) { //nolint:unparam +func (p *LDAPUserProvider) resolveGroupsFilter(username string, profile *ldapUserProfile) (filter string, err error) { //nolint:unparam filter = p.config.GroupsFilter if p.groupsFilterReplacementInput { // The {input} placeholder is replaced by the users username input. - filter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderInput, ldapEscape(inputUsername)) + filter = strings.ReplaceAll(p.config.GroupsFilter, ldapPlaceholderInput, ldapEscape(username)) } if profile != nil { @@ -388,8 +408,8 @@ func (p *LDAPUserProvider) resolveGroupsFilter(inputUsername string, profile *ld return filter, nil } -func (p *LDAPUserProvider) modify(conn LDAPConnection, modifyRequest *ldap.ModifyRequest) (err error) { - if err = conn.Modify(modifyRequest); err != nil { +func (p *LDAPUserProvider) modify(client LDAPClient, modifyRequest *ldap.ModifyRequest) (err error) { + if err = client.Modify(modifyRequest); err != nil { var ( referral string ok bool @@ -402,28 +422,28 @@ func (p *LDAPUserProvider) modify(conn LDAPConnection, modifyRequest *ldap.Modif p.log.Debugf("Attempting Modify on referred URL %s", referral) var ( - connReferral LDAPConnection - errReferral error + clientRef LDAPClient + errRef error ) - if connReferral, errReferral = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); errReferral != nil { - p.log.Errorf("Failed to connect during referred modify request (referred to %s): %v", referral, errReferral) - - return err + if clientRef, errRef = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); errRef != nil { + return fmt.Errorf("error occurred connecting to referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } - defer connReferral.Close() + defer clientRef.Close() - if errReferral = connReferral.Modify(modifyRequest); errReferral != nil { - p.log.Errorf("Failed to perform modify operation during referred modify request (referred to %s): %v", referral, errReferral) + if errRef = clientRef.Modify(modifyRequest); errRef != nil { + return fmt.Errorf("error occurred performing modify on referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } + + return nil } - return err + return nil } -func (p *LDAPUserProvider) pwdModify(conn LDAPConnection, pwdModifyRequest *ldap.PasswordModifyRequest) (err error) { - if _, err = conn.PasswordModify(pwdModifyRequest); err != nil { +func (p *LDAPUserProvider) pwdModify(client LDAPClient, pwdModifyRequest *ldap.PasswordModifyRequest) (err error) { + if _, err = client.PasswordModify(pwdModifyRequest); err != nil { var ( referral string ok bool @@ -436,24 +456,24 @@ func (p *LDAPUserProvider) pwdModify(conn LDAPConnection, pwdModifyRequest *ldap p.log.Debugf("Attempting PwdModify ExOp (1.3.6.1.4.1.4203.1.11.1) on referred URL %s", referral) var ( - connReferral LDAPConnection - errReferral error + clientRef LDAPClient + errRef error ) - if connReferral, errReferral = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); errReferral != nil { - p.log.Errorf("Failed to connect during referred password modify request (referred to %s): %v", referral, errReferral) - - return err + if clientRef, errRef = p.connectCustom(referral, p.config.User, p.config.Password, p.config.StartTLS, p.dialOpts...); errRef != nil { + return fmt.Errorf("error occurred connecting to referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } - defer connReferral.Close() + defer clientRef.Close() - if _, errReferral = connReferral.PasswordModify(pwdModifyRequest); errReferral != nil { - p.log.Errorf("Failed to perform modify operation during referred modify request (referred to %s): %v", referral, errReferral) + if _, errRef = clientRef.PasswordModify(pwdModifyRequest); errRef != nil { + return fmt.Errorf("error occurred performing password modify on referred LDAP server '%s': %+v. Original Error: %w", referral, errRef, err) } + + return nil } - return err + return nil } func (p *LDAPUserProvider) getReferral(err error) (referral string, ok bool) { diff --git a/internal/authentication/ldap_user_provider_startup.go b/internal/authentication/ldap_user_provider_startup.go index bde3eea6..71c88129 100644 --- a/internal/authentication/ldap_user_provider_startup.go +++ b/internal/authentication/ldap_user_provider_startup.go @@ -10,52 +10,77 @@ import ( // StartupCheck implements the startup check provider interface. func (p *LDAPUserProvider) StartupCheck() (err error) { - var ( - conn LDAPConnection - searchResult *ldap.SearchResult - ) + var client LDAPClient - if conn, err = p.connect(); err != nil { + if client, err = p.connect(); err != nil { return err } - defer conn.Close() + defer client.Close() - searchRequest := ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, - 1, 0, false, "(objectClass=*)", []string{ldapSupportedExtensionAttribute}, nil) - - if searchResult, err = conn.Search(searchRequest); err != nil { + if p.features, err = p.getServerSupportedFeatures(client); err != nil { return err } - if len(searchResult.Entries) != 1 { - return nil - } - - // Iterate the attribute values to see what the server supports. - for _, attr := range searchResult.Entries[0].Attributes { - if attr.Name == ldapSupportedExtensionAttribute { - p.log.Tracef("LDAP Supported Extension OIDs: %s", strings.Join(attr.Values, ", ")) - - for _, oid := range attr.Values { - if oid == ldapOIDPasswdModifyExtension { - p.supportExtensionPasswdModify = true - break - } - } - } - } - - if !p.supportExtensionPasswdModify && !p.disableResetPassword && + if !p.features.Extensions.PwdModifyExOp && !p.disableResetPassword && p.config.Implementation != schema.LDAPImplementationActiveDirectory { p.log.Warn("Your LDAP server implementation may not support a method for password hashing " + "known to Authelia, it's strongly recommended you ensure your directory server hashes the password " + "attribute when users reset their password via Authelia.") } + if p.features.Extensions.TLS && !p.config.StartTLS && !strings.HasPrefix(p.config.URL, "ldaps://") { + p.log.Error("Your LDAP Server supports TLS but you don't appear to be utilizing it. We strongly" + + "recommend enabling the StartTLS option or using the scheme 'ldaps://' to secure connections with your" + + "LDAP Server.") + } + + if !p.features.Extensions.TLS && p.config.StartTLS { + p.log.Info("Your LDAP Server does not appear to support TLS but you enabled StartTLS which may result" + + "in an error.") + } + return nil } +func (p *LDAPUserProvider) getServerSupportedFeatures(client LDAPClient) (features LDAPSupportedFeatures, err error) { + var ( + searchRequest *ldap.SearchRequest + searchResult *ldap.SearchResult + ) + + searchRequest = ldap.NewSearchRequest("", ldap.ScopeBaseObject, ldap.NeverDerefAliases, + 1, 0, false, "(objectClass=*)", []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute}, nil) + + if searchResult, err = client.Search(searchRequest); err != nil { + return features, err + } + + if len(searchResult.Entries) != 1 { + p.log.Errorf("The LDAP Server did not respond appropriately to a RootDSE search. This may result in reduced functionality.") + + return features, nil + } + + var controlTypeOIDs, extensionOIDs []string + + controlTypeOIDs, extensionOIDs, features = ldapGetFeatureSupportFromEntry(searchResult.Entries[0]) + + controlTypes, extensions := none, none + + if len(controlTypeOIDs) != 0 { + controlTypes = strings.Join(controlTypeOIDs, ", ") + } + + if len(extensionOIDs) != 0 { + extensions = strings.Join(extensionOIDs, ", ") + } + + p.log.Debugf("LDAP Supported OIDs. Control Types: %s. Extensions: %s", controlTypes, extensions) + + return features, nil +} + func (p *LDAPUserProvider) parseDynamicUsersConfiguration() { p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{username_attribute}", p.config.UsernameAttribute) p.config.UsersFilter = strings.ReplaceAll(p.config.UsersFilter, "{mail_attribute}", p.config.MailAttribute) diff --git a/internal/authentication/ldap_user_provider_test.go b/internal/authentication/ldap_user_provider_test.go index 95d4c63d..d158fd60 100644 --- a/internal/authentication/ldap_user_provider_test.go +++ b/internal/authentication/ldap_user_provider_test.go @@ -19,8 +19,8 @@ func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -34,9 +34,9 @@ func TestShouldCreateRawConnectionWhenSchemeIsLDAP(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) @@ -51,8 +51,8 @@ func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -66,9 +66,9 @@ func TestShouldCreateTLSConnectionWhenSchemeIsLDAPS(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldaps://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) @@ -102,7 +102,7 @@ func TestEscapeSpecialCharsInGroupsFilter(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -159,8 +159,8 @@ func TestShouldCheckLDAPServerExtensions(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -180,14 +180,14 @@ func TestShouldCheckLDAPServerExtensions(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - searchOIDs := mockConn.EXPECT(). - Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute})). + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -195,29 +195,37 @@ func TestShouldCheckLDAPServerExtensions(t *testing.T) { Attributes: []*ldap.EntryAttribute{ { Name: ldapSupportedExtensionAttribute, - Values: []string{ldapOIDPasswdModifyExtension}, + Values: []string{ldapOIDExtensionPwdModifyExOp, ldapOIDExtensionTLS}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, }, }, }, }, }, nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() gomock.InOrder(dialURL, connBind, searchOIDs, connClose) err := ldapClient.StartupCheck() assert.NoError(t, err) - assert.True(t, ldapClient.supportExtensionPasswdModify) + assert.True(t, ldapClient.features.Extensions.PwdModifyExOp) + assert.True(t, ldapClient.features.Extensions.TLS) + + assert.False(t, ldapClient.features.ControlTypes.MsftPwdPolHints) + assert.False(t, ldapClient.features.ControlTypes.MsftPwdPolHintsDeprecated) } -func TestShouldNotEnablePasswdModifyExtension(t *testing.T) { +func TestShouldNotCheckLDAPServerExtensionsWhenRootDSEReturnsMoreThanOneEntry(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -237,14 +245,80 @@ func TestShouldNotEnablePasswdModifyExtension(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - searchOIDs := mockConn.EXPECT(). - Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute})). + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{ldapOIDExtensionPwdModifyExOp, ldapOIDExtensionTLS}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + {}, + }, + }, nil) + + connClose := mockClient.EXPECT().Close() + + gomock.InOrder(dialURL, connBind, searchOIDs, connClose) + + err := ldapClient.StartupCheck() + assert.NoError(t, err) + + assert.False(t, ldapClient.features.Extensions.PwdModifyExOp) + assert.False(t, ldapClient.features.Extensions.TLS) + + assert.False(t, ldapClient.features.ControlTypes.MsftPwdPolHints) + assert.False(t, ldapClient.features.ControlTypes.MsftPwdPolHintsDeprecated) +} + +func TestShouldCheckLDAPServerControlTypes(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -254,27 +328,100 @@ func TestShouldNotEnablePasswdModifyExtension(t *testing.T) { Name: ldapSupportedExtensionAttribute, Values: []string{}, }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, }, }, }, }, nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() gomock.InOrder(dialURL, connBind, searchOIDs, connClose) err := ldapClient.StartupCheck() assert.NoError(t, err) - assert.False(t, ldapClient.supportExtensionPasswdModify) + assert.False(t, ldapClient.features.Extensions.PwdModifyExOp) + assert.False(t, ldapClient.features.Extensions.TLS) + + assert.True(t, ldapClient.features.ControlTypes.MsftPwdPolHints) + assert.True(t, ldapClient.features.ControlTypes.MsftPwdPolHintsDeprecated) +} + +func TestShouldNotEnablePasswdModifyExtensionOrControlTypes(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connClose := mockClient.EXPECT().Close() + + gomock.InOrder(dialURL, connBind, searchOIDs, connClose) + + err := ldapClient.StartupCheck() + assert.NoError(t, err) + + assert.False(t, ldapClient.features.Extensions.PwdModifyExOp) + assert.False(t, ldapClient.features.Extensions.TLS) + + assert.False(t, ldapClient.features.ControlTypes.MsftPwdPolHints) + assert.False(t, ldapClient.features.ControlTypes.MsftPwdPolHintsDeprecated) } func TestShouldReturnCheckServerConnectError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -294,20 +441,20 @@ func TestShouldReturnCheckServerConnectError(t *testing.T) { mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, errors.New("could not connect")) + Return(mockClient, errors.New("could not connect")) err := ldapClient.StartupCheck() assert.EqualError(t, err, "dial failed with error: could not connect") - assert.False(t, ldapClient.supportExtensionPasswdModify) + assert.False(t, ldapClient.features.Extensions.PwdModifyExOp) } func TestShouldReturnCheckServerSearchError(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -327,24 +474,24 @@ func TestShouldReturnCheckServerSearchError(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - searchOIDs := mockConn.EXPECT(). - Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute})). + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). Return(nil, errors.New("could not perform the search")) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() gomock.InOrder(dialURL, connBind, searchOIDs, connClose) err := ldapClient.StartupCheck() assert.EqualError(t, err, "could not perform the search") - assert.False(t, ldapClient.supportExtensionPasswdModify) + assert.False(t, ldapClient.features.Extensions.PwdModifyExOp) } type SearchRequestMatcher struct { @@ -368,8 +515,8 @@ func TestShouldEscapeUserInput(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -382,17 +529,18 @@ func TestShouldEscapeUserInput(t *testing.T) { Password: "password", AdditionalUsersDN: "ou=users", BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, mockFactory) - mockConn.EXPECT(). + mockClient.EXPECT(). // Here we ensure that the input has been correctly escaped. Search(NewSearchRequestMatcher("(|(uid=john\\=abc)(mail=john\\=abc))")). Return(&ldap.SearchResult{}, nil) - _, err := ldapClient.getUserProfile(mockConn, "john=abc") + _, err := ldapClient.getUserProfile(mockClient, "john=abc") require.Error(t, err) assert.EqualError(t, err, "user not found") } @@ -401,8 +549,8 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -415,6 +563,7 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) { BaseDN: "dc=example,dc=com", MailAttribute: "mail", DisplayNameAttribute: "displayName", + PermitReferrals: true, }, false, nil, @@ -422,11 +571,11 @@ func TestShouldCombineUsernameFilterAndUsersFilter(t *testing.T) { assert.True(t, ldapClient.usersFilterReplacementInput) - mockConn.EXPECT(). + mockClient.EXPECT(). Search(NewSearchRequestMatcher("(&(uid=john)(&(objectCategory=person)(objectClass=user)))")). Return(&ldap.SearchResult{}, nil) - _, err := ldapClient.getUserProfile(mockConn, "john") + _, err := ldapClient.getUserProfile(mockClient, "john") require.Error(t, err) assert.EqualError(t, err, "user not found") } @@ -449,8 +598,8 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -463,6 +612,7 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { UsersFilter: "uid={input}", AdditionalUsersDN: "ou=users", BaseDN: "dc=example,dc=com", + PermitReferrals: true, }, false, nil, @@ -470,19 +620,19 @@ func TestShouldNotCrashWhenGroupsAreNotRetrievedFromLDAP(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchGroups := mockConn.EXPECT(). + searchGroups := mockClient.EXPECT(). Search(gomock.Any()). Return(createSearchResultWithAttributes(), nil) - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -521,8 +671,8 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -540,19 +690,19 @@ func TestShouldNotCrashWhenEmailsAreNotRetrievedFromLDAP(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchGroups := mockConn.EXPECT(). + searchGroups := mockClient.EXPECT(). Search(gomock.Any()). Return(createSearchResultWithAttributeValues("group1", "group2"), nil) - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -582,8 +732,8 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -603,19 +753,19 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchGroups := mockConn.EXPECT(). + searchGroups := mockClient.EXPECT(). Search(gomock.Any()). Return(createSearchResultWithAttributeValues("group1", "group2"), nil) - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -650,12 +800,1171 @@ func TestShouldReturnUsernameFromLDAP(t *testing.T) { assert.Equal(t, details.Username, "John") } +func TestShouldReturnUsernameFromLDAPWithReferrals(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{}, + Referrals: []string{"ldap://192.168.2.1"}, + }, nil) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.2.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + searchProfileReferral := mockClientReferral.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(dialURL, connBind, searchProfile, dialURLReferral, connBindReferral, searchProfileReferral, connCloseReferral, searchGroups, connClose) + + 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") +} + +func TestShouldReturnUsernameFromLDAPWithReferralsInErrorAndResult(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + mockClientReferralAlt := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{}, + Referrals: []string{"ldap://192.168.2.1"}, + }, &ldap.Error{ResultCode: ldap.LDAPResultReferral, Err: errors.New("referral"), Packet: &testBERPacketReferral}) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.2.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + searchProfileReferral := mockClientReferral.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) + + dialURLReferralAlt := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferralAlt, nil) + + connBindReferralAlt := mockClientReferralAlt.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferralAlt := mockClientReferralAlt.EXPECT().Close() + + searchProfileReferralAlt := mockClientReferralAlt.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(dialURL, connBind, searchProfile, dialURLReferral, connBindReferral, searchProfileReferral, connCloseReferral, dialURLReferralAlt, connBindReferralAlt, searchProfileReferralAlt, connCloseReferralAlt, searchGroups, connClose) + + 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") +} + +func TestShouldReturnUsernameFromLDAPWithReferralsErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchGroups := mockClient.EXPECT(). + Search(gomock.Any()). + Return(createSearchResultWithAttributeValues("group1", "group2"), nil) + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{}, &ldap.Error{ResultCode: ldap.LDAPResultReferral, Err: errors.New("referral"), Packet: &testBERPacketReferral}) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + searchProfileReferral := mockClientReferral.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(dialURL, connBind, searchProfile, dialURLReferral, connBindReferral, searchProfileReferral, connCloseReferral, searchGroups, connClose) + + 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") +} + +func TestShouldNotUpdateUserPasswordConnect(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: false, + }, + false, + nil, + mockFactory) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{ldapOIDExtensionPwdModifyExOp}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(nil, errors.New("tcp timeout")) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: dial failed with error: tcp timeout") +} + +func TestShouldNotUpdateUserPasswordGetDetails(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: false, + }, + false, + nil, + mockFactory) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{ldapOIDExtensionPwdModifyExOp}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(nil, &ldap.Error{ResultCode: ldap.LDAPResultProtocolError, Err: errors.New("permission error")}) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: cannot find user DN of user 'john'. Cause: LDAP Result Code 2 \"Protocol Error\": permission error") +} + +func TestShouldUpdateUserPassword(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + }, + false, + nil, + mockFactory) + + modifyRequest := ldap.NewModifyRequest( + "uid=test,dc=example,dc=com", + nil, + ) + + modifyRequest.Replace(ldapAttributeUserPassword, []string{"password"}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + modify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(nil) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, modify, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + require.NoError(t, err) +} + +func TestShouldUpdateUserPasswordMSAD(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + schema.LDAPAuthenticationBackendConfiguration{ + Implementation: "activedirectory", + 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", + }, + false, + nil, + mockFactory) + + modifyRequest := ldap.NewModifyRequest( + "uid=test,dc=example,dc=com", + []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, + ) + + pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) + modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + modify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(nil) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, modify, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + require.NoError(t, err) +} + +func TestShouldUpdateUserPasswordMSADWithReferrals(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + schema.LDAPAuthenticationBackendConfiguration{ + Implementation: "activedirectory", + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + modifyRequest := ldap.NewModifyRequest( + "uid=test,dc=example,dc=com", + []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, + ) + + pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) + modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + modify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(&ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + modifyReferral := mockClientReferral.EXPECT(). + Modify(modifyRequest). + Return(nil) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, modify, dialURLReferral, connBindReferral, modifyReferral, connCloseReferral, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + require.NoError(t, err) +} + +func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralConnectErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + schema.LDAPAuthenticationBackendConfiguration{ + Implementation: "activedirectory", + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + modifyRequest := ldap.NewModifyRequest( + "uid=test,dc=example,dc=com", + []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, + ) + + pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) + modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + modify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(&ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(nil, errors.New("tcp timeout")) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, modify, dialURLReferral, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: error occurred connecting to referred LDAP server 'ldap://192.168.0.1': dial failed with error: tcp timeout. Original Error: LDAP Result Code 10 \"Referral\": error occurred") +} + +func TestShouldUpdateUserPasswordMSADWithReferralsWithReferralModifyErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + schema.LDAPAuthenticationBackendConfiguration{ + Implementation: "activedirectory", + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + modifyRequest := ldap.NewModifyRequest( + "uid=test,dc=example,dc=com", + []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, + ) + + pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) + modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + modify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(&ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + modifyReferral := mockClientReferral.EXPECT(). + Modify(modifyRequest). + Return(&ldap.Error{ + ResultCode: ldap.LDAPResultBusy, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, modify, dialURLReferral, connBindReferral, modifyReferral, connCloseReferral, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: error occurred performing modify on referred LDAP server 'ldap://192.168.0.1': LDAP Result Code 51 \"Busy\": error occurred. Original Error: LDAP Result Code 10 \"Referral\": error occurred") +} + +func TestShouldUpdateUserPasswordMSADWithoutReferrals(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + schema.LDAPAuthenticationBackendConfiguration{ + Implementation: "activedirectory", + 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", + PermitReferrals: false, + }, + false, + nil, + mockFactory) + + modifyRequest := ldap.NewModifyRequest( + "uid=test,dc=example,dc=com", + []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, + ) + + pwdEncoded, _ := utf16LittleEndian.NewEncoder().String(fmt.Sprintf("\"%s\"", "password")) + modifyRequest.Replace(ldapAttributeUnicodePwd, []string{pwdEncoded}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + modify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(&ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, modify, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: LDAP Result Code 10 \"Referral\": error occurred") +} + func TestShouldUpdateUserPasswordPasswdModifyExtension(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -681,14 +1990,14 @@ func TestShouldUpdateUserPasswordPasswdModifyExtension(t *testing.T) { dialURLOIDs := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBindOIDs := mockConn.EXPECT(). + connBindOIDs := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - searchOIDs := mockConn.EXPECT(). - Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute})). + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -696,26 +2005,30 @@ func TestShouldUpdateUserPasswordPasswdModifyExtension(t *testing.T) { Attributes: []*ldap.EntryAttribute{ { Name: ldapSupportedExtensionAttribute, - Values: []string{ldapOIDPasswdModifyExtension}, + Values: []string{ldapOIDExtensionPwdModifyExOp}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, }, }, }, }, }, nil) - connCloseOIDs := mockConn.EXPECT().Close() + connCloseOIDs := mockClient.EXPECT().Close() dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -739,7 +2052,7 @@ func TestShouldUpdateUserPasswordPasswdModifyExtension(t *testing.T) { }, }, nil) - passwdModify := mockConn.EXPECT(). + passwdModify := mockClient.EXPECT(). PasswordModify(pwdModifyRequest). Return(nil, nil) @@ -752,12 +2065,724 @@ func TestShouldUpdateUserPasswordPasswdModifyExtension(t *testing.T) { require.NoError(t, err) } +func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferrals(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + pwdModifyRequest := ldap.NewPasswordModifyRequest( + "uid=test,dc=example,dc=com", + "", + "password", + ) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{ldapOIDExtensionPwdModifyExOp}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + passwdModify := mockClient.EXPECT(). + PasswordModify(pwdModifyRequest). + Return(&ldap.PasswordModifyResult{ + Referral: "ldap://192.168.0.1", + }, &ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + passwdModifyReferral := mockClientReferral.EXPECT(). + PasswordModify(pwdModifyRequest). + Return(&ldap.PasswordModifyResult{}, nil) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, passwdModify, dialURLReferral, connBindReferral, passwdModifyReferral, connCloseReferral, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + require.NoError(t, err) +} + +func TestShouldUpdateUserPasswordPasswdModifyExtensionWithoutReferrals(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: false, + }, + false, + nil, + mockFactory) + + pwdModifyRequest := ldap.NewPasswordModifyRequest( + "uid=test,dc=example,dc=com", + "", + "password", + ) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{ldapOIDExtensionPwdModifyExOp}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + passwdModify := mockClient.EXPECT(). + PasswordModify(pwdModifyRequest). + Return(&ldap.PasswordModifyResult{ + Referral: "ldap://192.168.0.1", + }, &ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, passwdModify, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: LDAP Result Code 10 \"Referral\": error occurred") +} + +func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferralsReferralConnectErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + pwdModifyRequest := ldap.NewPasswordModifyRequest( + "uid=test,dc=example,dc=com", + "", + "password", + ) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{ldapOIDExtensionPwdModifyExOp}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + passwdModify := mockClient.EXPECT(). + PasswordModify(pwdModifyRequest). + Return(&ldap.PasswordModifyResult{ + Referral: "ldap://192.168.0.1", + }, &ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(nil, errors.New("tcp timeout")) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, passwdModify, dialURLReferral, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: error occurred connecting to referred LDAP server 'ldap://192.168.0.1': dial failed with error: tcp timeout. Original Error: LDAP Result Code 10 \"Referral\": error occurred") +} + +func TestShouldUpdateUserPasswordPasswdModifyExtensionWithReferralsReferralPasswordModifyErr(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + mockClientReferral := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + PermitReferrals: true, + }, + false, + nil, + mockFactory) + + pwdModifyRequest := ldap.NewPasswordModifyRequest( + "uid=test,dc=example,dc=com", + "", + "password", + ) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{ldapOIDExtensionPwdModifyExOp}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.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) + + passwdModify := mockClient.EXPECT(). + PasswordModify(pwdModifyRequest). + Return(&ldap.PasswordModifyResult{ + Referral: "ldap://192.168.0.1", + }, &ldap.Error{ + ResultCode: ldap.LDAPResultReferral, + Err: errors.New("error occurred"), + Packet: &testBERPacketReferral, + }) + + dialURLReferral := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://192.168.0.1"), gomock.Any()). + Return(mockClientReferral, nil) + + connBindReferral := mockClientReferral.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connCloseReferral := mockClientReferral.EXPECT().Close() + + passwdModifyReferral := mockClientReferral.EXPECT(). + PasswordModify(pwdModifyRequest). + Return(nil, &ldap.Error{ + ResultCode: ldap.LDAPResultBusy, + Err: errors.New("too busy"), + Packet: &testBERPacketReferral, + }) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, passwdModify, dialURLReferral, connBindReferral, passwdModifyReferral, connCloseReferral, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.EqualError(t, err, "unable to update password. Cause: error occurred performing password modify on referred LDAP server 'ldap://192.168.0.1': LDAP Result Code 51 \"Busy\": too busy. Original Error: LDAP Result Code 10 \"Referral\": error occurred") +} + +func TestShouldUpdateUserPasswordActiveDirectoryWithServerPolicyHints(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + schema.LDAPAuthenticationBackendConfiguration{ + Implementation: "activedirectory", + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "sAMAccountName", + MailAttribute: "mail", + DisplayNameAttribute: "displayName", + UsersFilter: "cn={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + pwdEncoded, _ := utf16.NewEncoder().String("\"password\"") + + modifyRequest := ldap.NewModifyRequest( + "cn=test,dc=example,dc=com", + []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHints}}, + ) + + modifyRequest.Replace("unicodePwd", []string{pwdEncoded}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "cn=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "sAMAccountName", + Values: []string{"john"}, + }, + }, + }, + }, + }, nil) + + passwdModify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(nil) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, passwdModify, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + assert.NoError(t, err) +} + +func TestShouldUpdateUserPasswordActiveDirectoryWithServerPolicyHintsDeprecated(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + schema.LDAPAuthenticationBackendConfiguration{ + Implementation: "activedirectory", + URL: "ldap://127.0.0.1:389", + User: "cn=admin,dc=example,dc=com", + Password: "password", + UsernameAttribute: "sAMAccountName", + MailAttribute: "mail", + DisplayNameAttribute: "displayName", + UsersFilter: "cn={input}", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + utf16 := unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM) + pwdEncoded, _ := utf16.NewEncoder().String("\"password\"") + + modifyRequest := ldap.NewModifyRequest( + "cn=test,dc=example,dc=com", + []ldap.Control{&controlMsftServerPolicyHints{ldapOIDControlMsftServerPolicyHintsDeprecated}}, + ) + + modifyRequest.Replace("unicodePwd", []string{pwdEncoded}) + + dialURLOIDs := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBindOIDs := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: ldapSupportedExtensionAttribute, + Values: []string{}, + }, + { + Name: ldapSupportedControlAttribute, + Values: []string{ldapOIDControlMsftServerPolicyHintsDeprecated}, + }, + }, + }, + }, + }, nil) + + connCloseOIDs := mockClient.EXPECT().Close() + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + connBind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + connClose := mockClient.EXPECT().Close() + + searchProfile := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "cn=test,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "sAMAccountName", + Values: []string{"john"}, + }, + }, + }, + }, + }, nil) + + passwdModify := mockClient.EXPECT(). + Modify(modifyRequest). + Return(nil) + + gomock.InOrder(dialURLOIDs, connBindOIDs, searchOIDs, connCloseOIDs, dialURL, connBind, searchProfile, passwdModify, connClose) + + err := ldapClient.StartupCheck() + require.NoError(t, err) + + err = ldapClient.UpdatePassword("john", "password") + require.NoError(t, err) +} + func TestShouldUpdateUserPasswordActiveDirectory(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -788,14 +2813,14 @@ func TestShouldUpdateUserPasswordActiveDirectory(t *testing.T) { dialURLOIDs := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBindOIDs := mockConn.EXPECT(). + connBindOIDs := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - searchOIDs := mockConn.EXPECT(). - Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute})). + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -805,24 +2830,28 @@ func TestShouldUpdateUserPasswordActiveDirectory(t *testing.T) { Name: ldapSupportedExtensionAttribute, Values: []string{}, }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, }, }, }, }, nil) - connCloseOIDs := mockConn.EXPECT().Close() + connCloseOIDs := mockClient.EXPECT().Close() dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -838,15 +2867,15 @@ func TestShouldUpdateUserPasswordActiveDirectory(t *testing.T) { Values: []string{"test@example.com"}, }, { - Name: "uid", - Values: []string{"John"}, + Name: "sAMAccountName", + Values: []string{"john"}, }, }, }, }, }, nil) - passwdModify := mockConn.EXPECT(). + passwdModify := mockClient.EXPECT(). Modify(modifyRequest). Return(nil) @@ -863,8 +2892,8 @@ func TestShouldUpdateUserPasswordBasic(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -892,14 +2921,14 @@ func TestShouldUpdateUserPasswordBasic(t *testing.T) { dialURLOIDs := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBindOIDs := mockConn.EXPECT(). + connBindOIDs := mockClient.EXPECT(). Bind(gomock.Eq("uid=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - searchOIDs := mockConn.EXPECT(). - Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute})). + searchOIDs := mockClient.EXPECT(). + Search(NewExtendedSearchRequestMatcher("(objectClass=*)", "", ldap.ScopeBaseObject, ldap.NeverDerefAliases, false, []string{ldapSupportedExtensionAttribute, ldapSupportedControlAttribute})). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ { @@ -909,24 +2938,28 @@ func TestShouldUpdateUserPasswordBasic(t *testing.T) { Name: ldapSupportedExtensionAttribute, Values: []string{}, }, + { + Name: ldapSupportedControlAttribute, + Values: []string{}, + }, }, }, }, }, nil) - connCloseOIDs := mockConn.EXPECT().Close() + connCloseOIDs := mockClient.EXPECT().Close() dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("uid=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -950,7 +2983,7 @@ func TestShouldUpdateUserPasswordBasic(t *testing.T) { }, }, nil) - passwdModify := mockConn.EXPECT(). + passwdModify := mockClient.EXPECT(). Modify(modifyRequest). Return(nil) @@ -963,12 +2996,355 @@ func TestShouldUpdateUserPasswordBasic(t *testing.T) { require.NoError(t, err) } +func TestShouldReturnErrorWhenMultipleUsernameAttributes(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + bind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + search := mockClient.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", "Jacob"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, bind, search) + + client, err := ldapClient.connect() + assert.NoError(t, err) + + profile, err := ldapClient.getUserProfile(client, "john") + + assert.Nil(t, profile) + assert.EqualError(t, err, "user 'john' has 2 values for for attribute 'uid' but the attribute must be a single value attribute") +} + +func TestShouldReturnErrorWhenZeroUsernameAttributes(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + bind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + search := mockClient.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{}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, bind, search) + + client, err := ldapClient.connect() + assert.NoError(t, err) + + profile, err := ldapClient.getUserProfile(client, "john") + + assert.Nil(t, profile) + assert.EqualError(t, err, "user 'john' must have value for attribute 'uid'") +} + +func TestShouldReturnErrorWhenUsernameAttributeNotReturned(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + bind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + search := mockClient.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"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, bind, search) + + client, err := ldapClient.connect() + assert.NoError(t, err) + + profile, err := ldapClient.getUserProfile(client, "john") + + assert.Nil(t, profile) + assert.EqualError(t, err, "user 'john' must have value for attribute 'uid'") +} + +func TestShouldReturnErrorWhenMultipleUsersFound(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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})(uid=*))", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + bind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + search := mockClient.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"}, + }, + }, + }, + { + DN: "uid=sam,dc=example,dc=com", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"sam"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, bind, search) + + client, err := ldapClient.connect() + assert.NoError(t, err) + + profile, err := ldapClient.getUserProfile(client, "john") + + assert.Nil(t, profile) + assert.EqualError(t, err, "there were 2 users found when searching for 'john' but there should only be 1") +} + +func TestShouldReturnErrorWhenNoDN(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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})(uid=*))", + AdditionalUsersDN: "ou=users", + BaseDN: "dc=example,dc=com", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + bind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(nil) + + search := mockClient.EXPECT(). + Search(gomock.Any()). + Return(&ldap.SearchResult{ + Entries: []*ldap.Entry{ + { + DN: "", + Attributes: []*ldap.EntryAttribute{ + { + Name: "displayName", + Values: []string{"John Doe"}, + }, + { + Name: "mail", + Values: []string{"test@example.com"}, + }, + { + Name: "uid", + Values: []string{"John"}, + }, + }, + }, + }, + }, nil) + + gomock.InOrder(dialURL, bind, search) + + client, err := ldapClient.connect() + assert.NoError(t, err) + + profile, err := ldapClient.getUserProfile(client, "john") + + assert.Nil(t, profile) + assert.EqualError(t, err, "user 'john' must have a distinguished name but the result returned an empty distinguished name") +} + func TestShouldCheckValidUserPassword(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -989,11 +3365,11 @@ func TestShouldCheckValidUserPassword(t *testing.T) { gomock.InOrder( mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil), - mockConn.EXPECT(). + Return(mockClient, nil), + mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil), - mockConn.EXPECT(). + mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -1018,11 +3394,11 @@ func TestShouldCheckValidUserPassword(t *testing.T) { }, nil), mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil), - mockConn.EXPECT(). + Return(mockClient, nil), + mockClient.EXPECT(). Bind(gomock.Eq("uid=test,dc=example,dc=com"), gomock.Eq("password")). Return(nil), - mockConn.EXPECT().Close().Times(2), + mockClient.EXPECT().Close().Times(2), ) valid, err := ldapClient.CheckUserPassword("john", "password") @@ -1031,12 +3407,51 @@ func TestShouldCheckValidUserPassword(t *testing.T) { require.NoError(t, err) } +func TestShouldNotCheckValidUserPasswordWithConnectError(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) + + ldapClient := newLDAPUserProvider( + 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", + }, + false, + nil, + mockFactory) + + dialURL := mockFactory.EXPECT(). + DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). + Return(mockClient, nil) + + bind := mockClient.EXPECT(). + Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). + Return(&ldap.Error{ResultCode: ldap.LDAPResultInvalidCredentials, Err: errors.New("invalid username or password")}) + + gomock.InOrder(dialURL, bind, mockClient.EXPECT().Close()) + + valid, err := ldapClient.CheckUserPassword("john", "password") + + assert.False(t, valid) + assert.EqualError(t, err, "bind failed with error: LDAP Result Code 49 \"Invalid Credentials\": invalid username or password") +} + func TestShouldCheckInvalidUserPassword(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -1057,11 +3472,11 @@ func TestShouldCheckInvalidUserPassword(t *testing.T) { gomock.InOrder( mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil), - mockConn.EXPECT(). + Return(mockClient, nil), + mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil), - mockConn.EXPECT(). + mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -1086,11 +3501,11 @@ func TestShouldCheckInvalidUserPassword(t *testing.T) { }, nil), mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil), - mockConn.EXPECT(). + Return(mockClient, nil), + mockClient.EXPECT(). Bind(gomock.Eq("uid=test,dc=example,dc=com"), gomock.Eq("password")). Return(errors.New("invalid username or password")), - mockConn.EXPECT().Close(), + mockClient.EXPECT().Close().Times(2), ) valid, err := ldapClient.CheckUserPassword("john", "password") @@ -1103,8 +3518,8 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -1125,22 +3540,22 @@ func TestShouldCallStartTLSWhenEnabled(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connStartTLS := mockConn.EXPECT(). + connStartTLS := mockClient.EXPECT(). StartTLS(ldapClient.tlsConfig) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchGroups := mockConn.EXPECT(). + searchGroups := mockClient.EXPECT(). Search(gomock.Any()). Return(createSearchResultWithAttributes(), nil) - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -1179,7 +3594,7 @@ func TestShouldParseDynamicConfiguration(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -1216,8 +3631,8 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -1245,22 +3660,22 @@ func TestShouldCallStartTLSWithInsecureSkipVerifyWhenSkipVerifyTrue(t *testing.T dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldap://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connBind := mockConn.EXPECT(). + connBind := mockClient.EXPECT(). Bind(gomock.Eq("cn=admin,dc=example,dc=com"), gomock.Eq("password")). Return(nil) - connStartTLS := mockConn.EXPECT(). + connStartTLS := mockClient.EXPECT(). StartTLS(ldapClient.tlsConfig) - connClose := mockConn.EXPECT().Close() + connClose := mockClient.EXPECT().Close() - searchGroups := mockConn.EXPECT(). + searchGroups := mockClient.EXPECT(). Search(gomock.Any()). Return(createSearchResultWithAttributes(), nil) - searchProfile := mockConn.EXPECT(). + searchProfile := mockClient.EXPECT(). Search(gomock.Any()). Return(&ldap.SearchResult{ Entries: []*ldap.Entry{ @@ -1299,8 +3714,8 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - mockFactory := NewMockLDAPConnectionFactory(ctrl) - mockConn := NewMockLDAPConnection(ctrl) + mockFactory := NewMockLDAPClientFactory(ctrl) + mockClient := NewMockLDAPClient(ctrl) ldapClient := newLDAPUserProvider( schema.LDAPAuthenticationBackendConfiguration{ @@ -1324,13 +3739,13 @@ func TestShouldReturnLDAPSAlreadySecuredWhenStartTLSAttempted(t *testing.T) { dialURL := mockFactory.EXPECT(). DialURL(gomock.Eq("ldaps://127.0.0.1:389"), gomock.Any()). - Return(mockConn, nil) + Return(mockClient, nil) - connStartTLS := mockConn.EXPECT(). + connStartTLS := mockClient.EXPECT(). StartTLS(ldapClient.tlsConfig). Return(errors.New("LDAP Result Code 200 \"Network Error\": ldap: already encrypted")) - gomock.InOrder(dialURL, connStartTLS) + gomock.InOrder(dialURL, connStartTLS, mockClient.EXPECT().Close()) _, err := ldapClient.GetDetails("john") assert.EqualError(t, err, "starttls failed with error: LDAP Result Code 200 \"Network Error\": ldap: already encrypted") diff --git a/internal/authentication/ldap_util.go b/internal/authentication/ldap_util.go index 887bc300..3c6025ec 100644 --- a/internal/authentication/ldap_util.go +++ b/internal/authentication/ldap_util.go @@ -9,6 +9,10 @@ import ( ) func ldapEntriesContainsEntry(needle *ldap.Entry, haystack []*ldap.Entry) bool { + if needle == nil || len(haystack) == 0 { + return false + } + for i := 0; i < len(haystack); i++ { if haystack[i].DN == needle.DN { return true @@ -18,6 +22,41 @@ func ldapEntriesContainsEntry(needle *ldap.Entry, haystack []*ldap.Entry) bool { return false } +func ldapGetFeatureSupportFromEntry(entry *ldap.Entry) (controlTypeOIDs, extensionOIDs []string, features LDAPSupportedFeatures) { + if entry == nil { + return controlTypeOIDs, extensionOIDs, features + } + + for _, attr := range entry.Attributes { + switch attr.Name { + case ldapSupportedControlAttribute: + controlTypeOIDs = attr.Values + + for _, oid := range attr.Values { + switch oid { + case ldapOIDControlMsftServerPolicyHints: + features.ControlTypes.MsftPwdPolHints = true + case ldapOIDControlMsftServerPolicyHintsDeprecated: + features.ControlTypes.MsftPwdPolHintsDeprecated = true + } + } + case ldapSupportedExtensionAttribute: + extensionOIDs = attr.Values + + for _, oid := range attr.Values { + switch oid { + case ldapOIDExtensionPwdModifyExOp: + features.Extensions.PwdModifyExOp = true + case ldapOIDExtensionTLS: + features.Extensions.TLS = true + } + } + } + } + + return controlTypeOIDs, extensionOIDs, features +} + func ldapEscape(inputUsername string) string { inputUsername = ldap.EscapeFilter(inputUsername) for _, c := range specialLDAPRunes { @@ -34,10 +73,18 @@ func ldapGetReferral(err error) (referral string, ok bool) { switch e := err.(type) { case *ldap.Error: + if e.Packet == nil { + return "", false + } + if len(e.Packet.Children) < 2 { return "", false } + if e.Packet.Children[1].Tag != ber.TagObjectDescriptor { + return "", false + } + for i := 0; i < len(e.Packet.Children[1].Children); i++ { if e.Packet.Children[1].Children[i].Tag != ber.TagBitString || len(e.Packet.Children[1].Children[i].Children) < 1 { continue diff --git a/internal/authentication/ldap_util_test.go b/internal/authentication/ldap_util_test.go new file mode 100644 index 00000000..cc3a0423 --- /dev/null +++ b/internal/authentication/ldap_util_test.go @@ -0,0 +1,320 @@ +package authentication + +import ( + "errors" + "testing" + + ber "github.com/go-asn1-ber/asn1-ber" + "github.com/go-ldap/ldap/v3" + "github.com/stretchr/testify/assert" +) + +func TestLDAPGetFeatureSupportFromNilEntry(t *testing.T) { + control, extension, feature := ldapGetFeatureSupportFromEntry(nil) + assert.Len(t, control, 0) + assert.Len(t, extension, 0) + assert.Equal(t, LDAPSupportedFeatures{}, feature) +} + +func TestLDAPGetFeatureSupportFromEntry(t *testing.T) { + testCases := []struct { + description string + haveControlOIDs, haveExtensionOIDs []string + expected LDAPSupportedFeatures + }{ + { + description: "ShouldReturnExtensionPwdModifyExOp", + haveControlOIDs: []string{}, + haveExtensionOIDs: []string{ldapOIDExtensionPwdModifyExOp}, + expected: LDAPSupportedFeatures{Extensions: LDAPSupportedExtensions{PwdModifyExOp: true}}, + }, + { + description: "ShouldReturnExtensionTLS", + haveControlOIDs: []string{}, + haveExtensionOIDs: []string{ldapOIDExtensionTLS}, + expected: LDAPSupportedFeatures{Extensions: LDAPSupportedExtensions{TLS: true}}, + }, + { + description: "ShouldReturnExtensionAll", + haveControlOIDs: []string{}, + haveExtensionOIDs: []string{ldapOIDExtensionTLS, ldapOIDExtensionPwdModifyExOp}, + expected: LDAPSupportedFeatures{Extensions: LDAPSupportedExtensions{TLS: true, PwdModifyExOp: true}}, + }, + { + description: "ShouldReturnControlMsftPPolHints", + haveControlOIDs: []string{ldapOIDControlMsftServerPolicyHints}, + haveExtensionOIDs: []string{}, + expected: LDAPSupportedFeatures{ControlTypes: LDAPSupportedControlTypes{MsftPwdPolHints: true}}, + }, + { + description: "ShouldReturnControlMsftPPolHintsDeprecated", + haveControlOIDs: []string{ldapOIDControlMsftServerPolicyHintsDeprecated}, + haveExtensionOIDs: []string{}, + expected: LDAPSupportedFeatures{ControlTypes: LDAPSupportedControlTypes{MsftPwdPolHintsDeprecated: true}}, + }, + { + description: "ShouldReturnControlAll", + haveControlOIDs: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + haveExtensionOIDs: []string{}, + expected: LDAPSupportedFeatures{ControlTypes: LDAPSupportedControlTypes{MsftPwdPolHints: true, MsftPwdPolHintsDeprecated: true}}, + }, + { + description: "ShouldReturnExtensionAndControlAll", + haveControlOIDs: []string{ldapOIDControlMsftServerPolicyHints, ldapOIDControlMsftServerPolicyHintsDeprecated}, + haveExtensionOIDs: []string{ldapOIDExtensionTLS, ldapOIDExtensionPwdModifyExOp}, + expected: LDAPSupportedFeatures{ + ControlTypes: LDAPSupportedControlTypes{MsftPwdPolHints: true, MsftPwdPolHintsDeprecated: true}, + Extensions: LDAPSupportedExtensions{TLS: true, PwdModifyExOp: true}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + entry := &ldap.Entry{ + DN: "", + Attributes: []*ldap.EntryAttribute{ + {Name: ldapSupportedExtensionAttribute, Values: tc.haveExtensionOIDs}, + {Name: ldapSupportedControlAttribute, Values: tc.haveControlOIDs}, + }, + } + + actualControlOIDs, actualExtensionOIDs, actual := ldapGetFeatureSupportFromEntry(entry) + + assert.Equal(t, tc.haveExtensionOIDs, actualExtensionOIDs) + assert.Equal(t, tc.haveControlOIDs, actualControlOIDs) + assert.Equal(t, tc.expected, actual) + }) + } +} + +func TestLDAPEntriesContainsEntry(t *testing.T) { + testCases := []struct { + description string + have []*ldap.Entry + lookingFor *ldap.Entry + expected bool + }{ + { + description: "ShouldNotMatchNil", + have: []*ldap.Entry{ + {DN: "test"}, + }, + lookingFor: nil, + expected: false, + }, + { + description: "ShouldMatch", + have: []*ldap.Entry{ + {DN: "test"}, + }, + lookingFor: &ldap.Entry{DN: "test"}, + expected: true, + }, + { + description: "ShouldMatchWhenMultiple", + have: []*ldap.Entry{ + {DN: "False"}, + {DN: "test"}, + }, + lookingFor: &ldap.Entry{DN: "test"}, + expected: true, + }, + { + description: "ShouldNotMatchDifferent", + have: []*ldap.Entry{ + {DN: "False"}, + {DN: "test"}, + }, + lookingFor: &ldap.Entry{DN: "not a result"}, + expected: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + assert.Equal(t, tc.expected, ldapEntriesContainsEntry(tc.lookingFor, tc.have)) + }) + } +} + +func TestLDAPGetReferral(t *testing.T) { + testCases := []struct { + description string + have error + expectedReferral string + expectedOK bool + }{ + { + description: "ShouldGetValidPacket", + have: &ldap.Error{ResultCode: ldap.LDAPResultReferral, Packet: &testBERPacketReferral}, + expectedReferral: "ldap://192.168.0.1", + expectedOK: true, + }, + { + description: "ShouldNotGetNilPacket", + have: &ldap.Error{ResultCode: ldap.LDAPResultReferral, Packet: nil}, + expectedReferral: "", + expectedOK: false, + }, + { + description: "ShouldNotGetInvalidPacketWithNoObjectDescriptor", + have: &ldap.Error{ResultCode: ldap.LDAPResultReferral, Packet: &testBERPacketReferralInvalidObjectDescriptor}, + expectedReferral: "", + expectedOK: false, + }, + { + description: "ShouldNotGetInvalidPacketWithBadErrorCode", + have: &ldap.Error{ResultCode: ldap.LDAPResultBusy, Packet: &testBERPacketReferral}, + expectedReferral: "", + expectedOK: false, + }, + { + description: "ShouldNotGetInvalidPacketWithoutBitString", + have: &ldap.Error{ResultCode: ldap.LDAPResultReferral, Packet: &testBERPacketReferralWithoutBitString}, + expectedReferral: "", + expectedOK: false, + }, + { + description: "ShouldNotGetInvalidPacketWithInvalidBitString", + have: &ldap.Error{ResultCode: ldap.LDAPResultReferral, Packet: &testBERPacketReferralWithInvalidBitString}, + expectedReferral: "", + expectedOK: false, + }, + { + description: "ShouldNotGetInvalidPacketWithoutEnoughChildren", + have: &ldap.Error{ResultCode: ldap.LDAPResultReferral, Packet: &testBERPacketReferralWithoutEnoughChildren}, + expectedReferral: "", + expectedOK: false, + }, + { + description: "ShouldNotGetInvalidErrType", + have: errors.New("not an err"), + expectedReferral: "", + expectedOK: false, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + referral, ok := ldapGetReferral(tc.have) + + assert.Equal(t, tc.expectedOK, ok) + assert.Equal(t, tc.expectedReferral, referral) + }) + } +} + +var testBERPacketReferral = ber.Packet{ + Children: []*ber.Packet{ + {}, + { + Identifier: ber.Identifier{ + Tag: ber.TagObjectDescriptor, + }, + Children: []*ber.Packet{ + { + Identifier: ber.Identifier{ + Tag: ber.TagBitString, + }, + Children: []*ber.Packet{ + { + Value: "ldap://192.168.0.1", + }, + }, + }, + }, + }, + }, +} + +var testBERPacketReferralInvalidObjectDescriptor = ber.Packet{ + Children: []*ber.Packet{ + {}, + { + Identifier: ber.Identifier{ + Tag: ber.TagEOC, + }, + Children: []*ber.Packet{ + { + Identifier: ber.Identifier{ + Tag: ber.TagBitString, + }, + Children: []*ber.Packet{ + { + Value: "ldap://192.168.0.1", + }, + }, + }, + }, + }, + }, +} + +var testBERPacketReferralWithoutBitString = ber.Packet{ + Children: []*ber.Packet{ + {}, + { + Identifier: ber.Identifier{ + Tag: ber.TagObjectDescriptor, + }, + Children: []*ber.Packet{ + { + Identifier: ber.Identifier{ + Tag: ber.TagSequence, + }, + Children: []*ber.Packet{ + { + Value: "ldap://192.168.0.1", + }, + }, + }, + }, + }, + }, +} + +var testBERPacketReferralWithInvalidBitString = ber.Packet{ + Children: []*ber.Packet{ + {}, + { + Identifier: ber.Identifier{ + Tag: ber.TagObjectDescriptor, + }, + Children: []*ber.Packet{ + { + Identifier: ber.Identifier{ + Tag: ber.TagBitString, + }, + Children: []*ber.Packet{ + { + Value: 55, + }, + }, + }, + }, + }, + }, +} + +var testBERPacketReferralWithoutEnoughChildren = ber.Packet{ + Children: []*ber.Packet{ + { + Identifier: ber.Identifier{ + Tag: ber.TagEOC, + }, + Children: []*ber.Packet{ + { + Identifier: ber.Identifier{ + Tag: ber.TagBitString, + }, + Children: []*ber.Packet{ + { + Value: "ldap://192.168.0.1", + }, + }, + }, + }, + }, + }, +} diff --git a/internal/authentication/types.go b/internal/authentication/types.go index 64425f0f..053e5479 100644 --- a/internal/authentication/types.go +++ b/internal/authentication/types.go @@ -7,21 +7,24 @@ import ( "golang.org/x/text/encoding/unicode" ) -// LDAPConnectionFactory an interface of factory of ldap connections. -type LDAPConnectionFactory interface { - DialURL(addr string, opts ...ldap.DialOpt) (LDAPConnection, error) +// LDAPClientFactory an interface of factory of LDAP clients. +type LDAPClientFactory interface { + DialURL(addr string, opts ...ldap.DialOpt) (client LDAPClient, err error) } -// LDAPConnection interface representing a connection to the ldap. -type LDAPConnection interface { - Bind(username, password string) (err error) +// LDAPClient is a cut down version of the ldap.Client interface with just the methods we use. +// +// Methods added to this interface that have a direct correlation with one from ldap.Client should have the same signature. +type LDAPClient interface { Close() StartTLS(config *tls.Config) (err error) - Search(searchRequest *ldap.SearchRequest) (searchResult *ldap.SearchResult, err error) + Bind(username, password string) (err error) Modify(modifyRequest *ldap.ModifyRequest) (err error) - PasswordModify(pwdModifyRequest *ldap.PasswordModifyRequest) (result *ldap.PasswordModifyResult, err error) + PasswordModify(pwdModifyRequest *ldap.PasswordModifyRequest) (pwdModifyResult *ldap.PasswordModifyResult, err error) + + Search(searchRequest *ldap.SearchRequest) (searchResult *ldap.SearchResult, err error) } // UserDetails represent the details retrieved for a given user. @@ -39,4 +42,22 @@ type ldapUserProfile struct { Username string } +// LDAPSupportedFeatures represents features which a server may support which are implemented in code. +type LDAPSupportedFeatures struct { + Extensions LDAPSupportedExtensions + ControlTypes LDAPSupportedControlTypes +} + +// LDAPSupportedExtensions represents extensions which a server may support which are implemented in code. +type LDAPSupportedExtensions struct { + TLS bool + PwdModifyExOp bool +} + +// LDAPSupportedControlTypes represents control types which a server may support which are implemented in code. +type LDAPSupportedControlTypes struct { + MsftPwdPolHints bool + MsftPwdPolHintsDeprecated bool +} + var utf16LittleEndian = unicode.UTF16(unicode.LittleEndian, unicode.IgnoreBOM)