diff --git a/hack/prepare-for-integration-tests.sh b/hack/prepare-for-integration-tests.sh index 30399f00..25581795 100755 --- a/hack/prepare-for-integration-tests.sh +++ b/hack/prepare-for-integration-tests.sh @@ -84,7 +84,7 @@ while (("$#")); do shift # If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error if [[ "$#" == "0" || "$1" == -* ]]; then - log_error "-g|--get-active-directory-vars requires a script name to be specified" + log_error "--get-active-directory-vars requires a script name to be specified" exit 1 fi get_active_directory_vars=$1 @@ -107,10 +107,11 @@ if [[ "$help" == "yes" ]]; then log_note " $me [flags]" log_note log_note "Flags:" - log_note " -h, --help: print this usage" - log_note " -c, --clean: destroy the current kind cluster and make a new one" - log_note " -g, --api-group-suffix: deploy Pinniped with an alternate API group suffix" - log_note " -s, --skip-build: reuse the most recently built image of the app instead of building" + log_note " -h, --help: print this usage" + log_note " -c, --clean: destroy the current kind cluster and make a new one" + log_note " -g, --api-group-suffix: deploy Pinniped with an alternate API group suffix" + log_note " -s, --skip-build: reuse the most recently built image of the app instead of building" + log_note " --get-active-directory-vars: specify a script that exports active directory environment variables" exit 1 fi diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go index c6dcd0e7..60702e5a 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher.go @@ -100,9 +100,9 @@ func (s *activeDirectoryUpstreamGenericLDAPSpec) DetectAndSetSearchBase(ctx cont if config.GroupSearch.Base != "" && config.UserSearch.Base != "" { // Both were already set in spec so just return; no need to query the RootDSE return &v1alpha1.Condition{ - Type: "SearchBaseFound", + Type: upstreamwatchers.TypeSearchBaseFound, Status: v1alpha1.ConditionTrue, - Reason: "Success", + Reason: upstreamwatchers.ReasonUsingConfigurationFromSpec, Message: "Using search base from ActiveDirectoryIdentityProvider config.", } } @@ -115,7 +115,7 @@ func (s *activeDirectoryUpstreamGenericLDAPSpec) DetectAndSetSearchBase(ctx cont return &v1alpha1.Condition{ Type: upstreamwatchers.TypeSearchBaseFound, Status: v1alpha1.ConditionFalse, - Reason: "Error", + Reason: upstreamwatchers.ReasonErrorFetchingSearchBase, Message: fmt.Sprintf(`Error finding search base: %s`, err.Error()), } } diff --git a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go index 6fbd2d73..932c4816 100644 --- a/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go +++ b/internal/controller/supervisorconfig/activedirectoryupstreamwatcher/active_directory_upstream_watcher_test.go @@ -273,7 +273,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Type: "SearchBaseFound", Status: "True", LastTransitionTime: now, - Reason: "Success", + Reason: "UsingConfigurationFromSpec", Message: "Using search base from ActiveDirectoryIdentityProvider config.", ObservedGeneration: gen, } @@ -284,7 +284,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { Type: "SearchBaseFound", Status: "False", LastTransitionTime: now, - Reason: "Error", + Reason: "ErrorFetchingSearchBase", Message: message, ObservedGeneration: gen, } @@ -878,7 +878,7 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { testHost, testBindUsername, testBindUsername), ObservedGeneration: 1234, }, - searchBaseFoundErrorCondition(1234, "Error finding search base: error binding as \"test-bind-username\" before user search: some bind error"), + searchBaseFoundErrorCondition(1234, "Error finding search base: error binding as \"test-bind-username\" before querying for defaultNamingContext: some bind error"), tlsConfigurationValidLoadedTrueCondition(1234), }, }, @@ -1344,9 +1344,6 @@ func TestActiveDirectoryUpstreamWatcherControllerSync(t *testing.T) { }, { name: "when the input activedirectoryidentityprovider leaves group search base blank and query for defaultNamingContext fails", - // TODO is this a fatal error? I think so because leaving the search base blank and trying anyway does not seem expected. - // queries with an empty search base could potentially succeed but return something unexpected, like if you were - // pointing at global catalog but not intending to use the GC functionality... inputUpstreams: []runtime.Object{editedValidUpstream(func(upstream *v1alpha1.ActiveDirectoryIdentityProvider) { upstream.Spec.UserSearch.Attributes = v1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{} upstream.Spec.GroupSearch.Base = "" diff --git a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go index 58522d60..cbfb5129 100644 --- a/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go +++ b/internal/controller/supervisorconfig/upstreamwatchers/upstream_watchers.go @@ -33,13 +33,15 @@ const ( TestLDAPConnectionTimeout = 90 * time.Second // Constants related to conditions. - typeBindSecretValid = "BindSecretValid" - typeTLSConfigurationValid = "TLSConfigurationValid" - typeLDAPConnectionValid = "LDAPConnectionValid" - TypeSearchBaseFound = "SearchBaseFound" - reasonLDAPConnectionError = "LDAPConnectionError" - noTLSConfigurationMessage = "no TLS configuration provided" - loadedTLSConfigurationMessage = "loaded TLS configuration" + typeBindSecretValid = "BindSecretValid" + typeTLSConfigurationValid = "TLSConfigurationValid" + typeLDAPConnectionValid = "LDAPConnectionValid" + TypeSearchBaseFound = "SearchBaseFound" + reasonLDAPConnectionError = "LDAPConnectionError" + noTLSConfigurationMessage = "no TLS configuration provided" + loadedTLSConfigurationMessage = "loaded TLS configuration" + ReasonUsingConfigurationFromSpec = "UsingConfigurationFromSpec" + ReasonErrorFetchingSearchBase = "ErrorFetchingSearchBase" ) // An in-memory cache with an entry for each ActiveDirectoryIdentityProvider, to keep track of which ResourceVersion @@ -187,7 +189,7 @@ func HasPreviousSuccessfulSearchBaseConditionForCurrentGeneration(secretVersionC // Found a previously successful condition for the current spec generation. // Now figure out which version of the bind Secret was used during that previous validation, if any. validatedSettings := secretVersionCache.ValidatedSettingsByName[upstreamName] - // Reload the TLS vs StartTLS setting that was previously validated. + // Reload the user search and group search base settings that were previously validated. config.UserSearch.Base = validatedSettings.UserSearchBase config.GroupSearch.Base = validatedSettings.GroupSearchBase return true diff --git a/internal/oidc/auth/auth_handler_test.go b/internal/oidc/auth/auth_handler_test.go index e12c1004..1d6f0fe3 100644 --- a/internal/oidc/auth/auth_handler_test.go +++ b/internal/oidc/auth/auth_handler_test.go @@ -612,6 +612,17 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: htmlContentType, wantBodyString: "Bad Gateway: unexpected error during upstream authentication\n", }, + { + name: "error during upstream Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&erroringUpstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), + wantStatus: http.StatusBadGateway, + wantContentType: htmlContentType, + wantBodyString: "Bad Gateway: unexpected error during upstream authentication\n", + }, { name: "wrong upstream password for LDAP authentication", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), @@ -624,6 +635,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "wrong upstream password for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr("wrong-password"), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, { name: "wrong upstream username for LDAP authentication", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), @@ -636,6 +659,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "wrong upstream username for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr("wrong-username"), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithBadUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, { name: "missing upstream username on request for LDAP authentication", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), @@ -648,6 +683,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "missing upstream username on request for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: nil, // do not send header + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, { name: "missing upstream password on request for LDAP authentication", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithLDAP(&upstreamLDAPIdentityProvider).Build(), @@ -660,6 +707,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), wantBodyString: "", }, + { + name: "missing upstream password on request for Active Directory authentication", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: happyGetRequestPath, + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: nil, // do not send header + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeAccessDeniedWithMissingUsernamePasswordHintErrorQuery), + wantBodyString: "", + }, { name: "downstream redirect uri does not match what is configured for client when using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), @@ -764,6 +823,18 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), wantBodyString: "", }, + { + name: "downstream scopes do not match what is configured for client using LDAP upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"scope": "openid tuna"}), + customUsernameHeader: pointer.StringPtr(happyLDAPUsername), + customPasswordHeader: pointer.StringPtr(happyLDAPPassword), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeInvalidScopeErrorQuery), + wantBodyString: "", + }, { name: "missing response type in request using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), @@ -789,6 +860,16 @@ func TestAuthorizationEndpoint(t *testing.T) { wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), wantBodyString: "", }, + { + name: "missing response type in request using Active Directory upstream", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), + method: http.MethodGet, + path: modifiedHappyGetRequestPath(map[string]string{"response_type": ""}), + wantStatus: http.StatusFound, + wantContentType: "application/json; charset=utf-8", + wantLocationHeader: urlWithQuery(downstreamRedirectURI, fositeMissingResponseTypeErrorQuery), + wantBodyString: "", + }, { name: "missing client id in request using OIDC upstream", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), @@ -1122,6 +1203,15 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "text/plain; charset=utf-8", wantBodyString: "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n", }, + { + name: "too many upstream providers are configured: multiple Active Directory", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithActiveDirectory(&upstreamLDAPIdentityProvider, &upstreamLDAPIdentityProvider).Build(), // more than one not allowed + method: http.MethodGet, + path: happyGetRequestPath, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: "text/plain; charset=utf-8", + wantBodyString: "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n", + }, { name: "too many upstream providers are configured: both OIDC and LDAP", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).WithLDAP(&upstreamLDAPIdentityProvider).Build(), // more than one not allowed @@ -1131,6 +1221,15 @@ func TestAuthorizationEndpoint(t *testing.T) { wantContentType: "text/plain; charset=utf-8", wantBodyString: "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n", }, + { + name: "too many upstream providers are configured: OIDC, LDAP and AD", + idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).WithLDAP(&upstreamLDAPIdentityProvider).WithActiveDirectory(&upstreamLDAPIdentityProvider).Build(), // more than one not allowed + method: http.MethodGet, + path: happyGetRequestPath, + wantStatus: http.StatusUnprocessableEntity, + wantContentType: "text/plain; charset=utf-8", + wantBodyString: "Unprocessable Entity: Too many upstream providers are configured (support for multiple upstreams is not yet implemented)\n", + }, { name: "PUT is a bad method", idpLister: oidctestutil.NewUpstreamIDPListerBuilder().WithOIDC(&upstreamOIDCIdentityProvider).Build(), diff --git a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go index f4836818..f6bb641b 100644 --- a/internal/oidc/idpdiscovery/idp_discovery_handler_test.go +++ b/internal/oidc/idpdiscovery/idp_discovery_handler_test.go @@ -41,6 +41,7 @@ func TestIDPDiscovery(t *testing.T) { {Name: "a-some-oidc-idp", Type: "oidc"}, {Name: "x-some-idp", Type: "ldap"}, {Name: "x-some-idp", Type: "oidc"}, + {Name: "y-some-ad-idp", Type: "activedirectory"}, {Name: "z-some-ad-idp", Type: "activedirectory"}, {Name: "z-some-ldap-idp", Type: "ldap"}, {Name: "z-some-oidc-idp", Type: "oidc"}, @@ -49,6 +50,7 @@ func TestIDPDiscovery(t *testing.T) { wantSecondResponseBodyJSON: &response{ IDPs: []identityProviderResponse{ {Name: "some-other-ad-idp-1", Type: "activedirectory"}, + {Name: "some-other-ad-idp-2", Type: "activedirectory"}, {Name: "some-other-ldap-idp-1", Type: "ldap"}, {Name: "some-other-ldap-idp-2", Type: "ldap"}, {Name: "some-other-oidc-idp-1", Type: "oidc"}, @@ -76,6 +78,7 @@ func TestIDPDiscovery(t *testing.T) { WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ldap-idp"}). WithLDAP(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "x-some-idp"}). WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "z-some-ad-idp"}). + WithActiveDirectory(&oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "y-some-ad-idp"}). Build() handler := NewHandler(idpLister) @@ -108,6 +111,7 @@ func TestIDPDiscovery(t *testing.T) { }) idpLister.SetActiveDirectoryIdentityProviders([]provider.UpstreamLDAPIdentityProviderI{ + &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-2"}, &oidctestutil.TestUpstreamLDAPIdentityProvider{Name: "some-other-ad-idp-1"}, }) diff --git a/internal/upstreamldap/upstreamldap.go b/internal/upstreamldap/upstreamldap.go index 443e23b8..099ad3a3 100644 --- a/internal/upstreamldap/upstreamldap.go +++ b/internal/upstreamldap/upstreamldap.go @@ -393,7 +393,7 @@ func (p *Provider) validateConfig() error { } func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, error) { - t := trace.FromContext(ctx).Nest("slow ldap authenticate user attempt", trace.Field{Key: "providerName", Value: p.GetName()}) + t := trace.FromContext(ctx).Nest("slow ldap attempt when searching for default naming context", trace.Field{Key: "providerName", Value: p.GetName()}) defer t.LogIfLong(500 * time.Millisecond) // to help users debug slow LDAP searches conn, err := p.dial(ctx) @@ -406,7 +406,7 @@ func (p *Provider) SearchForDefaultNamingContext(ctx context.Context) (string, e err = conn.Bind(p.c.BindUsername, p.c.BindPassword) if err != nil { p.traceAuthFailure(t, err) - return "", fmt.Errorf(`error binding as "%s" before user search: %w`, p.c.BindUsername, err) + return "", fmt.Errorf(`error binding as "%s" before querying for defaultNamingContext: %w`, p.c.BindUsername, err) } searchResult, err := conn.Search(p.defaultNamingContextRequest()) diff --git a/test/integration/supervisor_login_test.go b/test/integration/supervisor_login_test.go index d820879f..bf4dd9e1 100644 --- a/test/integration/supervisor_login_test.go +++ b/test/integration/supervisor_login_test.go @@ -281,12 +281,79 @@ 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.SupervisorUpstreamActiveDirectory.Host + - "?base=" + url.QueryEscape("DC=activedirectory,DC=test,DC=pinniped,DC=dev") + + "?base=" + url.QueryEscape(env.SupervisorUpstreamActiveDirectory.DefaultNamingContextSearchBase) + "&sub=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, ), // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserSAMAccountNameValue), wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountNames, + }, { + name: "activedirectory with custom 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.SupervisorUpstreamActiveDirectory.Host, + TLS: &idpv1alpha1.TLSSpec{ + CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamActiveDirectory.CABundle)), + }, + Bind: idpv1alpha1.ActiveDirectoryIdentityProviderBind{ + SecretName: secret.Name, + }, + UserSearch: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearch{ + Base: env.SupervisorUpstreamActiveDirectory.UserSearchBase, + Filter: env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeName + "={}", + Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderUserSearchAttributes{ + Username: env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeName, + }, + }, + GroupSearch: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearch{ + Filter: "member={}", // excluding nested groups + Base: env.SupervisorUpstreamActiveDirectory.GroupSearchBase, + Attributes: idpv1alpha1.ActiveDirectoryIdentityProviderGroupSearchAttributes{ + GroupName: "dn", + }, + }, + }, 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) + }, + requestAuthorization: func(t *testing.T, downstreamAuthorizeURL, _ string, httpClient *http.Client) { + requestAuthorizationUsingLDAPIdentityProvider(t, + downstreamAuthorizeURL, + env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue, // username to present to server during login + env.SupervisorUpstreamActiveDirectory.TestUserPassword, // password to present to server during login + httpClient, + false, + ) + }, + // 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=" + env.SupervisorUpstreamActiveDirectory.TestUserUniqueIDAttributeValue, + ), + // the ID token Username should have been pulled from the requested UserSearch.Attributes.Username attribute + wantDownstreamIDTokenUsernameToMatch: regexp.QuoteMeta(env.SupervisorUpstreamActiveDirectory.TestUserMailAttributeValue), + wantDownstreamIDTokenGroups: env.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs, }, { name: "logging in to activedirectory with a deactivated user fails", @@ -395,11 +462,18 @@ func requireSuccessfulActiveDirectoryIdentityProviderConditions(t *testing.T, ad } } + expectedUserSearchReason := "" + if adIDP.Spec.UserSearch.Base == "" || adIDP.Spec.GroupSearch.Base == "" { + expectedUserSearchReason = "Success" + } else { + expectedUserSearchReason = "UsingConfigurationFromSpec" + } + require.ElementsMatch(t, [][]string{ {"BindSecretValid", "True", "Success"}, {"TLSConfigurationValid", "True", "Success"}, {"LDAPConnectionValid", "True", "Success"}, - {"SearchBaseFound", "True", "Success"}, + {"SearchBaseFound", "True", expectedUserSearchReason}, }, conditionsSummary) } diff --git a/test/testlib/env.go b/test/testlib/env.go index 875964ef..148086e2 100644 --- a/test/testlib/env.go +++ b/test/testlib/env.go @@ -88,6 +88,7 @@ type TestLDAPUpstream struct { BindUsername string `json:"bindUsername"` BindPassword string `json:"bindPassword"` UserSearchBase string `json:"userSearchBase"` + DefaultNamingContextSearchBase string `json:"defaultNamingContextSearchBase"` GroupSearchBase string `json:"groupSearchBase"` TestUserDN string `json:"testUserDN"` TestUserCN string `json:"testUserCN"` @@ -281,11 +282,16 @@ func loadEnvVars(t *testing.T, result *TestEnv) { TestUserUniqueIDAttributeName: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_NAME", ""), TestUserUniqueIDAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_UNIQUE_ID_ATTRIBUTE_VALUE", ""), TestUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_AD_USERNAME_ATTRIBUTE_VALUE", ""), + TestUserMailAttributeValue: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_VALUE", ""), + TestUserMailAttributeName: wantEnv("PINNIPED_TEST_AD_USER_EMAIL_ATTRIBUTE_NAME", ""), TestUserDirectGroupsDNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_DN", ""), ";")), TestUserDirectGroupsCNs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_CN", ""), ";")), TestUserIndirectGroupsSAMAccountNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_AD_USER_EXPECTED_GROUPS_SAMACCOUNTNAME", ""), ";")), TestDeactivatedUserSAMAccountNameValue: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_SAMACCOUNTNAME", ""), TestDeactivatedUserPassword: wantEnv("PINNIPED_TEST_DEACTIVATED_AD_USER_PASSWORD", ""), + DefaultNamingContextSearchBase: wantEnv("PINNIPED_TEST_AD_DEFAULTNAMINGCONTEXT_DN", ""), + UserSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), + GroupSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""), } sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs)