Merge pull request #647 from vmware-tanzu/ldap_binary_uids

Use base64 binary-encoded value as UID for LDAP
This commit is contained in:
Ryan Richard 2021-05-27 14:28:21 -07:00 committed by GitHub
commit c8dc03b06a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 67 additions and 26 deletions

View File

@ -8,6 +8,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"crypto/x509" "crypto/x509"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -417,7 +418,9 @@ func (p *Provider) searchAndBindUser(conn Conn, username string, bindFunc func(c
return "", "", nil, err return "", "", nil, err
} }
mappedUID, err := p.getSearchResultAttributeValue(p.c.UserSearch.UIDAttribute, userEntry, username) // We would like to support binary typed attributes for UIDs, so always read them as binary and encode them,
// even when the attribute may not be binary.
mappedUID, err := p.getSearchResultAttributeRawValueEncoded(p.c.UserSearch.UIDAttribute, userEntry, username)
if err != nil { if err != nil {
return "", "", nil, err return "", "", nil, err
} }
@ -526,6 +529,30 @@ func (p *Provider) escapeUsernameForSearchFilter(username string) string {
return ldap.EscapeFilter(username) return ldap.EscapeFilter(username)
} }
// Returns the (potentially) binary data of the attribute's value, base64 URL encoded.
func (p *Provider) getSearchResultAttributeRawValueEncoded(attributeName string, entry *ldap.Entry, username string) (string, error) {
if attributeName == distinguishedNameAttributeName {
return base64.RawURLEncoding.EncodeToString([]byte(entry.DN)), nil
}
attributeValues := entry.GetRawAttributeValues(attributeName)
if len(attributeValues) != 1 {
return "", fmt.Errorf(`found %d values for attribute "%s" while searching for user "%s", but expected 1 result`,
len(attributeValues), attributeName, username,
)
}
attributeValue := attributeValues[0]
if len(attributeValue) == 0 {
return "", fmt.Errorf(`found empty value for attribute "%s" while searching for user "%s", but expected value to be non-empty`,
attributeName, username,
)
}
return base64.RawURLEncoding.EncodeToString(attributeValue), nil
}
func (p *Provider) getSearchResultAttributeValue(attributeName string, entry *ldap.Entry, username string) (string, error) { func (p *Provider) getSearchResultAttributeValue(attributeName string, entry *ldap.Entry, username string) (string, error) {
if attributeName == distinguishedNameAttributeName { if attributeName == distinguishedNameAttributeName {
return entry.DN, nil return entry.DN, nil

View File

@ -6,6 +6,7 @@ package upstreamldap
import ( import (
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/base64"
"errors" "errors"
"fmt" "fmt"
"net" "net"
@ -153,7 +154,7 @@ func TestEndUserAuthentication(t *testing.T) {
expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticator.Response { expectedAuthResponse := func(editFunc func(r *user.DefaultInfo)) *authenticator.Response {
u := &user.DefaultInfo{ u := &user.DefaultInfo{
Name: testUserSearchResultUsernameAttributeValue, Name: testUserSearchResultUsernameAttributeValue,
UID: testUserSearchResultUIDAttributeValue, UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2}, Groups: []string{testGroupSearchResultGroupNameAttributeValue1, testGroupSearchResultGroupNameAttributeValue2},
} }
if editFunc != nil { if editFunc != nil {
@ -311,7 +312,7 @@ func TestEndUserAuthentication(t *testing.T) {
conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1) conn.EXPECT().Bind(testUserSearchResultDNValue, testUpstreamPassword).Times(1)
}, },
wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) { wantAuthResponse: expectedAuthResponse(func(r *user.DefaultInfo) {
r.UID = testUserSearchResultDNValue r.UID = base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultDNValue))
}), }),
}, },
{ {
@ -477,7 +478,7 @@ func TestEndUserAuthentication(t *testing.T) {
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{ User: &user.DefaultInfo{
Name: testUserSearchResultUsernameAttributeValue, Name: testUserSearchResultUsernameAttributeValue,
UID: testUserSearchResultUIDAttributeValue, UID: base64.RawURLEncoding.EncodeToString([]byte(testUserSearchResultUIDAttributeValue)),
Groups: []string{"a", "b", "c"}, Groups: []string{"a", "b", "c"},
}, },
}, },

View File

@ -5,6 +5,7 @@ package integration
import ( import (
"context" "context"
"encoding/base64"
"fmt" "fmt"
"io" "io"
"net" "net"
@ -58,6 +59,10 @@ func TestLDAPSearch(t *testing.T) {
pinnyPassword := env.SupervisorUpstreamLDAP.TestUserPassword pinnyPassword := env.SupervisorUpstreamLDAP.TestUserPassword
b64 := func(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
}
tests := []struct { tests := []struct {
name string name string
username string username string
@ -73,7 +78,7 @@ func TestLDAPSearch(t *testing.T) {
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(nil)), provider: upstreamldap.New(*providerConfig(nil)),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -85,7 +90,7 @@ func TestLDAPSearch(t *testing.T) {
p.ConnectionProtocol = upstreamldap.StartTLS p.ConnectionProtocol = upstreamldap.StartTLS
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -94,7 +99,7 @@ func TestLDAPSearch(t *testing.T) {
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -103,7 +108,7 @@ func TestLDAPSearch(t *testing.T) {
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -115,7 +120,7 @@ func TestLDAPSearch(t *testing.T) {
p.UserSearch.Filter = "cn={}" p.UserSearch.Filter = "cn={}"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -126,7 +131,7 @@ func TestLDAPSearch(t *testing.T) {
p.UserSearch.Filter = "(|(cn={})(mail={}))" p.UserSearch.Filter = "(|(cn={})(mail={}))"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -137,7 +142,7 @@ func TestLDAPSearch(t *testing.T) {
p.UserSearch.Filter = "(|(cn={})(mail={}))" p.UserSearch.Filter = "(|(cn={})(mail={}))"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -146,7 +151,7 @@ func TestLDAPSearch(t *testing.T) {
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("cn=pinny,ou=users,dc=pinniped,dc=dev"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -155,7 +160,7 @@ func TestLDAPSearch(t *testing.T) {
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("Seal"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -164,7 +169,7 @@ func TestLDAPSearch(t *testing.T) {
password: pinnyPassword, password: pinnyPassword,
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })), provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, // note that the final answer has case preserved from the entry User: &user.DefaultInfo{Name: "Seal", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}}, // note that the final answer has case preserved from the entry
}, },
}, },
{ {
@ -177,7 +182,7 @@ func TestLDAPSearch(t *testing.T) {
p.UserSearch.UIDAttribute = "givenName" p.UserSearch.UIDAttribute = "givenName"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: "Pinny the 🦭", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "Pinny the 🦭", UID: b64("Pinny the 🦭"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -189,7 +194,7 @@ func TestLDAPSearch(t *testing.T) {
p.UserSearch.UsernameAttribute = "cn" p.UserSearch.UsernameAttribute = "cn"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, },
}, },
{ {
@ -210,7 +215,7 @@ func TestLDAPSearch(t *testing.T) {
p.GroupSearch.Base = "" p.GroupSearch.Base = ""
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
}, },
}, },
{ {
@ -221,7 +226,7 @@ func TestLDAPSearch(t *testing.T) {
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
}, },
}, },
{ {
@ -232,7 +237,7 @@ func TestLDAPSearch(t *testing.T) {
p.GroupSearch.GroupNameAttribute = "dn" p.GroupSearch.GroupNameAttribute = "dn"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{ User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev", "cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
"cn=seals,ou=groups,dc=pinniped,dc=dev", "cn=seals,ou=groups,dc=pinniped,dc=dev",
}}, }},
@ -246,7 +251,7 @@ func TestLDAPSearch(t *testing.T) {
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"groupOfNames", "groupOfNames"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"groupOfNames", "groupOfNames"}},
}, },
}, },
{ {
@ -257,7 +262,7 @@ func TestLDAPSearch(t *testing.T) {
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))" p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"seals"}},
}, },
}, },
{ {
@ -268,7 +273,7 @@ func TestLDAPSearch(t *testing.T) {
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
})), })),
wantAuthResponse: &authenticator.Response{ wantAuthResponse: &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{}},
}, },
}, },
{ {
@ -593,7 +598,7 @@ func TestLDAPSearch(t *testing.T) {
} }
} }
func TestSimultaneousRequestsOnSingleProvider(t *testing.T) { func TestSimultaneousLDAPRequestsOnSingleProvider(t *testing.T) {
env := library.IntegrationEnv(t) env := library.IntegrationEnv(t)
// Note that these tests depend on the values hard-coded in the LDIF file in test/deploy/tools/ldap.yaml. // Note that these tests depend on the values hard-coded in the LDIF file in test/deploy/tools/ldap.yaml.
@ -614,6 +619,10 @@ func TestSimultaneousRequestsOnSingleProvider(t *testing.T) {
provider := upstreamldap.New(*defaultProviderConfig(env, ldapHostPort)) provider := upstreamldap.New(*defaultProviderConfig(env, ldapHostPort))
b64 := func(s string) string {
return base64.RawURLEncoding.EncodeToString([]byte(s))
}
// Making multiple simultaneous requests on the same upstreamldap.Provider instance should all succeed // Making multiple simultaneous requests on the same upstreamldap.Provider instance should all succeed
// without triggering the race detector. // without triggering the race detector.
iterations := 150 iterations := 150
@ -639,7 +648,7 @@ func TestSimultaneousRequestsOnSingleProvider(t *testing.T) {
assert.NoError(t, result.err) assert.NoError(t, result.err)
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not") assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
assert.Equal(t, &authenticator.Response{ assert.Equal(t, &authenticator.Response{
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, User: &user.DefaultInfo{Name: "pinny", UID: b64("1000"), Groups: []string{"ball-game-players", "seals"}},
}, result.response) }, result.response)
} }
} }

View File

@ -119,7 +119,9 @@ func TestSupervisorLogin(t *testing.T) {
}, },
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(
"ldaps://" + env.SupervisorUpstreamLDAP.Host + "?base=" + url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase) + "&sub=" + env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue, "ldaps://" + env.SupervisorUpstreamLDAP.Host +
"?base=" + url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase) +
"&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
), ),
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue), wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue),
@ -176,7 +178,9 @@ func TestSupervisorLogin(t *testing.T) {
}, },
// the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute // the ID token Subject should be the Host URL plus the value pulled from the requested UserSearch.Attributes.UID attribute
wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta( wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(
"ldaps://" + env.SupervisorUpstreamLDAP.StartTLSOnlyHost + "?base=" + url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase) + "&sub=" + env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue, "ldaps://" + env.SupervisorUpstreamLDAP.StartTLSOnlyHost +
"?base=" + url.QueryEscape(env.SupervisorUpstreamLDAP.UserSearchBase) +
"&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)),
), ),
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN), wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN),