mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-08 07:11:53 +00:00
Merge branch 'main' into jtc/add-importas-linter
This commit is contained in:
@@ -5,7 +5,10 @@
|
||||
package browsertest
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"regexp"
|
||||
@@ -16,10 +19,12 @@ import (
|
||||
"time"
|
||||
|
||||
chromedpbrowser "github.com/chromedp/cdproto/browser"
|
||||
"github.com/chromedp/cdproto/dom"
|
||||
chromedpruntime "github.com/chromedp/cdproto/runtime"
|
||||
"github.com/chromedp/chromedp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"go.pinniped.dev/internal/testutil/totp"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
@@ -162,12 +167,52 @@ func OpenBrowser(t *testing.T) *Browser {
|
||||
for _, e := range b.exceptionEvents {
|
||||
t.Logf("exception: %s", e)
|
||||
}
|
||||
|
||||
// If the test failed, dump helpful debugging info from the browser's final page.
|
||||
if t.Failed() {
|
||||
b.dumpPage(t)
|
||||
}
|
||||
})
|
||||
|
||||
// Done. The browser is ready to be driven by the test.
|
||||
return b
|
||||
}
|
||||
|
||||
func (b *Browser) dumpPage(t *testing.T) {
|
||||
// Log the URL of the current page.
|
||||
var url string
|
||||
b.runWithTimeout(t, b.timeout(), chromedp.Location(&url))
|
||||
t.Logf("Browser URL from end of test %q: %s", t.Name(), url)
|
||||
|
||||
// Log the title of the current page.
|
||||
t.Logf("Browser page title from end of test %q: %q", t.Name(), b.Title(t))
|
||||
|
||||
// Log a screenshot of the current page.
|
||||
var screenBuf []byte
|
||||
b.runWithTimeout(t, b.timeout(), chromedp.FullScreenshot(&screenBuf, 10)) // low quality to make it smaller
|
||||
t.Logf("Browser screenshot (base64 encoded jpeg format) from end of test %q:\n%s\n",
|
||||
t.Name(), base64.StdEncoding.EncodeToString(screenBuf))
|
||||
|
||||
// Log the HTML of the current page.
|
||||
var html string
|
||||
b.runWithTimeout(t, b.timeout(), chromedp.ActionFunc(func(ctx context.Context) error {
|
||||
node, err := dom.GetDocument().Do(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
html, err = dom.GetOuterHTML().WithNodeID(node.NodeID).Do(ctx)
|
||||
return err
|
||||
}))
|
||||
var htmlBuf bytes.Buffer
|
||||
gz := gzip.NewWriter(&htmlBuf)
|
||||
_, err := gz.Write([]byte(html))
|
||||
require.NoError(t, err)
|
||||
err = gz.Close()
|
||||
require.NoError(t, err)
|
||||
t.Logf("Browser html (gzip and base64 encoded) from end of test %q:\n%s\n",
|
||||
t.Name(), base64.StdEncoding.EncodeToString(htmlBuf.Bytes()))
|
||||
}
|
||||
|
||||
func (b *Browser) timeout() time.Duration {
|
||||
return 30 * time.Second
|
||||
}
|
||||
@@ -357,6 +402,154 @@ 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)
|
||||
|
||||
handleGithubOTPLoginPage(t, b, upstream)
|
||||
|
||||
// Keep looping until we get to a page that we do not know how to handle. Then return to allow the test to move on.
|
||||
for handleOccasionalGithubLoginPage(t, b, upstream) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
func handleGithubOTPLoginPage(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) {
|
||||
// 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)
|
||||
|
||||
// Sleep for a bit to make it less likely that we use the same OTP code twice when multiple tests are run in serial.
|
||||
// GitHub gets upset when the same OTP code gets reused.
|
||||
// GitHub seems to also get upset when any OTP codes are used often, like when all our GitHub tests run sequentially,
|
||||
// because sometimes auth will go to a GitHub page that says: "We were unable to authenticate your request because too
|
||||
// many codes have been submitted. Please wait a few minutes and contact support if you continue to have problems."
|
||||
otpSleepSeconds := 60
|
||||
t.Logf("sleeping %d seconds before generating a GitHub OTP code", otpSleepSeconds)
|
||||
time.Sleep(time.Duration(otpSleepSeconds) * time.Second)
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// handleOccasionalGithubLoginPage handles the interstitial pages which GitHub might show during a login flow.
|
||||
// None of these will always happen.
|
||||
func handleOccasionalGithubLoginPage(t *testing.T, b *Browser, upstream testlib.TestGithubUpstream) bool {
|
||||
t.Helper()
|
||||
|
||||
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)
|
||||
lowercaseTitle := strings.ToLower(pageTitle)
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(lowercaseTitle, "authorize "): // the title is "Authorize <App Name>"
|
||||
// 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.
|
||||
// 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)
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(lowercaseTitle, "confirm your account recovery settings"):
|
||||
// Next GitHub might occasionally as you to confirm your recovery settings.
|
||||
// Wait for the page to be rendered.
|
||||
t.Logf("waiting for GitHub confirm button")
|
||||
// There are several buttons and links. We want to click this confirm button to confirm our settings:
|
||||
// <button type="submit" name="type" value="confirmed" class="btn btn-block btn-primary ml-3">Confirm</button>
|
||||
submitConfirmButtonSelector := "button.btn-primary"
|
||||
b.WaitForVisibleElements(t, submitConfirmButtonSelector)
|
||||
t.Logf("clicking confirm button")
|
||||
b.ClickFirstMatch(t, submitConfirmButtonSelector)
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(lowercaseTitle, "verify two-factor authentication"):
|
||||
// Next GitHub might occasionally as you to confirm your MFA settings.
|
||||
// Wait for the page to be rendered.
|
||||
t.Logf("waiting for GitHub skip link")
|
||||
// There are several buttons and links. We want to click this link to "skip 2FA verification":
|
||||
// <button type="submit" data-view-component="true" class="Button--link Button--medium Button">
|
||||
submitSkipButtonSelector := "button.Button--link[type=submit]"
|
||||
b.WaitForVisibleElements(t, submitSkipButtonSelector)
|
||||
t.Logf("clicking skip link")
|
||||
b.ClickFirstMatch(t, submitSkipButtonSelector)
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(lowercaseTitle, "configure passwordless authentication"):
|
||||
// Next GitHub might occasionally ask if we want to configure a passkey for auth.
|
||||
// The URL bar shows https://github.com/sessions/trusted-device for this page.
|
||||
// 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)
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(lowercaseTitle, "two-factor authentication"):
|
||||
// Sometimes this happens after the OTP page when we try to use the same OTP code again too quickly.
|
||||
// GitHub stays on the same page and shows an error banner saying that we used the same code again.
|
||||
// Sleep for a long time to try to avoid this error from GitHub, which seems to be some type of rate limiting on OTP codes:
|
||||
// "We were unable to authenticate your request because too many codes have been submitted".
|
||||
otpSleepSeconds := 60
|
||||
t.Logf("sleeping %d seconds before generating another GitHub OTP code after a previous code failed", otpSleepSeconds)
|
||||
time.Sleep(time.Duration(otpSleepSeconds) * time.Second)
|
||||
handleGithubOTPLoginPage(t, b, upstream)
|
||||
return true
|
||||
|
||||
case strings.HasPrefix(lowercaseTitle, "server error"):
|
||||
// Sometimes this happens after the OTP page. Not sure why. The page has a cute cartoon, but no helpful information.
|
||||
// The URL bar shows https://github.com/sessions/trusted-device for this error page, which is the URL that usually
|
||||
// asks if you want to configure passwordless authentication (aka passkey).
|
||||
t.Fatal("Got GitHub server internal error page during login flow. This is not expected, but is unfortunately unrecoverable.")
|
||||
return false // we recognized the title, but we don't know how to handle this page because it has no buttons or other way forward
|
||||
|
||||
default:
|
||||
// We did not know how to handle the page given its title.
|
||||
// Maybe we successfully got through all the interstitial pages and finished the login.
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -34,6 +34,7 @@ import (
|
||||
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
|
||||
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
|
||||
supervisorclientset "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned"
|
||||
alpha1 "go.pinniped.dev/generated/latest/client/supervisor/clientset/versioned/typed/idp/v1alpha1"
|
||||
"go.pinniped.dev/internal/groupsuffix"
|
||||
"go.pinniped.dev/internal/kubeclient"
|
||||
|
||||
@@ -185,7 +186,7 @@ func CreateTestWebhookAuthenticator(
|
||||
defer cancel()
|
||||
|
||||
webhook, err := webhooks.Create(createContext, &authenticationv1alpha1.WebhookAuthenticator{
|
||||
ObjectMeta: testObjectMeta(t, "webhook"),
|
||||
ObjectMeta: TestObjectMeta(t, "webhook"),
|
||||
Spec: *webhookSpec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "could not create test WebhookAuthenticator")
|
||||
@@ -294,7 +295,7 @@ func CreateTestJWTAuthenticator(
|
||||
defer cancel()
|
||||
|
||||
jwtAuthenticator, err := jwtAuthenticators.Create(createContext, &authenticationv1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: testObjectMeta(t, "jwt-authenticator"),
|
||||
ObjectMeta: TestObjectMeta(t, "jwt-authenticator"),
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "could not create test JWTAuthenticator")
|
||||
@@ -368,7 +369,7 @@ func CreateTestFederationDomain(
|
||||
|
||||
federationDomainsClient := NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(testEnv.SupervisorNamespace)
|
||||
federationDomain, err := federationDomainsClient.Create(createContext, &supervisorconfigv1alpha1.FederationDomain{
|
||||
ObjectMeta: testObjectMeta(t, "oidc-provider"),
|
||||
ObjectMeta: TestObjectMeta(t, "oidc-provider"),
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err, "could not create test FederationDomain")
|
||||
@@ -459,7 +460,7 @@ func CreateTestSecret(t *testing.T, namespace string, baseName string, secretTyp
|
||||
defer cancel()
|
||||
|
||||
created, err := client.CoreV1().Secrets(namespace).Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: testObjectMeta(t, baseName),
|
||||
ObjectMeta: TestObjectMeta(t, baseName),
|
||||
Type: secretType,
|
||||
StringData: stringData,
|
||||
}, metav1.CreateOptions{})
|
||||
@@ -481,7 +482,7 @@ func CreateTestSecretBytes(t *testing.T, namespace string, baseName string, secr
|
||||
defer cancel()
|
||||
|
||||
created, err := client.CoreV1().Secrets(namespace).Create(ctx, &corev1.Secret{
|
||||
ObjectMeta: testObjectMeta(t, baseName),
|
||||
ObjectMeta: TestObjectMeta(t, baseName),
|
||||
Type: secretType,
|
||||
Data: data,
|
||||
}, metav1.CreateOptions{})
|
||||
@@ -496,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,
|
||||
@@ -584,9 +595,53 @@ func createOIDCClientSecret(t *testing.T, forOIDCClient *supervisorconfigv1alpha
|
||||
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)
|
||||
return CreateTestOIDCIdentityProviderWithObjectMeta(t, spec, TestObjectMeta(t, "upstream-oidc-idp"), expectedPhase)
|
||||
}
|
||||
|
||||
func CreateTestOIDCIdentityProviderWithObjectMeta(t *testing.T, spec idpv1alpha1.OIDCIdentityProviderSpec, objectMeta metav1.ObjectMeta, expectedPhase idpv1alpha1.OIDCIdentityProviderPhase) *idpv1alpha1.OIDCIdentityProvider {
|
||||
@@ -639,7 +694,7 @@ func CreateTestLDAPIdentityProvider(t *testing.T, spec idpv1alpha1.LDAPIdentityP
|
||||
|
||||
// Create the LDAPIdentityProvider using GenerateName to get a random name.
|
||||
created, err := upstreams.Create(ctx, &idpv1alpha1.LDAPIdentityProvider{
|
||||
ObjectMeta: testObjectMeta(t, "upstream-ldap-idp"),
|
||||
ObjectMeta: TestObjectMeta(t, "upstream-ldap-idp"),
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -680,7 +735,7 @@ func CreateTestActiveDirectoryIdentityProvider(t *testing.T, spec idpv1alpha1.Ac
|
||||
|
||||
// Create the ActiveDirectoryIdentityProvider using GenerateName to get a random name.
|
||||
created, err := upstreams.Create(ctx, &idpv1alpha1.ActiveDirectoryIdentityProvider{
|
||||
ObjectMeta: testObjectMeta(t, "upstream-ad-idp"),
|
||||
ObjectMeta: TestObjectMeta(t, "upstream-ad-idp"),
|
||||
Spec: spec,
|
||||
}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -710,6 +765,41 @@ func CreateTestActiveDirectoryIdentityProvider(t *testing.T, spec idpv1alpha1.Ac
|
||||
return result
|
||||
}
|
||||
|
||||
func CreateGitHubIdentityProvider(t *testing.T, spec idpv1alpha1.GitHubIdentityProviderSpec, 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: TestObjectMeta(t, "upstream-github-idp"),
|
||||
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{})
|
||||
require.NoError(t, err)
|
||||
})
|
||||
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 CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef rbacv1.RoleRef) *rbacv1.ClusterRoleBinding {
|
||||
t.Helper()
|
||||
client := NewKubernetesClientset(t)
|
||||
@@ -720,7 +810,7 @@ func CreateTestClusterRoleBinding(t *testing.T, subject rbacv1.Subject, roleRef
|
||||
|
||||
// Create the ClusterRoleBinding using GenerateName to get a random name.
|
||||
created, err := clusterRoles.Create(ctx, &rbacv1.ClusterRoleBinding{
|
||||
ObjectMeta: testObjectMeta(t, "cluster-role"),
|
||||
ObjectMeta: TestObjectMeta(t, "cluster-role"),
|
||||
Subjects: []rbacv1.Subject{subject},
|
||||
RoleRef: roleRef,
|
||||
}, metav1.CreateOptions{})
|
||||
@@ -771,7 +861,7 @@ func CreatePod(ctx context.Context, t *testing.T, name, namespace string, spec c
|
||||
ctx, cancel := context.WithTimeout(ctx, podCreateTimeout+time.Minute)
|
||||
defer cancel()
|
||||
|
||||
created, err := pods.Create(ctx, &corev1.Pod{ObjectMeta: testObjectMeta(t, name), Spec: spec}, metav1.CreateOptions{})
|
||||
created, err := pods.Create(ctx, &corev1.Pod{ObjectMeta: TestObjectMeta(t, name), Spec: spec}, metav1.CreateOptions{})
|
||||
require.NoError(t, err)
|
||||
t.Logf("created test Pod %s", created.Name)
|
||||
|
||||
@@ -836,7 +926,60 @@ func WaitForUserToHaveAccess(t *testing.T, user string, groups []string, shouldH
|
||||
}, time.Minute, 500*time.Millisecond)
|
||||
}
|
||||
|
||||
func testObjectMeta(t *testing.T, baseName string) metav1.ObjectMeta {
|
||||
func WaitForGitHubIDPPhase(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
client alpha1.GitHubIdentityProviderInterface,
|
||||
gitHubIDPName string,
|
||||
expectPhase idpv1alpha1.GitHubIdentityProviderPhase,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
|
||||
idp, err := client.Get(ctx, gitHubIDPName, metav1.GetOptions{})
|
||||
requireEventually.NoError(err)
|
||||
requireEventually.Equalf(expectPhase, idp.Status.Phase, "actual status conditions were: %#v", idp.Status.Conditions)
|
||||
}, 60*time.Second, 1*time.Second, "expected the GitHubIDP to have status %q", expectPhase)
|
||||
}
|
||||
|
||||
func WaitForGitHubIdentityProviderStatusConditions(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
client alpha1.GitHubIdentityProviderInterface,
|
||||
gitHubIDPName string,
|
||||
expectConditions []*metav1.Condition,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
|
||||
idp, err := client.Get(ctx, gitHubIDPName, metav1.GetOptions{})
|
||||
requireEventually.NoError(err)
|
||||
|
||||
actualConditions := make([]*metav1.Condition, len(idp.Status.Conditions))
|
||||
for i, c := range idp.Status.Conditions {
|
||||
actualConditions[i] = c.DeepCopy()
|
||||
}
|
||||
|
||||
requireEventually.Lenf(actualConditions, len(expectConditions),
|
||||
"wanted status conditions: %#v", expectConditions)
|
||||
|
||||
for i, wantCond := range expectConditions {
|
||||
actualCond := actualConditions[i]
|
||||
|
||||
// This is a cheat to avoid needing to make equality assertions on these fields.
|
||||
requireEventually.NotZero(actualCond.LastTransitionTime)
|
||||
wantCond.LastTransitionTime = actualCond.LastTransitionTime
|
||||
requireEventually.NotZero(actualCond.ObservedGeneration)
|
||||
wantCond.ObservedGeneration = actualCond.ObservedGeneration
|
||||
|
||||
requireEventually.Equalf(wantCond, actualCond,
|
||||
"wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d",
|
||||
expectConditions, &actualConditions, i)
|
||||
}
|
||||
}, 60*time.Second, 1*time.Second, "wanted conditions for GitHubIdentityProvider %q", gitHubIDPName)
|
||||
}
|
||||
|
||||
func TestObjectMeta(t *testing.T, baseName string) metav1.ObjectMeta {
|
||||
return metav1.ObjectMeta{
|
||||
GenerateName: fmt.Sprintf("test-%s-", baseName),
|
||||
Labels: map[string]string{"pinniped.dev/test": ""},
|
||||
|
||||
@@ -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,21 @@ type TestLDAPUpstream struct {
|
||||
TestDeactivatedUserPassword string `json:"TestDeactivatedUserPassword"`
|
||||
}
|
||||
|
||||
type TestGithubUpstream struct {
|
||||
GithubAppClientID string `json:"githubAppClientId"` // GitHub's new-style GitHub App
|
||||
GithubAppClientSecret string `json:"githubAppClientSecret"`
|
||||
GithubOAuthAppClientID string `json:"githubOAuthAppClientId"` // GitHub's old-style OAuth App
|
||||
GithubOAuthAppClientSecret string `json:"githubOAuthAppClientSecret"`
|
||||
GithubOAuthAppAllowedCallbackURL string `json:"githubOAuthAppAllowedCallbackURL"` // the callback URL that was configured in GitHub for this App
|
||||
TestUserUsername string `json:"testUserUsername"` // the "login" attribute value for the user
|
||||
TestUserPassword string `json:"testUserPassword"`
|
||||
TestUserOTPSecret string `json:"testUserOTPSecret"`
|
||||
TestUserID string `json:"testUserID"` // the "id" attribute value for the user
|
||||
TestUserOrganization string `json:"testUserOrganization"` // an org to which the user belongs
|
||||
TestUserExpectedTeamNames []string `json:"testUserExpectedTeamNames"`
|
||||
TestUserExpectedTeamSlugs []string `json:"testUserExpectedTeamSlugs"`
|
||||
}
|
||||
|
||||
// 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,11 +335,28 @@ 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", ""),
|
||||
GithubOAuthAppClientID: wantEnv("PINNIPED_TEST_GITHUB_OAUTH_APP_CLIENT_ID", ""),
|
||||
GithubOAuthAppClientSecret: wantEnv("PINNIPED_TEST_GITHUB_OAUTH_APP_CLIENT_SECRET", ""),
|
||||
GithubOAuthAppAllowedCallbackURL: wantEnv("PINNIPED_TEST_GITHUB_OAUTH_APP_ALLOWED_CALLBACK_URL", ""),
|
||||
TestUserUsername: wantEnv("PINNIPED_TEST_GITHUB_USER_USERNAME", ""),
|
||||
TestUserPassword: wantEnv("PINNIPED_TEST_GITHUB_USER_PASSWORD", ""),
|
||||
TestUserOTPSecret: wantEnv("PINNIPED_TEST_GITHUB_USER_OTP_SECRET", ""),
|
||||
TestUserID: wantEnv("PINNIPED_TEST_GITHUB_USERID", ""),
|
||||
TestUserOrganization: wantEnv("PINNIPED_TEST_GITHUB_ORG", ""),
|
||||
TestUserExpectedTeamNames: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_GITHUB_EXPECTED_TEAM_NAMES", ""), ",")),
|
||||
TestUserExpectedTeamSlugs: filterEmpty(strings.Split(wantEnv("PINNIPED_TEST_GITHUB_EXPECTED_TEAM_SLUGS", ""), ",")),
|
||||
}
|
||||
|
||||
sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsCNs)
|
||||
sort.Strings(result.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs)
|
||||
sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsCNs)
|
||||
sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserDirectGroupsDNs)
|
||||
sort.Strings(result.SupervisorUpstreamActiveDirectory.TestUserIndirectGroupsSAMAccountNames)
|
||||
sort.Strings(result.SupervisorUpstreamGithub.TestUserExpectedTeamNames)
|
||||
sort.Strings(result.SupervisorUpstreamGithub.TestUserExpectedTeamSlugs)
|
||||
}
|
||||
|
||||
func (e *TestEnv) HasCapability(cap Capability) bool {
|
||||
|
||||
@@ -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
|
||||
@@ -33,3 +33,22 @@ func SkipTestWhenActiveDirectoryIsUnavailable(t *testing.T, env *TestEnv) {
|
||||
t.Skip("Active Directory hostname not specified")
|
||||
}
|
||||
}
|
||||
|
||||
func SkipTestWhenGitHubIsUnavailable(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
if IntegrationEnv(t).SupervisorUpstreamGithub.GithubAppClientID == "" {
|
||||
t.Skip("GitHub test env vars not specified")
|
||||
}
|
||||
}
|
||||
|
||||
func SkipTestWhenGitHubOAuthClientCallbackDoesNotMatchFederationDomainIssuerCallback(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
SkipTestWhenGitHubIsUnavailable(t)
|
||||
|
||||
env := IntegrationEnv(t)
|
||||
if env.SupervisorUpstreamGithub.GithubOAuthAppAllowedCallbackURL != env.SupervisorUpstreamOIDC.CallbackURL {
|
||||
t.Skip("GitHub OAuth App client allowed callback URL does not match the callback URL for the FederationDomain")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user