first draft of an e2e integration test for GitHub login (skip while WIP)

This commit is contained in:
Ryan Richard
2024-05-09 15:35:37 -07:00
parent 6be92f92fb
commit 7c85a511a2
11 changed files with 355 additions and 21 deletions
+22 -1
View File
@@ -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
@@ -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&"+
+84
View File
@@ -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
}
+82 -7
View File
@@ -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)
+1 -1
View File
@@ -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,
},
}
}
+3 -3
View File
@@ -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)
+1 -1
View File
@@ -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)
+80
View File
@@ -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
View File
@@ -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
View File
@@ -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)