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-04-13 00:50:25 +00:00
"errors"
2021-04-10 01:49:43 +00:00
"fmt"
"net"
"net/http"
"net/url"
2021-04-09 15:38:53 +00:00
"testing"
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/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"go.pinniped.dev/internal/mocks/mockldapconn"
2021-04-10 01:49:43 +00:00
"go.pinniped.dev/internal/testutil"
2021-04-09 15:38:53 +00:00
)
2021-04-13 00:50:25 +00:00
const (
testHost = "ldap.example.com:8443"
2021-04-13 22:23:14 +00:00
testBindUsername = "cn=some-bind-username,dc=pinniped,dc=dev"
2021-04-13 00:50:25 +00:00
testBindPassword = "some-bind-password"
testUpstreamUsername = "some-upstream-username"
testUpstreamPassword = "some-upstream-password"
testUserSearchBase = "some-upstream-base-dn"
testUserSearchFilter = "some-filter={}-and-more-filter={}"
testUserSearchUsernameAttribute = "some-upstream-username-attribute"
testUserSearchUIDAttribute = "some-upstream-uid-attribute"
testSearchResultDNValue = "some-upstream-user-dn"
testSearchResultUsernameAttributeValue = "some-upstream-username-value"
testSearchResultUIDAttributeValue = "some-upstream-uid-value"
)
2021-04-09 15:38:53 +00:00
var (
2021-04-13 22:23:14 +00:00
testUserSearchFilterInterpolated = fmt . Sprintf ( "(some-filter=%s-and-more-filter=%s)" , testUpstreamUsername , testUpstreamUsername )
2021-04-09 15:38:53 +00:00
)
func TestAuthenticateUser ( t * testing . T ) {
2021-04-15 17:25:35 +00:00
providerConfig := func ( editFunc func ( p * ProviderConfig ) ) * ProviderConfig {
config := & ProviderConfig {
2021-04-14 00:16:57 +00:00
Name : "some-provider-name" ,
2021-04-13 00:50:25 +00:00
Host : testHost ,
2021-04-14 00:16:57 +00:00
CABundle : nil , // this field is only used by the production dialer, which is replaced by a mock for this test
2021-04-13 00:50:25 +00:00
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 ,
} ,
}
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
}
expectedSearch := func ( editFunc func ( r * ldap . SearchRequest ) ) * ldap . SearchRequest {
request := & ldap . SearchRequest {
BaseDN : testUserSearchBase ,
Scope : ldap . ScopeWholeSubtree ,
DerefAliases : ldap . DerefAlways ,
SizeLimit : 2 ,
TimeLimit : 90 ,
TypesOnly : false ,
Filter : testUserSearchFilterInterpolated ,
Attributes : [ ] string { testUserSearchUsernameAttribute , testUserSearchUIDAttribute } ,
Controls : nil ,
}
if editFunc != nil {
editFunc ( request )
}
return request
}
2021-04-09 15:38:53 +00:00
tests := [ ] struct {
2021-04-13 23:22:13 +00:00
name string
username string
password string
2021-04-15 17:25:35 +00:00
providerConfig * ProviderConfig
2021-04-13 23:22:13 +00:00
setupMocks func ( conn * mockldapconn . MockConn )
dialError error
wantError string
wantToSkipDial bool
wantAuthResponse * authenticator . Response
wantUnauthenticated bool
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-13 00:50:25 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
Referrals : [ ] string { } , // note that we are not following referrals at this time
Controls : [ ] ldap . Control { } , // TODO are there any response controls that we need to be able to handle?
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantAuthResponse : & authenticator . Response {
User : & user . DefaultInfo {
Name : testSearchResultUsernameAttributeValue ,
UID : testSearchResultUIDAttributeValue ,
2021-04-13 22:23:14 +00:00
Groups : [ ] string { } ,
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 + ")"
} ) ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantAuthResponse : & authenticator . Response {
User : & user . DefaultInfo {
Name : testSearchResultUsernameAttributeValue ,
UID : testSearchResultUIDAttributeValue ,
Groups : [ ] string { } ,
} ,
} ,
} ,
{
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"
} ) ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { testUserSearchUIDAttribute }
} ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantAuthResponse : & authenticator . Response {
User : & user . DefaultInfo {
Name : testSearchResultDNValue ,
UID : testSearchResultUIDAttributeValue ,
2021-04-13 22:23:14 +00:00
Groups : [ ] string { } ,
2021-04-09 15:38:53 +00:00
} ,
} ,
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"
} ) ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( func ( r * ldap . SearchRequest ) {
r . Attributes = [ ] string { testUserSearchUsernameAttribute }
} ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
2021-04-09 15:38:53 +00:00
wantAuthResponse : & authenticator . Response {
User : & user . DefaultInfo {
2021-04-13 00:50:25 +00:00
Name : testSearchResultUsernameAttributeValue ,
UID : testSearchResultDNValue ,
2021-04-13 22:23:14 +00:00
Groups : [ ] string { } ,
2021-04-09 15:38:53 +00:00
} ,
} ,
} ,
2021-04-13 00:50:25 +00:00
{
name : "when Filter is blank it derives a search filter from the UsernameAttribute" ,
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 . Filter = ""
} ) ,
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( func ( r * ldap . SearchRequest ) {
2021-04-13 22:23:14 +00:00
r . Filter = "(" + testUserSearchUsernameAttribute + "=" + testUpstreamUsername + ")"
2021-04-13 00:50:25 +00:00
} ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantAuthResponse : & authenticator . Response {
User : & user . DefaultInfo {
Name : testSearchResultUsernameAttributeValue ,
UID : testSearchResultUIDAttributeValue ,
2021-04-13 22:23:14 +00:00
Groups : [ ] string { } ,
2021-04-13 00:50:25 +00:00
} ,
} ,
} ,
{
2021-04-15 17:25:35 +00:00
name : "when the username has special LDAP search filter characters then they must be properly escaped in the search filter" ,
username : ` a&b|c(d)e\f*g ` ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-13 00:50:25 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( func ( r * ldap . SearchRequest ) {
2021-04-13 22:23:14 +00:00
r . Filter = fmt . Sprintf ( "(some-filter=%s-and-more-filter=%s)" , ` a&b|c\28d\29e\5cf\2ag ` , ` a&b|c\28d\29e\5cf\2ag ` )
2021-04-13 00:50:25 +00:00
} ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantAuthResponse : & authenticator . Response {
User : & user . DefaultInfo {
Name : testSearchResultUsernameAttributeValue ,
UID : testSearchResultUIDAttributeValue ,
2021-04-13 22:23:14 +00:00
Groups : [ ] string { } ,
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-13 15:38:04 +00:00
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" 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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( nil , errors . New ( "some search error" ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` error searching for user "%s": some search error ` , testUpstreamUsername ) ,
} ,
{
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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{ DN : testSearchResultDNValue } ,
{ 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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
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-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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , 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 ` , testUserSearchUsernameAttribute , 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 username attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string {
testSearchResultUsernameAttributeValue ,
"unexpected-additional-value" ,
} ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , 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 ` , testUserSearchUsernameAttribute , 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 username attribute" ,
username : testUpstreamUsername ,
password : testUpstreamPassword ,
providerConfig : providerConfig ( nil ) ,
2021-04-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { "" } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , 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 ` , testUserSearchUsernameAttribute , testUpstreamUsername ) ,
} ,
{
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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
} ,
} ,
} ,
} , 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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string {
testSearchResultUIDAttributeValue ,
"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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
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-13 15:38:04 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Return ( errors . New ( "some bind error" ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantError : fmt . Sprintf ( ` error binding for user "%s" using provided password against DN "%s": some bind error ` , testUpstreamUsername , testSearchResultDNValue ) ,
} ,
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-13 23:22:13 +00:00
setupMocks : func ( conn * mockldapconn . MockConn ) {
conn . EXPECT ( ) . Bind ( testBindUsername , testBindPassword ) . Times ( 1 )
conn . EXPECT ( ) . Search ( expectedSearch ( nil ) ) . Return ( & ldap . SearchResult {
Entries : [ ] * ldap . Entry {
{
DN : testSearchResultDNValue ,
Attributes : [ ] * ldap . EntryAttribute {
ldap . NewEntryAttribute ( testUserSearchUsernameAttribute , [ ] string { testSearchResultUsernameAttributeValue } ) ,
ldap . NewEntryAttribute ( testUserSearchUIDAttribute , [ ] string { testSearchResultUIDAttributeValue } ) ,
} ,
} ,
} ,
} , nil ) . Times ( 1 )
conn . EXPECT ( ) . Bind ( testSearchResultDNValue , testUpstreamPassword ) . Return ( errors . New ( ` LDAP Result Code 49 "Invalid Credentials": some bind error ` ) ) . Times ( 1 )
conn . EXPECT ( ) . Close ( ) . Times ( 1 )
} ,
wantUnauthenticated : true ,
} ,
{
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-13 00:50:25 +00:00
if tt . setupMocks != nil {
tt . setupMocks ( conn )
}
2021-04-09 15:38:53 +00:00
2021-04-10 01:49:43 +00:00
dialWasAttempted := false
2021-04-15 17:25:35 +00:00
tt . providerConfig . Dialer = LDAPDialerFunc ( func ( ctx context . Context , hostAndPort string ) ( Conn , error ) {
2021-04-10 01:49:43 +00:00
dialWasAttempted = true
2021-04-15 17:25:35 +00:00
require . Equal ( t , tt . providerConfig . Host , hostAndPort )
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 )
authResponse , authenticated , err := provider . AuthenticateUser ( context . Background ( ) , tt . username , tt . password )
2021-04-13 00:50:25 +00:00
2021-04-13 22:23:14 +00:00
require . Equal ( t , ! tt . wantToSkipDial , dialWasAttempted )
2021-04-13 00:50:25 +00:00
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-04-15 21:44:43 +00:00
func TestTestConnection ( t * testing . T ) {
providerConfig := func ( editFunc func ( p * ProviderConfig ) ) * ProviderConfig {
config := & 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
BindUsername : testBindUsername ,
BindPassword : testBindPassword ,
UserSearch : UserSearchConfig { } , // not used by TestConnection
}
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
tt . providerConfig . Dialer = LDAPDialerFunc ( func ( ctx context . Context , hostAndPort string ) ( Conn , error ) {
dialWasAttempted = true
require . Equal ( t , tt . providerConfig . Host , hostAndPort )
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-04-15 17:25:35 +00:00
require . Equal ( t , "ldaps://ldap.example.com:1234" , New ( ProviderConfig { Host : "ldap.example.com:1234" } ) . GetURL ( ) )
require . Equal ( t , "ldaps://ldap.example.com" , New ( ProviderConfig { Host : "ldap.example.com" } ) . GetURL ( ) )
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 ) {
testServerCABundle , testServerURL := testutil . TLSTestServer ( t , func ( w http . ResponseWriter , r * http . Request ) { } )
parsedURL , err := url . Parse ( testServerURL )
require . NoError ( t , err )
testServerHostAndPort := parsedURL . Host
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
caBundle [ ] byte
context context . Context
wantError string
} {
{
name : "happy path" ,
host : testServerHostAndPort ,
caBundle : [ ] byte ( testServerCABundle ) ,
context : context . Background ( ) ,
} ,
{
name : "invalid CA bundle" ,
host : testServerHostAndPort ,
caBundle : [ ] byte ( "not a ca bundle" ) ,
context : context . Background ( ) ,
wantError : ` LDAP Result Code 200 "Network Error": could not parse CA bundle ` ,
} ,
{
name : "missing CA bundle when it is required because the host is not using a trusted CA" ,
host : testServerHostAndPort ,
caBundle : nil ,
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 ,
caBundle : [ ] byte ( testServerCABundle ) ,
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 ,
caBundle : [ ] byte ( testServerCABundle ) ,
context : alreadyCancelledContext ,
wantError : fmt . Sprintf ( ` LDAP Result Code 200 "Network Error": dial tcp %s: operation was canceled ` , testServerHostAndPort ) ,
} ,
}
for _ , test := range tests {
test := test
t . Run ( test . name , func ( t * testing . T ) {
2021-04-15 17:25:35 +00:00
provider := New ( ProviderConfig {
2021-04-10 01:49:43 +00:00
Host : test . host ,
CABundle : test . caBundle ,
2021-04-12 18:23:08 +00:00
Dialer : nil , // this test is for the default (production) dialer
2021-04-15 17:25:35 +00:00
} )
2021-04-10 01:49:43 +00:00
conn , err := provider . dial ( test . context )
if conn != nil {
defer conn . Close ( )
}
if test . wantError != "" {
require . Nil ( t , conn )
require . EqualError ( t , err , test . wantError )
} 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.
err := conn . ( * ldap . Conn ) . StartTLS ( & tls . Config { } )
require . EqualError ( t , err , ` LDAP Result Code 200 "Network Error": ldap: already encrypted ` )
}
} )
}
}
// Test various cases of host and port parsing.
func TestHostAndPortWithDefaultPort ( t * testing . T ) {
tests := [ ] struct {
name string
hostAndPort string
defaultPort string
wantError string
wantHostAndPort string
} {
{
name : "host already has port" ,
hostAndPort : "host.example.com:99" ,
defaultPort : "42" ,
wantHostAndPort : "host.example.com:99" ,
} ,
{
name : "host does not have port" ,
hostAndPort : "host.example.com" ,
defaultPort : "42" ,
wantHostAndPort : "host.example.com:42" ,
} ,
{
name : "host does not have port and default port is empty" ,
hostAndPort : "host.example.com" ,
defaultPort : "" ,
wantHostAndPort : "host.example.com" ,
} ,
{
name : "IPv6 host already has port" ,
hostAndPort : "[::1%lo0]:80" ,
defaultPort : "42" ,
wantHostAndPort : "[::1%lo0]:80" ,
} ,
{
name : "IPv6 host does not have port" ,
hostAndPort : "[::1%lo0]" ,
defaultPort : "42" ,
wantHostAndPort : "[::1%lo0]:42" ,
} ,
{
name : "IPv6 host does not have port and default port is empty" ,
hostAndPort : "[::1%lo0]" ,
defaultPort : "" ,
wantHostAndPort : "[::1%lo0]" ,
} ,
{
name : "host is not valid" ,
hostAndPort : "host.example.com:port1:port2" ,
defaultPort : "42" ,
wantError : "address host.example.com:port1:port2: too many colons in address" ,
} ,
}
for _ , test := range tests {
test := test
t . Run ( test . name , func ( t * testing . T ) {
hostAndPort , err := hostAndPortWithDefaultPort ( test . hostAndPort , test . defaultPort )
if test . wantError != "" {
require . EqualError ( t , err , test . wantError )
} else {
require . NoError ( t , err )
}
require . Equal ( t , test . wantHostAndPort , hostAndPort )
} )
}
}