Merge branch 'main' into ldap-login-ui

This commit is contained in:
Ryan Richard 2022-05-11 11:32:15 -07:00
commit 0f2a984308
4 changed files with 221 additions and 188 deletions

View File

@ -36,9 +36,8 @@ The following table includes the current roadmap for Pinniped. If you have any q
Last Updated: March 2022 Last Updated: March 2022
|Theme|Description|Timeline| |Theme|Description|Timeline|
|--|--|--| |--|--|--|
|Improving Security Posture|Support FIPS compliant Boring crypto libraries |March/April 2022| |Improving Security Posture|Support Audit logging of security events related to Authentication |May/June 2022|
|Improving Security Posture|Support Audit logging of security events related to Authentication |April/May 2022| |Improving Usability|Support for integrating with UI/Dashboards |May/June 2022|
|Improving Usability|Support for integrating with UI/Dashboards |June/July 2022|
|Improving Security Posture|TLS hardening contd|June/July 2022| |Improving Security Posture|TLS hardening contd|June/July 2022|
|Multiple IDP support|Support multiple IDPs configured on a single Supervisor|Exploring/Ongoing| |Multiple IDP support|Support multiple IDPs configured on a single Supervisor|Exploring/Ongoing|
|Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing| |Improving Security Posture|mTLS for Supervisor sessions |Exploring/Ongoing|

View File

