mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-06 16:17:08 +00:00
Add authentication dry run validation to LDAPIdentityProvider
Also force the LDAP server pod to restart whenever the LDIF file changes, so whenever you redeploy the tools deployment with a new test user password the server will be updated.
This commit is contained in:
+120
-111
@@ -2,6 +2,122 @@
|
||||
#! SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#@ load("@ytt:data", "data")
|
||||
#@ load("@ytt:sha256", "sha256")
|
||||
#@ load("@ytt:yaml", "yaml")
|
||||
|
||||
#@ def ldapLIDIF():
|
||||
#@yaml/text-templated-strings
|
||||
ldap.ldif: |
|
||||
# ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! ***
|
||||
# Here's a good explanation of LDIF:
|
||||
# https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system
|
||||
|
||||
# pinniped.dev (organization, root)
|
||||
dn: dc=pinniped,dc=dev
|
||||
objectClass: dcObject
|
||||
objectClass: organization
|
||||
dc: pinniped
|
||||
o: example
|
||||
|
||||
# users, pinniped.dev (organization unit)
|
||||
dn: ou=users,dc=pinniped,dc=dev
|
||||
objectClass: organizationalUnit
|
||||
ou: users
|
||||
|
||||
# groups, pinniped.dev (organization unit)
|
||||
dn: ou=groups,dc=pinniped,dc=dev
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
# beach-groups, groups, pinniped.dev (organization unit)
|
||||
dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev
|
||||
objectClass: organizationalUnit
|
||||
ou: beach-groups
|
||||
|
||||
# pinny, users, pinniped.dev (user)
|
||||
dn: cn=pinny,ou=users,dc=pinniped,dc=dev
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
cn: pinny
|
||||
sn: Seal
|
||||
givenName: Pinny
|
||||
mail: pinny.ldap@example.com
|
||||
userPassword: (@= data.values.pinny_ldap_password @)
|
||||
uid: pinny
|
||||
uidNumber: 1000
|
||||
gidNumber: 1000
|
||||
homeDirectory: /home/pinny
|
||||
loginShell: /bin/bash
|
||||
gecos: pinny-the-seal
|
||||
|
||||
# wally, users, pinniped.dev (user without password)
|
||||
dn: cn=wally,ou=users,dc=pinniped,dc=dev
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
cn: wally
|
||||
sn: Walrus
|
||||
givenName: Wally
|
||||
mail: wally.ldap@example.com
|
||||
mail: wally.alternate@example.com
|
||||
uid: wally
|
||||
uidNumber: 1001
|
||||
gidNumber: 1001
|
||||
homeDirectory: /home/wally
|
||||
loginShell: /bin/bash
|
||||
gecos: wally-the-walrus
|
||||
|
||||
# olive, users, pinniped.dev (user without password)
|
||||
dn: cn=olive,ou=users,dc=pinniped,dc=dev
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
cn: olive
|
||||
sn: Boston Terrier
|
||||
givenName: Olive
|
||||
mail: olive.ldap@example.com
|
||||
uid: olive
|
||||
uidNumber: 1002
|
||||
gidNumber: 1002
|
||||
homeDirectory: /home/olive
|
||||
loginShell: /bin/bash
|
||||
gecos: olive-the-dog
|
||||
|
||||
# ball-game-players, beach-groups, groups, pinniped.dev (group of users)
|
||||
dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev
|
||||
cn: ball-game-players
|
||||
objectClass: groupOfNames
|
||||
member: cn=pinny,ou=users,dc=pinniped,dc=dev
|
||||
member: cn=olive,ou=users,dc=pinniped,dc=dev
|
||||
|
||||
# seals, groups, pinniped.dev (group of users)
|
||||
dn: cn=seals,ou=groups,dc=pinniped,dc=dev
|
||||
cn: seals
|
||||
objectClass: groupOfNames
|
||||
member: cn=pinny,ou=users,dc=pinniped,dc=dev
|
||||
|
||||
# walruses, groups, pinniped.dev (group of users)
|
||||
dn: cn=walruses,ou=groups,dc=pinniped,dc=dev
|
||||
cn: walruses
|
||||
objectClass: groupOfNames
|
||||
member: cn=wally,ou=users,dc=pinniped,dc=dev
|
||||
|
||||
# pinnipeds, users, pinniped.dev (group of groups)
|
||||
dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev
|
||||
cn: pinnipeds
|
||||
objectClass: groupOfNames
|
||||
member: cn=seals,ou=groups,dc=pinniped,dc=dev
|
||||
member: cn=walruses,ou=groups,dc=pinniped,dc=dev
|
||||
|
||||
# mammals, groups, pinniped.dev (group of both groups and users)
|
||||
dn: cn=mammals,ou=groups,dc=pinniped,dc=dev
|
||||
cn: mammals
|
||||
objectClass: groupOfNames
|
||||
member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev
|
||||
member: cn=olive,ou=users,dc=pinniped,dc=dev
|
||||
#@ end
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
@@ -9,117 +125,7 @@ metadata:
|
||||
name: ldap-ldif-files
|
||||
namespace: tools
|
||||
type: Opaque
|
||||
stringData:
|
||||
#@yaml/text-templated-strings
|
||||
ldap.ldif: |
|
||||
# ** CAUTION: Blank lines separate entries in the LDIF format! Do not remove them! ***
|
||||
# Here's a good explanation of LDIF:
|
||||
# https://www.digitalocean.com/community/tutorials/how-to-use-ldif-files-to-make-changes-to-an-openldap-system
|
||||
|
||||
# pinniped.dev (organization, root)
|
||||
dn: dc=pinniped,dc=dev
|
||||
objectClass: dcObject
|
||||
objectClass: organization
|
||||
dc: pinniped
|
||||
o: example
|
||||
|
||||
# users, pinniped.dev (organization unit)
|
||||
dn: ou=users,dc=pinniped,dc=dev
|
||||
objectClass: organizationalUnit
|
||||
ou: users
|
||||
|
||||
# groups, pinniped.dev (organization unit)
|
||||
dn: ou=groups,dc=pinniped,dc=dev
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
# beach-groups, groups, pinniped.dev (organization unit)
|
||||
dn: ou=beach-groups,ou=groups,dc=pinniped,dc=dev
|
||||
objectClass: organizationalUnit
|
||||
ou: beach-groups
|
||||
|
||||
# pinny, users, pinniped.dev (user)
|
||||
dn: cn=pinny,ou=users,dc=pinniped,dc=dev
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
cn: pinny
|
||||
sn: Seal
|
||||
givenName: Pinny
|
||||
mail: pinny.ldap@example.com
|
||||
userPassword: (@= data.values.pinny_ldap_password @)
|
||||
uid: pinny
|
||||
uidNumber: 1000
|
||||
gidNumber: 1000
|
||||
homeDirectory: /home/pinny
|
||||
loginShell: /bin/bash
|
||||
gecos: pinny-the-seal
|
||||
|
||||
# wally, users, pinniped.dev (user without password)
|
||||
dn: cn=wally,ou=users,dc=pinniped,dc=dev
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
cn: wally
|
||||
sn: Walrus
|
||||
givenName: Wally
|
||||
mail: wally.ldap@example.com
|
||||
mail: wally.alternate@example.com
|
||||
uid: wally
|
||||
uidNumber: 1001
|
||||
gidNumber: 1001
|
||||
homeDirectory: /home/wally
|
||||
loginShell: /bin/bash
|
||||
gecos: wally-the-walrus
|
||||
|
||||
# olive, users, pinniped.dev (user without password)
|
||||
dn: cn=olive,ou=users,dc=pinniped,dc=dev
|
||||
objectClass: inetOrgPerson
|
||||
objectClass: posixAccount
|
||||
objectClass: shadowAccount
|
||||
cn: olive
|
||||
sn: Boston Terrier
|
||||
givenName: Olive
|
||||
mail: olive.ldap@example.com
|
||||
uid: olive
|
||||
uidNumber: 1002
|
||||
gidNumber: 1002
|
||||
homeDirectory: /home/olive
|
||||
loginShell: /bin/bash
|
||||
gecos: olive-the-dog
|
||||
|
||||
# ball-game-players, beach-groups, groups, pinniped.dev (group of users)
|
||||
dn: cn=ball-game-players,ou=beach-groups,ou=groups,dc=pinniped,dc=dev
|
||||
cn: ball-game-players
|
||||
objectClass: groupOfNames
|
||||
member: cn=pinny,ou=users,dc=pinniped,dc=dev
|
||||
member: cn=olive,ou=users,dc=pinniped,dc=dev
|
||||
|
||||
# seals, groups, pinniped.dev (group of users)
|
||||
dn: cn=seals,ou=groups,dc=pinniped,dc=dev
|
||||
cn: seals
|
||||
objectClass: groupOfNames
|
||||
member: cn=pinny,ou=users,dc=pinniped,dc=dev
|
||||
|
||||
# walruses, groups, pinniped.dev (group of users)
|
||||
dn: cn=walruses,ou=groups,dc=pinniped,dc=dev
|
||||
cn: walruses
|
||||
objectClass: groupOfNames
|
||||
member: cn=wally,ou=users,dc=pinniped,dc=dev
|
||||
|
||||
# pinnipeds, users, pinniped.dev (group of groups)
|
||||
dn: cn=pinnipeds,ou=groups,dc=pinniped,dc=dev
|
||||
cn: pinnipeds
|
||||
objectClass: groupOfNames
|
||||
member: cn=seals,ou=groups,dc=pinniped,dc=dev
|
||||
member: cn=walruses,ou=groups,dc=pinniped,dc=dev
|
||||
|
||||
# mammals, groups, pinniped.dev (group of both groups and users)
|
||||
dn: cn=mammals,ou=groups,dc=pinniped,dc=dev
|
||||
cn: mammals
|
||||
objectClass: groupOfNames
|
||||
member: cn=pinninpeds,ou=groups,dc=pinniped,dc=dev
|
||||
member: cn=olive,ou=users,dc=pinniped,dc=dev
|
||||
stringData: #@ ldapLIDIF()
|
||||
---
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
@@ -137,6 +143,9 @@ spec:
|
||||
metadata:
|
||||
labels:
|
||||
app: ldap
|
||||
annotations:
|
||||
#! Cause the pod to get recreated whenever the LDIF file changes.
|
||||
ldifConfigHash: #@ sha256.sum(yaml.encode(ldapLIDIF()))
|
||||
spec:
|
||||
containers:
|
||||
- name: ldap
|
||||
|
||||
@@ -66,9 +66,8 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
// the ID token Username should include the upstream user ID after the upstream issuer name
|
||||
wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamOIDC.Issuer+"?sub=") + ".+",
|
||||
},
|
||||
// TODO add more variations of this LDAP test to try using different user search filters and attributes
|
||||
{
|
||||
name: "ldap",
|
||||
name: "ldap with email as username and with dry run",
|
||||
createIDP: func(t *testing.T) {
|
||||
t.Helper()
|
||||
secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
@@ -77,7 +76,7 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
|
||||
},
|
||||
)
|
||||
library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
ldapIDP := library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamLDAP.Host,
|
||||
TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
|
||||
@@ -93,7 +92,15 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
DryRunAuthenticationUsername: env.SupervisorUpstreamLDAP.TestUserMailAttributeValue,
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
expectedMsg := fmt.Sprintf(
|
||||
`successful authentication dry run for end user "%s": selected username "%s" and UID "%s" [validated with Secret "%s" at version "%s"]`,
|
||||
env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, env.SupervisorUpstreamLDAP.TestUserMailAttributeValue,
|
||||
env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue,
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingLDAPIdentityProvider(t,
|
||||
@@ -110,6 +117,56 @@ 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),
|
||||
},
|
||||
{
|
||||
name: "ldap with CN as username and without dry run", // try another variation of configuration options
|
||||
createIDP: func(t *testing.T) {
|
||||
t.Helper()
|
||||
secret := library.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", v1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
v1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername,
|
||||
v1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
|
||||
},
|
||||
)
|
||||
ldapIDP := library.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamLDAP.Host,
|
||||
TLS: &idpv1alpha1.LDAPIdentityProviderTLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
|
||||
},
|
||||
Bind: idpv1alpha1.LDAPIdentityProviderBindSpec{
|
||||
SecretName: secret.Name,
|
||||
},
|
||||
UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearchSpec{
|
||||
Base: env.SupervisorUpstreamLDAP.UserSearchBase,
|
||||
Filter: "cn={}", // try using a non-default search filter
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributesSpec{
|
||||
Username: "dn", // try using the user's DN as the downstream username
|
||||
UniqueID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName,
|
||||
},
|
||||
},
|
||||
DryRunAuthenticationUsername: "", // try without dry run
|
||||
}, 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,
|
||||
secret.Name, secret.ResourceVersion,
|
||||
)
|
||||
requireSuccessfulLDAPIdentityProviderConditions(t, ldapIDP, expectedMsg)
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) {
|
||||
requestAuthorizationUsingLDAPIdentityProvider(t,
|
||||
downstreamAuthorizeURL,
|
||||
env.SupervisorUpstreamLDAP.TestUserCN, // username to present to server during login
|
||||
env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login
|
||||
httpClient,
|
||||
)
|
||||
},
|
||||
// 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,
|
||||
),
|
||||
// the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute
|
||||
wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamLDAP.TestUserDN),
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
@@ -124,6 +181,31 @@ func TestSupervisorLogin(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func requireSuccessfulLDAPIdentityProviderConditions(t *testing.T, ldapIDP *idpv1alpha1.LDAPIdentityProvider, expectedLDAPConnectionValidMessage string) {
|
||||
require.Len(t, ldapIDP.Status.Conditions, 3)
|
||||
|
||||
conditionsSummary := [][]string{}
|
||||
for _, condition := range ldapIDP.Status.Conditions {
|
||||
conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason})
|
||||
t.Logf("Saw LDAPIdentityProvider Status.Condition Type=%s Status=%s Reason=%s Message=%s",
|
||||
condition.Type, string(condition.Status), condition.Reason, condition.Message)
|
||||
switch condition.Type {
|
||||
case "BindSecretValid":
|
||||
require.Equal(t, "loaded bind secret", condition.Message)
|
||||
case "TLSConfigurationValid":
|
||||
require.Equal(t, "loaded TLS configuration", condition.Message)
|
||||
case "LDAPConnectionValid":
|
||||
require.Equal(t, expectedLDAPConnectionValidMessage, condition.Message)
|
||||
}
|
||||
}
|
||||
|
||||
require.ElementsMatch(t, [][]string{
|
||||
{"BindSecretValid", "True", "Success"},
|
||||
{"TLSConfigurationValid", "True", "Success"},
|
||||
{"LDAPConnectionValid", "True", "Success"},
|
||||
}, conditionsSummary)
|
||||
}
|
||||
|
||||
func testSupervisorLogin(
|
||||
t *testing.T,
|
||||
createIDP func(t *testing.T),
|
||||
|
||||
Reference in New Issue
Block a user