mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-07-30 03:52:53 +00:00
first draft of an e2e integration test for GitHub login (skip while WIP)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
# Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#
|
||||
@@ -31,6 +31,7 @@ clean_kind=no
|
||||
api_group_suffix="pinniped.dev" # same default as in the values.yaml ytt file
|
||||
dockerfile_path=""
|
||||
get_active_directory_vars="" # specify a filename for a script to get AD related env variables
|
||||
get_github_vars="" # specify a filename for a script to get GitHub related env variables
|
||||
alternate_deploy="undefined"
|
||||
pre_install="undefined"
|
||||
|
||||
@@ -68,6 +69,16 @@ while (("$#")); do
|
||||
get_active_directory_vars=$1
|
||||
shift
|
||||
;;
|
||||
--get-github-vars)
|
||||
shift
|
||||
# If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error
|
||||
if [[ "$#" == "0" || "$1" == -* ]]; then
|
||||
log_error "--get-github-vars requires a script name to be specified"
|
||||
exit 1
|
||||
fi
|
||||
get_github_vars=$1
|
||||
shift
|
||||
;;
|
||||
--dockerfile-path)
|
||||
shift
|
||||
# If there are no more command line arguments, or there is another command line argument but it starts with a dash, then error
|
||||
@@ -121,6 +132,7 @@ if [[ "$help" == "yes" ]]; then
|
||||
log_note " -g, --api-group-suffix: deploy Pinniped with an alternate API group suffix"
|
||||
log_note " -s, --skip-build: reuse the most recently built image of the app instead of building"
|
||||
log_note " -a, --get-active-directory-vars: specify a script that exports active directory environment variables"
|
||||
log_note " --get-github-vars: specify a script that exports GitHub environment variables"
|
||||
log_note " --alternate-deploy: specify an alternate deploy script to install all components of Pinniped"
|
||||
log_note " --pre-install: specify an pre-install script such as a build script"
|
||||
exit 1
|
||||
@@ -453,6 +465,15 @@ if [[ "$get_active_directory_vars" != "" ]]; then
|
||||
source $get_active_directory_vars
|
||||
fi
|
||||
|
||||
# We can't set up an in-cluster GitHub instance, but
|
||||
# if you have a GitHub account that you wish to run the tests against,
|
||||
# specify a script to set the GitHub environment variables.
|
||||
# You will need to set the environment variables that start with "PINNIPED_TEST_GITHUB_"
|
||||
# found in pinniped/test/testlib/env.go.
|
||||
if [[ "$get_github_vars" != "" ]]; then
|
||||
source $get_github_vars
|
||||
fi
|
||||
|
||||
read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true
|
||||
${pinniped_cluster_capability_file_content}
|
||||
PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF
|
||||
|
||||
+3
@@ -87,6 +87,9 @@ func TestFederationDomainResolvedGitHubIdentityProvider(t *testing.T) {
|
||||
"https://localhost/fake/path",
|
||||
)
|
||||
require.NoError(t, err)
|
||||
// Note that GitHub does not require (or document) the standard response_type=code param, but in manual testing
|
||||
// of GitHub authorize endpoint, it seems to ignore the param. The oauth2 package wants to add the param, so
|
||||
// we will let it.
|
||||
require.Equal(t,
|
||||
"https://fake-authorization-url?"+
|
||||
"client_id=fake-client-id&"+
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
package totp
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha1" //nolint:gosec // This is an implementation of an RFC that used SHA-1
|
||||
"encoding/base32"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This code is borrowed from
|
||||
// https://github.com/yitsushi/totp-cli/blob/b26f5673ae2e5cc682fc1f5ed771cb08a6403283/internal/security/otp.go
|
||||
// and
|
||||
// https://github.com/yitsushi/totp-cli/blob/b26f5673ae2e5cc682fc1f5ed771cb08a6403283/internal/security/error.go
|
||||
// which is MIT licensed. The MIT license allows copying.
|
||||
// We are choosing to copying rather than take on a whole new project dependency just for a small test helper.
|
||||
|
||||
const (
|
||||
mask1 = 0xf
|
||||
mask2 = 0x7f
|
||||
mask3 = 0xff
|
||||
timeSplitInSeconds = 30
|
||||
shift24 = 24
|
||||
shift16 = 16
|
||||
shift8 = 8
|
||||
sumByteLength = 8
|
||||
)
|
||||
|
||||
// OTPError is an error describing an error during generation.
|
||||
type OTPError struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e OTPError) Error() string {
|
||||
return "otp error: " + e.Message
|
||||
}
|
||||
|
||||
// GenerateOTPCode generates a 6 digit TOTP from the secret Token.
|
||||
func GenerateOTPCode(t *testing.T, token string, when time.Time) (string, int64) {
|
||||
t.Helper()
|
||||
|
||||
require.NotEmpty(t, token)
|
||||
|
||||
timer := uint64(math.Floor(float64(when.Unix()) / float64(timeSplitInSeconds)))
|
||||
remainingTime := timeSplitInSeconds - when.Unix()%timeSplitInSeconds
|
||||
|
||||
// Remove spaces, some providers are giving us in a readable format,
|
||||
// so they add spaces in there. If it's not removed while pasting in,
|
||||
// remove it now.
|
||||
token = strings.ReplaceAll(token, " ", "")
|
||||
|
||||
// It should be uppercase always
|
||||
token = strings.ToUpper(token)
|
||||
|
||||
secretBytes, err := base32.StdEncoding.WithPadding(base32.NoPadding).DecodeString(token)
|
||||
require.NoError(t, err)
|
||||
|
||||
length := 6
|
||||
|
||||
buf := make([]byte, sumByteLength)
|
||||
mac := hmac.New(sha1.New, secretBytes)
|
||||
|
||||
binary.BigEndian.PutUint64(buf, timer)
|
||||
_, _ = mac.Write(buf)
|
||||
sum := mac.Sum(nil)
|
||||
|
||||
// http://tools.ietf.org/html/rfc4226#section-5.4
|
||||
offset := sum[len(sum)-1] & mask1
|
||||
value := int64(((int(sum[offset]) & mask2) << shift24) |
|
||||
((int(sum[offset+1] & mask3)) << shift16) |
|
||||
((int(sum[offset+2] & mask3)) << shift8) |
|
||||
(int(sum[offset+3]) & mask3))
|
||||
|
||||
modulo := int32(value % int64(math.Pow10(length)))
|
||||
|
||||
format := fmt.Sprintf("%%0%dd", length)
|
||||
|
||||
return fmt.Sprintf(format, modulo), remainingTime
|
||||
}
|
||||
@@ -158,7 +158,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, configv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -244,7 +244,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, configv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -332,7 +332,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, configv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -456,7 +456,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, configv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -587,7 +587,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, configv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -660,7 +660,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, configv1alpha1.FederationDomainPhaseReady)
|
||||
@@ -1219,6 +1219,81 @@ 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) {
|
||||
// TODO only skip this test when the GitHub test env vars are not set
|
||||
t.Skip("always skipping for now, this test is still a work in progress and it always fails at the moment")
|
||||
|
||||
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)
|
||||
|
||||
// TODO create clusterrolebinding for expected user and WaitForUserToHaveAccess. doesn't matter until login fully works.
|
||||
|
||||
// Create upstream GitHub provider and wait for it to become ready.
|
||||
// TODO use return value when calling requireUserCanUseKubectlWithoutAuthenticatingAgain below
|
||||
_ = testlib.CreateTestGitHubIdentityProvider(t, idpv1alpha1.GitHubIdentityProviderSpec{
|
||||
AllowAuthentication: idpv1alpha1.GitHubAllowAuthenticationSpec{
|
||||
Organizations: idpv1alpha1.GitHubOrganizationsSpec{
|
||||
Policy: ptr.To(idpv1alpha1.GitHubAllowedAuthOrganizationsPolicyAllGitHubUsers),
|
||||
},
|
||||
},
|
||||
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.
|
||||
// TODO use return value when calling requireKubectlGetNamespaceOutput below
|
||||
_ = 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)))
|
||||
|
||||
// TODO When you turn off headless and watch this test run,
|
||||
// the browser is indeed redirected back to the Supervisor at this point with a code,
|
||||
// but the Supervisor's callback endpoint does not yet work for github IDPs so it returns an error page,
|
||||
// and the Supervisor's form_post page is not loaded, so it does not automatically post the callback to the CLI's callback listener.
|
||||
// The test eventually times out and fails at this point.
|
||||
|
||||
// TODO
|
||||
// formpostExpectSuccessState
|
||||
// requireKubectlGetNamespaceOutput
|
||||
// requireUserCanUseKubectlWithoutAuthenticatingAgain
|
||||
})
|
||||
|
||||
t.Run("with multiple IDPs: one OIDC and one LDAP", func(t *testing.T) {
|
||||
testlib.SkipTestWhenLDAPIsUnavailable(t, env)
|
||||
|
||||
@@ -1280,7 +1355,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)
|
||||
|
||||
|
||||
@@ -828,7 +828,7 @@ func requireIDPsListedByIDPDiscoveryEndpoint(
|
||||
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)
|
||||
|
||||
|
||||
@@ -68,7 +68,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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
@@ -60,7 +60,7 @@ Get "https://127.0.0.1:444444/invalid-url-that-is-really-really-long-nananananan
|
||||
AdditionalScopes: []string{"email", "profile"},
|
||||
},
|
||||
Client: v1alpha1.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, v1alpha1.PhaseError)
|
||||
@@ -98,7 +98,7 @@ oidc: issuer did not match the issuer returned by provider, expected "` + env.Su
|
||||
AdditionalScopes: []string{"email", "profile"},
|
||||
},
|
||||
Client: v1alpha1.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, v1alpha1.PhaseReady)
|
||||
|
||||
@@ -417,7 +417,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, configv1alpha1.FederationDomainPhaseReady)
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/testutil/totp"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
@@ -357,6 +358,85 @@ func LoginToUpstreamOIDC(t *testing.T, b *Browser, upstream testlib.TestOIDCUpst
|
||||
b.ClickFirstMatch(t, cfg.LoginButtonSelector)
|
||||
}
|
||||
|
||||
// LoginToUpstreamGitHub expects the page to be redirected to GitHub.
|
||||
// It knows how to enter the test username/password and submit the upstream login form.
|
||||
func LoginToUpstreamGitHub(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) {
|
||||
t.Helper()
|
||||
|
||||
// Expect to be redirected to the login page.
|
||||
t.Logf("waiting for redirect to GitHub login page")
|
||||
b.WaitForURL(t, regexp.MustCompile(`\Ahttps://github\.com/login.+\z`))
|
||||
|
||||
usernameSelector := "input#login_field"
|
||||
passwordSelector := "input#password"
|
||||
loginButtonSelector := "input[type=submit]"
|
||||
|
||||
// Wait for the login page to be rendered.
|
||||
b.WaitForVisibleElements(t, usernameSelector, passwordSelector, loginButtonSelector)
|
||||
|
||||
// Fill in the username and password and click "submit".
|
||||
t.Logf("logging into GitHub")
|
||||
b.SendKeysToFirstMatch(t, usernameSelector, upstream.TestUserUsername)
|
||||
b.SendKeysToFirstMatch(t, passwordSelector, upstream.TestUserPassword)
|
||||
b.ClickFirstMatch(t, loginButtonSelector)
|
||||
|
||||
// Next, GitHub should go to a new page and prompt for the six digit MFA/OTP code.
|
||||
otpSelector := "input#app_totp"
|
||||
|
||||
// Wait for the MFA page to be rendered.
|
||||
t.Logf("waiting for GitHub MFA page")
|
||||
b.WaitForVisibleElements(t, otpSelector)
|
||||
|
||||
code, codeRemainingLifetimeSeconds := totp.GenerateOTPCode(t, upstream.TestUserOTPSecret, time.Now())
|
||||
if codeRemainingLifetimeSeconds < 2 {
|
||||
t.Log("sleeping for 2 seconds before generating another OTP code")
|
||||
time.Sleep(2 * time.Second)
|
||||
code, _ = totp.GenerateOTPCode(t, upstream.TestUserOTPSecret, time.Now())
|
||||
}
|
||||
|
||||
// Fill in the OTP code. We do not need to click "verify" because entering the code automatically submits the page.
|
||||
t.Logf("entering GitHub OTP code")
|
||||
b.SendKeysToFirstMatch(t, otpSelector, code)
|
||||
|
||||
t.Log("sleeping for 2 seconds before looking at page title")
|
||||
time.Sleep(2 * time.Second)
|
||||
pageTitle := b.Title(t)
|
||||
t.Logf("saw page title %q", pageTitle)
|
||||
|
||||
// Next Github might go to another page asking if you authorize the GitHub App to act on your behalf,
|
||||
// if this user has never authorized this app.
|
||||
if strings.HasPrefix(pageTitle, "Authorize ") { // the title is "Authorize <App Name>"
|
||||
// Wait for the authorize app page to be rendered.
|
||||
t.Logf("waiting for GitHub authorize button")
|
||||
// There are unfortunately two very similar buttons on this page:
|
||||
// <button name="authorize" value="0" type="submit" data-view-component="true" class="ws-normal btn width-full mr-2">Cancel
|
||||
// <button name="authorize" value="1" type="submit" data-view-component="true" class="js-oauth-authorize-btn ws-normal btn-primary btn width-full">Authorize
|
||||
submitAuthorizeAppButtonSelector := "button.btn-primary"
|
||||
b.WaitForVisibleElements(t, submitAuthorizeAppButtonSelector)
|
||||
t.Logf("clicking authorize button")
|
||||
b.ClickFirstMatch(t, submitAuthorizeAppButtonSelector)
|
||||
|
||||
t.Log("sleeping for 2 seconds before looking at page title again")
|
||||
time.Sleep(2 * time.Second)
|
||||
pageTitle = b.Title(t)
|
||||
t.Logf("saw page title %q", pageTitle)
|
||||
}
|
||||
|
||||
// TODO I only saw this happen once, so I did not get a chance to finish this code. Not sure if it will happen again?
|
||||
// Next GitHub might ask if we want to configure a passkey for auth.
|
||||
if strings.HasPrefix(pageTitle, "Passkey TODO GET THIS PAGE TITLE") {
|
||||
// The link that we want to click looks like this:
|
||||
// <input class="btn-link" type="submit" value="Don't ask again for this browser">
|
||||
dontAskAgainLinkSelector := `input[value="Don't ask again for this browser"]`
|
||||
// Wait for the passkey page to be rendered.
|
||||
t.Logf("waiting for GitHub's don't ask again button")
|
||||
b.WaitForVisibleElements(t, dontAskAgainLinkSelector)
|
||||
// Tell it that we do not want to use a passkey.
|
||||
t.Logf("clicking don't ask again button")
|
||||
b.ClickFirstMatch(t, dontAskAgainLinkSelector)
|
||||
}
|
||||
}
|
||||
|
||||
// LoginToUpstreamLDAP expects the page to be redirected to the Supervisor's login UI for an LDAP/AD IDP.
|
||||
// It knows how to enter the test username/password and submit the upstream login form.
|
||||
func LoginToUpstreamLDAP(t *testing.T, b *Browser, issuer, username, password string) {
|
||||
|
||||
+56
-2
@@ -497,13 +497,23 @@ func CreateTestSecretBytes(t *testing.T, namespace string, baseName string, secr
|
||||
return created
|
||||
}
|
||||
|
||||
func CreateClientCredsSecret(t *testing.T, clientID string, clientSecret string) *corev1.Secret {
|
||||
func CreateOIDCClientCredentialsSecret(t *testing.T, clientID string, clientSecret string) *corev1.Secret {
|
||||
t.Helper()
|
||||
return createClientCredentialsSecret(t, clientID, clientSecret, "secrets.pinniped.dev/oidc-client")
|
||||
}
|
||||
|
||||
func CreateGitHubClientCredentialsSecret(t *testing.T, clientID string, clientSecret string) *corev1.Secret {
|
||||
t.Helper()
|
||||
return createClientCredentialsSecret(t, clientID, clientSecret, "secrets.pinniped.dev/github-client")
|
||||
}
|
||||
|
||||
func createClientCredentialsSecret(t *testing.T, clientID string, clientSecret string, secretType string) *corev1.Secret {
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
return CreateTestSecret(t,
|
||||
env.SupervisorNamespace,
|
||||
"client-creds",
|
||||
"secrets.pinniped.dev/oidc-client",
|
||||
corev1.SecretType(secretType),
|
||||
map[string]string{
|
||||
"clientID": clientID,
|
||||
"clientSecret": clientSecret,
|
||||
@@ -585,6 +595,50 @@ func createOIDCClientSecret(t *testing.T, forOIDCClient *configv1alpha1.OIDCClie
|
||||
return generatedSecret
|
||||
}
|
||||
|
||||
func CreateTestGitHubIdentityProvider(t *testing.T, spec idpv1alpha1.GitHubIdentityProviderSpec, expectedPhase idpv1alpha1.GitHubIdentityProviderPhase) *idpv1alpha1.GitHubIdentityProvider {
|
||||
t.Helper()
|
||||
return CreateTestGitHubIdentityProviderWithObjectMeta(t, spec, TestObjectMeta(t, "upstream-github-idp"), expectedPhase)
|
||||
}
|
||||
|
||||
func CreateTestGitHubIdentityProviderWithObjectMeta(t *testing.T, spec idpv1alpha1.GitHubIdentityProviderSpec, objectMeta metav1.ObjectMeta, expectedPhase idpv1alpha1.GitHubIdentityProviderPhase) *idpv1alpha1.GitHubIdentityProvider {
|
||||
t.Helper()
|
||||
env := IntegrationEnv(t)
|
||||
client := NewSupervisorClientset(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
||||
defer cancel()
|
||||
|
||||
upstreams := client.IDPV1alpha1().GitHubIdentityProviders(env.SupervisorNamespace)
|
||||
|
||||
// Create the GitHubIdentityProvider using GenerateName to get a random name.
|
||||
created, err := upstreams.Create(ctx, &idpv1alpha1.GitHubIdentityProvider{
|
||||
ObjectMeta: objectMeta,
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Always clean this up after this point.
|
||||
t.Cleanup(func() {
|
||||
t.Logf("cleaning up test GitHubIdentityProvider %s/%s", created.Namespace, created.Name)
|
||||
err := upstreams.Delete(context.Background(), created.Name, metav1.DeleteOptions{})
|
||||
notFound := k8serrors.IsNotFound(err)
|
||||
// It's okay if it is not found, because it might have been deleted by another part of this test.
|
||||
if !notFound {
|
||||
require.NoErrorf(t, err, "could not cleanup test GitHubIdentityProvider %s/%s", created.Namespace, created.Name)
|
||||
}
|
||||
})
|
||||
t.Logf("created test GitHubIdentityProvider %s", created.Name)
|
||||
|
||||
// Wait for the GitHubIdentityProvider to enter the expected phase (or time out).
|
||||
var result *idpv1alpha1.GitHubIdentityProvider
|
||||
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
|
||||
var err error
|
||||
result, err = upstreams.Get(ctx, created.Name, metav1.GetOptions{})
|
||||
requireEventually.NoErrorf(err, "error while getting GitHubIdentityProvider %s/%s", created.Namespace, created.Name)
|
||||
requireEventually.Equal(expectedPhase, result.Status.Phase)
|
||||
}, 60*time.Second, 1*time.Second, "expected the GitHubIdentityProvider to go into phase %s, GitHubIdentityProvider was: %s", expectedPhase, Sdump(result))
|
||||
return result
|
||||
}
|
||||
|
||||
func CreateTestOIDCIdentityProvider(t *testing.T, spec idpv1alpha1.OIDCIdentityProviderSpec, expectedPhase idpv1alpha1.OIDCIdentityProviderPhase) *idpv1alpha1.OIDCIdentityProvider {
|
||||
t.Helper()
|
||||
return CreateTestOIDCIdentityProviderWithObjectMeta(t, spec, TestObjectMeta(t, "upstream-oidc-idp"), expectedPhase)
|
||||
|
||||
+22
-5
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testlib
|
||||
@@ -62,10 +62,11 @@ type TestEnv struct {
|
||||
ExpectedGroups []string `json:"expectedGroups"`
|
||||
} `json:"testUser"`
|
||||
|
||||
CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"`
|
||||
SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"`
|
||||
SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"`
|
||||
SupervisorUpstreamActiveDirectory TestLDAPUpstream `json:"supervisorActiveDirectoryUpstream"`
|
||||
CLIUpstreamOIDC TestOIDCUpstream `json:"cliOIDCUpstream"`
|
||||
SupervisorUpstreamOIDC TestOIDCUpstream `json:"supervisorOIDCUpstream"`
|
||||
SupervisorUpstreamLDAP TestLDAPUpstream `json:"supervisorLDAPUpstream"`
|
||||
SupervisorUpstreamActiveDirectory TestLDAPUpstream `json:"supervisorActiveDirectoryUpstream"`
|
||||
SupervisorUpstreamGithub TestGithubUpstream `json:"supervisorGithubUpstream"`
|
||||
}
|
||||
|
||||
type TestOIDCUpstream struct {
|
||||
@@ -110,6 +111,14 @@ type TestLDAPUpstream struct {
|
||||
TestDeactivatedUserPassword string `json:"TestDeactivatedUserPassword"`
|
||||
}
|
||||
|
||||
type TestGithubUpstream struct {
|
||||
GithubAppClientID string `json:"githubAppClientId"`
|
||||
GithubAppClientSecret string `json:"githubAppClientSecret"`
|
||||
TestUserUsername string `json:"testUserUsername"`
|
||||
TestUserPassword string `json:"testUserPassword"`
|
||||
TestUserOTPSecret string `json:"testUserOTPSecret"`
|
||||
}
|
||||
|
||||
// ProxyEnv returns a set of environment variable strings (e.g., to combine with os.Environ()) which set up the configured test HTTP proxy.
|
||||
func (e *TestEnv) ProxyEnv() []string {
|
||||
if e.Proxy == "" {
|
||||
@@ -319,6 +328,14 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
|
||||
GroupSearchBase: wantEnv("PINNIPED_TEST_AD_USERS_DN", ""),
|
||||
}
|
||||
|
||||
result.SupervisorUpstreamGithub = TestGithubUpstream{
|
||||
GithubAppClientID: wantEnv("PINNIPED_TEST_GITHUB_APP_CLIENT_ID", ""),
|
||||
GithubAppClientSecret: wantEnv("PINNIPED_TEST_GITHUB_APP_CLIENT_SECRET", ""),
|
||||
TestUserUsername: wantEnv("PINNIPED_TEST_GITHUB_USER_USERNAME", ""),
|
||||
TestUserPassword: wantEnv("PINNIPED_TEST_GITHUB_USER_PASSWORD", ""),
|
||||
TestUserOTPSecret: wantEnv("PINNIPED_TEST_GITHUB_USER_OTP_SECRET", ""),
|
||||
}
|
||||
|
||||
sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs)
|
||||
sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs)
|
||||
sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs)
|
||||
|
||||
Reference in New Issue
Block a user