diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index fd631822..1eb8f724 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -386,7 +386,7 @@ func TestE2EFullIntegration(t *testing.T) { t.Skip("LDAP integration test requires connectivity to an LDAP server") } - expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue + expectedUsername := env.SupervisorUpstreamLDAP.TestUsernameAttributeValue expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs // Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster. @@ -422,7 +422,7 @@ func TestE2EFullIntegration(t *testing.T) { Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "", Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + Username: env.SupervisorUpstreamLDAP.TestUsernameAttributeName, UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index e1d1049f..1303f8d6 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -128,7 +128,7 @@ func TestSupervisorLogin(t *testing.T) { Base: env.SupervisorUpstreamLDAP.UserSearchBase, Filter: "", Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{ - Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName, + Username: env.SupervisorUpstreamLDAP.TestUsernameAttributeName, UID: env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeName, }, }, @@ -150,7 +150,7 @@ func TestSupervisorLogin(t *testing.T) { requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { requestAuthorizationUsingLDAPIdentityProvider(t, downstreamAuthorizeURL, - env.SupervisorUpstreamLDAP.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamLDAP.TestUsernameAttributeValue, // username to present to server during login env.SupervisorUpstreamLDAP.TestUserPassword, // password to present to server during login httpClient, ) @@ -162,7 +162,7 @@ func TestSupervisorLogin(t *testing.T) { "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.TestUserUniqueIDAttributeValue)), ), // 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.TestUsernameAttributeValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs, }, { @@ -232,6 +232,56 @@ func TestSupervisorLogin(t *testing.T) { }, { name: "activedirectory with all default options", + maybeSkip: func(t *testing.T) { + t.Helper() + if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) { + t.Skip("LDAP integration test requires connectivity to an LDAP server") + } + if env.SupervisorUpstreamActiveDirectory.Host == "" { + t.Skip("Active Directory hostname not specified") + } + }, + createIDP: func(t *testing.T) { + t.Helper() + secret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ad-service-account", v1.SecretTypeBasicAuth, + map[string]string{ + v1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername, + v1.BasicAuthPasswordKey: env.SupervisorUpstreamActiveDirectory.BindPassword, + }, + ) + adIDP := testlib.CreateTestActiveDirectoryIdentityProvider(t, idpv1alpha1.ActiveDirectoryIdentityProviderSpec{ + Host: env.SupervisorUpstreamLDAP.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: secret.Name, + }, + }, idpv1alpha1.ActiveDirectoryPhaseReady) + expectedMsg := fmt.Sprintf( + `successfully able to connect to "%s" and bind as user "%s" [validated with Secret "%s" at version "%s"]`, + env.SupervisorUpstreamActiveDirectory.Host, env.SupervisorUpstreamActiveDirectory.BindUsername, + secret.Name, secret.ResourceVersion, + ) + requireSuccessfulActiveDirectoryIdentityProviderConditions(t, adIDP, expectedMsg) // TODO refactor to be same as LDAP func + }, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestUsernameAttributeName, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.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.SupervisorUpstreamActiveDirectory.Host + + "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.UserSearchBase) + + "&sub=" + base64.RawURLEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue)), + ), + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserDN), + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs, }, } for _, test := range tests { @@ -274,6 +324,30 @@ func requireSuccessfulLDAPIdentityProviderConditions(t *testing.T, ldapIDP *idpv {"LDAPConnectionValid", "True", "Success"}, }, conditionsSummary) } +func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider, expectedActiveDirectoryConnectionValidMessage string) { + require.Len(t, adIDP.Status.Conditions, 3) + + conditionsSummary := [][]string{} + for _, condition := range adIDP.Status.Conditions { + conditionsSummary = append(conditionsSummary, []string{condition.Type, string(condition.Status), condition.Reason}) + t.Logf("Saw ActiveDirectoryIdentityProvider 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, expectedActiveDirectoryConnectionValidMessage, condition.Message) + } + } + + require.ElementsMatch(t, [][]string{ + {"BindSecretValid", "True", "Success"}, + {"TLSConfigurationValid", "True", "Success"}, + {"LDAPConnectionValid", "True", "Success"}, + }, conditionsSummary) +} func testSupervisorLogin( t *testing.T, diff --git a/test/testlib/client.go b/test/testlib/client.go index 71de190a..a206c2d5 100644 --- a/test/testlib/client.go +++ b/test/testlib/client.go @@ -434,6 +434,47 @@ func CreateTestLDAPIdentityProvider(t *testing.T, spec idpv1alpha1.LDAPIdentityP return result } +func CreateTestActiveDirectoryIdentityProvider(t *testing.T, spec idpv1alpha1.ActiveDirectoryIdentityProviderSpec, expectedPhase idpv1alpha1.ActiveDirectoryIdentityProviderPhase) *idpv1alpha1.ActiveDirectoryIdentityProvider { + t.Helper() + env := IntegrationEnv(t) + client := NewSupervisorClientset(t) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + // Create the LDAPIdentityProvider using GenerateName to get a random name. + upstreams := client.IDPV1alpha1().ActiveDirectoryIdentityProviders(env.SupervisorNamespace) + + created, err := upstreams.Create(ctx, &idpv1alpha1.ActiveDirectoryIdentityProvider{ + ObjectMeta: testObjectMeta(t, "upstream-ad-idp"), + Spec: spec, + }, metav1.CreateOptions{}) + require.NoError(t, err) + + // Always clean this up after this point. + t.Cleanup(func() { + t.Logf("cleaning up test ActiveDirectoryIdentityProvider %s/%s", created.Namespace, created.Name) + err := upstreams.Delete(context.Background(), created.Name, metav1.DeleteOptions{}) + require.NoError(t, err) + }) + t.Logf("created test ActiveDirectoryIdentityProvider %s", created.Name) + + // Wait for the LDAPIdentityProvider to enter the expected phase (or time out). + var result *idpv1alpha1.ActiveDirectoryIdentityProvider + RequireEventuallyf(t, + func(requireEventually *require.Assertions) { + var err error + result, err = upstreams.Get(ctx, created.Name, metav1.GetOptions{}) + requireEventually.NoErrorf(err, "error while getting ActiveDirectoryIdentityProvider %s/%s", created.Namespace, created.Name) + requireEventually.Equalf(expectedPhase, result.Status.Phase, "ActiveDirectoryIdentityProvider is not in phase %s: %v", expectedPhase, Sdump(result)) + }, + 2*time.Minute, // it takes 1 minute for a failed LDAP TLS connection test to timeout before it tries using StartTLS, so wait longer than that + 1*time.Second, + "expected the ActiveDirectoryIdentityProvider to go into phase %s", + expectedPhase, + ) + return result +} + func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef rbacv1.RoleRef) *rbacv1.ClusterRoleBinding { t.Helper() client := NewKubernetesClientset(t) diff --git a/test/testlib/env.go b/test/testlib/env.go index ea6834b6..c5afa9e8 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -61,9 +61,10 @@ type TestEnv struct { ExpectedGroups []string `json:"expectedGroups"` } `json:"testUser"` - CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"` - SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"` - SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"` + CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"` + SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"` + SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"` + SupervisorUpstreamActiveDirectory TestLDAPUpstream `json:"supervisorActiveDirectoryUpstream"` } type TestOIDCUpstream struct { @@ -91,8 +92,8 @@ type TestLDAPUpstream struct { TestUserDN string `json:"testUserDN"` TestUserCN string `json:"testUserCN"` TestUserPassword string `json:"testUserPassword"` - TestUserMailAttributeName string `json:"testUserMailAttributeName"` - TestUserMailAttributeValue string `json:"testUserMailAttributeValue"` + TestUsernameAttributeName string `json:"testUserMailAttributeName"` + TestUsernameAttributeValue string `json:"testUserMailAttributeValue"` TestUserUniqueIDAttributeName string `json:"testUserUniqueIDAttributeName"` TestUserUniqueIDAttributeValue string `json:"testUserUniqueIDAttributeValue"` TestUserDirectGroupsCNs []string `json:"testUserDirectGroupsCNs"` @@ -260,13 +261,25 @@ func loadEnvVars(t *testing.T, result *TestEnv) { TestUserCN: needEnv(t, "PINNIPED_TEST_LDAP_USER_CN"), TestUserUniqueIDAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_NAME"), TestUserUniqueIDAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_UNIQUE_ID_ATTRIBUTE_VALUE"), - TestUserMailAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME"), - TestUserMailAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE"), + TestUsernameAttributeName: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_NAME"), + TestUsernameAttributeValue: needEnv(t, "PINNIPED_TEST_LDAP_USER_EMAIL_ATTRIBUTE_VALUE"), TestUserDirectGroupsCNs: filterEmpty(strings.Split(needEnv(t, "PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_CN"), ";")), TestUserDirectGroupsDNs: filterEmpty(strings.Split(needEnv(t, "PINNIPED_TEST_LDAP_EXPECTED_DIRECT_GROUPS_DN"), ";")), TestUserPassword: needEnv(t, "PINNIPED_TEST_LDAP_USER_PASSWORD"), } + result.SupervisorUpstreamActiveDirectory = TestLDAPUpstream{ + Host: wantEnv("PINNIPED_TEST_AD_HOST", ""), + CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_AD_LDAPS_CA_BUNDLE")), + BindUsername: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_USERNAME", ""), + BindPassword: wantEnv("PINNIPED_TEST_AD_BIND_ACCOUNT_PASSWORD", ""), + TestUserPassword: wantEnv("PINNIPED_TEST_AD_USER_PASSWORD", ""), + TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), + TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), + TestUsernameAttributeName: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_NAME", ""), + TestUsernameAttributeValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + } + sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs) sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs) }