mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-28 10:45:40 +00:00
Merge branch 'main' of github.com:vmware-tanzu/pinniped into credentialissuer-spec-api
This commit is contained in:
@@ -278,7 +278,7 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands.
|
||||
t.Run("with Supervisor LDAP upstream IDP", func(t *testing.T) {
|
||||
expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue
|
||||
expectedGroups := []string{} // LDAP groups are not implemented yet
|
||||
expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs
|
||||
|
||||
// Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster.
|
||||
library.CreateTestClusterRoleBinding(t,
|
||||
@@ -317,6 +317,13 @@ func TestE2EFullIntegration(t *testing.T) {
|
||||
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.GroupSearchBase,
|
||||
Filter: "", // use the default value of "member={}"
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
|
||||
GroupName: "", // use the default value of "cn"
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
@@ -438,17 +445,10 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain(
|
||||
for _, g := range expectedGroups {
|
||||
expectedGroupsAsEmptyInterfaces = append(expectedGroupsAsEmptyInterfaces, g)
|
||||
}
|
||||
require.Equal(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim])
|
||||
require.ElementsMatch(t, expectedGroupsAsEmptyInterfaces, idTokenClaims[oidc.DownstreamGroupsClaim])
|
||||
|
||||
expectedYAMLGroups := func() string {
|
||||
var b strings.Builder
|
||||
for _, g := range expectedGroups {
|
||||
b.WriteString("\n")
|
||||
b.WriteString(` - `)
|
||||
b.WriteString(g)
|
||||
}
|
||||
return b.String()
|
||||
}()
|
||||
expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...)
|
||||
expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated")
|
||||
|
||||
// Confirm we are the right user according to Kube by calling the whoami API.
|
||||
kubectlCmd3 := exec.CommandContext(ctx, "kubectl", "create", "-f", "-", "-o", "yaml", "--kubeconfig", kubeconfigPath)
|
||||
@@ -456,28 +456,15 @@ func requireUserCanUseKubectlWithoutAuthenticatingAgain(
|
||||
kubectlCmd3.Stdin = strings.NewReader(here.Docf(`
|
||||
apiVersion: identity.concierge.%s/v1alpha1
|
||||
kind: WhoAmIRequest
|
||||
`, env.APIGroupSuffix))
|
||||
`, env.APIGroupSuffix))
|
||||
|
||||
kubectlOutput3, err := kubectlCmd3.CombinedOutput()
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, here.Docf(`
|
||||
apiVersion: identity.concierge.%s/v1alpha1
|
||||
kind: WhoAmIRequest
|
||||
metadata:
|
||||
creationTimestamp: null
|
||||
spec: {}
|
||||
status:
|
||||
kubernetesUserInfo:
|
||||
user:
|
||||
groups:%s
|
||||
- system:authenticated
|
||||
username: %s
|
||||
`, env.APIGroupSuffix, expectedYAMLGroups, expectedUsername),
|
||||
string(kubectlOutput3))
|
||||
whoAmI := deserializeWhoAmIRequest(t, string(kubectlOutput3), env.APIGroupSuffix)
|
||||
require.Equal(t, expectedUsername, whoAmI.Status.KubernetesUserInfo.User.Username)
|
||||
require.ElementsMatch(t, expectedGroupsPlusAuthenticated, whoAmI.Status.KubernetesUserInfo.User.Groups)
|
||||
|
||||
expectedGroupsPlusAuthenticated := append([]string{}, expectedGroups...)
|
||||
expectedGroupsPlusAuthenticated = append(expectedGroupsPlusAuthenticated, "system:authenticated")
|
||||
// Validate that `pinniped whoami` returns the correct identity.
|
||||
assertWhoami(
|
||||
ctx,
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
@@ -38,15 +37,19 @@ func TestLDAPSearch(t *testing.T) {
|
||||
cancelFunc() // this will send SIGKILL to the subprocess, just in case
|
||||
})
|
||||
|
||||
hostPorts := findRecentlyUnusedLocalhostPorts(t, 2)
|
||||
ldapHostPort := hostPorts[0]
|
||||
unusedHostPort := hostPorts[1]
|
||||
localhostPorts := findRecentlyUnusedLocalhostPorts(t, 3)
|
||||
ldapLocalhostPort := localhostPorts[0]
|
||||
ldapsLocalhostPort := localhostPorts[1]
|
||||
unusedLocalhostPort := localhostPorts[2]
|
||||
|
||||
// Expose the the test LDAP server's TLS port on the localhost.
|
||||
startKubectlPortForward(ctx, t, ldapHostPort, "ldaps", "ldap", env.ToolsNamespace)
|
||||
startKubectlPortForward(ctx, t, ldapsLocalhostPort, "ldaps", "ldap", env.ToolsNamespace)
|
||||
|
||||
// Expose the the test LDAP server's StartTLS port on the localhost.
|
||||
startKubectlPortForward(ctx, t, ldapLocalhostPort, "ldap", "ldap", env.ToolsNamespace)
|
||||
|
||||
providerConfig := func(editFunc func(p *upstreamldap.ProviderConfig)) *upstreamldap.ProviderConfig {
|
||||
providerConfig := defaultProviderConfig(env, ldapHostPort)
|
||||
providerConfig := defaultProviderConfig(env, ldapsLocalhostPort)
|
||||
if editFunc != nil {
|
||||
editFunc(providerConfig)
|
||||
}
|
||||
@@ -65,12 +68,24 @@ func TestLDAPSearch(t *testing.T) {
|
||||
wantUnauthenticated bool
|
||||
}{
|
||||
{
|
||||
name: "happy path",
|
||||
name: "happy path with TLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(nil)),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "happy path with StartTLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -79,7 +94,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "dc=pinniped,dc=dev" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -88,7 +103,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "(cn={})" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -100,7 +115,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
p.UserSearch.Filter = "cn={}"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "cn=pinny,ou=users,dc=pinniped,dc=dev", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -111,7 +126,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -122,7 +137,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
p.UserSearch.Filter = "(|(cn={})(mail={}))"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -131,7 +146,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "dn" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "cn=pinny,ou=users,dc=pinniped,dc=dev", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -140,7 +155,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "sn" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "Seal", Groups: []string{"ball-game-players", "seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -149,7 +164,7 @@ func TestLDAPSearch(t *testing.T) {
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UsernameAttribute = "sn" })),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{}}, // note that the final answer has case preserved from the entry
|
||||
User: &user.DefaultInfo{Name: "Seal", UID: "1000", Groups: []string{"ball-game-players", "seals"}}, // note that the final answer has case preserved from the entry
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -162,6 +177,75 @@ func TestLDAPSearch(t *testing.T) {
|
||||
})),
|
||||
wantError: `must specify UserSearch Filter when UserSearch UsernameAttribute is "dn"`,
|
||||
},
|
||||
{
|
||||
name: "group search disabled",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Base = ""
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "group search base causes no groups to be found for user",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Base = "ou=users,dc=pinniped,dc=dev" // there are no groups under this part of the tree
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "using dn as the group name attribute",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.GroupNameAttribute = "dn"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{
|
||||
"cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev",
|
||||
"cn=seals,ou=groups,dc=pinniped,dc=dev",
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "using some other custom group name attribute",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.GroupNameAttribute = "objectClass" // silly example, but still a meaningful test
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"groupOfNames", "groupOfNames"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "using a more complex group search filter",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Filter = "(&(&(objectClass=groupOfNames)(member={}))(cn=seals))"
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"seals"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "using a group filter which causes no groups to be found for the user",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.Filter = "foobar={}" // foobar is not a valid attribute name for this LDAP server's schema
|
||||
})),
|
||||
wantAuthResponse: &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "when the bind user username is not a valid DN",
|
||||
username: "pinny",
|
||||
@@ -183,6 +267,17 @@ func TestLDAPSearch(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.BindPassword = "wrong-password" })),
|
||||
wantError: `error binding as "cn=admin,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `,
|
||||
},
|
||||
{
|
||||
name: "when the bind user username is wrong with StartTLS: example of an error after successful connection with StartTLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||
p.BindUsername = "cn=wrong,dc=pinniped,dc=dev"
|
||||
})),
|
||||
wantError: `error binding as "cn=wrong,dc=pinniped,dc=dev" before user search: LDAP Result Code 49 "Invalid Credentials": `,
|
||||
},
|
||||
{
|
||||
name: "when the end user password is wrong",
|
||||
username: "pinny",
|
||||
@@ -211,6 +306,13 @@ func TestLDAPSearch(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Filter = "*" })),
|
||||
wantError: `error searching for user "pinny": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`,
|
||||
},
|
||||
{
|
||||
name: "when the group search filter does not compile",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Filter = "*" })),
|
||||
wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 201 "Filter Compile Error": ldap: error parsing filter`,
|
||||
},
|
||||
{
|
||||
name: "when there are too many search results for the user",
|
||||
username: "pinny",
|
||||
@@ -221,32 +323,89 @@ func TestLDAPSearch(t *testing.T) {
|
||||
wantError: `error searching for user "pinny": LDAP Result Code 4 "Size Limit Exceeded": `,
|
||||
},
|
||||
{
|
||||
name: "when the server is unreachable",
|
||||
name: "when the server is unreachable with TLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.Host = "127.0.0.1:" + unusedHostPort })),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:%s: connect: connection refused`, unusedHostPort, unusedHostPort),
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.Host = "127.0.0.1:" + unusedLocalhostPort })),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:%s: connect: connection refused`, unusedLocalhostPort, unusedLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when the server is not parsable",
|
||||
name: "when the server is unreachable with StartTLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.Host = "127.0.0.1:" + unusedLocalhostPort
|
||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||
})),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": dial tcp 127.0.0.1:%s: connect: connection refused`, unusedLocalhostPort, unusedLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when the server is not parsable with TLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.Host = "too:many:ports" })),
|
||||
wantError: `error dialing host "too:many:ports": LDAP Result Code 200 "Network Error": address too:many:ports: too many colons in address`,
|
||||
},
|
||||
{
|
||||
name: "when the CA bundle is not parsable",
|
||||
name: "when the server is not parsable with StartTLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||
p.Host = "too:many:ports"
|
||||
})),
|
||||
wantError: `error dialing host "too:many:ports": LDAP Result Code 200 "Network Error": address too:many:ports: too many colons in address`,
|
||||
},
|
||||
{
|
||||
name: "when the CA bundle is not parsable with TLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.CABundle = []byte("invalid-pem") })),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, ldapHostPort),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, ldapsLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when the CA bundle does not cause the host to be trusted",
|
||||
name: "when the CA bundle is not parsable with StartTLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||
p.CABundle = []byte("invalid-pem")
|
||||
})),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": could not parse CA bundle`, ldapLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when the CA bundle does not cause the host to be trusted with TLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.CABundle = nil })),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, ldapHostPort),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": x509: certificate signed by unknown authority`, ldapsLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when the CA bundle does not cause the host to be trusted with StartTLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.Host = "127.0.0.1:" + ldapLocalhostPort
|
||||
p.ConnectionProtocol = upstreamldap.StartTLS
|
||||
p.CABundle = nil
|
||||
})),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": TLS handshake failed (x509: certificate signed by unknown authority)`, ldapLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when trying to use TLS to connect to a port which only supports StartTLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.Host = "127.0.0.1:" + ldapLocalhostPort })),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": LDAP Result Code 200 "Network Error": EOF`, ldapLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when trying to use StartTLS to connect to a port which only supports TLS",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.ConnectionProtocol = upstreamldap.StartTLS })),
|
||||
wantError: fmt.Sprintf(`error dialing host "127.0.0.1:%s": unable to read LDAP response packet: unexpected EOF`, ldapsLocalhostPort),
|
||||
},
|
||||
{
|
||||
name: "when the UsernameAttribute attribute has multiple values in the entry",
|
||||
@@ -293,6 +452,13 @@ func TestLDAPSearch(t *testing.T) {
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.UIDAttribute = "SN" })), // this is case-sensitive
|
||||
wantError: `found 0 values for attribute "SN" while searching for user "pinny", but expected 1 result`,
|
||||
},
|
||||
{
|
||||
name: "when the GroupNameAttribute has the wrong case",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.GroupNameAttribute = "CN" })), // this is case-sensitive
|
||||
wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": found 0 values for attribute "CN" while searching for user "cn=pinny,ou=users,dc=pinniped,dc=dev", but expected 1 result`,
|
||||
},
|
||||
{
|
||||
name: "when the UsernameAttribute is DN and has the wrong case",
|
||||
username: "pinny",
|
||||
@@ -313,21 +479,44 @@ func TestLDAPSearch(t *testing.T) {
|
||||
wantError: `found 0 values for attribute "DN" while searching for user "pinny", but expected 1 result`,
|
||||
},
|
||||
{
|
||||
name: "when the search base is invalid",
|
||||
name: "when the GroupNameAttribute is DN and has the wrong case",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) {
|
||||
p.GroupSearch.GroupNameAttribute = "DN" // dn must be lower-case
|
||||
})),
|
||||
wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": found 0 values for attribute "DN" while searching for user "cn=pinny,ou=users,dc=pinniped,dc=dev", but expected 1 result`,
|
||||
},
|
||||
{
|
||||
name: "when the user search base is invalid",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "invalid-base" })),
|
||||
wantError: `error searching for user "pinny": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`,
|
||||
},
|
||||
{
|
||||
name: "when the search base does not exist",
|
||||
name: "when the group search base is invalid",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Base = "invalid-base" })),
|
||||
wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 34 "Invalid DN Syntax": invalid DN`,
|
||||
},
|
||||
{
|
||||
name: "when the user search base does not exist",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" })),
|
||||
wantError: `error searching for user "pinny": LDAP Result Code 32 "No Such Object": `,
|
||||
},
|
||||
{
|
||||
name: "when the search base causes no search results",
|
||||
name: "when the group search base does not exist",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.GroupSearch.Base = "ou=does-not-exist,dc=pinniped,dc=dev" })),
|
||||
wantError: `error searching for group memberships for user with DN "cn=pinny,ou=users,dc=pinniped,dc=dev": LDAP Result Code 32 "No Such Object": `,
|
||||
},
|
||||
{
|
||||
name: "when the user search base causes no search results",
|
||||
username: "pinny",
|
||||
password: pinnyPassword,
|
||||
provider: upstreamldap.New(*providerConfig(func(p *upstreamldap.ProviderConfig) { p.UserSearch.Base = "ou=groups,dc=pinniped,dc=dev" })),
|
||||
@@ -425,7 +614,7 @@ func TestSimultaneousRequestsOnSingleProvider(t *testing.T) {
|
||||
assert.NoError(t, result.err)
|
||||
assert.True(t, result.authenticated, "expected the user to be authenticated, but they were not")
|
||||
assert.Equal(t, &authenticator.Response{
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{}},
|
||||
User: &user.DefaultInfo{Name: "pinny", UID: "1000", Groups: []string{"ball-game-players", "seals"}},
|
||||
}, result.response)
|
||||
}
|
||||
}
|
||||
@@ -436,19 +625,25 @@ type authUserResult struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func defaultProviderConfig(env *library.TestEnv, ldapHostPort string) *upstreamldap.ProviderConfig {
|
||||
func defaultProviderConfig(env *library.TestEnv, port string) *upstreamldap.ProviderConfig {
|
||||
return &upstreamldap.ProviderConfig{
|
||||
Name: "test-ldap-provider",
|
||||
Host: "127.0.0.1:" + ldapHostPort,
|
||||
CABundle: []byte(env.SupervisorUpstreamLDAP.CABundle),
|
||||
BindUsername: "cn=admin,dc=pinniped,dc=dev",
|
||||
BindPassword: "password",
|
||||
Name: "test-ldap-provider",
|
||||
Host: "127.0.0.1:" + port,
|
||||
ConnectionProtocol: upstreamldap.TLS,
|
||||
CABundle: []byte(env.SupervisorUpstreamLDAP.CABundle),
|
||||
BindUsername: "cn=admin,dc=pinniped,dc=dev",
|
||||
BindPassword: "password",
|
||||
UserSearch: upstreamldap.UserSearchConfig{
|
||||
Base: "ou=users,dc=pinniped,dc=dev",
|
||||
Filter: "", // defaults to UsernameAttribute={}, i.e. "cn={}" in this case
|
||||
UsernameAttribute: "cn",
|
||||
UIDAttribute: "uidNumber",
|
||||
},
|
||||
GroupSearch: upstreamldap.GroupSearchConfig{
|
||||
Base: "ou=groups,dc=pinniped,dc=dev",
|
||||
Filter: "", // defaults to member={}
|
||||
GroupNameAttribute: "", // defaults to cn
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -45,6 +45,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client)
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
wantDownstreamIDTokenUsernameToMatch string
|
||||
wantDownstreamIDTokenGroups []string
|
||||
}{
|
||||
{
|
||||
name: "oidc",
|
||||
@@ -65,9 +66,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamOIDC.ExpectedGroups,
|
||||
},
|
||||
{
|
||||
name: "ldap with email as username",
|
||||
name: "ldap with email as username and groups names as DNs and using an LDAP provider which supports TLS",
|
||||
createIDP: func(t *testing.T) {
|
||||
t.Helper()
|
||||
secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
@@ -92,6 +94,13 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.GroupSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
|
||||
GroupName: "dn",
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
expectedMsg := fmt.Sprintf(
|
||||
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||
@@ -114,9 +123,10 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
),
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserMailAttributeValue),
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs,
|
||||
},
|
||||
{
|
||||
name: "ldap with CN as username ", // try another variation of configuration options
|
||||
name: "ldap with CN as username and group names as CNs and using an LDAP provider which only supports StartTLS", // try another variation of configuration options
|
||||
createIDP: func(t *testing.T) {
|
||||
t.Helper()
|
||||
secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
@@ -126,7 +136,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
},
|
||||
)
|
||||
ldapIDP := library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamLDAP.Host,
|
||||
Host: env.SupervisorUpstreamLDAP.StartTLSOnlyHost,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
|
||||
},
|
||||
@@ -141,10 +151,17 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
GroupSearch: idpv1alpha1.LDAPIdentityProviderGroupSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.GroupSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderGroupSearchAttributes{
|
||||
GroupName: "cn",
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
expectedMsg := fmt.Sprintf(
|
||||
`successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`,
|
||||
env.SupervisorUpstreamLDAP.Host, env.SupervisorUpstreamLDAP.BindUsername,
|
||||
env.SupervisorUpstreamLDAP.StartTLSOnlyHost, env.SupervisorUpstreamLDAP.BindUsername,
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
@@ -159,10 +176,11 @@ 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
|
||||
wantDownstreamIDTokenSubjectToMatch: regexp.QuoteMeta(
|
||||
"ldaps://" + env.SupervisorUpstreamLDAP.Host + "?sub=" + env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue,
|
||||
"ldaps://" + env.SupervisorUpstreamLDAP.StartTLSOnlyHost + "?sub=" + env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue,
|
||||
),
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN),
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs,
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
@@ -173,6 +191,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
test.requestAuthorization,
|
||||
test.wantDownstreamIDTokenSubjectToMatch,
|
||||
test.wantDownstreamIDTokenUsernameToMatch,
|
||||
test.wantDownstreamIDTokenGroups,
|
||||
)
|
||||
})
|
||||
}
|
||||
@@ -207,7 +226,7 @@ func testSupervisorLogin(
|
||||
t *testing.T,
|
||||
createIDP func(t *testing.T),
|
||||
requestAuthorization func(t *testing.T, downstreamAuthorizeURL, downstreamCallbackURL string, httpClient *http.Client),
|
||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string,
|
||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
||||
) {
|
||||
env := library.IntegrationEnv(t)
|
||||
|
||||
@@ -350,7 +369,7 @@ func testSupervisorLogin(
|
||||
expectedIDTokenClaims := []string{"iss", "exp", "sub", "aud", "auth_time", "iat", "jti", "nonce", "rat", "username", "groups"}
|
||||
verifyTokenResponse(t,
|
||||
tokenResponse, discovery, downstreamOAuth2Config, nonceParam,
|
||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch)
|
||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
||||
|
||||
// token exchange on the original token
|
||||
doTokenExchange(t, &downstreamOAuth2Config, tokenResponse, httpClient, discovery)
|
||||
@@ -363,7 +382,7 @@ func testSupervisorLogin(
|
||||
expectedIDTokenClaims = append(expectedIDTokenClaims, "at_hash")
|
||||
verifyTokenResponse(t,
|
||||
refreshedTokenResponse, discovery, downstreamOAuth2Config, "",
|
||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch)
|
||||
expectedIDTokenClaims, wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch, wantDownstreamIDTokenGroups)
|
||||
|
||||
require.NotEqual(t, tokenResponse.AccessToken, refreshedTokenResponse.AccessToken)
|
||||
require.NotEqual(t, tokenResponse.RefreshToken, refreshedTokenResponse.RefreshToken)
|
||||
@@ -380,7 +399,7 @@ func verifyTokenResponse(
|
||||
downstreamOAuth2Config oauth2.Config,
|
||||
nonceParam nonce.Nonce,
|
||||
expectedIDTokenClaims []string,
|
||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string,
|
||||
wantDownstreamIDTokenSubjectToMatch, wantDownstreamIDTokenUsernameToMatch string, wantDownstreamIDTokenGroups []string,
|
||||
) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
|
||||
defer cancel()
|
||||
@@ -415,6 +434,9 @@ func verifyTokenResponse(
|
||||
// Check username claim of the ID token.
|
||||
require.Regexp(t, wantDownstreamIDTokenUsernameToMatch, idTokenClaims["username"].(string))
|
||||
|
||||
// Check the groups claim.
|
||||
require.ElementsMatch(t, wantDownstreamIDTokenGroups, idTokenClaims["groups"])
|
||||
|
||||
// Some light verification of the other tokens that were returned.
|
||||
require.NotEmpty(t, tokenResponse.AccessToken)
|
||||
require.Equal(t, "bearer", tokenResponse.TokenType)
|
||||
|
||||
Reference in New Issue
Block a user