2021-04-09 15:38:53 +00:00
// Copyright 2021 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package upstreamldap
import (
"context"
2021-04-10 01:49:43 +00:00
"crypto/tls"
2021-05-27 20:47:10 +00:00
"encoding/base64"
2021-04-13 00:50:25 +00:00
"errors"
2021-04-10 01:49:43 +00:00
"fmt"
"net"
"net/http"
2021-10-20 11:59:24 +00:00
"net/http/httptest"
2021-04-10 01:49:43 +00:00
"net/url"
2021-04-09 15:38:53 +00:00
"testing"
2021-05-21 19:44:01 +00:00
"time"
2021-04-09 15:38:53 +00:00
2021-04-10 01:49:43 +00:00
"github.com/go-ldap/ldap/v3"
2021-04-09 15:38:53 +00:00
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/require"
"k8s.io/apiserver/pkg/authentication/user"
2021-11-03 17:33:22 +00:00
"go.pinniped.dev/internal/authenticators"
2021-05-21 19:44:01 +00:00
"go.pinniped.dev/internal/certauthority"
2021-10-20 11:59:24 +00:00
"go.pinniped.dev/internal/crypto/ptls"
2021-05-25 19:46:50 +00:00
"go.pinniped.dev/internal/endpointaddr"
2021-04-09 15:38:53 +00:00
"go.pinniped.dev/internal/mocks/mockldapconn"
2021-11-17 00:31:32 +00:00
provider2 "go.pinniped.dev/internal/oidc/provider"
2021-04-10 01:49:43 +00:00
"go.pinniped.dev/internal/testutil"
2021-10-20 11:59:24 +00:00
"go.pinniped.dev/internal/testutil/tlsserver"
2021-04-09 15:38:53 +00:00
)
2021-04-13 00:50:25 +00:00
const (
2021-05-17 18:10:26 +00:00
testHost = "ldap.example.com:8443"
testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev"
testBindPassword = "some-bind-password"
testUpstreamUsername = "some-upstream-username"
testUpstreamPassword = "some-upstream-password"
testUserSearchBase = "some-upstream-user-base-dn"
testGroupSearchBase = "some-upstream-group-base-dn"
testUserSearchFilter = "some-user-filter={}-and-more-filter={}"
testGroupSearchFilter = "some-group-filter={}-and-more-filter={}"
testUserSearchUsernameAttribute = "some-upstream-username-attribute"
testUserSearchUIDAttribute = "some-upstream-uid-attribute"
testGroupSearchGroupNameAttribute = "some-upstream-group-name-attribute"
testUserSearchResultDNValue = "some-upstream-user-dn"
testGroupSearchResultDNValue1 = "some-upstream-group-dn1"
testGroupSearchResultDNValue2 = "some-upstream-group-dn2"
testUserSearchResultUsernameAttributeValue = "some-upstream-username-value"
testUserSearchResultUIDAttributeValue = "some-upstream-uid-value"
testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1"
testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2"
expectedGroupSearchPageSize = uint32 ( 250 )
2021-04-13 00:50:25 +00:00
)
2021-04-09 15:38:53 +00:00
var (
2021-05-17 18:10:26 +00:00
testUserSearchFilterInterpolated = fmt . Sprintf ( "(some-user-filter=%s-and-more-filter=%s)" , testUpstreamUsername , testUpstreamUsername )
testGroupSearchFilterInterpolated = fmt . Sprintf ( "(some-group-filter=%s-and-more-filter=%s)" , testUserSearchResultDNValue , testUserSearchResultDNValue )
2021-04-09 15:38:53 +00:00
)
2021-04-16 21:04:05 +00:00
func TestEndUserAuthentication ( t * testing . T ) {
2021-04-15 17:25:35 +00:00
providerConfig := func ( editFunc func ( p * ProviderConfig ) ) * ProviderConfig {
config := & ProviderConfig {
2021-05-20 19:46:33 +00:00
Name : "some-provider-name" ,
Host : testHost ,
CABundle : nil , // this field is only used by the production dialer, which is replaced by a mock for this test
ConnectionProtocol : TLS ,
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
2021-04-15 17:25:35 +00:00
UserSearch : UserSearchConfig {
2021-04-13 00:50:25 +00:00
Base : testUserSearchBase ,
Filter : testUserSearchFilter ,
UsernameAttribute : testUserSearchUsernameAttribute ,
UIDAttribute : testUserSearchUIDAttribute ,
} ,
2021-05-17 18:10:26 +00:00
GroupSearch : GroupSearchConfig {
Base : testGroupSearchBase ,
Filter : testGroupSearchFilter ,
GroupNameAttribute : testGroupSearchGroupNameAttribute ,
} ,
2021-04-13 00:50:25 +00:00
}
if editFunc != nil {
2021-04-15 17:25:35 +00:00
editFunc ( config )
2021-04-13 00:50:25 +00:00
}
2021-04-15 17:25:35 +00:00
return config
2021-04-13 00:50:25 +00:00
}
2021-05-17 18:10:26 +00:00
expectedUserSearch := func ( editFunc func ( r * ldap . SearchRequest ) ) * ldap . SearchRequest {
2021-04-13 00:50:25 +00:00
request := & ldap . SearchRequest {
BaseDN : testUserSearchBase ,
Scope : ldap . ScopeWholeSubtree ,
2021-04-27 19:43:09 +00:00
DerefAliases : ldap . NeverDerefAliases ,
2021-04-13 00:50:25 +00:00
SizeLimit : 2 ,
TimeLimit : 90 ,
TypesOnly : false ,
Filter : testUserSearchFilterInterpolated ,
Attributes : [ ] string { testUserSearchUsernameAttribute , testUserSearchUIDAttribute } ,
2021-05-17 18:10:26 +00:00
Controls : nil , // don't need paging because we set the SizeLimit so small
}
if editFunc != nil {
editFunc ( request )
}
return request
}
expectedGroupSearch := func ( editFunc func ( r * ldap . SearchRequest ) ) * ldap . SearchRequest {
request := & ldap . SearchRequest {
BaseDN : testGroupSearchBase ,
Scope : ldap . ScopeWholeSubtree ,
DerefAliases : ldap . NeverDerefAliases ,
SizeLimit : 0 , // unlimited size because we will search with paging
TimeLimit : 90 ,
TypesOnly : false ,
Filter : testGroupSearchFilterInterpolated ,
Attributes : [ ] string { testGroupSearchGroupNameAttribute } ,
Controls : nil , // nil because ldap.SearchWithPaging() will set the appropriate controls for us
2021-04-13 00:50:25 +00:00
}
if editFunc != nil {
editFunc ( request )
}
return request
}
2021-04-09 15:38:53 +00:00
2021-05-17 18:10:26 +00:00
exampleUserSearchResult := & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testUserSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testUserSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
}
exampleGroupSearchResult := & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { testGroupSearchResultGroupNameAttributeValue1 } ) ,
} ,
} ,
{
DN : testGroupSearchResultDNValue2 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { testGroupSearchResultGroupNameAttributeValue2 } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
}
// The auth response which matches the exampleUserSearchResult and exampleGroupSearchResult.
2021-11-03 17:33:22 +00:00
expectedAuthResponse := func ( editFunc func ( r * user . DefaultInfo ) ) * authenticators . Response {
2021-05-17 18:10:26 +00:00
u := & user . DefaultInfo {
Name : testUserSearchResultUsernameAttributeValue ,
2021-05-27 20:47:10 +00:00
UID : base64 . RawURLEncoding . EncodeToString ( [ ] byte ( testUserSearchResultUIDAttributeValue ) ) ,
2021-05-17 18:10:26 +00:00
Groups : [ ] string { testGroupSearchResultGroupNameAttributeValue1 , testGroupSearchResultGroupNameAttributeValue2 } ,
}
if editFunc != nil {
editFunc ( u )
}
2021-11-03 17:33:22 +00:00
return & authenticators . Response { User : u , DN : testUserSearchResultDNValue }
2021-05-17 18:10:26 +00:00
}
2021-04-09 15:38:53 +00:00
tests := [ ] struct {
2021-04-16 21:04:05 +00:00
name string
username string
password string
providerConfig * ProviderConfig
searchMocks func ( conn * mockldapconn . MockConn )
bindEndUserMocks func ( conn * mockldapconn . MockConn )
dialError error
wantError string
wantToSkipDial bool
2021-11-03 17:33:22 +00:00
wantAuthResponse * authenticators . Response
2021-04-16 21:04:05 +00:00
wantUnauthenticated bool
skipDryRunAuthenticateUser bool // tests about when the end user bind fails don't make sense for DryRunAuthenticateUser()
2021-04-09 15:38:53 +00:00
} {
{
2021-04-15 17:25:35 +00:00
name : "happy path" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
2021-04-13 00:50:25 +00:00
} ,
2021-05-17 18:10:26 +00:00
wantAuthResponse : expectedAuthResponse ( nil ) ,
2021-04-13 00:50:25 +00:00
} ,
{
2021-04-13 22:23:14 +00:00
name : "when the user search filter is already wrapped by parenthesis then it is not wrapped again" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
2021-04-15 17:25:35 +00:00
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
2021-04-13 22:23:14 +00:00
p . UserSearch . Filter = "(" + testUserSearchFilter + ")"
} ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 22:23:14 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
2021-04-13 22:23:14 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
2021-04-16 21:04:05 +00:00
} ,
2021-05-17 18:10:26 +00:00
wantAuthResponse : expectedAuthResponse ( nil ) ,
} ,
{
name : "when the group search filter is already wrapped by parenthesis then it is not wrapped again" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . Filter = "(" + testGroupSearchFilter + ")"
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( nil ) ,
} ,
{
name : "when the group search base is empty then skip the group search entirely" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . Base = "" // this configuration means that the user does not want group search to happen
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
2021-04-13 22:23:14 +00:00
} ,
2021-05-17 18:10:26 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( func ( r * user . DefaultInfo ) {
r . Groups = [ ] string { }
} ) ,
2021-04-13 22:23:14 +00:00
} ,
{
name : "when the UsernameAttribute is dn and there is a user search filter provided" ,
2021-04-13 00:50:25 +00:00
username : testUpstreamUsername ,
password : testUpstreamPassword ,
2021-04-15 17:25:35 +00:00
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
2021-04-13 00:50:25 +00:00
p . UserSearch . UsernameAttribute = "dn"
} ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( func ( r * ldap . SearchRequest ) {
2021-04-13 00:50:25 +00:00
r . Attributes = [ ] string { testUserSearchUIDAttribute }
} ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 00:50:25 +00:00
Attributes : [ ] * ldap . EntryAttribute {
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testUserSearchResultUIDAttributeValue } ) ,
2021-04-13 00:50:25 +00:00
} ,
} ,
} ,
} , nil ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
2021-04-09 15:38:53 +00:00
} ,
2021-05-17 18:10:26 +00:00
wantAuthResponse : expectedAuthResponse ( func ( r * user . DefaultInfo ) {
r . Name = testUserSearchResultDNValue
} ) ,
2021-04-13 00:50:25 +00:00
} ,
{
name : "when the UIDAttribute is dn" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
2021-04-15 17:25:35 +00:00
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
2021-04-13 00:50:25 +00:00
p . UserSearch . UIDAttribute = "dn"
} ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( func ( r * ldap . SearchRequest ) {
2021-04-13 00:50:25 +00:00
r . Attributes = [ ] string { testUserSearchUsernameAttribute }
} ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 00:50:25 +00:00
Attributes : [ ] * ldap . EntryAttribute {
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testUserSearchResultUsernameAttributeValue } ) ,
2021-04-13 00:50:25 +00:00
} ,
} ,
} ,
} , nil ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
2021-04-16 21:04:05 +00:00
} ,
2021-05-17 18:10:26 +00:00
wantAuthResponse : expectedAuthResponse ( func ( r * user . DefaultInfo ) {
2021-05-27 20:47:10 +00:00
r . UID = base64 . RawURLEncoding . EncodeToString ( [ ] byte ( testUserSearchResultDNValue ) )
2021-05-17 18:10:26 +00:00
} ) ,
} ,
2021-05-28 20:27:11 +00:00
{
name : "when the GroupNameAttribute is empty then it defaults to dn" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . GroupNameAttribute = "" // blank means to use dn
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { }
} ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( func ( r * user . DefaultInfo ) {
r . Groups = [ ] string { testGroupSearchResultDNValue1 , testGroupSearchResultDNValue2 }
} ) ,
} ,
2021-05-17 18:10:26 +00:00
{
name : "when the GroupNameAttribute is dn" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . GroupNameAttribute = "dn"
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { }
} ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
2021-04-09 15:38:53 +00:00
} ,
2021-05-17 18:10:26 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( func ( r * user . DefaultInfo ) {
r . Groups = [ ] string { testGroupSearchResultDNValue1 , testGroupSearchResultDNValue2 }
} ) ,
2021-04-09 15:38:53 +00:00
} ,
2021-04-13 00:50:25 +00:00
{
2021-05-28 20:27:11 +00:00
name : "when the GroupNameAttribute is cn" ,
2021-04-13 00:50:25 +00:00
username : testUpstreamUsername ,
password : testUpstreamPassword ,
2021-04-15 17:25:35 +00:00
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
2021-05-28 20:27:11 +00:00
p . GroupSearch . GroupNameAttribute = "cn"
2021-04-13 00:50:25 +00:00
} ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { "cn" }
} ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "cn" , [ ] string { testGroupSearchResultGroupNameAttributeValue1 } ) ,
} ,
} ,
{
DN : testGroupSearchResultDNValue2 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "cn" , [ ] string { testGroupSearchResultGroupNameAttributeValue2 } ) ,
} ,
2021-04-13 00:50:25 +00:00
} ,
} ,
2021-05-17 18:10:26 +00:00
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
2021-04-16 21:04:05 +00:00
} ,
2021-05-17 18:10:26 +00:00
wantAuthResponse : expectedAuthResponse ( nil ) ,
} ,
{
name : "when user search Filter is blank it derives a search filter from the UsernameAttribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . UserSearch . Filter = ""
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( func ( r * ldap . SearchRequest ) {
r . Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")"
} ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( nil ) ,
} ,
{
name : "when group search Filter is blank it uses a default search filter of member={}" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . Filter = ""
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Filter = "(member=" + testUserSearchResultDNValue + ")"
} ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
2021-04-13 00:50:25 +00:00
} ,
2021-05-17 18:10:26 +00:00
wantAuthResponse : expectedAuthResponse ( nil ) ,
2021-04-13 00:50:25 +00:00
} ,
{
2021-05-17 18:10:26 +00:00
name : "when the username has special LDAP search filter characters then they must be properly escaped in the search filter, because the username is end-user input" ,
2021-04-15 17:25:35 +00:00
username : ` a&b|c(d)e\f*g ` ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( func ( r * ldap . SearchRequest ) {
r . Filter = fmt . Sprintf ( "(some-user-filter=%s-and-more-filter=%s)" , ` a&b|c\28d\29e\5cf\2ag ` , ` a&b|c\28d\29e\5cf\2ag ` )
} ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( nil ) ,
} ,
{
name : "group names are sorted to make the result more stable/predictable" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { "c" } ) ,
} ,
} ,
{
DN : testGroupSearchResultDNValue2 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { "a" } ) ,
} ,
} ,
{
DN : testGroupSearchResultDNValue2 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { "b" } ) ,
} ,
2021-04-13 00:50:25 +00:00
} ,
} ,
2021-05-17 18:10:26 +00:00
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
2021-04-13 00:50:25 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
2021-04-16 21:04:05 +00:00
} ,
2021-11-03 17:33:22 +00:00
wantAuthResponse : & authenticators . Response {
2021-04-13 00:50:25 +00:00
User : & user . DefaultInfo {
2021-05-17 18:10:26 +00:00
Name : testUserSearchResultUsernameAttributeValue ,
2021-05-27 20:47:10 +00:00
UID : base64 . RawURLEncoding . EncodeToString ( [ ] byte ( testUserSearchResultUIDAttributeValue ) ) ,
2021-05-17 18:10:26 +00:00
Groups : [ ] string { "a" , "b" , "c" } ,
2021-04-13 00:50:25 +00:00
} ,
2021-11-03 17:33:22 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 00:50:25 +00:00
} ,
} ,
2021-07-27 18:08:23 +00:00
{
name : "override UID parsing to work with microsoft style objectGUIDs" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
2021-08-19 21:21:18 +00:00
p . UIDAttributeParsingOverrides = map [ string ] func ( entry * ldap . Entry ) ( string , error ) {
"objectGUID" : MicrosoftUUIDFromBinary ( "objectGUID" ) ,
}
2021-07-27 18:08:23 +00:00
p . UserSearch . UIDAttribute = "objectGUID"
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { testUserSearchUsernameAttribute , "objectGUID" }
} ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testUserSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( "objectGUID" , [ ] string { "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16" } ) ,
} ,
} ,
} } , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( func ( r * user . DefaultInfo ) {
r . UID = "04030201-0605-0807-0910-111213141516"
} ) ,
} ,
{
name : "override UID parsing when the attribute name doesn't match what's returned does default parsing" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
2021-08-19 21:21:18 +00:00
p . UIDAttributeParsingOverrides = map [ string ] func ( entry * ldap . Entry ) ( string , error ) {
"objectGUID" : MicrosoftUUIDFromBinary ( "objectGUID" ) ,
}
2021-07-27 18:08:23 +00:00
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( nil ) ,
} ,
2021-08-17 23:53:26 +00:00
{
name : "override group parsing to create new group names" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . GroupNameAttribute = "sAMAccountName"
2021-08-19 21:21:18 +00:00
p . GroupAttributeParsingOverrides = map [ string ] func ( * ldap . Entry ) ( string , error ) {
"sAMAccountName" : GroupSAMAccountNameWithDomainSuffix ,
2021-08-18 17:11:18 +00:00
}
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { "sAMAccountName" }
} ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com" ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "sAMAccountName" , [ ] string { "Mammals" } ) ,
} ,
} ,
{
DN : "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com" ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "sAMAccountName" , [ ] string { "Animals" } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
} ,
wantAuthResponse : expectedAuthResponse ( func ( r * user . DefaultInfo ) {
r . Groups = [ ] string { "Animals@activedirectory.mycompany.example.com" , "Mammals@activedirectory.mycompany.example.com" }
} ) ,
} ,
2021-08-17 23:53:26 +00:00
{
name : "override group parsing when domain can't be determined from dn" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . GroupNameAttribute = "sAMAccountName"
2021-08-19 21:21:18 +00:00
p . GroupAttributeParsingOverrides = map [ string ] func ( * ldap . Entry ) ( string , error ) {
"sAMAccountName" : GroupSAMAccountNameWithDomainSuffix ,
}
2021-08-17 23:53:26 +00:00
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { "sAMAccountName" }
} ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : "no-domain-components" ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "sAMAccountName" , [ ] string { "Mammals" } ) ,
} ,
} ,
{
DN : "CN=Animals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com" ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "sAMAccountName" , [ ] string { "Animals" } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-08-18 17:11:18 +00:00
wantError : "error finding groups for user some-upstream-user-dn: did not find domain components in group dn: no-domain-components" ,
} ,
{
name : "override group parsing when entry has multiple values for attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . GroupNameAttribute = "sAMAccountName"
2021-08-19 21:21:18 +00:00
p . GroupAttributeParsingOverrides = map [ string ] func ( * ldap . Entry ) ( string , error ) {
"sAMAccountName" : GroupSAMAccountNameWithDomainSuffix ,
}
2021-08-18 17:11:18 +00:00
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { "sAMAccountName" }
} ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : "no-domain-components" ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "sAMAccountName" , [ ] string { "Mammals" , "Eukaryotes" } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : "error finding groups for user some-upstream-user-dn: found 2 values for attribute \"sAMAccountName\", but expected 1 result" ,
} , {
name : "override group parsing when entry has no values for attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
p . GroupSearch . GroupNameAttribute = "sAMAccountName"
2021-08-19 21:21:18 +00:00
p . GroupAttributeParsingOverrides = map [ string ] func ( * ldap . Entry ) ( string , error ) {
"sAMAccountName" : GroupSAMAccountNameWithDomainSuffix ,
}
2021-08-18 17:11:18 +00:00
} ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { "sAMAccountName" }
} ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : "no-domain-components" ,
Attributes : [ ] * ldap . EntryAttribute { } ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : "error finding groups for user some-upstream-user-dn: found 0 values for attribute \"sAMAccountName\", but expected 1 result" ,
2021-08-17 23:53:26 +00:00
} ,
2021-04-13 00:50:25 +00:00
{
2021-04-15 17:25:35 +00:00
name : "when dial fails" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
dialError : errors . New ( "some dial error" ) ,
wantError : fmt . Sprintf ( ` error dialing host "%s": some dial error ` , testHost ) ,
2021-04-13 00:50:25 +00:00
} ,
2021-04-13 22:23:14 +00:00
{
name : "when the UsernameAttribute is dn and there is not a user search filter provided" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
2021-04-15 17:25:35 +00:00
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
2021-04-13 22:23:14 +00:00
p . UserSearch . UsernameAttribute = "dn"
p . UserSearch . Filter = ""
} ) ,
wantToSkipDial : true ,
wantError : ` must specify UserSearch Filter when UserSearch UsernameAttribute is "dn" ` ,
} ,
2021-04-13 15:38:04 +00:00
{
2021-04-15 17:25:35 +00:00
name : "when binding as the bind user returns an error" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Return ( errors . New ( "some bind error" ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` error binding as "%s" before user search: some bind error ` , testBindUsername ) ,
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns an error" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( nil , errors . New ( "some user search error" ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-05-28 21:37:31 +00:00
wantError : ` error searching for user: some user search error ` ,
2021-05-17 18:10:26 +00:00
} ,
{
name : "when searching for the user's groups returns an error" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( nil , errors . New ( "some group search error" ) ) . Times ( 1 )
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-05-17 18:10:26 +00:00
wantError : fmt . Sprintf ( ` error searching for group memberships for user with DN "%s": some group search error ` , testUserSearchResultDNValue ) ,
2021-04-13 15:38:04 +00:00
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns no results" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-13 23:22:13 +00:00
wantUnauthenticated : true ,
2021-04-13 15:38:04 +00:00
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns multiple results" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
2021-05-17 18:10:26 +00:00
{ DN : testUserSearchResultDNValue } ,
2021-04-13 15:38:04 +00:00
{ DN : "some-other-dn" } ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` searching for user "%s" resulted in 2 search results, but expected 1 result ` , testUpstreamUsername ) ,
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns a user without a DN" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
{ DN : "" } ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` searching for user "%s" resulted in search result without DN ` , testUpstreamUsername ) ,
} ,
2021-05-17 18:10:26 +00:00
{
name : "when searching for the user's groups returns a group without a DN" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { testGroupSearchResultGroupNameAttributeValue1 } ) ,
} ,
} ,
{
DN : "" ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { testGroupSearchResultGroupNameAttributeValue2 } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf (
` searching for group memberships for user with DN "%s" resulted in search result without DN ` ,
testUserSearchResultDNValue ) ,
} ,
2021-04-13 15:38:04 +00:00
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns a user without an expected username attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 15:38:04 +00:00
Attributes : [ ] * ldap . EntryAttribute {
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testUserSearchResultUIDAttributeValue } ) ,
2021-04-13 15:38:04 +00:00
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-05-17 18:10:26 +00:00
wantError : fmt . Sprintf (
` found 0 values for attribute "%s" while searching for user "%s", but expected 1 result ` ,
testUserSearchUsernameAttribute , testUpstreamUsername ) ,
} ,
{
name : "when searching for the group memberships returns a group without an expected group name attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { testGroupSearchResultGroupNameAttributeValue1 } ) ,
} ,
} ,
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( "unrelated attribute" , [ ] string { "anything" } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf (
` error searching for group memberships for user with DN "%s": found 0 values for attribute "%s" while searching for user "%s", but expected 1 result ` ,
testUserSearchResultDNValue , testGroupSearchGroupNameAttribute , testUserSearchResultDNValue ) ,
2021-04-13 15:38:04 +00:00
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns a user with too many values for the expected username attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 15:38:04 +00:00
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string {
2021-05-17 18:10:26 +00:00
testUserSearchResultUsernameAttributeValue ,
2021-04-13 15:38:04 +00:00
"unexpected-additional-value" ,
} ) ,
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testUserSearchResultUIDAttributeValue } ) ,
2021-04-13 15:38:04 +00:00
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-05-17 18:10:26 +00:00
wantError : fmt . Sprintf (
` found 2 values for attribute "%s" while searching for user "%s", but expected 1 result ` ,
testUserSearchUsernameAttribute , testUpstreamUsername ) ,
} ,
{
name : "when searching for the group memberships returns a group with too many values for the expected group name attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { testGroupSearchResultGroupNameAttributeValue1 } ) ,
} ,
} ,
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string {
testGroupSearchResultGroupNameAttributeValue1 ,
"unexpected-additional-value" ,
} ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf (
` error searching for group memberships for user with DN "%s": found 2 values for attribute "%s" while searching for user "%s", but expected 1 result ` ,
testUserSearchResultDNValue , testGroupSearchGroupNameAttribute , testUserSearchResultDNValue ) ,
2021-04-13 15:38:04 +00:00
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns a user with an empty value for the expected username attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 15:38:04 +00:00
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { "" } ) ,
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testUserSearchResultUIDAttributeValue } ) ,
2021-04-13 15:38:04 +00:00
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-05-17 18:10:26 +00:00
wantError : fmt . Sprintf (
` found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty ` ,
testUserSearchUsernameAttribute , testUpstreamUsername ) ,
} ,
{
name : "when searching for the group memberships returns a group with an empty value for for the expected group name attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
searchMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { testGroupSearchResultGroupNameAttributeValue1 } ) ,
} ,
} ,
{
DN : testGroupSearchResultDNValue1 ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testGroupSearchGroupNameAttribute , [ ] string { "" } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf (
` error searching for group memberships for user with DN "%s": found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty ` ,
testUserSearchResultDNValue , testGroupSearchGroupNameAttribute , testUserSearchResultDNValue ) ,
2021-04-13 15:38:04 +00:00
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns a user without an expected UID attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 15:38:04 +00:00
Attributes : [ ] * ldap . EntryAttribute {
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testUserSearchResultUsernameAttributeValue } ) ,
2021-04-13 15:38:04 +00:00
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` found 0 values for attribute "%s" while searching for user "%s", but expected 1 result ` , testUserSearchUIDAttribute , testUpstreamUsername ) ,
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns a user with too many values for the expected UID attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 15:38:04 +00:00
Attributes : [ ] * ldap . EntryAttribute {
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testUserSearchResultUsernameAttributeValue } ) ,
2021-04-13 15:38:04 +00:00
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string {
2021-05-17 18:10:26 +00:00
testUserSearchResultUIDAttributeValue ,
2021-04-13 15:38:04 +00:00
"unexpected-additional-value" ,
} ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` found 2 values for attribute "%s" while searching for user "%s", but expected 1 result ` , testUserSearchUIDAttribute , testUpstreamUsername ) ,
} ,
{
2021-04-15 17:25:35 +00:00
name : "when searching for the user returns a user with an empty value for the expected UID attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( & ldap . SearchResult {
2021-04-13 15:38:04 +00:00
Entries : [ ] * ldap . Entry {
{
2021-05-17 18:10:26 +00:00
DN : testUserSearchResultDNValue ,
2021-04-13 15:38:04 +00:00
Attributes : [ ] * ldap . EntryAttribute {
2021-05-17 18:10:26 +00:00
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testUserSearchResultUsernameAttributeValue } ) ,
2021-04-13 15:38:04 +00:00
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { "" } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty ` , testUserSearchUIDAttribute , testUpstreamUsername ) ,
} ,
{
2021-04-15 17:25:35 +00:00
name : "when binding as the found user returns an error" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
2021-04-13 15:38:04 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Return ( errors . New ( "some bind error" ) ) . Times ( 1 )
2021-04-16 21:04:05 +00:00
} ,
skipDryRunAuthenticateUser : true ,
2021-05-17 18:10:26 +00:00
wantError : fmt . Sprintf ( ` error binding for user "%s" using provided password against DN "%s": some bind error ` , testUpstreamUsername , testUserSearchResultDNValue ) ,
2021-04-13 15:38:04 +00:00
} ,
2021-04-13 23:22:13 +00:00
{
2021-04-15 17:25:35 +00:00
name : "when binding as the found user returns a specific invalid credentials error" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-16 21:04:05 +00:00
searchMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-13 23:22:13 +00:00
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Search ( expectedUserSearch ( nil ) ) . Return ( exampleUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . SearchWithPaging ( expectedGroupSearch ( nil ) , expectedGroupSearchPageSize ) .
Return ( exampleGroupSearchResult , nil ) . Times ( 1 )
2021-04-13 23:22:13 +00:00
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-16 21:04:05 +00:00
wantUnauthenticated : true ,
skipDryRunAuthenticateUser : true ,
bindEndUserMocks : func ( conn * mockldapconn . MockConn ) {
2021-04-27 19:43:09 +00:00
err := & ldap . Error {
Err : errors . New ( "some bind error" ) ,
ResultCode : ldap . LDAPResultInvalidCredentials ,
}
2021-05-17 18:10:26 +00:00
conn . EXPECT ( ) . Bind ( testUserSearchResultDNValue , testUpstreamPassword ) . Return ( err ) . Times ( 1 )
2021-04-16 21:04:05 +00:00
} ,
2021-04-13 23:22:13 +00:00
} ,
{
name : "when no username is specified" ,
username : "" ,
password : testUpstreamPassword ,
2021-04-15 17:25:35 +00:00
providerConfig : providerConfig ( nil ) ,
2021-04-13 23:22:13 +00:00
wantToSkipDial : true ,
wantUnauthenticated : true ,
} ,
2021-04-09 15:38:53 +00:00
}
2021-04-13 00:50:25 +00:00
2021-04-09 15:38:53 +00:00
for _ , test := range tests {
2021-04-13 00:50:25 +00:00
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
2021-04-09 15:38:53 +00:00
ctrl := gomock . NewController ( t )
t . Cleanup ( ctrl . Finish )
2021-04-13 00:50:25 +00:00
2021-04-09 15:38:53 +00:00
conn := mockldapconn . NewMockConn ( ctrl )
2021-04-16 21:04:05 +00:00
if tt . searchMocks != nil {
tt . searchMocks ( conn )
}
if tt . bindEndUserMocks != nil {
tt . bindEndUserMocks ( conn )
2021-04-13 00:50:25 +00:00
}
2021-04-09 15:38:53 +00:00
2021-04-10 01:49:43 +00:00
dialWasAttempted := false
2021-05-25 19:46:50 +00:00
tt . providerConfig . Dialer = LDAPDialerFunc ( func ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error ) {
2021-04-10 01:49:43 +00:00
dialWasAttempted = true
2021-05-25 19:46:50 +00:00
require . Equal ( t , tt . providerConfig . Host , addr . Endpoint ( ) )
2021-04-13 00:50:25 +00:00
if tt . dialError != nil {
return nil , tt . dialError
}
2021-04-09 15:38:53 +00:00
return conn , nil
2021-04-12 18:23:08 +00:00
} )
2021-04-09 15:38:53 +00:00
2021-04-15 17:25:35 +00:00
provider := New ( * tt . providerConfig )
2021-04-13 00:50:25 +00:00
2021-04-16 21:04:05 +00:00
authResponse , authenticated , err := provider . AuthenticateUser ( context . Background ( ) , tt . username , tt . password )
2021-04-13 22:23:14 +00:00
require . Equal ( t , ! tt . wantToSkipDial , dialWasAttempted )
2021-04-16 21:04:05 +00:00
switch {
case tt . wantError != "" :
require . EqualError ( t , err , tt . wantError )
require . False ( t , authenticated )
require . Nil ( t , authResponse )
case tt . wantUnauthenticated :
require . NoError ( t , err )
require . False ( t , authenticated )
require . Nil ( t , authResponse )
default :
require . NoError ( t , err )
require . True ( t , authenticated )
require . Equal ( t , tt . wantAuthResponse , authResponse )
}
2021-04-13 00:50:25 +00:00
2021-04-16 21:04:05 +00:00
// DryRunAuthenticateUser() should have the same behavior as AuthenticateUser() except that it does not bind
// as the end user to confirm their password. Since it should behave the same, all of the same test cases
// apply, except for those which are specifically testing what happens when the end user bind fails.
if tt . skipDryRunAuthenticateUser {
return // move on to the next test
}
// Reset some variables to get ready to call DryRunAuthenticateUser().
dialWasAttempted = false
conn = mockldapconn . NewMockConn ( ctrl )
if tt . searchMocks != nil {
tt . searchMocks ( conn )
}
// Skip tt.bindEndUserMocks since DryRunAuthenticateUser() never binds as the end user.
authResponse , authenticated , err = provider . DryRunAuthenticateUser ( context . Background ( ) , tt . username )
require . Equal ( t , ! tt . wantToSkipDial , dialWasAttempted )
2021-04-13 23:22:13 +00:00
switch {
case tt . wantError != "" :
2021-04-13 00:50:25 +00:00
require . EqualError ( t , err , tt . wantError )
require . False ( t , authenticated )
require . Nil ( t , authResponse )
2021-04-13 23:22:13 +00:00
case tt . wantUnauthenticated :
require . NoError ( t , err )
require . False ( t , authenticated )
require . Nil ( t , authResponse )
default :
2021-04-13 00:50:25 +00:00
require . NoError ( t , err )
require . True ( t , authenticated )
require . Equal ( t , tt . wantAuthResponse , authResponse )
2021-04-09 15:38:53 +00:00
}
} )
}
}
2021-04-10 01:49:43 +00:00
2021-10-22 20:57:30 +00:00
func TestUpstreamRefresh ( t * testing . T ) {
2021-10-28 19:00:56 +00:00
pwdLastSetAttribute := "pwdLastSet"
2021-10-22 20:57:30 +00:00
expectedUserSearch := & ldap . SearchRequest {
BaseDN : testUserSearchResultDNValue ,
Scope : ldap . ScopeBaseObject ,
DerefAliases : ldap . NeverDerefAliases ,
SizeLimit : 2 ,
TimeLimit : 90 ,
TypesOnly : false ,
Filter : "(objectClass=*)" ,
2021-10-28 19:00:56 +00:00
Attributes : [ ] string { testUserSearchUsernameAttribute , testUserSearchUIDAttribute , pwdLastSetAttribute } ,
2021-10-22 20:57:30 +00:00
Controls : nil , // don't need paging because we set the SizeLimit so small
}
happyPathUserSearchResult := & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
2021-10-25 21:25:43 +00:00
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { testUserSearchResultUsernameAttributeValue } ,
} ,
{
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { [ ] byte ( testUserSearchResultUIDAttributeValue ) } ,
} ,
2021-10-28 19:00:56 +00:00
{
Name : pwdLastSetAttribute ,
Values : [ ] string { "132801740800000000" } ,
} ,
2021-10-25 21:25:43 +00:00
} ,
2021-10-22 20:57:30 +00:00
} ,
} ,
}
providerConfig := & ProviderConfig {
Name : "some-provider-name" ,
Host : testHost ,
CABundle : nil , // this field is only used by the production dialer, which is replaced by a mock for this test
ConnectionProtocol : TLS ,
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
UserSearch : UserSearchConfig {
2021-10-25 21:25:43 +00:00
Base : testUserSearchBase ,
UIDAttribute : testUserSearchUIDAttribute ,
UsernameAttribute : testUserSearchUsernameAttribute ,
2021-10-22 20:57:30 +00:00
} ,
2021-10-28 19:00:56 +00:00
RefreshAttributeChecks : map [ string ] func ( * ldap . Entry , provider2 . StoredRefreshAttributes ) error { pwdLastSetAttribute : PwdUnchangedSinceLogin } ,
2021-10-22 20:57:30 +00:00
}
tests := [ ] struct {
name string
providerConfig * ProviderConfig
setupMocks func ( conn * mockldapconn . MockConn )
dialError error
wantErr string
} {
{
name : "happy path where searching the dn returns a single entry" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( happyPathUserSearchResult , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
} ,
{
name : "error where dial fails" ,
providerConfig : providerConfig ,
dialError : errors . New ( "some dial error" ) ,
wantErr : "error dialing host \"ldap.example.com:8443\": some dial error" ,
} ,
{
name : "error binding" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Return ( errors . New ( "some bind error" ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "error binding as \"cn=some-bind-username,dc=pinniped,dc=dev\" before user search: some bind error" ,
} ,
{
name : "search result returns no entries" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry { } ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "searching for user \"some-upstream-user-dn\" resulted in 0 search results, but expected 1 result" ,
} ,
{
name : "error searching" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( nil , errors . New ( "some search error" ) )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "error searching for user \"some-upstream-user-dn\": some search error" ,
} ,
{
name : "search result returns more than one entry" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute { } ,
} ,
{
DN : "doesn't-matter" ,
Attributes : [ ] * ldap . EntryAttribute { } ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "searching for user \"some-upstream-user-dn\" resulted in 2 search results, but expected 1 result" ,
} ,
2021-10-25 21:25:43 +00:00
{
name : "search result has wrong uid" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { testUserSearchResultUsernameAttributeValue } ,
} ,
{
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { [ ] byte ( "wrong-uid" ) } ,
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "searching for user \"some-upstream-user-dn\" produced a different subject than the previous value. expected: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU\", actual: \"ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=d3JvbmctdWlk\"" ,
} ,
{
name : "search result has wrong username" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { "wrong-username" } ,
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "searching for user \"some-upstream-user-dn\" returned a different username than the previous value. expected: \"some-upstream-username-value\", actual: \"wrong-username\"" ,
} ,
{
name : "search result has no dn" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { testUserSearchResultUsernameAttributeValue } ,
} ,
{
2021-10-25 23:45:30 +00:00
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { [ ] byte ( testUserSearchResultUIDAttributeValue ) } ,
2021-10-25 21:25:43 +00:00
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "searching for user with original DN \"some-upstream-user-dn\" resulted in search result without DN" ,
} ,
2021-10-25 23:45:30 +00:00
{
name : "search result has 0 values for username attribute" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { } ,
} ,
{
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { [ ] byte ( testUserSearchResultUIDAttributeValue ) } ,
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "found 0 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result" ,
} ,
{
name : "search result has more than one value for username attribute" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { testUserSearchResultUsernameAttributeValue , "something-else" } ,
} ,
{
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { [ ] byte ( testUserSearchResultUIDAttributeValue ) } ,
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "found 2 values for attribute \"some-upstream-username-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result" ,
} ,
{
name : "search result has 0 values for uid attribute" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { testUserSearchResultUsernameAttributeValue } ,
} ,
{
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { } ,
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "found 0 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result" ,
} ,
{
name : "search result has 2 values for uid attribute" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { testUserSearchResultUsernameAttributeValue } ,
} ,
{
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { [ ] byte ( testUserSearchResultUIDAttributeValue ) , [ ] byte ( "other-uid-value" ) } ,
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "found 2 values for attribute \"some-upstream-uid-attribute\" while searching for user \"some-upstream-user-dn\", but expected 1 result" ,
} ,
2021-10-28 19:00:56 +00:00
{
name : "search result has a recent pwdLastSet value" ,
providerConfig : providerConfig ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedUserSearch ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testUserSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : testUserSearchUsernameAttribute ,
Values : [ ] string { testUserSearchResultUsernameAttributeValue } ,
} ,
{
Name : testUserSearchUIDAttribute ,
ByteValues : [ ] [ ] byte { [ ] byte ( testUserSearchResultUIDAttributeValue ) } ,
} ,
{
Name : pwdLastSetAttribute ,
Values : [ ] string { "132803468800000000" } ,
} ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantErr : "validation for attribute \"pwdLastSet\" failed during upstream refresh: password has changed since login. login time: 2021-11-01 23:43:19 +0000 UTC, password set time: 2021-11-02 17:14:40 +0000 UTC" ,
} ,
2021-10-22 20:57:30 +00:00
}
2021-11-05 21:18:54 +00:00
for _ , tt := range tests {
tt := tt
2021-10-22 20:57:30 +00:00
t . Run ( tt . name , func ( t * testing . T ) {
ctrl := gomock . NewController ( t )
t . Cleanup ( ctrl . Finish )
conn := mockldapconn . NewMockConn ( ctrl )
if tt . setupMocks != nil {
tt . setupMocks ( conn )
}
dialWasAttempted := false
providerConfig . Dialer = LDAPDialerFunc ( func ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error ) {
dialWasAttempted = true
require . Equal ( t , providerConfig . Host , addr . Endpoint ( ) )
if tt . dialError != nil {
return nil , tt . dialError
}
return conn , nil
} )
provider := New ( * providerConfig )
2021-10-25 21:25:43 +00:00
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
2021-10-28 19:00:56 +00:00
authTime := time . Date ( 2021 , time . November , 1 , 23 , 43 , 19 , 0 , time . UTC )
err := provider . PerformRefresh ( context . Background ( ) , provider2 . StoredRefreshAttributes {
Username : testUserSearchResultUsernameAttributeValue ,
Subject : subject ,
DN : testUserSearchResultDNValue ,
AuthTime : authTime ,
} )
2021-10-22 20:57:30 +00:00
if tt . wantErr != "" {
2021-10-25 21:25:43 +00:00
require . Error ( t , err )
2021-10-22 20:57:30 +00:00
require . Equal ( t , tt . wantErr , err . Error ( ) )
} else {
require . NoError ( t , err )
}
require . Equal ( t , true , dialWasAttempted )
} )
}
}
2021-04-15 21:44:43 +00:00
func TestTestConnection ( t * testing . T ) {
providerConfig := func ( editFunc func ( p * ProviderConfig ) ) * ProviderConfig {
config := & ProviderConfig {
2021-05-20 19:46:33 +00:00
Name : "some-provider-name" ,
Host : testHost ,
CABundle : nil , // this field is only used by the production dialer, which is replaced by a mock for this test
ConnectionProtocol : TLS ,
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
UserSearch : UserSearchConfig { } , // not used by TestConnection
2021-04-15 21:44:43 +00:00
}
if editFunc != nil {
editFunc ( config )
}
return config
}
tests := [ ] struct {
name string
providerConfig * ProviderConfig
setupMocks func ( conn * mockldapconn . MockConn )
dialError error
wantError string
wantToSkipDial bool
} {
{
name : "happy path" ,
providerConfig : providerConfig ( nil ) ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
} ,
{
name : "when dial fails" ,
providerConfig : providerConfig ( nil ) ,
dialError : errors . New ( "some dial error" ) ,
wantError : fmt . Sprintf ( ` error dialing host "%s": some dial error ` , testHost ) ,
} ,
{
name : "when binding as the bind user returns an error" ,
providerConfig : providerConfig ( nil ) ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Return ( errors . New ( "some bind error" ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` error binding as "%s": some bind error ` , testBindUsername ) ,
} ,
{
name : "when the config is invalid" ,
providerConfig : providerConfig ( func ( p * ProviderConfig ) {
// This particular combination of options is not allowed.
p . UserSearch . UsernameAttribute = "dn"
p . UserSearch . Filter = ""
} ) ,
wantToSkipDial : true ,
wantError : ` must specify UserSearch Filter when UserSearch UsernameAttribute is "dn" ` ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
ctrl := gomock . NewController ( t )
t . Cleanup ( ctrl . Finish )
conn := mockldapconn . NewMockConn ( ctrl )
if tt . setupMocks != nil {
tt . setupMocks ( conn )
}
dialWasAttempted := false
2021-05-25 19:46:50 +00:00
tt . providerConfig . Dialer = LDAPDialerFunc ( func ( ctx context . Context , addr endpointaddr . HostPort ) ( Conn , error ) {
2021-04-15 21:44:43 +00:00
dialWasAttempted = true
2021-05-25 19:46:50 +00:00
require . Equal ( t , tt . providerConfig . Host , addr . Endpoint ( ) )
2021-04-15 21:44:43 +00:00
if tt . dialError != nil {
return nil , tt . dialError
}
return conn , nil
} )
provider := New ( * tt . providerConfig )
err := provider . TestConnection ( context . Background ( ) )
require . Equal ( t , ! tt . wantToSkipDial , dialWasAttempted )
switch {
case tt . wantError != "" :
require . EqualError ( t , err , tt . wantError )
default :
require . NoError ( t , err )
}
} )
}
}
2021-04-15 17:25:35 +00:00
func TestGetConfig ( t * testing . T ) {
c := ProviderConfig {
Name : "original-provider-name" ,
Host : testHost ,
CABundle : [ ] byte ( "some-ca-bundle" ) ,
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
UserSearch : UserSearchConfig {
Base : testUserSearchBase ,
Filter : testUserSearchFilter ,
UsernameAttribute : testUserSearchUsernameAttribute ,
UIDAttribute : testUserSearchUIDAttribute ,
} ,
}
p := New ( c )
require . Equal ( t , c , p . c )
require . Equal ( t , c , p . GetConfig ( ) )
// The original config can be changed without impacting the provider, since the provider made a copy of the config.
c . Name = "changed-name"
require . Equal ( t , "original-provider-name" , p . c . Name )
// The return value of GetConfig can be modified without impacting the provider, since it is a copy of the config.
returnedConfig := p . GetConfig ( )
returnedConfig . Name = "changed-name"
require . Equal ( t , "original-provider-name" , p . c . Name )
}
2021-04-10 01:49:43 +00:00
func TestGetURL ( t * testing . T ) {
2021-05-27 00:04:20 +00:00
require . Equal ( t ,
"ldaps://ldap.example.com:1234?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev" ,
New ( ProviderConfig {
Host : "ldap.example.com:1234" ,
UserSearch : UserSearchConfig { Base : "ou=users,dc=pinniped,dc=dev" } ,
} ) . GetURL ( ) . String ( ) )
require . Equal ( t ,
"ldaps://ldap.example.com?base=ou%3Dusers%2Cdc%3Dpinniped%2Cdc%3Ddev" ,
New ( ProviderConfig {
Host : "ldap.example.com" ,
UserSearch : UserSearchConfig { Base : "ou=users,dc=pinniped,dc=dev" } ,
} ) . GetURL ( ) . String ( ) )
2021-04-10 01:49:43 +00:00
}
// Testing of host parsing, TLS negotiation, and CA bundle, etc. for the production code's dialer.
func TestRealTLSDialing ( t * testing . T ) {
2021-10-20 11:59:24 +00:00
testServer := tlsserver . TLSTestServer ( t , http . HandlerFunc ( func ( w http . ResponseWriter , r * http . Request ) { } ) ,
func ( server * httptest . Server ) {
tlsserver . RecordTLSHello ( server )
recordFunc := server . TLS . GetConfigForClient
server . TLS . GetConfigForClient = func ( info * tls . ClientHelloInfo ) ( * tls . Config , error ) {
_ , _ = recordFunc ( info )
r , err := http . NewRequestWithContext ( info . Context ( ) , http . MethodGet , "/this-is-ldap" , nil )
require . NoError ( t , err )
tlsserver . AssertTLS ( t , r , ptls . DefaultLDAP )
return nil , nil
}
} )
parsedURL , err := url . Parse ( testServer . URL )
2021-04-10 01:49:43 +00:00
require . NoError ( t , err )
testServerHostAndPort := parsedURL . Host
2021-10-20 11:59:24 +00:00
testServerCABundle := tlsserver . TLSTestServerCA ( testServer )
2021-04-10 01:49:43 +00:00
2021-05-21 19:44:01 +00:00
caForTestServerWithBadCertName , err := certauthority . New ( "Test CA" , time . Hour )
require . NoError ( t , err )
wrongIP := net . ParseIP ( "10.2.3.4" )
cert , err := caForTestServerWithBadCertName . IssueServerCert ( [ ] string { "wrong-dns-name" } , [ ] net . IP { wrongIP } , time . Hour )
require . NoError ( t , err )
testServerWithBadCertNameAddr := testutil . TLSTestServerWithCert ( t , func ( w http . ResponseWriter , r * http . Request ) { } , cert )
2021-04-10 01:49:43 +00:00
unusedPortGrabbingListener , err := net . Listen ( "tcp" , "127.0.0.1:0" )
require . NoError ( t , err )
recentlyClaimedHostAndPort := unusedPortGrabbingListener . Addr ( ) . String ( )
require . NoError ( t , unusedPortGrabbingListener . Close ( ) )
alreadyCancelledContext , cancelFunc := context . WithCancel ( context . Background ( ) )
cancelFunc ( ) // cancel it immediately
tests := [ ] struct {
name string
host string
2021-05-20 00:17:44 +00:00
connProto LDAPConnectionProtocol
2021-04-10 01:49:43 +00:00
caBundle [ ] byte
context context . Context
wantError string
} {
{
2021-05-20 00:17:44 +00:00
name : "happy path" ,
host : testServerHostAndPort ,
2021-10-20 11:59:24 +00:00
caBundle : testServerCABundle ,
2021-05-20 00:17:44 +00:00
connProto : TLS ,
context : context . Background ( ) ,
} ,
2021-05-21 19:44:01 +00:00
{
name : "server cert name does not match the address to which the client connected" ,
host : testServerWithBadCertNameAddr ,
caBundle : caForTestServerWithBadCertName . Bundle ( ) ,
connProto : TLS ,
context : context . Background ( ) ,
wantError : ` LDAP Result Code 200 "Network Error": x509: certificate is valid for 10.2.3.4, not 127.0.0.1 ` ,
} ,
2021-05-20 00:17:44 +00:00
{
name : "invalid CA bundle with TLS" ,
host : testServerHostAndPort ,
caBundle : [ ] byte ( "not a ca bundle" ) ,
connProto : TLS ,
context : context . Background ( ) ,
wantError : ` LDAP Result Code 200 "Network Error": could not parse CA bundle ` ,
2021-04-10 01:49:43 +00:00
} ,
{
2021-05-20 00:17:44 +00:00
name : "invalid CA bundle with StartTLS" ,
2021-04-10 01:49:43 +00:00
host : testServerHostAndPort ,
caBundle : [ ] byte ( "not a ca bundle" ) ,
2021-05-20 00:17:44 +00:00
connProto : StartTLS ,
2021-04-10 01:49:43 +00:00
context : context . Background ( ) ,
wantError : ` LDAP Result Code 200 "Network Error": could not parse CA bundle ` ,
} ,
2021-05-20 00:17:44 +00:00
{
name : "invalid host with TLS" ,
host : "this:is:not:a:valid:hostname" ,
2021-10-20 11:59:24 +00:00
caBundle : testServerCABundle ,
2021-05-20 00:17:44 +00:00
connProto : TLS ,
context : context . Background ( ) ,
2021-05-25 19:46:50 +00:00
wantError : ` LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address ` ,
2021-05-20 00:17:44 +00:00
} ,
{
name : "invalid host with StartTLS" ,
host : "this:is:not:a:valid:hostname" ,
2021-10-20 11:59:24 +00:00
caBundle : testServerCABundle ,
2021-05-20 00:17:44 +00:00
connProto : StartTLS ,
context : context . Background ( ) ,
2021-05-25 19:46:50 +00:00
wantError : ` LDAP Result Code 200 "Network Error": host "this:is:not:a:valid:hostname" is not a valid hostname or IP address ` ,
2021-05-20 00:17:44 +00:00
} ,
2021-04-10 01:49:43 +00:00
{
name : "missing CA bundle when it is required because the host is not using a trusted CA" ,
host : testServerHostAndPort ,
caBundle : nil ,
2021-05-20 00:17:44 +00:00
connProto : TLS ,
2021-04-10 01:49:43 +00:00
context : context . Background ( ) ,
wantError : ` LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority ` ,
} ,
{
name : "cannot connect to host" ,
// This is assuming that this port was not reclaimed by another app since the test setup ran. Seems safe enough.
host : recentlyClaimedHostAndPort ,
2021-10-20 11:59:24 +00:00
caBundle : testServerCABundle ,
2021-05-20 00:17:44 +00:00
connProto : TLS ,
2021-04-10 01:49:43 +00:00
context : context . Background ( ) ,
wantError : fmt . Sprintf ( ` LDAP Result Code 200 "Network Error": dial tcp %s: connect: connection refused ` , recentlyClaimedHostAndPort ) ,
} ,
{
name : "pays attention to the passed context" ,
host : testServerHostAndPort ,
2021-10-20 11:59:24 +00:00
caBundle : testServerCABundle ,
2021-05-20 00:17:44 +00:00
connProto : TLS ,
2021-04-10 01:49:43 +00:00
context : alreadyCancelledContext ,
wantError : fmt . Sprintf ( ` LDAP Result Code 200 "Network Error": dial tcp %s: operation was canceled ` , testServerHostAndPort ) ,
} ,
2021-05-20 00:17:44 +00:00
{
name : "unsupported connection protocol" ,
host : testServerHostAndPort ,
2021-10-20 11:59:24 +00:00
caBundle : testServerCABundle ,
2021-05-20 00:17:44 +00:00
connProto : "bad usage of this type" ,
context : alreadyCancelledContext ,
wantError : ` LDAP Result Code 200 "Network Error": did not specify valid ConnectionProtocol ` ,
} ,
2021-04-10 01:49:43 +00:00
}
for _ , test := range tests {
2021-05-20 00:17:44 +00:00
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
2021-04-15 17:25:35 +00:00
provider := New ( ProviderConfig {
2021-05-20 00:17:44 +00:00
Host : tt . host ,
CABundle : tt . caBundle ,
ConnectionProtocol : tt . connProto ,
Dialer : nil , // this test is for the default (production) TLS dialer
2021-04-15 17:25:35 +00:00
} )
2021-05-20 00:17:44 +00:00
conn , err := provider . dial ( tt . context )
2021-04-10 01:49:43 +00:00
if conn != nil {
defer conn . Close ( )
}
2021-05-20 00:17:44 +00:00
if tt . wantError != "" {
2021-04-10 01:49:43 +00:00
require . Nil ( t , conn )
2021-05-20 00:17:44 +00:00
require . EqualError ( t , err , tt . wantError )
2021-04-10 01:49:43 +00:00
} else {
require . NoError ( t , err )
require . NotNil ( t , conn )
// Should be an instance of the real production LDAP client type.
// Can't test its methods here because we are not dialed to a real LDAP server.
require . IsType ( t , & ldap . Conn { } , conn )
2021-04-12 18:23:08 +00:00
// Indirectly checking that the Dialer method constructed the ldap.Conn with isTLS set to true,
2021-04-10 01:49:43 +00:00
// since this is always the correct behavior unless/until we want to support StartTLS.
2021-10-20 11:59:24 +00:00
err := conn . ( * ldap . Conn ) . StartTLS ( ptls . DefaultLDAP ( nil ) )
2021-04-10 01:49:43 +00:00
require . EqualError ( t , err , ` LDAP Result Code 200 "Network Error": ldap: already encrypted ` )
}
} )
}
}
2021-07-27 18:08:23 +00:00
func TestGetMicrosoftFormattedUUID ( t * testing . T ) {
tests := [ ] struct {
name string
binaryUUID [ ] byte
wantString string
wantErr string
} {
{
name : "happy path" ,
binaryUUID : [ ] byte ( "\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11\x12\x13\x14\x15\x16" ) ,
wantString : "04030201-0605-0807-0910-111213141516" ,
} ,
{
name : "not the right length" ,
binaryUUID : [ ] byte ( "2\xf8\xb0\xaa\xb6V\xb1D\x8b(\xee" ) ,
wantErr : "invalid UUID (got 11 bytes)" ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
2021-08-17 23:53:26 +00:00
actualUUIDString , err := microsoftUUIDFromBinary ( tt . binaryUUID )
2021-07-27 18:08:23 +00:00
if tt . wantErr != "" {
require . EqualError ( t , err , tt . wantErr )
} else {
require . NoError ( t , err )
}
require . Equal ( t , tt . wantString , actualUUIDString )
} )
}
}
2021-08-17 23:53:26 +00:00
func TestGetDomainFromDistinguishedName ( t * testing . T ) {
tests := [ ] struct {
name string
distinguishedName string
wantDomain string
wantErr string
} {
{
name : "happy path" ,
distinguishedName : "CN=Mammals,OU=Users,OU=pinniped-ad,DC=activedirectory,DC=mycompany,DC=example,DC=com" ,
wantDomain : "activedirectory.mycompany.example.com" ,
} ,
{
name : "lowercased happy path" ,
distinguishedName : "cn=Mammals,ou=Users,ou=pinniped-ad,dc=activedirectory,dc=mycompany,dc=example,dc=com" ,
wantDomain : "activedirectory.mycompany.example.com" ,
} ,
{
name : "no domain components" ,
distinguishedName : "not-a-dn" ,
wantErr : "did not find domain components in group dn: not-a-dn" ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
actualDomain , err := getDomainFromDistinguishedName ( tt . distinguishedName )
if tt . wantErr != "" {
require . EqualError ( t , err , tt . wantErr )
} else {
require . NoError ( t , err )
}
require . Equal ( t , tt . wantDomain , actualDomain )
} )
}
}
2021-10-28 19:00:56 +00:00
func TestPwdUnchangedSinceLogin ( t * testing . T ) {
authTime := "2021-11-01T23:43:19.826433579Z" // this is the format that fosite automatically stores
authTimeParsed , err := time . Parse ( time . RFC3339Nano , authTime )
require . NoError ( t , err )
pwdResetTimeAfterAuthTime := "132803468800000000" // Nov 2
pwdResetTimeBeforeAuthTime := "132801740800000000" // Oct 31
tests := [ ] struct {
name string
authTime * time . Time
entry * ldap . Entry
wantResult bool
wantErr string
} {
{
name : "happy path where password has not been reset since login" ,
authTime : & authTimeParsed ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "pwdLastSet" ,
Values : [ ] string { pwdResetTimeBeforeAuthTime } ,
} ,
} ,
} ,
} ,
{
name : "password has been reset since login" ,
authTime : & authTimeParsed ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "pwdLastSet" ,
Values : [ ] string { pwdResetTimeAfterAuthTime } ,
} ,
} ,
} ,
wantErr : "password has changed since login. login time: 2021-11-01 23:43:19.826433579 +0000 UTC, password set time: 2021-11-02 17:14:40 +0000 UTC" ,
} ,
{
name : "ldap timestamp is in the wrong format" ,
authTime : & authTimeParsed ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "pwdLastSet" ,
Values : [ ] string { "invalid" } ,
} ,
} ,
} ,
wantErr : "couldn't parse as timestamp" ,
} ,
{
name : "no value for pwdLastSet attribute" ,
authTime : & authTimeParsed ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute { } ,
} ,
wantErr : "expected to find 1 value for pwdLastSet attribute, but found 0" ,
} ,
{
name : "too many values for pwdLastSet attribute" ,
authTime : & authTimeParsed ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "pwdLastSet" ,
Values : [ ] string { "val1" , "val2" } ,
} ,
} ,
} ,
wantErr : "expected to find 1 value for pwdLastSet attribute, but found 2" ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
err := PwdUnchangedSinceLogin ( tt . entry , provider2 . StoredRefreshAttributes { AuthTime : * tt . authTime } )
if tt . wantErr != "" {
require . Error ( t , err )
require . Equal ( t , tt . wantErr , err . Error ( ) )
} else {
require . NoError ( t , err )
}
} )
}
}
func TestWin32TimestampToTime ( t * testing . T ) {
happyPasswordChangeTime := time . Date ( 2021 , time . January , 2 , 0 , 12 , 21 , 0 , time . UTC ) . UTC ( )
tests := [ ] struct {
name string
timestampString string
wantTime * time . Time
wantErr string
} {
{
name : "happy case with a valid timestamp" ,
timestampString : "132540199410000000" ,
wantTime : & happyPasswordChangeTime ,
} ,
{
name : "handles error with a string thats not a timestamp" ,
timestampString : "not timestamp" ,
wantErr : "couldn't parse as timestamp" ,
} ,
{
name : "handles error with too big of a timestamp" ,
timestampString : "132540199410000000000" ,
wantErr : "couldn't parse as timestamp" ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
actualTime , err := win32timestampToTime ( tt . timestampString )
require . Equal ( t , tt . wantTime , actualTime )
if tt . wantErr != "" {
require . Error ( t , err )
require . Equal ( t , tt . wantErr , err . Error ( ) )
} else {
require . NoError ( t , err )
}
} )
}
}
2021-11-05 18:53:07 +00:00
func TestValidUserAccountControl ( t * testing . T ) {
tests := [ ] struct {
name string
entry * ldap . Entry
wantErr string
} {
{
name : "happy normal user" ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "userAccountControl" ,
Values : [ ] string { "512" } ,
} ,
} ,
} ,
} ,
{
name : "happy user whose password doesn't expire" ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "userAccountControl" ,
Values : [ ] string { "65536" } ,
} ,
} ,
} ,
} ,
{
name : "deactivated user" ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "userAccountControl" ,
Values : [ ] string { "514" } ,
} ,
} ,
} ,
wantErr : "user has been deactivated" ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
err := ValidUserAccountControl ( tt . entry , provider2 . StoredRefreshAttributes { } )
if tt . wantErr != "" {
require . Error ( t , err )
require . Equal ( t , tt . wantErr , err . Error ( ) )
} else {
require . NoError ( t , err )
}
} )
}
}
2021-11-17 00:31:32 +00:00
func TestValidComputedUserAccountControl ( t * testing . T ) {
tests := [ ] struct {
name string
entry * ldap . Entry
wantErr string
} {
{
name : "happy normal user" ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "msDS-User-Account-Control-Computed" ,
Values : [ ] string { "0" } ,
} ,
} ,
} ,
} ,
{
name : "locked user" ,
entry : & ldap . Entry {
DN : "some-dn" ,
Attributes : [ ] * ldap . EntryAttribute {
{
Name : "msDS-User-Account-Control-Computed" ,
Values : [ ] string { "16" } ,
} ,
} ,
} ,
wantErr : "user has been locked" ,
} ,
}
for _ , test := range tests {
tt := test
t . Run ( tt . name , func ( t * testing . T ) {
err := ValidComputedUserAccountControl ( tt . entry , provider2 . StoredRefreshAttributes { } )
if tt . wantErr != "" {
require . Error ( t , err )
require . Equal ( t , tt . wantErr , err . Error ( ) )
} else {
require . NoError ( t , err )
}
} )
}
}