@ -51,6 +51,8 @@ const (
testUserSearchResultUIDAttributeValue = "some-upstream-uid-value" testUserSearchResultUIDAttributeValue = "some-upstream-uid-value"
testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1" testGroupSearchResultGroupNameAttributeValue1 = "some-upstream-group-name-value1"
testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2" testGroupSearchResultGroupNameAttributeValue2 = "some-upstream-group-name-value2"
testUserDNWithSpecialChars = `user DN with * \ special characters ()`
testUserDNWithSpecialCharsEscaped = `user DN with \2a \5c special characters \28\29`
expectedGroupSearchPageSize = uint32(250) expectedGroupSearchPageSize = uint32(250)
) )
@ -529,7 +531,7 @@ func TestEndUserAuthentication(t *testing.T) {
Return(&ldap.SearchResult{ Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: `result DN with * \ special characters ()`, DN: testUserDNWithSpecialChars,
Attributes: []*ldap.EntryAttribute{ Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
@ -538,16 +540,16 @@ func TestEndUserAuthentication(t *testing.T) {
}, },
}, nil).Times(1) }, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
escapedDN := `result DN with \2a \5c special characters \28\29` escapedDN := testUserDNWithSpecialCharsEscaped
r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", escapedDN, escapedDN) r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", escapedDN, escapedDN)
}), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1) }), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
bindEndUserMocks: func(conn *mockldapconn.MockConn) { bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1) conn.EXPECT().Bind(testUserDNWithSpecialChars, testUpstreamPassword).Times(1)
}, },
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) { wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
r.DN = `result DN with * \ special characters ()` r.DN = testUserDNWithSpecialChars
}), }),
}, },
{ {
@ -563,7 +565,7 @@ func TestEndUserAuthentication(t *testing.T) {
Return(&ldap.SearchResult{ Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: `result DN with * \ special characters ()`, DN: testUserDNWithSpecialChars,
Attributes: []*ldap.EntryAttribute{ Attributes: []*ldap.EntryAttribute{
ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}), ldap.NewEntryAttribute(testUserSearchUsernameAttribute, []string{testUserSearchResultUsernameAttributeValue}),
ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}), ldap.NewEntryAttribute(testUserSearchUIDAttribute, []string{testUserSearchResultUIDAttributeValue}),
@ -572,15 +574,15 @@ func TestEndUserAuthentication(t *testing.T) {
}, },
}, nil).Times(1) }, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) { conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
r.Filter = fmt.Sprintf("(member=%s)", `result DN with \2a \5c special characters \28\29`) r.Filter = fmt.Sprintf("(member=%s)", testUserDNWithSpecialCharsEscaped)
}), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1) }), expectedGroupSearchPageSize).Return(exampleGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
bindEndUserMocks: func(conn *mockldapconn.MockConn) { bindEndUserMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(`result DN with * \ special characters ()`, testUpstreamPassword).Times(1) conn.EXPECT().Bind(testUserDNWithSpecialChars, testUpstreamPassword).Times(1)
}, },
wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) { wantAuthResponse: expectedAuthResponse(func(r *authenticators.Response) {
r.DN = `result DN with * \ special characters ()` r.DN = testUserDNWithSpecialChars
}), }),
}, },
{ {
@ -1219,28 +1221,41 @@ func TestEndUserAuthentication(t *testing.T) {
func TestUpstreamRefresh(t *testing.T) { func TestUpstreamRefresh(t *testing.T) {
pwdLastSetAttribute := "pwdLastSet" pwdLastSetAttribute := "pwdLastSet"
expectedUserSearch := &ldap.SearchRequest{
BaseDN: testUserSearchResultDNValue, expectedUserSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest {
Scope: ldap.ScopeBaseObject, request := &ldap.SearchRequest{
DerefAliases: ldap.NeverDerefAliases, BaseDN: testUserSearchResultDNValue,
SizeLimit: 2, Scope: ldap.ScopeBaseObject,
TimeLimit: 90, DerefAliases: ldap.NeverDerefAliases,
TypesOnly: false, SizeLimit: 2,
Filter: "(objectClass=*)", TimeLimit: 90,
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute}, TypesOnly: false,
Controls: nil, // don't need paging because we set the SizeLimit so small Filter: "(objectClass=*)",
Attributes: []string{testUserSearchUsernameAttribute, testUserSearchUIDAttribute, pwdLastSetAttribute},
Controls: nil, // don't need paging because we set the SizeLimit so small
}
if editFunc != nil {
editFunc(request)
}
return request
} }
expectedGroupSearch := &ldap.SearchRequest{ expectedGroupSearch := func(editFunc func(r *ldap.SearchRequest)) *ldap.SearchRequest {
BaseDN: testGroupSearchBase, request := &ldap.SearchRequest{
Scope: ldap.ScopeWholeSubtree, BaseDN: testGroupSearchBase,
DerefAliases: ldap.NeverDerefAliases, Scope: ldap.ScopeWholeSubtree,
SizeLimit: 0, // unlimited size because we will search with paging DerefAliases: ldap.NeverDerefAliases,
TimeLimit: 90, SizeLimit: 0, // unlimited size because we will search with paging
TypesOnly: false, TimeLimit: 90,
Filter: testGroupSearchFilterInterpolated, TypesOnly: false,
Attributes: []string{testGroupSearchGroupNameAttribute}, Filter: testGroupSearchFilterInterpolated,
Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us Attributes: []string{testGroupSearchGroupNameAttribute},
Controls: nil, // nil because ldap.SearchWithPaging() will set the appropriate controls for us
}
if editFunc != nil {
editFunc(request)
}
return request
} }
happyPathUserSearchResult := &ldap.SearchResult{ happyPathUserSearchResult := &ldap.SearchResult{
@ -1266,116 +1281,170 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
} }
providerConfig := &ProviderConfig{ happyPathGroupSearchResult := &ldap.SearchResult{
Name: "some-provider-name", Entries: []*ldap.Entry{
Host: testHost, {
CABundle: nil, // this field is only used by the production dialer, which is replaced by a mock for this test DN: testGroupSearchResultDNValue1,
ConnectionProtocol: TLS, Attributes: []*ldap.EntryAttribute{
BindUsername: testBindUsername, ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue1}),
BindPassword: testBindPassword, },
UserSearch: UserSearchConfig{ },
Base: testUserSearchBase, {
UIDAttribute: testUserSearchUIDAttribute, DN: testGroupSearchResultDNValue2,
UsernameAttribute: testUserSearchUsernameAttribute, Attributes: []*ldap.EntryAttribute{
}, ldap.NewEntryAttribute(testGroupSearchGroupNameAttribute, []string{testGroupSearchResultGroupNameAttributeValue2}),
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{ },
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute), },
}, },
Referrals: []string{}, // note that we are not following referrals at this time
Controls: []ldap.Control{},
}
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
ConnectionProtocol: TLS,
BindUsername: testBindUsername,
BindPassword: testBindPassword,
UserSearch: UserSearchConfig{
Base: testUserSearchBase,
UIDAttribute: testUserSearchUIDAttribute,
UsernameAttribute: testUserSearchUsernameAttribute,
},
GroupSearch: GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupSearchGroupNameAttribute,
},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
},
}
if editFunc != nil {
editFunc(config)
}
return config
} }
tests := []struct { tests := []struct {
name string name string
providerConfig *ProviderConfig providerConfig *ProviderConfig
setupMocks func(conn *mockldapconn.MockConn) setupMocks func(conn *mockldapconn.MockConn)
refreshUserDN string
dialError error dialError error
wantErr string wantErr string
wantGroups []string wantGroups []string
}{ }{
{ {
name: "happy path where searching the dn returns a single entry", name: "happy path without group search where searching the dn returns a single entry",
providerConfig: providerConfig, providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch = GroupSearchConfig{}
}),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantGroups: []string{}, wantGroups: []string{},
}, },
{ {
name: "happy path where group search returns groups", name: "happy path where group search returns groups",
providerConfig: &ProviderConfig{ providerConfig: providerConfig(nil),
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{
Base: testUserSearchBase,
UIDAttribute: testUserSearchUIDAttribute,
UsernameAttribute: testUserSearchUsernameAttribute,
},
GroupSearch: GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupSearchGroupNameAttribute,
},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
},
},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(&ldap.SearchResult{ conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1)
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{},
}, nil).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
}, },
{ {
name: "happy path where group search returns no groups", name: "happy path when the user DN has special LDAP search filter characters then they must be properly escaped in the custom group search filter",
providerConfig: &ProviderConfig{ providerConfig: providerConfig(nil),
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{
Base: testUserSearchBase,
UIDAttribute: testUserSearchUIDAttribute,
UsernameAttribute: testUserSearchUsernameAttribute,
},
GroupSearch: GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupSearchGroupNameAttribute,
},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
},
},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(&ldap.SearchResult{ r.BaseDN = testUserDNWithSpecialChars
})).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserDNWithSpecialChars,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
ByteValues: [][]byte{[]byte("132801740800000000")},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
r.Filter = fmt.Sprintf("(some-group-filter=%s-and-more-filter=%s)", testUserDNWithSpecialCharsEscaped, testUserDNWithSpecialCharsEscaped)
}), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
refreshUserDN: testUserDNWithSpecialChars,
wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
},
{
name: "when the user DN has special LDAP search filter characters then they must be properly escaped in the default group search filter",
providerConfig: providerConfig(func(p *ProviderConfig) {
p.GroupSearch.Filter = ""
}),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(func(r *ldap.SearchRequest) {
r.BaseDN = testUserDNWithSpecialChars
})).
Return(&ldap.SearchResult{
Entries: []*ldap.Entry{
{
DN: testUserDNWithSpecialChars,
Attributes: []*ldap.EntryAttribute{
{
Name: testUserSearchUsernameAttribute,
Values: []string{testUserSearchResultUsernameAttributeValue},
},
{
Name: testUserSearchUIDAttribute,
ByteValues: [][]byte{[]byte(testUserSearchResultUIDAttributeValue)},
},
{
Name: pwdLastSetAttribute,
Values: []string{"132801740800000000"},
ByteValues: [][]byte{[]byte("132801740800000000")},
},
},
},
},
}, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(func(r *ldap.SearchRequest) {
r.Filter = fmt.Sprintf("(member=%s)", testUserDNWithSpecialCharsEscaped)
}), expectedGroupSearchPageSize).Return(happyPathGroupSearchResult, nil).Times(1)
conn.EXPECT().Close().Times(1)
},
refreshUserDN: testUserDNWithSpecialChars,
wantGroups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
},
{
name: "happy path where group search returns no groups",
providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{}, Entries: []*ldap.Entry{},
Referrals: []string{}, // note that we are not following referrals at this time Referrals: []string{}, // note that we are not following referrals at this time
Controls: []ldap.Control{}, Controls: []ldap.Control{},
@ -1386,44 +1455,25 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "happy path where group search is configured but skipGroupRefresh is set", name: "happy path where group search is configured but skipGroupRefresh is set",
providerConfig: &ProviderConfig{ providerConfig: providerConfig(func(p *ProviderConfig) {
Name: "some-provider-name", p.GroupSearch.SkipGroupRefresh = true
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{
Base: testUserSearchBase,
UIDAttribute: testUserSearchUIDAttribute,
UsernameAttribute: testUserSearchUsernameAttribute,
},
GroupSearch: GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupSearchGroupNameAttribute,
SkipGroupRefresh: true,
},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
},
},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) // note that group search is not expected conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1) // note that group search is not expected
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantGroups: nil, // do not update groups wantGroups: nil, // do not update groups
}, },
{ {
name: "error where dial fails", name: "error where dial fails",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
dialError: errors.New("some dial error"), dialError: errors.New("some dial error"),
wantErr: "error dialing host \"ldap.example.com:8443\": some dial error", wantErr: "error dialing host \"ldap.example.com:8443\": some dial error",
}, },
{ {
name: "error binding", name: "error binding",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Return(errors.New("some bind error")).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
@ -1432,10 +1482,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result returns no entries", name: "search result returns no entries",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{}, Entries: []*ldap.Entry{},
}, nil).Times(1) }, nil).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
@ -1444,20 +1494,20 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "error searching", name: "error searching",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(nil, errors.New("some search error")) conn.EXPECT().Search(expectedUserSearch(nil)).Return(nil, errors.New("some search error"))
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantErr: "error searching for user \"some-upstream-user-dn\": some search error", wantErr: "error searching for user \"some-upstream-user-dn\": some search error",
}, },
{ {
name: "search result returns more than one entry", name: "search result returns more than one entry",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1475,10 +1525,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has wrong uid", name: "search result has wrong uid",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1501,10 +1551,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has wrong username", name: "search result has wrong username",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1523,10 +1573,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has no dn", name: "search result has no dn",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
Attributes: []*ldap.EntryAttribute{ Attributes: []*ldap.EntryAttribute{
@ -1548,10 +1598,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has 0 values for username attribute", name: "search result has 0 values for username attribute",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1574,10 +1624,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has more than one value for username attribute", name: "search result has more than one value for username attribute",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1600,10 +1650,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has 0 values for uid attribute", name: "search result has 0 values for uid attribute",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1626,10 +1676,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has 2 values for uid attribute", name: "search result has 2 values for uid attribute",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1652,10 +1702,10 @@ func TestUpstreamRefresh(t *testing.T) {
}, },
{ {
name: "search result has a changed pwdLastSet value", name: "search result has a changed pwdLastSet value",
providerConfig: providerConfig, providerConfig: providerConfig(nil),
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(&ldap.SearchResult{ conn.EXPECT().Search(expectedUserSearch(nil)).Return(&ldap.SearchResult{
Entries: []*ldap.Entry{ Entries: []*ldap.Entry{
{ {
DN: testUserSearchResultDNValue, DN: testUserSearchResultDNValue,
@ -1681,32 +1731,12 @@ func TestUpstreamRefresh(t *testing.T) {
wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: value for attribute \"pwdLastSet\" has changed since initial value at login", wantErr: "validation for attribute \"pwdLastSet\" failed during upstream refresh: value for attribute \"pwdLastSet\" has changed since initial value at login",
}, },
{ {
name: "group search returns an error", name: "group search returns an error",
providerConfig: &ProviderConfig{ providerConfig: providerConfig(nil),
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{
Base: testUserSearchBase,
UIDAttribute: testUserSearchUIDAttribute,
UsernameAttribute: testUserSearchUsernameAttribute,
},
GroupSearch: GroupSearchConfig{
Base: testGroupSearchBase,
Filter: testGroupSearchFilter,
GroupNameAttribute: testGroupSearchGroupNameAttribute,
},
RefreshAttributeChecks: map[string]func(*ldap.Entry, provider.StoredRefreshAttributes) error{
pwdLastSetAttribute: AttributeUnchangedSinceLogin(pwdLastSetAttribute),
},
},
setupMocks: func(conn *mockldapconn.MockConn) { setupMocks: func(conn *mockldapconn.MockConn) {
conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1) conn.EXPECT().Bind(testBindUsername, testBindPassword).Times(1)
conn.EXPECT().Search(expectedUserSearch).Return(happyPathUserSearchResult, nil).Times(1) conn.EXPECT().Search(expectedUserSearch(nil)).Return(happyPathUserSearchResult, nil).Times(1)
conn.EXPECT().SearchWithPaging(expectedGroupSearch, expectedGroupSearchPageSize).Return(nil, errors.New("some search error")).Times(1) conn.EXPECT().SearchWithPaging(expectedGroupSearch(nil), expectedGroupSearchPageSize).Return(nil, errors.New("some search error")).Times(1)
conn.EXPECT().Close().Times(1) conn.EXPECT().Close().Times(1)
}, },
wantErr: "error searching for group memberships for user with DN \"some-upstream-user-dn\": some search error", wantErr: "error searching for group memberships for user with DN \"some-upstream-user-dn\": some search error",
@ -1735,13 +1765,17 @@ func TestUpstreamRefresh(t *testing.T) {
return conn, nil return conn, nil
}) })
if tt.refreshUserDN == "" {
tt.refreshUserDN = testUserSearchResultDNValue // default for all tests
}
initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000")) initialPwdLastSetEncoded := base64.RawURLEncoding.EncodeToString([]byte("132801740800000000"))
ldapProvider := New(*tt.providerConfig) ldapProvider := New(*tt.providerConfig)
subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU" subject := "ldaps://ldap.example.com:8443?base=some-upstream-user-base-dn&sub=c29tZS11cHN0cmVhbS11aWQtdmFsdWU"
groups, err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{ groups, err := ldapProvider.PerformRefresh(context.Background(), provider.StoredRefreshAttributes{
Username: testUserSearchResultUsernameAttributeValue, Username: testUserSearchResultUsernameAttributeValue,
Subject: subject, Subject: subject,
DN: testUserSearchResultDNValue, DN: tt.refreshUserDN,
AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded}, AdditionalAttributes: map[string]string{pwdLastSetAttribute: initialPwdLastSetEncoded},
}) })
if tt.wantErr != "" { if tt.wantErr != "" {

View File

@ -7,7 +7,7 @@ params:
github_url: "https://github.com/vmware-tanzu/pinniped" github_url: "https://github.com/vmware-tanzu/pinniped"
slack_url: "https://kubernetes.slack.com/messages/pinniped" slack_url: "https://kubernetes.slack.com/messages/pinniped"
community_url: "https://go.pinniped.dev/community" community_url: "https://go.pinniped.dev/community"
latest_version: v0.16.0 latest_version: v0.17.0
latest_codegen_version: 1.23 latest_codegen_version: 1.23
pygmentsCodefences: true pygmentsCodefences: true
pygmentsStyle: "pygments" pygmentsStyle: "pygments"

View File

@ -1,4 +1,4 @@
#! Copyright 2021 the Pinniped contributors. All Rights Reserved. #! Copyright 2021-2022 the Pinniped contributors. All Rights Reserved.
#! SPDX-License-Identifier: Apache-2.0 #! SPDX-License-Identifier: Apache-2.0
#@ load("@ytt:data", "data") #@ load("@ytt:data", "data")
@ -114,7 +114,7 @@ ldap.ldif: |
dn: cn=mammals,ou=groups,dc=pinniped,dc=dev dn: cn=mammals,ou=groups,dc=pinniped,dc=dev
cn: mammals cn: mammals
objectClass: groupOfNames objectClass: groupOfNames
member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev member: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev
member: cn=olive,ou=users,dc=pinniped,dc=dev member: cn=olive,ou=users,dc=pinniped,dc=dev
#@ end #@ end