mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-09-19 06:31:47 +00:00
Merge branch 'main' into jtc/add-importas-linter
This commit is contained in:
@@ -160,7 +160,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -246,7 +246,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -334,7 +334,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -458,7 +458,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -589,7 +589,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -662,7 +662,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -1209,6 +1209,93 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||
})
|
||||
|
||||
t.Run("with Supervisor GitHub upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) {
|
||||
testlib.SkipTestWhenGitHubIsUnavailable(t)
|
||||
|
||||
testCtx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
tempDir := t.TempDir() // per-test tmp dir to avoid sharing files between tests
|
||||
|
||||
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
|
||||
browser := browsertest.OpenBrowser(t)
|
||||
|
||||
expectedUsername := env.SupervisorUpstreamGithub.TestUserUsername + ":" + env.SupervisorUpstreamGithub.TestUserID
|
||||
expectedGroups := env.SupervisorUpstreamGithub.TestUserExpectedTeamSlugs
|
||||
|
||||
// Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster.
|
||||
testlib.CreateTestClusterRoleBinding(t,
|
||||
rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: expectedUsername},
|
||||
rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"},
|
||||
)
|
||||
testlib.WaitForUserToHaveAccess(t, expectedUsername, []string{}, &authorizationv1.ResourceAttributes{
|
||||
Verb: "get",
|
||||
Group: "",
|
||||
Version: "v1",
|
||||
Resource: "namespaces",
|
||||
})
|
||||
|
||||
// Create upstream GitHub provider and wait for it to become ready.
|
||||
createdProvider := testlib.CreateTestGitHubIdentityProvider(t, idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Claims: idpv1alpha1.GitHubClaims{
|
||||
Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
|
||||
Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: testlib.CreateGitHubClientCredentialsSecret(t,
|
||||
env.SupervisorUpstreamGithub.GithubAppClientID,
|
||||
env.SupervisorUpstreamGithub.GithubAppClientSecret,
|
||||
).Name,
|
||||
},
|
||||
}, idpv1alpha1.GitHubPhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
credentialCachePath := tempDir + "/test-credentials.yaml"
|
||||
|
||||
kubeconfigPath := runPinnipedGetKubeconfig(t, env, pinnipedExe, tempDir, []string{
|
||||
"get", "kubeconfig",
|
||||
"--concierge-api-group-suffix", env.APIGroupSuffix,
|
||||
"--concierge-authenticator-type", "jwt",
|
||||
"--concierge-authenticator-name", authenticator.Name,
|
||||
"--oidc-skip-browser",
|
||||
"--oidc-ca-bundle", testCABundlePath,
|
||||
"--oidc-session-cache", sessionCachePath,
|
||||
"--credential-cache", credentialCachePath,
|
||||
// use default for --oidc-scopes, which is to request all relevant scopes
|
||||
})
|
||||
|
||||
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
|
||||
kubectlCmd := exec.CommandContext(testCtx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath, "-v", "6")
|
||||
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
|
||||
|
||||
// Run the kubectl command, wait for the Pinniped CLI to print the authorization URL, and open it in the browser.
|
||||
kubectlOutputChan := startKubectlAndOpenAuthorizationURLInBrowser(testCtx, t, kubectlCmd, browser)
|
||||
|
||||
// Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form.
|
||||
browsertest.LoginToUpstreamGitHub(t, browser, env.SupervisorUpstreamGithub)
|
||||
|
||||
// Expect to be redirected to the downstream callback which is serving the form_post HTML.
|
||||
t.Logf("waiting for response page %s", federationDomain.Spec.Issuer)
|
||||
browser.WaitForURL(t, regexp.MustCompile(regexp.QuoteMeta(federationDomain.Spec.Issuer)))
|
||||
|
||||
// The response page should have done the background fetch() and POST'ed to the CLI's callback.
|
||||
// It should now be in the "success" state.
|
||||
formpostExpectSuccessState(t, browser)
|
||||
|
||||
requireKubectlGetNamespaceOutput(t, env, waitForKubectlOutput(t, kubectlOutputChan))
|
||||
|
||||
requireUserCanUseKubectlWithoutAuthenticatingAgain(testCtx, t, env, federationDomain, createdProvider.Name, kubeconfigPath,
|
||||
sessionCachePath, pinnipedExe, expectedUsername, expectedGroups, allScopes)
|
||||
})
|
||||
|
||||
t.Run("with multiple IDPs: one OIDC and one LDAP", func(t *testing.T) {
|
||||
testlib.SkipTestWhenLDAPIsUnavailable(t, env)
|
||||
|
||||
@@ -1270,7 +1357,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
|
||||
@@ -1591,7 +1678,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
|
||||
|
||||
@@ -248,6 +248,20 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr
|
||||
Kind: "ActiveDirectoryIdentityProvider",
|
||||
Verbs: []string{"get", "patch", "update"},
|
||||
},
|
||||
{
|
||||
Name: "githubidentityproviders",
|
||||
SingularName: "githubidentityprovider",
|
||||
Namespaced: true,
|
||||
Kind: "GitHubIdentityProvider",
|
||||
Verbs: []string{"delete", "deletecollection", "get", "list", "patch", "create", "update", "watch"},
|
||||
Categories: []string{"pinniped", "pinniped-idp", "pinniped-idps"},
|
||||
},
|
||||
{
|
||||
Name: "githubidentityproviders/status",
|
||||
Namespaced: true,
|
||||
Kind: "GitHubIdentityProvider",
|
||||
Verbs: []string{"get", "patch", "update"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -438,7 +452,7 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr
|
||||
}
|
||||
|
||||
// manually update this value whenever you add additional fields to an API resource and then run the generator
|
||||
totalExpectedAPIFields := 263
|
||||
totalExpectedAPIFields := 289
|
||||
|
||||
// Because we are parsing text from `kubectl explain` and because the format of that text can change
|
||||
// over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all
|
||||
@@ -578,6 +592,13 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) {
|
||||
{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
|
||||
},
|
||||
},
|
||||
addSuffix("githubidentityproviders.idp.supervisor"): {
|
||||
"v1alpha1": []apiextensionsv1.CustomResourceColumnDefinition{
|
||||
{Name: "Host", Type: "string", JSONPath: ".spec.githubAPI.host"},
|
||||
{Name: "Status", Type: "string", JSONPath: ".status.phase"},
|
||||
{Name: "Age", Type: "date", JSONPath: ".metadata.creationTimestamp"},
|
||||
},
|
||||
},
|
||||
addSuffix("oidcclients.config.supervisor"): {
|
||||
"v1alpha1": []apiextensionsv1.CustomResourceColumnDefinition{
|
||||
{Name: "Privileged Scopes", Type: "string", JSONPath: `.spec.allowedScopes[?(@ == "pinniped:request-audience")]`},
|
||||
@@ -588,8 +609,20 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
actualPinnipedCRDCount := 0
|
||||
expectedPinnipedCRDCount := 8 // the current number of CRDs that we ship as part of Pinniped
|
||||
// the current CRDs that we ship as part of Pinniped
|
||||
expectedPinnipedCRDNames := []string{
|
||||
"activedirectoryidentityproviders.idp.supervisor." + env.APIGroupSuffix,
|
||||
"credentialissuers.config.concierge." + env.APIGroupSuffix,
|
||||
"federationdomains.config.supervisor." + env.APIGroupSuffix,
|
||||
"githubidentityproviders.idp.supervisor." + env.APIGroupSuffix,
|
||||
"jwtauthenticators.authentication.concierge." + env.APIGroupSuffix,
|
||||
"ldapidentityproviders.idp.supervisor." + env.APIGroupSuffix,
|
||||
"oidcclients.config.supervisor." + env.APIGroupSuffix,
|
||||
"oidcidentityproviders.idp.supervisor." + env.APIGroupSuffix,
|
||||
"webhookauthenticators.authentication.concierge." + env.APIGroupSuffix,
|
||||
}
|
||||
|
||||
actualPinnipedCRDNames := make([]string, 0)
|
||||
|
||||
for _, crd := range crdList.Items {
|
||||
if !strings.Contains(crd.Spec.Group, env.APIGroupSuffix) {
|
||||
@@ -597,7 +630,7 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) {
|
||||
}
|
||||
|
||||
// Found a Pinniped CRD, so let's check it for AdditionalPrinterColumns.
|
||||
actualPinnipedCRDCount++
|
||||
actualPinnipedCRDNames = append(actualPinnipedCRDNames, crd.Name)
|
||||
|
||||
for _, version := range crd.Spec.Versions {
|
||||
expectedColumns, ok := expectedColumnsPerCRDVersion[crd.Name][version.Name]
|
||||
@@ -611,7 +644,7 @@ func TestCRDAdditionalPrinterColumns_Parallel(t *testing.T) {
|
||||
}
|
||||
|
||||
// Make sure that the logic of this test did not accidentally skip a CRD that it should have interrogated.
|
||||
require.Equal(t, expectedPinnipedCRDCount, actualPinnipedCRDCount,
|
||||
require.ElementsMatch(t, expectedPinnipedCRDNames, actualPinnipedCRDNames,
|
||||
"did not find expected number of Pinniped CRDs to check for additionalPrinterColumns")
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/util/retry"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
supervisorconfigv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
|
||||
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
@@ -92,7 +94,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) {
|
||||
// Test that there is no default discovery endpoint available when there are no FederationDomains.
|
||||
requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, fmt.Sprintf("%s://%s", scheme, addr))
|
||||
|
||||
// Define several unique issuer strings. Always use https in the issuer name even when we are accessing the http port.
|
||||
// Define several unique issuer URLs. Always use https in the issuer URL even when we are accessing the http port.
|
||||
issuer1 := fmt.Sprintf("https://%s/nested/issuer1", addr)
|
||||
issuer2 := fmt.Sprintf("https://%s/nested/issuer2", addr)
|
||||
issuer3 := fmt.Sprintf("https://%s/issuer3", addr)
|
||||
@@ -101,7 +103,7 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) {
|
||||
issuer6 := fmt.Sprintf("https://%s/issuer6", addr)
|
||||
badIssuer := fmt.Sprintf("https://%s/badIssuer?cannot-use=queries", addr)
|
||||
|
||||
// When FederationDomain are created in sequence they each cause a discovery endpoint to appear only for as long as the FederationDomain exists.
|
||||
// When FederationDomains are created in sequence they each cause a discovery endpoint to appear only for as long as the FederationDomain exists.
|
||||
config1, jwks1 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer1, client)
|
||||
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config1, client, ns, scheme, addr, caBundle, issuer1)
|
||||
config2, jwks2 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer2, client)
|
||||
@@ -113,15 +115,15 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) {
|
||||
// When multiple FederationDomains exist at the same time they each serve a unique discovery endpoint.
|
||||
config3, jwks3 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer3, client)
|
||||
config4, jwks4 := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer4, client)
|
||||
requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer3, nil) // discovery for issuer3 is still working after issuer4 started working
|
||||
requireStandardDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer3, nil) // discovery for issuer3 is still working after issuer4 started working
|
||||
// The auto-created JWK's were different from each other.
|
||||
require.NotEqual(t, jwks3.Keys[0]["x"], jwks4.Keys[0]["x"])
|
||||
require.NotEqual(t, jwks3.Keys[0]["y"], jwks4.Keys[0]["y"])
|
||||
|
||||
// Editing a provider to change the issuer name updates the endpoints that are being served.
|
||||
// Editing a FederationDomain to change the issuer URL updates the endpoints that are being served.
|
||||
updatedConfig4 := editFederationDomainIssuerName(t, config4, client, ns, issuer5)
|
||||
requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer4)
|
||||
jwks5 := requireDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer5, nil)
|
||||
jwks5 := requireStandardDiscoveryEndpointsAreWorking(t, scheme, addr, caBundle, issuer5, nil)
|
||||
// The JWK did not change when the issuer name was updated.
|
||||
require.Equal(t, jwks4.Keys[0], jwks5.Keys[0])
|
||||
|
||||
@@ -129,31 +131,37 @@ func TestSupervisorOIDCDiscovery_Disruptive(t *testing.T) {
|
||||
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config3, client, ns, scheme, addr, caBundle, issuer3)
|
||||
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, updatedConfig4, client, ns, scheme, addr, caBundle, issuer5)
|
||||
|
||||
// When the same issuer is added twice, both issuers are marked as duplicates, and neither provider is serving.
|
||||
// When the same issuer URL is added to two FederationDomains, both FederationDomains are marked as duplicates, and neither is serving.
|
||||
config6Duplicate1, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer6, client)
|
||||
config6Duplicate2 := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{Issuer: issuer6}, supervisorconfigv1alpha1.FederationDomainPhaseError)
|
||||
requireStatus(t, client, ns, config6Duplicate1.Name, supervisorconfigv1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerIsUnique"}))
|
||||
requireStatus(t, client, ns, config6Duplicate2.Name, supervisorconfigv1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerIsUnique"}))
|
||||
requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, issuer6)
|
||||
|
||||
// If we delete the first duplicate issuer, the second duplicate issuer starts serving.
|
||||
// If we delete the first duplicate FederationDomain, the second duplicate FederationDomain starts serving.
|
||||
requireDelete(t, client, ns, config6Duplicate1.Name)
|
||||
requireWellKnownEndpointIsWorking(t, scheme, addr, caBundle, issuer6, nil)
|
||||
requireStatus(t, client, ns, config6Duplicate2.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions())
|
||||
|
||||
// When we finally delete all issuers, the endpoint should be down.
|
||||
// When we finally delete all FederationDomains, the discovery endpoints should be down.
|
||||
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config6Duplicate2, client, ns, scheme, addr, caBundle, issuer6)
|
||||
|
||||
// "Host" headers can be used to send requests to discovery endpoints when the public address is different from the issuer name.
|
||||
// "Host" headers can be used to send requests to discovery endpoints when the public address is different from the issuer URL.
|
||||
issuer7 := "https://some-issuer-host-and-port-that-doesnt-match-public-supervisor-address.com:2684/issuer7"
|
||||
config7, _ := requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(ctx, t, scheme, addr, caBundle, issuer7, client)
|
||||
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config7, client, ns, scheme, addr, caBundle, issuer7)
|
||||
|
||||
// When we create a provider with an invalid issuer, the status is set to invalid.
|
||||
// When we create a FederationDomain with an invalid issuer url, the status is set to invalid.
|
||||
badConfig := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{Issuer: badIssuer}, supervisorconfigv1alpha1.FederationDomainPhaseError)
|
||||
requireStatus(t, client, ns, badConfig.Name, supervisorconfigv1alpha1.FederationDomainPhaseError, withFalseConditions([]string{"Ready", "IssuerURLValid"}))
|
||||
requireDiscoveryEndpointsAreNotFound(t, scheme, addr, caBundle, badIssuer)
|
||||
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, badConfig, client, ns, scheme, addr, caBundle, badIssuer)
|
||||
|
||||
issuer8 := fmt.Sprintf("https://%s/issuer8multipleIDP", addr)
|
||||
config8 := requireIDPsListedByIDPDiscoveryEndpoint(t, env, ctx, kubeClient, ns, scheme, addr, caBundle, issuer8)
|
||||
|
||||
// requireJWKSEndpointIsWorking() will give us a bit of an idea what to do...
|
||||
requireDeletingFederationDomainCausesDiscoveryEndpointsToDisappear(t, config8, client, ns, scheme, addr, caBundle, issuer8)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -198,7 +206,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) {
|
||||
ca1 := createTLSCertificateSecret(ctx, t, ns, hostname1, nil, certSecretName1, kubeClient)
|
||||
|
||||
// Now that the Secret exists, we should be able to access the endpoints by hostname using the CA.
|
||||
_ = requireDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1.Bundle()), issuer1, nil)
|
||||
_ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1.Bundle()), issuer1, nil)
|
||||
|
||||
// Update the config to with a new .spec.tls.secretName.
|
||||
certSecretName1update := "integration-test-cert-1-update"
|
||||
@@ -219,7 +227,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) {
|
||||
ca1update := createTLSCertificateSecret(ctx, t, ns, hostname1, nil, certSecretName1update, kubeClient)
|
||||
|
||||
// Now that the Secret exists at the new name, we should be able to access the endpoints by hostname using the CA.
|
||||
_ = requireDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1update.Bundle()), issuer1, nil)
|
||||
_ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, address, string(ca1update.Bundle()), issuer1, nil)
|
||||
|
||||
// To test SNI virtual hosting, send requests to discovery endpoints when the public address is different from the issuer name.
|
||||
hostname2 := "some-issuer-host-and-port-that-doesnt-match-public-supervisor-address.com"
|
||||
@@ -239,7 +247,7 @@ func TestSupervisorTLSTerminationWithSNI_Disruptive(t *testing.T) {
|
||||
ca2 := createTLSCertificateSecret(ctx, t, ns, hostname2, nil, certSecretName2, kubeClient)
|
||||
|
||||
// Now that the Secret exists, we should be able to access the endpoints by hostname using the CA.
|
||||
_ = requireDiscoveryEndpointsAreWorking(t, scheme, hostname2+":"+hostnamePort2, string(ca2.Bundle()), issuer2, map[string]string{
|
||||
_ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, hostname2+":"+hostnamePort2, string(ca2.Bundle()), issuer2, map[string]string{
|
||||
hostname2 + ":" + hostnamePort2: address,
|
||||
})
|
||||
}
|
||||
@@ -292,7 +300,7 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) {
|
||||
defaultCA := createTLSCertificateSecret(ctx, t, ns, "cert-hostname-doesnt-matter", []net.IP{ips[0]}, defaultTLSCertSecretName(env), kubeClient)
|
||||
|
||||
// Now that the Secret exists, we should be able to access the endpoints by IP address using the CA.
|
||||
_ = requireDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil)
|
||||
_ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil)
|
||||
|
||||
// Create an FederationDomain with a spec.tls.secretName.
|
||||
certSecretName := "integration-test-cert-1"
|
||||
@@ -309,10 +317,10 @@ func TestSupervisorTLSTerminationWithDefaultCerts_Disruptive(t *testing.T) {
|
||||
// Now that the Secret exists, we should be able to access the endpoints by hostname using the CA from the SNI cert.
|
||||
// Hostnames are case-insensitive, so the request should still work even if the case of the hostname is different
|
||||
// from the case of the issuer URL's hostname.
|
||||
_ = requireDiscoveryEndpointsAreWorking(t, scheme, strings.ToUpper(hostname)+":"+port, string(certCA.Bundle()), issuerUsingHostname, nil)
|
||||
_ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, strings.ToUpper(hostname)+":"+port, string(certCA.Bundle()), issuerUsingHostname, nil)
|
||||
|
||||
// And we can still access the other issuer using the default cert.
|
||||
_ = requireDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil)
|
||||
_ = requireStandardDiscoveryEndpointsAreWorking(t, scheme, ipWithPort, string(defaultCA.Bundle()), issuerUsingIPAddress, nil)
|
||||
}
|
||||
|
||||
func defaultTLSCertSecretName(env *testlib.TestEnv) string {
|
||||
@@ -488,12 +496,12 @@ func requireCreatingFederationDomainCausesDiscoveryEndpointsToAppear(
|
||||
) (*supervisorconfigv1alpha1.FederationDomain, *ExpectedJWKSResponseFormat) {
|
||||
t.Helper()
|
||||
newFederationDomain := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{Issuer: issuerName}, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
jwksResult := requireDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil)
|
||||
jwksResult := requireStandardDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil)
|
||||
requireStatus(t, client, newFederationDomain.Namespace, newFederationDomain.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady, withAllSuccessfulConditions())
|
||||
return newFederationDomain, jwksResult
|
||||
}
|
||||
|
||||
func requireDiscoveryEndpointsAreWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string, dnsOverrides map[string]string) *ExpectedJWKSResponseFormat {
|
||||
func requireStandardDiscoveryEndpointsAreWorking(t *testing.T, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string, dnsOverrides map[string]string) *ExpectedJWKSResponseFormat {
|
||||
requireWellKnownEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, dnsOverrides)
|
||||
jwksResult := requireJWKSEndpointIsWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, dnsOverrides)
|
||||
return jwksResult
|
||||
@@ -737,3 +745,192 @@ func newHTTPClient(t *testing.T, caBundle string, dnsOverrides map[string]string
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func requireIDPsListedByIDPDiscoveryEndpoint(
|
||||
t *testing.T,
|
||||
env *testlib.TestEnv,
|
||||
ctx context.Context,
|
||||
kubeClient kubernetes.Interface,
|
||||
ns, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName string) *supervisorconfigv1alpha1.FederationDomain {
|
||||
// github
|
||||
gitHubIDPSecretName := "github-idp-secret" //nolint:gosec // this is not a credential
|
||||
_, err := kubeClient.CoreV1().Secrets(ns).Create(ctx, &corev1.Secret{
|
||||
Type: "secrets.pinniped.dev/github-client",
|
||||
TypeMeta: metav1.TypeMeta{},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: gitHubIDPSecretName,
|
||||
Namespace: ns,
|
||||
},
|
||||
StringData: map[string]string{
|
||||
"clientID": "foo",
|
||||
"clientSecret": "bar",
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
_ = kubeClient.CoreV1().Secrets(ns).Delete(ctx, gitHubIDPSecretName, metav1.DeleteOptions{})
|
||||
})
|
||||
|
||||
ghIDP := testlib.CreateGitHubIdentityProvider(t, idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: gitHubIDPSecretName,
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.GitHubPhaseReady)
|
||||
|
||||
ldapBindSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", corev1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
corev1.BasicAuthUsernameKey: env.SupervisorUpstreamLDAP.BindUsername,
|
||||
corev1.BasicAuthPasswordKey: env.SupervisorUpstreamLDAP.BindPassword,
|
||||
},
|
||||
)
|
||||
ldapIDP := testlib.CreateTestLDAPIdentityProvider(t, idpv1alpha1.LDAPIdentityProviderSpec{
|
||||
Host: env.SupervisorUpstreamLDAP.Host,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamLDAP.CABundle)),
|
||||
},
|
||||
Bind: idpv1alpha1.LDAPIdentityProviderBind{
|
||||
SecretName: ldapBindSecret.Name,
|
||||
},
|
||||
UserSearch: idpv1alpha1.LDAPIdentityProviderUserSearch{
|
||||
Base: env.SupervisorUpstreamLDAP.UserSearchBase,
|
||||
Filter: "",
|
||||
Attributes: idpv1alpha1.LDAPIdentityProviderUserSearchAttributes{
|
||||
Username: env.SupervisorUpstreamLDAP.TestUserMailAttributeName,
|
||||
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 "dn"
|
||||
},
|
||||
},
|
||||
}, idpv1alpha1.LDAPPhaseReady)
|
||||
|
||||
oidcIDP := testlib.CreateTestOIDCIdentityProvider(t, idpv1alpha1.OIDCIdentityProviderSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
},
|
||||
AuthorizationConfig: idpv1alpha1.OIDCAuthorizationConfig{
|
||||
AdditionalScopes: env.SupervisorUpstreamOIDC.AdditionalScopes,
|
||||
},
|
||||
Claims: idpv1alpha1.OIDCClaims{
|
||||
Username: env.SupervisorUpstreamOIDC.UsernameClaim,
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
|
||||
var adIDP *idpv1alpha1.ActiveDirectoryIdentityProvider
|
||||
if activeDirectoryAvailable(t, env) {
|
||||
activeDirectoryBindSecret := testlib.CreateTestSecret(t, env.SupervisorNamespace, "ldap-service-account", corev1.SecretTypeBasicAuth,
|
||||
map[string]string{
|
||||
corev1.BasicAuthUsernameKey: env.SupervisorUpstreamActiveDirectory.BindUsername,
|
||||
corev1.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: activeDirectoryBindSecret.Name,
|
||||
},
|
||||
}, idpv1alpha1.ActiveDirectoryPhaseReady)
|
||||
}
|
||||
|
||||
idpsForFD := []supervisorconfigv1alpha1.FederationDomainIdentityProvider{{
|
||||
DisplayName: ghIDP.Name,
|
||||
ObjectRef: corev1.TypedLocalObjectReference{
|
||||
APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix),
|
||||
Kind: "GitHubIdentityProvider",
|
||||
Name: ghIDP.Name,
|
||||
},
|
||||
}, {
|
||||
DisplayName: ldapIDP.Name,
|
||||
ObjectRef: corev1.TypedLocalObjectReference{
|
||||
APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix),
|
||||
Kind: "LDAPIdentityProvider",
|
||||
Name: ldapIDP.Name,
|
||||
},
|
||||
}, {
|
||||
DisplayName: oidcIDP.Name,
|
||||
ObjectRef: corev1.TypedLocalObjectReference{
|
||||
APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix),
|
||||
Kind: "OIDCIdentityProvider",
|
||||
Name: oidcIDP.Name,
|
||||
},
|
||||
}}
|
||||
if activeDirectoryAvailable(t, env) {
|
||||
idpsForFD = append(idpsForFD, supervisorconfigv1alpha1.FederationDomainIdentityProvider{
|
||||
DisplayName: adIDP.Name,
|
||||
ObjectRef: corev1.TypedLocalObjectReference{
|
||||
APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix),
|
||||
Kind: "ActiveDirectoryIdentityProvider",
|
||||
Name: adIDP.Name,
|
||||
},
|
||||
})
|
||||
}
|
||||
federationDomainConfig := testlib.CreateTestFederationDomain(ctx, t, supervisorconfigv1alpha1.FederationDomainSpec{
|
||||
Issuer: issuerName,
|
||||
IdentityProviders: idpsForFD,
|
||||
}, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
|
||||
requireStandardDiscoveryEndpointsAreWorking(t, supervisorScheme, supervisorAddress, supervisorCABundle, issuerName, nil)
|
||||
issuer8URL, err := url.Parse(issuerName)
|
||||
require.NoError(t, err)
|
||||
wellKnownURL := wellKnownURLForIssuer(supervisorScheme, supervisorAddress, issuer8URL.Path)
|
||||
_, wellKnownResponseBody := requireSuccessEndpointResponse(t, wellKnownURL, issuerName, supervisorCABundle, nil) //nolint:bodyclose
|
||||
|
||||
type WellKnownResponse struct {
|
||||
Issuer string `json:"issuer"`
|
||||
AuthorizationEndpoint string `json:"authorization_endpoint"`
|
||||
TokenEndpoint string `json:"token_endpoint"`
|
||||
JWKSUri string `json:"jwks_uri"`
|
||||
DiscoverySupervisor struct {
|
||||
IdentityProvidersEndpoint string `json:"pinniped_identity_providers_endpoint"`
|
||||
} `json:"discovery.supervisor.pinniped.dev/v1alpha1"`
|
||||
}
|
||||
var wellKnownResponse WellKnownResponse
|
||||
err = json.Unmarshal([]byte(wellKnownResponseBody), &wellKnownResponse)
|
||||
require.NoError(t, err)
|
||||
discoveryIDPEndpoint := wellKnownResponse.DiscoverySupervisor.IdentityProvidersEndpoint
|
||||
_, discoveryIDPResponseBody := requireSuccessEndpointResponse(t, discoveryIDPEndpoint, issuerName, supervisorCABundle, nil) //nolint:bodyclose
|
||||
type IdentityProviderListResponse struct {
|
||||
IdentityProviders []struct {
|
||||
Name string `json:"name"`
|
||||
Type string `json:"type"`
|
||||
} `json:"pinniped_identity_providers"`
|
||||
}
|
||||
var identityProviderListResponse IdentityProviderListResponse
|
||||
err = json.Unmarshal([]byte(discoveryIDPResponseBody), &identityProviderListResponse)
|
||||
require.NoError(t, err)
|
||||
|
||||
allIDPs := []string{ghIDP.Name, ldapIDP.Name, oidcIDP.Name}
|
||||
if activeDirectoryAvailable(t, env) {
|
||||
allIDPs = append(allIDPs, adIDP.Name)
|
||||
}
|
||||
require.Equal(t, len(identityProviderListResponse.IdentityProviders), len(allIDPs), "all IDPs should be listed by idp discovery endpoint")
|
||||
for _, provider := range identityProviderListResponse.IdentityProviders {
|
||||
require.Contains(t, allIDPs, provider.Name, fmt.Sprintf("provider name should be listed in IDP discovery: %s", provider.Name))
|
||||
}
|
||||
|
||||
return federationDomainConfig
|
||||
}
|
||||
|
||||
func activeDirectoryAvailable(t *testing.T, env *testlib.TestEnv) bool {
|
||||
t.Helper()
|
||||
hasLDAPPorts := env.HasCapability(testlib.CanReachInternetLDAPPorts)
|
||||
hasADHost := testlib.IntegrationEnv(t).SupervisorUpstreamActiveDirectory.Host != ""
|
||||
return hasLDAPPorts && hasADHost
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) {
|
||||
{
|
||||
Type: "IdentityProvidersObjectRefKindValid", Status: "False", Reason: "KindUnrecognized",
|
||||
Message: `some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized ` +
|
||||
`(should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`,
|
||||
`(should be one of "ActiveDirectoryIdentityProvider", "GitHubIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`,
|
||||
},
|
||||
{
|
||||
Type: "Ready", Status: "False", Reason: "NotReady",
|
||||
@@ -493,7 +493,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) {
|
||||
{
|
||||
Type: "IdentityProvidersObjectRefKindValid", Status: "False", Reason: "KindUnrecognized",
|
||||
Message: `some kinds specified by .spec.identityProviders[].objectRef.kind are not recognized ` +
|
||||
`(should be one of "ActiveDirectoryIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`,
|
||||
`(should be one of "ActiveDirectoryIdentityProvider", "GitHubIdentityProvider", "LDAPIdentityProvider", "OIDCIdentityProvider"): "this is the wrong kind"`,
|
||||
},
|
||||
{
|
||||
Type: "Ready", Status: "False", Reason: "NotReady",
|
||||
|
||||
@@ -0,0 +1,746 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/utils/ptr"
|
||||
|
||||
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
const generateNamePrefix = "integration-test-github-idp-"
|
||||
|
||||
func TestGitHubIDPStaticValidationOnCreate_Parallel(t *testing.T) {
|
||||
adminClient := testlib.NewKubernetesClientset(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
namespaceClient := adminClient.CoreV1().Namespaces()
|
||||
skipCELTests := !testutil.KubeServerMinorVersionAtLeastInclusive(t, adminClient.Discovery(), 26)
|
||||
|
||||
ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{}))
|
||||
})
|
||||
|
||||
gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
usesCELValidation bool
|
||||
inputSpec idpv1alpha1.GitHubIdentityProviderSpec
|
||||
wantSpec idpv1alpha1.GitHubIdentityProviderSpec
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "all fields set",
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("some-host.example.com"),
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: func() string {
|
||||
return base64.StdEncoding.EncodeToString([]byte("-----BEGIN CERTIFICATE-----\ndata goes here"))
|
||||
}(),
|
||||
},
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Allowed: []string{
|
||||
"org1",
|
||||
"that-other-org",
|
||||
},
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
},
|
||||
},
|
||||
Claims: idpv1alpha1.GitHubClaims{
|
||||
Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
|
||||
Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "any-name-goes-here",
|
||||
},
|
||||
},
|
||||
wantSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("some-host.example.com"),
|
||||
TLS: &idpv1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCmRhdGEgZ29lcyBoZXJl",
|
||||
},
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Allowed: []string{
|
||||
"org1",
|
||||
"that-other-org",
|
||||
},
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
},
|
||||
},
|
||||
Claims: idpv1alpha1.GitHubClaims{
|
||||
Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
|
||||
Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "any-name-goes-here",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "minimum fields set - inherit defaults",
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
wantSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("github.com"),
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Claims: idpv1alpha1.GitHubClaims{
|
||||
Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
|
||||
Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf(
|
||||
"cannot set AllowedOrganizationsPolicy=%s and set AllowedOrganizations",
|
||||
string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)),
|
||||
usesCELValidation: true,
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Allowed: []string{
|
||||
"some-org",
|
||||
},
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
wantErr: "spec.allowAuthentication.organizations.policy must be 'OnlyUsersFromAllowedOrganizations' when spec.allowAuthentication.organizations.allowed has organizations listed",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("spec.allowAuthentication.organizations.policy must be '%s' when spec.allowAuthentication.organizations.allowed is empty (nil)", string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)),
|
||||
usesCELValidation: true,
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
wantErr: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("spec.allowAuthentication.organizations.policy must be '%s' when spec.allowAuthentication.organizations.allowed is empty", string(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers)),
|
||||
usesCELValidation: true,
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Allowed: []string{},
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
wantErr: "spec.allowAuthentication.organizations.policy must be 'AllGitHubUsers' when spec.allowAuthentication.organizations.allowed is empty",
|
||||
},
|
||||
{
|
||||
name: "spec.client.secretName in body should be at least 1 chars long",
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{},
|
||||
wantErr: "spec.client.secretName in body should be at least 1 chars long",
|
||||
},
|
||||
{
|
||||
name: "spec.githubAPI.host in body should be at least 1 chars long",
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To(""),
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
wantErr: "spec.githubAPI.host in body should be at least 1 chars long",
|
||||
},
|
||||
{
|
||||
name: "duplicates not permitted in spec.allowAuthentication.organizations.allowed",
|
||||
inputSpec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Allowed: []string{
|
||||
"org1",
|
||||
"org1",
|
||||
},
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
wantErr: `spec.allowAuthentication.organizations.allowed[1]: Duplicate value: "org1"`,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if tt.usesCELValidation && skipCELTests {
|
||||
t.Skip("CEL is not available for current K8s version")
|
||||
}
|
||||
|
||||
input := &idpv1alpha1.GitHubIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
},
|
||||
Spec: tt.inputSpec,
|
||||
}
|
||||
|
||||
outputGitHubIDP, err := gitHubIDPClient.Create(ctx, input, metav1.CreateOptions{})
|
||||
if tt.wantErr == "" {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tt.wantSpec, outputGitHubIDP.Spec)
|
||||
} else {
|
||||
require.ErrorContains(t, err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubIDPSetsDefaultsWithKubectl_Parallel(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
|
||||
adminClient := testlib.NewKubernetesClientset(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
namespaceClient := adminClient.CoreV1().Namespaces()
|
||||
|
||||
ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{}))
|
||||
})
|
||||
t.Logf("Created namespace %s", ns.Name)
|
||||
|
||||
idpName := generateNamePrefix + testlib.RandHex(t, 16)
|
||||
|
||||
githubIDPYaml := []byte(here.Doc(fmt.Sprintf(`
|
||||
---
|
||||
apiVersion: idp.supervisor.%s/v1alpha1
|
||||
kind: GitHubIdentityProvider
|
||||
metadata:
|
||||
name: %s
|
||||
namespace: %s
|
||||
spec:
|
||||
allowAuthentication:
|
||||
organizations:
|
||||
policy: AllGitHubUsers
|
||||
client:
|
||||
secretName: any-secret-name`, env.APIGroupSuffix, idpName, ns.Name)))
|
||||
|
||||
githubIDPYamlFilepath := filepath.Join(t.TempDir(), "github-idp.yaml")
|
||||
|
||||
require.NoError(t, os.WriteFile(githubIDPYamlFilepath, githubIDPYaml, 0600))
|
||||
|
||||
stdOut, stdErr := runTestKubectlCommand(t, "create", "-f", githubIDPYamlFilepath)
|
||||
|
||||
require.Equal(t, fmt.Sprintf("githubidentityprovider.idp.supervisor.%s/%s created\n", env.APIGroupSuffix, idpName), stdOut)
|
||||
require.Empty(t, stdErr)
|
||||
|
||||
gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name)
|
||||
|
||||
idp, err := gitHubIDPClient.Get(ctx, idpName, metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("github.com"),
|
||||
},
|
||||
Claims: idpv1alpha1.GitHubClaims{
|
||||
Username: ptr.To(idpv1alpha1.GitHubUsernameLoginAndID),
|
||||
Groups: ptr.To(idpv1alpha1.GitHubUseTeamSlugForGroupName),
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "any-secret-name",
|
||||
},
|
||||
}, idp.Spec)
|
||||
}
|
||||
|
||||
func TestGitHubIDPPhaseAndConditions_Parallel(t *testing.T) {
|
||||
// These operations must be performed in the Supervisor's namespace so that the controller can find GitHubIdentityProvider
|
||||
supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
kubernetesClient := testlib.NewKubernetesClientset(t)
|
||||
secretsClient := kubernetesClient.CoreV1().Secrets(supervisorNamespace)
|
||||
gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(supervisorNamespace)
|
||||
|
||||
happySecretName := generateNamePrefix + testlib.RandHex(t, 16)
|
||||
invalidSecretName := generateNamePrefix + testlib.RandHex(t, 16)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
secrets []*corev1.Secret // Secrets will be created first, and the first secret found will be listed as the configured GitHub Client secret
|
||||
idps []*idpv1alpha1.GitHubIdentityProvider
|
||||
wantPhase idpv1alpha1.GitHubIdentityProviderPhase
|
||||
wantConditions []*metav1.Condition
|
||||
}{
|
||||
{
|
||||
name: "Happy Path",
|
||||
secrets: []*corev1.Secret{
|
||||
{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: happySecretName,
|
||||
},
|
||||
Type: "secrets.pinniped.dev/github-client",
|
||||
Data: map[string][]byte{
|
||||
"clientID": []byte("foo"),
|
||||
"clientSecret": []byte("bar"),
|
||||
},
|
||||
},
|
||||
},
|
||||
idps: []*idpv1alpha1.GitHubIdentityProvider{
|
||||
{
|
||||
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("github.com"),
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPhase: idpv1alpha1.GitHubPhaseReady,
|
||||
wantConditions: []*metav1.Condition{
|
||||
{
|
||||
Type: "ClaimsValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: "spec.claims are valid",
|
||||
},
|
||||
{
|
||||
Type: "ClientCredentialsSecretValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: fmt.Sprintf("clientID and clientSecret have been read from spec.client.SecretName (%q)", happySecretName),
|
||||
},
|
||||
{
|
||||
Type: "GitHubConnectionValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.githubAPI.host ("github.com:443") is reachable and TLS verification succeeds`,
|
||||
},
|
||||
{
|
||||
Type: "HostValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.githubAPI.host ("github.com") is valid`,
|
||||
},
|
||||
{
|
||||
Type: "OrganizationsPolicyValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`,
|
||||
},
|
||||
{
|
||||
Type: "TLSConfigurationValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: "spec.githubAPI.tls.certificateAuthorityData is valid",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Invalid Client Secret",
|
||||
secrets: []*corev1.Secret{
|
||||
{
|
||||
Type: "secrets.pinniped.dev/github-client",
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: invalidSecretName,
|
||||
},
|
||||
},
|
||||
},
|
||||
idps: []*idpv1alpha1.GitHubIdentityProvider{
|
||||
{
|
||||
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("github.com"),
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: invalidSecretName,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantPhase: idpv1alpha1.GitHubPhaseError,
|
||||
wantConditions: []*metav1.Condition{
|
||||
{
|
||||
Type: "ClaimsValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: "spec.claims are valid",
|
||||
},
|
||||
{
|
||||
Type: "ClientCredentialsSecretValid",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "SecretNotFound",
|
||||
Message: fmt.Sprintf(`missing key "clientID": secret from spec.client.SecretName (%q) must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`,
|
||||
invalidSecretName,
|
||||
supervisorNamespace),
|
||||
},
|
||||
{
|
||||
Type: "GitHubConnectionValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.githubAPI.host ("github.com:443") is reachable and TLS verification succeeds`,
|
||||
},
|
||||
{
|
||||
Type: "HostValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.githubAPI.host ("github.com") is valid`,
|
||||
},
|
||||
{
|
||||
Type: "OrganizationsPolicyValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`,
|
||||
},
|
||||
{
|
||||
Type: "TLSConfigurationValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.githubAPI.tls.certificateAuthorityData is valid`,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var secretName string
|
||||
for _, secret := range tt.secrets {
|
||||
secret.GenerateName = generateNamePrefix
|
||||
|
||||
created, err := secretsClient.Create(ctx, secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
t.Cleanup(func() {
|
||||
err := secretsClient.Delete(ctx, created.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
if secretName == "" {
|
||||
secretName = created.Name
|
||||
}
|
||||
}
|
||||
|
||||
for _, idp := range tt.idps {
|
||||
idp.Name = ""
|
||||
idp.GenerateName = generateNamePrefix
|
||||
idp.Spec.Client.SecretName = secretName
|
||||
|
||||
created, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := gitHubIDPClient.Delete(ctx, created.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
testlib.WaitForGitHubIDPPhase(ctx, t, gitHubIDPClient, created.Name, tt.wantPhase)
|
||||
testlib.WaitForGitHubIdentityProviderStatusConditions(ctx, t, gitHubIDPClient, created.Name, tt.wantConditions)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitHubIDPInWrongNamespace_Parallel(t *testing.T) {
|
||||
// The GitHubIdentityProvider must be in the same namespace as the controller
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
kubernetesClient := testlib.NewKubernetesClientset(t)
|
||||
|
||||
namespaceClient := kubernetesClient.CoreV1().Namespaces()
|
||||
otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, namespaceClient.Delete(ctx, otherNamespace.Name, metav1.DeleteOptions{}))
|
||||
})
|
||||
|
||||
gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(otherNamespace.Name)
|
||||
|
||||
idp := &idpv1alpha1.GitHubIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
Namespace: otherNamespace.Name,
|
||||
},
|
||||
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("github.com"),
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "does-not-matter",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
createdIDP, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := gitHubIDPClient.Delete(ctx, createdIDP.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
// We require that there's never an error
|
||||
// ... and that the status phase is never anything but Pending
|
||||
// ... and that there are no status conditions
|
||||
require.Never(t, func() bool {
|
||||
idp, err := gitHubIDPClient.Get(ctx, createdIDP.Name, metav1.GetOptions{})
|
||||
return err != nil && idp.Status.Phase != idpv1alpha1.GitHubPhasePending && len(idp.Status.Conditions) > 0
|
||||
}, 2*time.Minute, 10*time.Second)
|
||||
}
|
||||
|
||||
func TestGitHubIDPSecretInOtherNamespace_Parallel(t *testing.T) {
|
||||
// The GitHubIdentityProvider must be in the same namespace as the controller
|
||||
supervisorNamespace := testlib.IntegrationEnv(t).SupervisorNamespace
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
kubernetesClient := testlib.NewKubernetesClientset(t)
|
||||
gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(supervisorNamespace)
|
||||
|
||||
namespaceClient := kubernetesClient.CoreV1().Namespaces()
|
||||
otherNamespace, err := namespaceClient.Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, namespaceClient.Delete(ctx, otherNamespace.Name, metav1.DeleteOptions{}))
|
||||
})
|
||||
|
||||
secretsClient := kubernetesClient.CoreV1().Secrets(otherNamespace.Name)
|
||||
|
||||
secret := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
Namespace: otherNamespace.Name,
|
||||
},
|
||||
Type: "secrets.pinniped.dev/github-client",
|
||||
Data: map[string][]byte{
|
||||
"clientID": []byte("foo"),
|
||||
"clientSecret": []byte("bar"),
|
||||
},
|
||||
}
|
||||
|
||||
// This secret will be cleaned up when its namespace is deleted
|
||||
createdSecret, err := secretsClient.Create(ctx, secret, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
idp := &idpv1alpha1.GitHubIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
Namespace: supervisorNamespace,
|
||||
},
|
||||
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
GitHubAPI: idpv1alpha1.GitHubAPIConfig{
|
||||
Host: ptr.To("github.com"),
|
||||
},
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: createdSecret.Name,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
created, err := gitHubIDPClient.Create(ctx, idp, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
err := gitHubIDPClient.Delete(ctx, created.Name, metav1.DeleteOptions{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
testlib.WaitForGitHubIDPPhase(ctx, t, gitHubIDPClient, created.Name, idpv1alpha1.GitHubPhaseError)
|
||||
|
||||
testlib.WaitForGitHubIdentityProviderStatusConditions(ctx, t, gitHubIDPClient, created.Name, []*metav1.Condition{
|
||||
{
|
||||
Type: "ClaimsValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: "spec.claims are valid",
|
||||
},
|
||||
{
|
||||
Type: "ClientCredentialsSecretValid",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "SecretNotFound",
|
||||
Message: fmt.Sprintf(`secret %q not found: secret from spec.client.SecretName (%q) must be found in namespace %q with type "secrets.pinniped.dev/github-client" and keys "clientID" and "clientSecret"`,
|
||||
idp.Spec.Client.SecretName,
|
||||
idp.Spec.Client.SecretName,
|
||||
supervisorNamespace),
|
||||
},
|
||||
{
|
||||
Type: "GitHubConnectionValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.githubAPI.host ("github.com:443") is reachable and TLS verification succeeds`,
|
||||
},
|
||||
{
|
||||
Type: "HostValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.githubAPI.host ("github.com") is valid`,
|
||||
},
|
||||
{
|
||||
Type: "OrganizationsPolicyValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: `spec.allowAuthentication.organizations.policy ("AllGitHubUsers") is valid`,
|
||||
},
|
||||
{
|
||||
Type: "TLSConfigurationValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: "spec.githubAPI.tls.certificateAuthorityData is valid",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestGitHubIDPTooManyOrganizationsStaticValidationOnCreate_Parallel(t *testing.T) {
|
||||
adminClient := testlib.NewKubernetesClientset(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
namespaceClient := adminClient.CoreV1().Namespaces()
|
||||
|
||||
ns, err := namespaceClient.Create(ctx, &corev1.Namespace{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
},
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Cleanup(func() {
|
||||
require.NoError(t, namespaceClient.Delete(ctx, ns.Name, metav1.DeleteOptions{}))
|
||||
})
|
||||
|
||||
gitHubIDPClient := testlib.NewSupervisorClientset(t).IDPV1alpha1().GitHubIdentityProviders(ns.Name)
|
||||
|
||||
input := &idpv1alpha1.GitHubIdentityProvider{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
GenerateName: generateNamePrefix,
|
||||
},
|
||||
Spec: idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Allowed: func() []string {
|
||||
orgs := make([]string, 100)
|
||||
for i := range 100 {
|
||||
orgs[i] = fmt.Sprintf("org-%d", i)
|
||||
}
|
||||
return orgs
|
||||
}(),
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: "name-of-a-secret",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
_, err = gitHubIDPClient.Create(ctx, input, metav1.CreateOptions{})
|
||||
|
||||
wantErr := "spec.allowAuthentication.organizations.allowed: Invalid value: 100: spec.allowAuthentication.organizations.allowed in body should have at most 64 items"
|
||||
if testutil.KubeServerMinorVersionAtLeastInclusive(t, adminClient.Discovery(), 24) {
|
||||
wantErr = "spec.allowAuthentication.organizations.allowed: Too many: 100: must have at most 64 items"
|
||||
}
|
||||
|
||||
require.ErrorContains(t, err, wantErr)
|
||||
}
|
||||
@@ -44,6 +44,113 @@ import (
|
||||
"go.pinniped.dev/test/testlib/browsertest"
|
||||
)
|
||||
|
||||
// These tests attempt to exercise the entire login and refresh flow of the Supervisor for various cases.
|
||||
// They do not use the Pinniped CLI as the client, which allows them to exercise the Supervisor as an
|
||||
// OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go.
|
||||
//
|
||||
// Each of these tests perform the following flow:
|
||||
// 1. Configure an IDP CR.
|
||||
// 2. Create a FederationDomain with TLS configured and wait for its JWKS endpoint to be available.
|
||||
// 3. Call the authorization endpoint and log in as a specific user.
|
||||
// Note that these tests do not use form_post response type (which is tested by e2e_test.go).
|
||||
// 4. Listen on a local callback server for the authorization redirect, and assert that it was success or failure.
|
||||
// 5. Call the token endpoint to exchange the authcode.
|
||||
// 6. Call the token endpoint to perform the RFC8693 token exchange for the cluster-scoped ID token.
|
||||
// 7. Potentially edit the refresh session data or IDP settings before the refresh.
|
||||
// 8. Call the token endpoint to perform a refresh, and expect it to succeed.
|
||||
// 9. Call the token endpoint again to perform another RFC8693 token exchange for the cluster-scoped ID token,
|
||||
// this time using the recently refreshed tokens when submitting the request.
|
||||
// 10. Potentially edit the refresh session data or IDP settings again, this time in such a way that the next
|
||||
// refresh should fail. If done, then perform one more refresh and expect failure.
|
||||
type supervisorLoginTestcase struct {
|
||||
name string
|
||||
|
||||
// This required function might choose to skip the test case, for example if the LDAP server is not
|
||||
// available for an LDAP test.
|
||||
maybeSkip func(t *testing.T)
|
||||
|
||||
// This required function should configure an IDP CR. It should also wait for it to be ready and schedule
|
||||
// its cleanup. Return the name of the IDP CR.
|
||||
createIDP func(t *testing.T) string
|
||||
|
||||
// Optionally specify the identityProviders part of the FederationDomain's spec by returning it from this function.
|
||||
// Also return the displayName of the IDP that should be used during authentication (or empty string for no IDP name in the auth request).
|
||||
// This function takes the name of the IDP CR which was returned by createIDP() as as argument.
|
||||
federationDomainIDPs func(t *testing.T, idpName string) (idps []configv1alpha1.FederationDomainIdentityProvider, useIDPDisplayName string)
|
||||
|
||||
// Optionally create an OIDCClient CR for the test to use. Return the client ID and client secret for the
|
||||
// test to use. When not set, the test will default to using the "pinniped-cli" static client with no secret.
|
||||
// When a client secret is returned, it will be used for authcode exchange, refresh requests, and RFC8693
|
||||
// token exchanges for cluster-scoped tokens (client secrets are not needed in authorization requests).
|
||||
createOIDCClient func(t *testing.T, callbackURL string) (string, string)
|
||||
|
||||
// Optionally return the username and password for the test to use when logging in. This username/password
|
||||
// will be passed to requestAuthorization(), or empty strings will be passed to indicate that the defaults
|
||||
// should be used. If there is any cleanup required, then this function should also schedule that cleanup.
|
||||
testUser func(t *testing.T) (string, string)
|
||||
|
||||
// This required function should call the authorization endpoint using the given URL and also perform whatever
|
||||
// interactions are needed to log in as the user.
|
||||
requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||
|
||||
// This string will be used as the requested audience in the RFC8693 token exchange for
|
||||
// the cluster-scoped ID token. When it is not specified, a default string will be used.
|
||||
requestTokenExchangeAud string
|
||||
|
||||
// The scopes to request from the authorization endpoint. Defaults will be used when not specified.
|
||||
downstreamScopes []string
|
||||
// The scopes to want granted from the authorization endpoint. Defaults to the downstreamScopes value when not,
|
||||
// specified, i.e. by default it expects that all requested scopes were granted.
|
||||
wantDownstreamScopes []string
|
||||
|
||||
// When we want the localhost callback to have never happened, then the flow will stop there. The login was
|
||||
// unable to finish so there is nothing to assert about what should have happened with the callback, and there
|
||||
// won't be any error sent to the callback either. This would happen, for example, when the user fails to log
|
||||
// in at the LDAP/AD login page, because then they would be redirected back to that page again, instead of
|
||||
// getting a callback success/error redirect.
|
||||
wantLocalhostCallbackToNeverHappen bool
|
||||
|
||||
// The expected ID token subject claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
// The expected ID token username claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||
// This function should return an empty string when there should be no username claim in the ID tokens.
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||
// The expected ID token groups claim value, for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenGroups []string
|
||||
// The expected ID token additional claims, which will be nested under claim "additionalClaims",
|
||||
// for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenAdditionalClaims map[string]interface{}
|
||||
// The expected ID token lifetime, as calculated by token claim 'exp' subtracting token claim 'iat'.
|
||||
// ID tokens issued through authcode exchange or token refresh should have the configured lifetime (or default if not configured).
|
||||
// ID tokens issued through a token exchange should have the default lifetime.
|
||||
wantDownstreamIDTokenLifetime *time.Duration
|
||||
|
||||
// Want the authorization endpoint to redirect to the callback with this error type.
|
||||
// The rest of the flow will be skipped since the initial authorization failed.
|
||||
wantAuthorizationErrorType string
|
||||
// Want the authorization endpoint to redirect to the callback with this error description.
|
||||
// Should be used with wantAuthorizationErrorType.
|
||||
wantAuthorizationErrorDescription string
|
||||
|
||||
// Optionally want to the authcode exchange at the token endpoint to fail. The rest of the flow will be
|
||||
// skipped since the authcode exchange failed.
|
||||
wantAuthcodeExchangeError string
|
||||
|
||||
// Optionally make all required assertions about the response of the RFC8693 token exchange for
|
||||
// the cluster-scoped ID token, given the http response status and response body from the token endpoint.
|
||||
// When this is not specified then the appropriate default assertions for a successful exchange are made.
|
||||
// Even if this expects failures, the rest of the flow will continue.
|
||||
wantTokenExchangeResponse func(t *testing.T, status int, body string)
|
||||
|
||||
// Optionally edit the refresh session data between the initial login and the first refresh,
|
||||
// which is still expected to succeed after these edits. Returns the group memberships expected after the
|
||||
// refresh is performed.
|
||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string
|
||||
// Optionally either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||
}
|
||||
|
||||
func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
|
||||
@@ -56,6 +163,16 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
testlib.SkipTestWhenLDAPIsUnavailable(t, env)
|
||||
}
|
||||
|
||||
skipAnyGitHubTests := func(t *testing.T) {
|
||||
t.Helper()
|
||||
testlib.SkipTestWhenGitHubIsUnavailable(t)
|
||||
}
|
||||
|
||||
skipGitHubOAuthAppTestsButRunOtherGitHubTests := func(t *testing.T) {
|
||||
t.Helper()
|
||||
testlib.SkipTestWhenGitHubOAuthClientCallbackDoesNotMatchFederationDomainIssuerCallback(t)
|
||||
}
|
||||
|
||||
skipActiveDirectoryTests := func(t *testing.T) {
|
||||
t.Helper()
|
||||
testlib.SkipTestWhenActiveDirectoryIsUnavailable(t, env)
|
||||
@@ -68,7 +185,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -205,112 +322,7 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
regexp.QuoteMeta("&sub=") + ".+" +
|
||||
"$"
|
||||
|
||||
// These tests attempt to exercise the entire login and refresh flow of the Supervisor for various cases.
|
||||
// They do not use the Pinniped CLI as the client, which allows them to exercise the Supervisor as an
|
||||
// OIDC provider in ways that the CLI might not use. Similar tests exist using the CLI in e2e_test.go.
|
||||
//
|
||||
// Each of these tests perform the following flow:
|
||||
// 1. Configure an IDP CR.
|
||||
// 2. Create a FederationDomain with TLS configured and wait for its JWKS endpoint to be available.
|
||||
// 3. Call the authorization endpoint and log in as a specific user.
|
||||
// Note that these tests do not use form_post response type (which is tested by e2e_test.go).
|
||||
// 4. Listen on a local callback server for the authorization redirect, and assert that it was success or failure.
|
||||
// 5. Call the token endpoint to exchange the authcode.
|
||||
// 6. Call the token endpoint to perform the RFC8693 token exchange for the cluster-scoped ID token.
|
||||
// 7. Potentially edit the refresh session data or IDP settings before the refresh.
|
||||
// 8. Call the token endpoint to perform a refresh, and expect it to succeed.
|
||||
// 9. Call the token endpoint again to perform another RFC8693 token exchange for the cluster-scoped ID token,
|
||||
// this time using the recently refreshed tokens when submitting the request.
|
||||
// 10. Potentially edit the refresh session data or IDP settings again, this time in such a way that the next
|
||||
// refresh should fail. If done, then perform one more refresh and expect failure.
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
// This required function might choose to skip the test case, for example if the LDAP server is not
|
||||
// available for an LDAP test.
|
||||
maybeSkip func(t *testing.T)
|
||||
|
||||
// This required function should configure an IDP CR. It should also wait for it to be ready and schedule
|
||||
// its cleanup. Return the name of the IDP CR.
|
||||
createIDP func(t *testing.T) string
|
||||
|
||||
// Optionally specify the identityProviders part of the FederationDomain's spec by returning it from this function.
|
||||
// Also return the displayName of the IDP that should be used during authentication (or empty string for no IDP name in the auth request).
|
||||
// This function takes the name of the IDP CR which was returned by createIDP() as as argument.
|
||||
federationDomainIDPs func(t *testing.T, idpName string) (idps []supervisorconfigv1alpha1.FederationDomainIdentityProvider, useIDPDisplayName string)
|
||||
|
||||
// Optionally create an OIDCClient CR for the test to use. Return the client ID and client secret for the
|
||||
// test to use. When not set, the test will default to using the "pinniped-cli" static client with no secret.
|
||||
// When a client secret is returned, it will be used for authcode exchange, refresh requests, and RFC8693
|
||||
// token exchanges for cluster-scoped tokens (client secrets are not needed in authorization requests).
|
||||
createOIDCClient func(t *testing.T, callbackURL string) (string, string)
|
||||
|
||||
// Optionally return the username and password for the test to use when logging in. This username/password
|
||||
// will be passed to requestAuthorization(), or empty strings will be passed to indicate that the defaults
|
||||
// should be used. If there is any cleanup required, then this function should also schedule that cleanup.
|
||||
testUser func(t *testing.T) (string, string)
|
||||
|
||||
// This required function should call the authorization endpoint using the given URL and also perform whatever
|
||||
// interactions are needed to log in as the user.
|
||||
requestAuthorization func(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, username, password string, httpClient *http.Client)
|
||||
|
||||
// This string will be used as the requested audience in the RFC8693 token exchange for
|
||||
// the cluster-scoped ID token. When it is not specified, a default string will be used.
|
||||
requestTokenExchangeAud string
|
||||
|
||||
// The scopes to request from the authorization endpoint. Defaults will be used when not specified.
|
||||
downstreamScopes []string
|
||||
// The scopes to want granted from the authorization endpoint. Defaults to the downstreamScopes value when not,
|
||||
// specified, i.e. by default it expects that all requested scopes were granted.
|
||||
wantDownstreamScopes []string
|
||||
|
||||
// When we want the localhost callback to have never happened, then the flow will stop there. The login was
|
||||
// unable to finish so there is nothing to assert about what should have happened with the callback, and there
|
||||
// won't be any error sent to the callback either. This would happen, for example, when the user fails to log
|
||||
// in at the LDAP/AD login page, because then they would be redirected back to that page again, instead of
|
||||
// getting a callback success/error redirect.
|
||||
wantLocalhostCallbackToNeverHappen bool
|
||||
|
||||
// The expected ID token subject claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenSubjectToMatch string
|
||||
// The expected ID token username claim value as a regexp, for the original ID token and the refreshed ID token.
|
||||
// This function should return an empty string when there should be no username claim in the ID tokens.
|
||||
wantDownstreamIDTokenUsernameToMatch func(username string) string
|
||||
// The expected ID token groups claim value, for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenGroups []string
|
||||
// The expected ID token additional claims, which will be nested under claim "additionalClaims",
|
||||
// for the original ID token and the refreshed ID token.
|
||||
wantDownstreamIDTokenAdditionalClaims map[string]any
|
||||
// The expected ID token lifetime, as calculated by token claim 'exp' subtracting token claim 'iat'.
|
||||
// ID tokens issued through authcode exchange or token refresh should have the configured lifetime (or default if not configured).
|
||||
// ID tokens issued through a token exchange should have the default lifetime.
|
||||
wantDownstreamIDTokenLifetime *time.Duration
|
||||
|
||||
// Want the authorization endpoint to redirect to the callback with this error type.
|
||||
// The rest of the flow will be skipped since the initial authorization failed.
|
||||
wantAuthorizationErrorType string
|
||||
// Want the authorization endpoint to redirect to the callback with this error description.
|
||||
// Should be used with wantAuthorizationErrorType.
|
||||
wantAuthorizationErrorDescription string
|
||||
|
||||
// Optionally want to the authcode exchange at the token endpoint to fail. The rest of the flow will be
|
||||
// skipped since the authcode exchange failed.
|
||||
wantAuthcodeExchangeError string
|
||||
|
||||
// Optionally make all required assertions about the response of the RFC8693 token exchange for
|
||||
// the cluster-scoped ID token, given the http response status and response body from the token endpoint.
|
||||
// When this is not specified then the appropriate default assertions for a successful exchange are made.
|
||||
// Even if this expects failures, the rest of the flow will continue.
|
||||
wantTokenExchangeResponse func(t *testing.T, status int, body string)
|
||||
|
||||
// Optionally edit the refresh session data between the initial login and the first refresh,
|
||||
// which is still expected to succeed after these edits. Returns the group memberships expected after the
|
||||
// refresh is performed.
|
||||
editRefreshSessionDataWithoutBreaking func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string) []string
|
||||
// Optionally either revoke the user's session on the upstream provider, or manipulate the user's session
|
||||
// data in such a way that it should cause the next upstream refresh attempt to fail.
|
||||
breakRefreshSessionData func(t *testing.T, sessionData *psession.PinnipedSession, idpName, username string)
|
||||
}{
|
||||
tests := []*supervisorLoginTestcase{
|
||||
{
|
||||
name: "oidc with default username and groups claim settings",
|
||||
maybeSkip: skipNever,
|
||||
@@ -2174,6 +2186,24 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
// Append testcases for GitHub using a GitHub App as the client.
|
||||
tests = append(tests,
|
||||
supervisorLoginGithubTestcases(env,
|
||||
env.SupervisorUpstreamGithub.GithubAppClientID,
|
||||
env.SupervisorUpstreamGithub.GithubAppClientSecret,
|
||||
"using GitHub App as client",
|
||||
skipAnyGitHubTests)...,
|
||||
)
|
||||
|
||||
// Append those same testcases for GitHub again, but this time using one of GitHub's old-style OAuth Apps as the client.
|
||||
tests = append(tests,
|
||||
supervisorLoginGithubTestcases(env,
|
||||
env.SupervisorUpstreamGithub.GithubOAuthAppClientID,
|
||||
env.SupervisorUpstreamGithub.GithubOAuthAppClientSecret,
|
||||
"using old-style GitHub OAuth App as client",
|
||||
skipGitHubOAuthAppTestsButRunOtherGitHubTests)...,
|
||||
)
|
||||
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
@@ -2206,7 +2236,163 @@ func TestSupervisorLogin_Browser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func wantGroupsInAdditionalClaimsIfGroupsExist(additionalClaims map[string]any, wantGroupsAdditionalClaimName string, wantGroups []string) map[string]any {
|
||||
func supervisorLoginGithubTestcases(
|
||||
env *testlib.TestEnv,
|
||||
clientID string,
|
||||
clientSecret string,
|
||||
nameNote string,
|
||||
maybeSkip func(t *testing.T),
|
||||
) []*supervisorLoginTestcase {
|
||||
basicGitHubIdentityProviderSpec := func(t *testing.T, clientID, clientSecret string) idpv1alpha1.GitHubIdentityProviderSpec {
|
||||
return idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
Client: idpv1alpha1.GitHubClientSpec{
|
||||
SecretName: testlib.CreateGitHubClientCredentialsSecret(t, clientID, clientSecret).Name,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// The downstream ID token Subject should include the upstream user ID after the upstream issuer name
|
||||
// and IDP display name.
|
||||
expectedIDTokenSubjectRegexForUpstreamGitHub := "^" +
|
||||
regexp.QuoteMeta("https://api.github.com?idpName=test-upstream-github-idp-") + `[\w]+` +
|
||||
regexp.QuoteMeta("&login=") + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserUsername) +
|
||||
regexp.QuoteMeta("&id=") + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserID) +
|
||||
"$"
|
||||
|
||||
return []*supervisorLoginTestcase{
|
||||
{
|
||||
name: fmt.Sprintf("github with all orgs allowed and default claim settings (%s)", nameNote),
|
||||
maybeSkip: maybeSkip,
|
||||
createIDP: func(t *testing.T) string {
|
||||
return testlib.CreateTestGitHubIdentityProvider(t,
|
||||
basicGitHubIdentityProviderSpec(t, clientID, clientSecret),
|
||||
idpv1alpha1.GitHubPhaseReady).Name
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowGitHub,
|
||||
editRefreshSessionDataWithoutBreaking: func(t *testing.T, sessionData *psession.PinnipedSession, _, _ string) []string {
|
||||
// Even if we update this group to some names that did not come from the GitHub API,
|
||||
// we expect that it will return to the real groups from the GitHub API after we refresh.
|
||||
initialGroupMembership := []string{"some-wrong-group", "some-other-group"}
|
||||
sessionData.Custom.UpstreamGroups = initialGroupMembership // upstream group names in session
|
||||
sessionData.Fosite.Claims.Extra["groups"] = initialGroupMembership // downstream group names in session
|
||||
return env.SupervisorUpstreamGithub.TestUserExpectedTeamSlugs
|
||||
},
|
||||
breakRefreshSessionData: func(t *testing.T, pinnipedSession *psession.PinnipedSession, _, _ string) {
|
||||
// Pretend that the github access token was revoked or expired by changing it to an
|
||||
// invalid access token in the user's session data. This should cause refresh to fail because
|
||||
// during the refresh the GitHub API will not accept this bad access token.
|
||||
customSessionData := pinnipedSession.Custom
|
||||
require.Equal(t, psession.ProviderTypeGitHub, customSessionData.ProviderType)
|
||||
require.NotEmpty(t, customSessionData.GitHub.UpstreamAccessToken)
|
||||
customSessionData.GitHub.UpstreamAccessToken = "purposely-using-bad-access-token-during-an-automated-integration-test"
|
||||
},
|
||||
wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamGitHub,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserUsername+":"+env.SupervisorUpstreamGithub.TestUserID) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamGithub.TestUserExpectedTeamSlugs,
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("github with list of allowed orgs, username as login, and groups as names (%s)", nameNote),
|
||||
maybeSkip: maybeSkip,
|
||||
createIDP: func(t *testing.T) string {
|
||||
spec := basicGitHubIdentityProviderSpec(t, clientID, clientSecret)
|
||||
spec.AllowAuthentication = idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
Allowed: []string{env.SupervisorUpstreamGithub.TestUserOrganization, "some-unrelated-org"},
|
||||
},
|
||||
}
|
||||
spec.Claims = idpv1alpha1.GitHubClaims{
|
||||
Username: ptr.To(idpv1alpha1.GitHubUsernameLogin),
|
||||
Groups: ptr.To(idpv1alpha1.GitHubUseTeamNameForGroupName),
|
||||
}
|
||||
return testlib.CreateTestGitHubIdentityProvider(t, spec, idpv1alpha1.GitHubPhaseReady).Name
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowGitHub,
|
||||
wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamGitHub,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserUsername) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamGithub.TestUserExpectedTeamNames,
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("github with list of allowed orgs differently cased, username as id, and groups as names (%s)", nameNote),
|
||||
maybeSkip: maybeSkip,
|
||||
createIDP: func(t *testing.T) string {
|
||||
spec := basicGitHubIdentityProviderSpec(t, clientID, clientSecret)
|
||||
spec.AllowAuthentication = idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
Allowed: []string{strings.ToUpper(env.SupervisorUpstreamGithub.TestUserOrganization), "some-unrelated-org"},
|
||||
},
|
||||
}
|
||||
spec.Claims = idpv1alpha1.GitHubClaims{
|
||||
Username: ptr.To(idpv1alpha1.GitHubUsernameID),
|
||||
Groups: ptr.To(idpv1alpha1.GitHubUseTeamNameForGroupName),
|
||||
}
|
||||
return testlib.CreateTestGitHubIdentityProvider(t, spec, idpv1alpha1.GitHubPhaseReady).Name
|
||||
},
|
||||
requestAuthorization: requestAuthorizationUsingBrowserAuthcodeFlowGitHub,
|
||||
wantDownstreamIDTokenSubjectToMatch: expectedIDTokenSubjectRegexForUpstreamGitHub,
|
||||
wantDownstreamIDTokenUsernameToMatch: func(_ string) string {
|
||||
return "^" + regexp.QuoteMeta(env.SupervisorUpstreamGithub.TestUserID) + "$"
|
||||
},
|
||||
wantDownstreamIDTokenGroups: env.SupervisorUpstreamGithub.TestUserExpectedTeamNames,
|
||||
},
|
||||
{
|
||||
name: fmt.Sprintf("github when user does not belong to any of the allowed orgs, should fail at the Supervisor callback endpoint (%s)", nameNote),
|
||||
maybeSkip: maybeSkip,
|
||||
createIDP: func(t *testing.T) string {
|
||||
spec := basicGitHubIdentityProviderSpec(t, clientID, clientSecret)
|
||||
spec.AllowAuthentication = idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyOnlyUsersFromAllowedOrganizations),
|
||||
Allowed: []string{"some-unrelated-org"},
|
||||
},
|
||||
}
|
||||
return testlib.CreateTestGitHubIdentityProvider(t, spec, idpv1alpha1.GitHubPhaseReady).Name
|
||||
},
|
||||
federationDomainIDPs: func(t *testing.T, idpName string) ([]configv1alpha1.FederationDomainIdentityProvider, string) {
|
||||
displayName := "some-github-identity-provider-name"
|
||||
return []configv1alpha1.FederationDomainIdentityProvider{
|
||||
{
|
||||
DisplayName: displayName,
|
||||
ObjectRef: corev1.TypedLocalObjectReference{
|
||||
APIGroup: ptr.To("idp.supervisor." + env.APIGroupSuffix),
|
||||
Kind: "GitHubIdentityProvider",
|
||||
Name: idpName,
|
||||
},
|
||||
},
|
||||
},
|
||||
displayName
|
||||
},
|
||||
requestAuthorization: func(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||
t.Helper()
|
||||
browser := openBrowserAndNavigateToAuthorizeURL(t, downstreamAuthorizeURL, httpClient)
|
||||
// Expect to be redirected to the upstream provider and log in.
|
||||
browsertest.LoginToUpstreamGitHub(t, browser, env.SupervisorUpstreamGithub)
|
||||
// Wait for the login to happen and us be redirected back to the Supervisor callback with an error showing.
|
||||
t.Logf("waiting for redirect to Supervisor callback endpoint, which should be showing an error")
|
||||
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.SupervisorUpstreamOIDC.CallbackURL) + `\?.+\z`)
|
||||
browser.WaitForURL(t, callbackURLPattern)
|
||||
// Get the text of the preformatted error message showing on the page.
|
||||
textOfPreTag := browser.TextOfFirstMatch(t, "pre")
|
||||
require.Equal(t,
|
||||
`Forbidden: login denied due to configuration on GitHubIdentityProvider with display name "some-github-identity-provider-name": user is not allowed to log in due to organization membership policy`+"\n",
|
||||
textOfPreTag)
|
||||
},
|
||||
wantLocalhostCallbackToNeverHappen: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func wantGroupsInAdditionalClaimsIfGroupsExist(additionalClaims map[string]interface{}, wantGroupsAdditionalClaimName string, wantGroups []string) map[string]interface{} {
|
||||
if len(wantGroups) > 0 {
|
||||
var wantGroupsAnyType []any
|
||||
for _, group := range wantGroups {
|
||||
@@ -2360,7 +2546,7 @@ func testSupervisorLogin(
|
||||
) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 7*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
// Infer the downstream issuer URL from the callback associated with the upstream test client registration.
|
||||
@@ -2427,7 +2613,7 @@ func testSupervisorLogin(
|
||||
}
|
||||
|
||||
// Create the downstream FederationDomain and expect it to go into the appropriate status condition.
|
||||
downstream := testlib.CreateTestFederationDomain(ctx, t,
|
||||
federationDomain := testlib.CreateTestFederationDomain(ctx, t,
|
||||
supervisorconfigv1alpha1.FederationDomainSpec{
|
||||
Issuer: issuerURL.String(),
|
||||
TLS: &supervisorconfigv1alpha1.FederationDomainTLSSpec{SecretName: certSecret.Name},
|
||||
@@ -2476,7 +2662,7 @@ func testSupervisorLogin(
|
||||
var discovery *coreosoidc.Provider
|
||||
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
|
||||
var err error
|
||||
discovery, err = coreosoidc.NewProvider(oidcHTTPClientContext, downstream.Spec.Issuer)
|
||||
discovery, err = coreosoidc.NewProvider(oidcHTTPClientContext, federationDomain.Spec.Issuer)
|
||||
requireEventually.NoError(err)
|
||||
}, 30*time.Second, 200*time.Millisecond)
|
||||
|
||||
@@ -2527,7 +2713,7 @@ func testSupervisorLogin(
|
||||
downstreamAuthorizeURL := downstreamOAuth2Config.AuthCodeURL(stateParam.String(), authorizeRequestParams...)
|
||||
|
||||
// Perform parameterized auth code acquisition.
|
||||
requestAuthorization(t, downstream.Spec.Issuer, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient)
|
||||
requestAuthorization(t, federationDomain.Spec.Issuer, downstreamAuthorizeURL, localCallbackServer.URL, username, password, httpClient)
|
||||
|
||||
// Expect that our callback handler was invoked.
|
||||
callback, err := localCallbackServer.waitForCallback(10 * time.Second)
|
||||
@@ -2718,6 +2904,7 @@ func testSupervisorLogin(
|
||||
// Should have got an error since the upstream refresh should have failed.
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, `oauth2: "error" "Error during upstream refresh. Upstream refresh failed."`)
|
||||
t.Log("successfully confirmed that breaking the refresh session data caused the refresh to fail")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2884,6 +3071,19 @@ func loginToUpstreamOIDCAndWaitForCallback(t *testing.T, b *browsertest.Browser,
|
||||
b.WaitForURL(t, callbackURLPattern)
|
||||
}
|
||||
|
||||
func loginToUpstreamGitHubAndWaitForCallback(t *testing.T, b *browsertest.Browser, downstreamCallbackURL string) {
|
||||
t.Helper()
|
||||
env := testlib.IntegrationEnv(t)
|
||||
|
||||
// Expect to be redirected to the upstream provider and log in.
|
||||
browsertest.LoginToUpstreamGitHub(t, b, env.SupervisorUpstreamGithub)
|
||||
|
||||
// Wait for the login to happen and us be redirected back to a localhost callback.
|
||||
t.Logf("waiting for redirect to callback")
|
||||
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(downstreamCallbackURL) + `\?.+\z`)
|
||||
b.WaitForURL(t, callbackURLPattern)
|
||||
}
|
||||
|
||||
func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||
t.Helper()
|
||||
|
||||
@@ -2892,6 +3092,14 @@ func requestAuthorizationUsingBrowserAuthcodeFlowOIDC(t *testing.T, _, downstrea
|
||||
loginToUpstreamOIDCAndWaitForCallback(t, browser, downstreamCallbackURL)
|
||||
}
|
||||
|
||||
func requestAuthorizationUsingBrowserAuthcodeFlowGitHub(t *testing.T, _, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||
t.Helper()
|
||||
|
||||
browser := openBrowserAndNavigateToAuthorizeURL(t, downstreamAuthorizeURL, httpClient)
|
||||
|
||||
loginToUpstreamGitHubAndWaitForCallback(t, browser, downstreamCallbackURL)
|
||||
}
|
||||
|
||||
func requestAuthorizationUsingBrowserAuthcodeFlowOIDCWithIDPChooserPage(t *testing.T, downstreamIssuer, downstreamAuthorizeURL, downstreamCallbackURL, _, _ string, httpClient *http.Client) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -93,7 +93,7 @@ func TestAuthorizeCodeStorage(t *testing.T) {
|
||||
// Note that CreateAuthorizeCodeSession() sets Active to true and also sets the Version before storing the session,
|
||||
// so expect those here.
|
||||
session.Active = true
|
||||
session.Version = "7" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant
|
||||
session.Version = "8" // this is the value of the authorizationcode.authorizeCodeStorageVersion constant
|
||||
expectedSessionStorageJSON, err := json.Marshal(session)
|
||||
require.NoError(t, err)
|
||||
require.JSONEq(t, string(expectedSessionStorageJSON), string(initialSecret.Data["pinniped-storage-data"]))
|
||||
|
||||
@@ -28,7 +28,7 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
|
||||
upstream := testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseError)
|
||||
expectUpstreamConditions(t, upstream, []metav1.Condition{
|
||||
{
|
||||
Type: "ClientCredentialsValid",
|
||||
Type: "ClientCredentialsSecretValid",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "SecretNotFound",
|
||||
Message: `secret "does-not-exist" not found`,
|
||||
@@ -60,13 +60,13 @@ Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nananananan
|
||||
AdditionalScopes: []string{"email", "profile"},
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, "test-client-id", "test-client-secret").Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, "test-client-id", "test-client-secret").Name,
|
||||
},
|
||||
}
|
||||
upstream := testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseError)
|
||||
expectUpstreamConditions(t, upstream, []metav1.Condition{
|
||||
{
|
||||
Type: "ClientCredentialsValid",
|
||||
Type: "ClientCredentialsSecretValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: "loaded client credentials",
|
||||
@@ -98,13 +98,13 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su
|
||||
AdditionalScopes: []string{"email", "profile"},
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, "test-client-id", "test-client-secret").Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, "test-client-id", "test-client-secret").Name,
|
||||
},
|
||||
}
|
||||
upstream := testlib.CreateTestOIDCIdentityProvider(t, spec, idpv1alpha1.PhaseReady)
|
||||
expectUpstreamConditions(t, upstream, []metav1.Condition{
|
||||
{
|
||||
Type: "ClientCredentialsValid",
|
||||
Type: "ClientCredentialsSecretValid",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "Success",
|
||||
Message: "loaded client credentials",
|
||||
|
||||
@@ -418,7 +418,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
Groups: env.SupervisorUpstreamOIDC.GroupsClaim,
|
||||
},
|
||||
Client: idpv1alpha1.OIDCClient{
|
||||
SecretName: testlib.CreateClientCredsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
SecretName: testlib.CreateOIDCClientCredentialsSecret(t, env.SupervisorUpstreamOIDC.ClientID, env.SupervisorUpstreamOIDC.ClientSecret).Name,
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, supervisorconfigv1alpha1.FederationDomainPhaseReady)
|
||||
|
||||
Reference in New Issue
Block a user