2022-01-19 21:20:49 +00:00
// Copyright 2020-2022 the Pinniped contributors. All Rights Reserved.
2020-12-15 18:26:54 +00:00
// SPDX-License-Identifier: Apache-2.0
package integration
import (
"bufio"
"bytes"
"context"
2021-05-12 16:05:13 +00:00
"encoding/base32"
2020-12-15 18:26:54 +00:00
"encoding/base64"
"errors"
"fmt"
2021-05-11 20:55:46 +00:00
"io"
2020-12-15 18:26:54 +00:00
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
2022-02-15 21:45:04 +00:00
"runtime"
2021-01-11 19:58:07 +00:00
"sort"
2020-12-15 18:26:54 +00:00
"strings"
2022-02-08 15:27:26 +00:00
"sync/atomic"
2020-12-15 18:26:54 +00:00
"testing"
"time"
2021-01-20 17:54:44 +00:00
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
2021-05-11 20:55:46 +00:00
"github.com/creack/pty"
2022-05-10 17:30:32 +00:00
"github.com/sclevine/agouti"
2020-12-15 18:26:54 +00:00
"github.com/stretchr/testify/require"
2021-02-23 01:23:11 +00:00
authorizationv1 "k8s.io/api/authorization/v1"
2021-01-11 19:58:07 +00:00
corev1 "k8s.io/api/core/v1"
2020-12-15 18:26:54 +00:00
rbacv1 "k8s.io/api/rbac/v1"
2021-05-12 16:05:13 +00:00
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2022-02-08 00:17:38 +00:00
utilerrors "k8s.io/apimachinery/pkg/util/errors"
2020-12-15 18:26:54 +00:00
2021-02-16 19:00:08 +00:00
authv1alpha "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
configv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/config/v1alpha1"
idpv1alpha1 "go.pinniped.dev/generated/latest/apis/supervisor/idp/v1alpha1"
2020-12-15 18:26:54 +00:00
"go.pinniped.dev/internal/certauthority"
2021-05-12 16:05:13 +00:00
"go.pinniped.dev/internal/crud"
2021-05-11 20:55:46 +00:00
"go.pinniped.dev/internal/here"
2021-01-11 19:58:07 +00:00
"go.pinniped.dev/internal/oidc"
2020-12-15 18:26:54 +00:00
"go.pinniped.dev/internal/testutil"
2021-01-11 19:58:07 +00:00
"go.pinniped.dev/pkg/oidcclient"
"go.pinniped.dev/pkg/oidcclient/filesession"
2022-01-20 16:52:16 +00:00
"go.pinniped.dev/pkg/oidcclient/oidctypes"
2021-06-22 15:23:19 +00:00
"go.pinniped.dev/test/testlib"
"go.pinniped.dev/test/testlib/browsertest"
2020-12-15 18:26:54 +00:00
)
2022-02-16 14:20:28 +00:00
// TestE2EFullIntegration_Browser tests a full integration scenario that combines the supervisor, concierge, and CLI.
2022-05-10 23:22:07 +00:00
func TestE2EFullIntegration_Browser ( t * testing . T ) {
2021-06-22 15:23:19 +00:00
env := testlib . IntegrationEnv ( t )
2020-12-16 13:23:54 +00:00
2022-06-02 18:27:54 +00:00
// Avoid allowing PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW to interfere with these tests.
originalFlowEnvVarValue , flowOverrideEnvVarSet := os . LookupEnv ( "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW" )
if flowOverrideEnvVarSet {
require . NoError ( t , os . Unsetenv ( "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW" ) )
t . Cleanup ( func ( ) {
require . NoError ( t , os . Setenv ( "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW" , originalFlowEnvVarValue ) )
} )
}
2022-03-22 17:17:45 +00:00
topSetupCtx , cancelFunc := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
2020-12-15 18:26:54 +00:00
defer cancelFunc ( )
// Build pinniped CLI.
2021-06-22 15:23:19 +00:00
pinnipedExe := testlib . PinnipedCLIPath ( t )
2020-12-15 18:26:54 +00:00
// Infer the downstream issuer URL from the callback associated with the upstream test client registration.
2021-04-07 19:56:09 +00:00
issuerURL , err := url . Parse ( env . SupervisorUpstreamOIDC . CallbackURL )
2020-12-15 18:26:54 +00:00
require . NoError ( t , err )
require . True ( t , strings . HasSuffix ( issuerURL . Path , "/callback" ) )
issuerURL . Path = strings . TrimSuffix ( issuerURL . Path , "/callback" )
t . Logf ( "testing with downstream issuer URL %s" , issuerURL . String ( ) )
// Generate a CA bundle with which to serve this provider.
t . Logf ( "generating test CA" )
2021-03-13 00:09:16 +00:00
ca , err := certauthority . New ( "Downstream Test CA" , 1 * time . Hour )
2020-12-15 18:26:54 +00:00
require . NoError ( t , err )
// Save that bundle plus the one that signs the upstream issuer, for test purposes.
2022-05-10 17:30:32 +00:00
testCABundlePath := filepath . Join ( testutil . TempDir ( t ) , "test-ca.pem" )
2021-04-07 19:56:09 +00:00
testCABundlePEM := [ ] byte ( string ( ca . Bundle ( ) ) + "\n" + env . SupervisorUpstreamOIDC . CABundle )
2020-12-15 18:26:54 +00:00
testCABundleBase64 := base64 . StdEncoding . EncodeToString ( testCABundlePEM )
2022-08-24 21:45:55 +00:00
require . NoError ( t , os . WriteFile ( testCABundlePath , testCABundlePEM , 0600 ) )
2020-12-15 18:26:54 +00:00
// Use the CA to issue a TLS server cert.
t . Logf ( "issuing test certificate" )
2021-03-13 00:09:16 +00:00
tlsCert , err := ca . IssueServerCert ( [ ] string { issuerURL . Hostname ( ) } , nil , 1 * time . Hour )
2020-12-15 18:26:54 +00:00
require . NoError ( t , err )
certPEM , keyPEM , err := certauthority . ToPEM ( tlsCert )
require . NoError ( t , err )
// Write the serving cert to a secret.
2021-06-22 15:23:19 +00:00
certSecret := testlib . CreateTestSecret ( t ,
2020-12-15 18:26:54 +00:00
env . SupervisorNamespace ,
"oidc-provider-tls" ,
2021-01-11 19:58:07 +00:00
corev1 . SecretTypeTLS ,
2020-12-15 18:26:54 +00:00
map [ string ] string { "tls.crt" : string ( certPEM ) , "tls.key" : string ( keyPEM ) } ,
)
2020-12-16 22:27:09 +00:00
// Create the downstream FederationDomain and expect it to go into the success status condition.
2022-03-22 17:17:45 +00:00
downstream := testlib . CreateTestFederationDomain ( topSetupCtx , t ,
2020-12-15 18:26:54 +00:00
issuerURL . String ( ) ,
certSecret . Name ,
2020-12-16 22:27:09 +00:00
configv1alpha1 . SuccessFederationDomainStatusCondition ,
2020-12-15 18:26:54 +00:00
)
// Create a JWTAuthenticator that will validate the tokens from the downstream issuer.
2021-06-22 15:23:19 +00:00
clusterAudience := "test-cluster-" + testlib . RandHex ( t , 8 )
2022-03-22 17:17:45 +00:00
authenticator := testlib . CreateTestJWTAuthenticator ( topSetupCtx , t , authv1alpha . JWTAuthenticatorSpec {
2020-12-15 18:26:54 +00:00
Issuer : downstream . Spec . Issuer ,
Audience : clusterAudience ,
TLS : & authv1alpha . TLSSpec { CertificateAuthorityData : testCABundleBase64 } ,
} )
2021-05-11 20:55:46 +00:00
// Add an OIDC upstream IDP and try using it to authenticate during kubectl commands.
2022-05-10 17:30:32 +00:00
t . Run ( "with Supervisor OIDC upstream IDP and browser flow with with form_post automatic authcode delivery to CLI" , func ( t * testing . T ) {
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
2022-02-08 15:27:26 +00:00
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-07-13 23:20:02 +00:00
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest . Open ( t )
2021-05-11 20:55:46 +00:00
expectedUsername := env . SupervisorUpstreamOIDC . Username
expectedGroups := env . SupervisorUpstreamOIDC . ExpectedGroups
// Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster.
2021-06-22 15:23:19 +00:00
testlib . CreateTestClusterRoleBinding ( t ,
2021-05-11 20:55:46 +00:00
rbacv1 . Subject { Kind : rbacv1 . UserKind , APIGroup : rbacv1 . GroupName , Name : expectedUsername } ,
rbacv1 . RoleRef { Kind : "ClusterRole" , APIGroup : rbacv1 . GroupName , Name : "view" } ,
)
2021-06-22 15:23:19 +00:00
testlib . WaitForUserToHaveAccess ( t , expectedUsername , [ ] string { } , & authorizationv1 . ResourceAttributes {
2021-05-11 20:55:46 +00:00
Verb : "get" ,
Group : "" ,
Version : "v1" ,
Resource : "namespaces" ,
} )
// Create upstream OIDC provider and wait for it to become ready.
2021-06-22 15:23:19 +00:00
testlib . CreateTestOIDCIdentityProvider ( t , idpv1alpha1 . OIDCIdentityProviderSpec {
2021-05-11 20:55:46 +00:00
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 {
2021-06-22 15:23:19 +00:00
SecretName : testlib . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
2021-05-11 20:55:46 +00:00
} ,
} , idpv1alpha1 . PhaseReady )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-05-11 20:55:46 +00:00
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 ,
} )
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
2022-02-08 21:00:49 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath , "-v" , "6" )
2021-05-11 20:55:46 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
2022-02-08 00:17:38 +00:00
2022-05-10 17:30:32 +00:00
// 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 , page )
2021-05-11 20:55:46 +00:00
2022-05-10 17:30:32 +00:00
// Confirm that we got to the upstream IDP's login page, fill out the form, and submit the form.
2022-05-09 22:43:36 +00:00
browsertest . LoginToUpstreamOIDC ( t , page , env . SupervisorUpstreamOIDC )
2021-03-03 22:49:33 +00:00
2021-06-30 19:40:16 +00:00
// Expect to be redirected to the downstream callback which is serving the form_post HTML.
t . Logf ( "waiting for response page %s" , downstream . Spec . Issuer )
browsertest . WaitForURL ( t , page , regexp . MustCompile ( regexp . QuoteMeta ( downstream . Spec . Issuer ) ) )
2020-12-15 18:26:54 +00:00
2021-06-30 19:40:16 +00:00
// 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 , page )
2021-05-11 20:55:46 +00:00
2022-05-10 17:30:32 +00:00
requireKubectlGetNamespaceOutput ( t , env , waitForKubectlOutput ( t , kubectlOutputChan ) )
2021-05-11 20:55:46 +00:00
2022-02-08 15:27:26 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2021-05-11 20:55:46 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
2020-12-15 18:26:54 +00:00
} )
2021-08-12 17:00:18 +00:00
t . Run ( "with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow" , func ( t * testing . T ) {
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-07-13 23:20:02 +00:00
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest . Open ( t )
2021-07-09 03:29:15 +00:00
expectedUsername := env . SupervisorUpstreamOIDC . Username
expectedGroups := env . SupervisorUpstreamOIDC . ExpectedGroups
// 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" ,
} )
2022-01-20 16:52:16 +00:00
// Create upstream OIDC provider and wait for it to become ready.
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 . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
} ,
} , idpv1alpha1 . PhaseReady )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2022-01-20 16:52:16 +00:00
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-skip-listen" ,
"--oidc-ca-bundle" , testCABundlePath ,
"--oidc-session-cache" , sessionCachePath ,
} )
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
start := time . Now ( )
2022-03-22 17:17:45 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
2022-01-20 16:52:16 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
// Wait for the subprocess to print the login prompt.
t . Logf ( "waiting for CLI to output login URL and manual prompt" )
output := readFromFileUntilStringIsSeen ( t , ptyFile , "Optionally, paste your authorization code: " )
require . Contains ( t , output , "Log in by visiting this link:" )
require . Contains ( t , output , "Optionally, paste your authorization code: " )
// Find the line with the login URL.
var loginURL string
for _ , line := range strings . Split ( output , "\n" ) {
trimmed := strings . TrimSpace ( line )
if strings . HasPrefix ( trimmed , "https://" ) {
loginURL = trimmed
}
}
require . NotEmptyf ( t , loginURL , "didn't find login URL in output: %s" , output )
t . Logf ( "navigating to login page" )
require . NoError ( t , page . Navigate ( loginURL ) )
// Expect to be redirected to the upstream provider and log in.
2022-05-09 22:43:36 +00:00
browsertest . LoginToUpstreamOIDC ( t , page , env . SupervisorUpstreamOIDC )
2022-01-20 16:52:16 +00:00
// Expect to be redirected to the downstream callback which is serving the form_post HTML.
t . Logf ( "waiting for response page %s" , downstream . Spec . Issuer )
browsertest . WaitForURL ( t , page , regexp . MustCompile ( regexp . QuoteMeta ( downstream . Spec . Issuer ) ) )
// The response page should have failed to automatically post, and should now be showing the manual instructions.
authCode := formpostExpectManualState ( t , page )
// Enter the auth code in the waiting prompt, followed by a newline.
t . Logf ( "'manually' pasting authorization code %q to waiting prompt" , authCode )
_ , err = ptyFile . WriteString ( authCode + "\n" )
require . NoError ( t , err )
// Read all of the remaining output from the subprocess until EOF.
t . Logf ( "waiting for kubectl to output namespace list" )
// Read all output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlOutputBytes , _ := io . ReadAll ( ptyFile )
2022-01-20 16:52:16 +00:00
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlOutputBytes ) )
t . Logf ( "first kubectl command took %s" , time . Since ( start ) . String ( ) )
2022-03-22 17:17:45 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2022-01-20 16:52:16 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
t . Run ( "access token based refresh with Supervisor OIDC upstream IDP and manual authcode copy-paste from browser flow" , func ( t * testing . T ) {
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2022-01-20 16:52:16 +00:00
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest . Open ( t )
expectedUsername := env . SupervisorUpstreamOIDC . Username
expectedGroups := env . SupervisorUpstreamOIDC . ExpectedGroups
// 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" ,
} )
2022-01-19 21:20:49 +00:00
var additionalScopes [ ] string
// To ensure that access token refresh happens rather than refresh token, don't ask for the offline_access scope.
2022-01-20 16:52:16 +00:00
for _ , additionalScope := range env . SupervisorUpstreamOIDC . AdditionalScopes {
if additionalScope != "offline_access" {
additionalScopes = append ( additionalScopes , additionalScope )
2022-01-19 21:20:49 +00:00
}
}
2022-01-20 16:52:16 +00:00
2021-07-09 03:29:15 +00:00
// Create upstream OIDC provider and wait for it to become ready.
testlib . CreateTestOIDCIdentityProvider ( t , idpv1alpha1 . OIDCIdentityProviderSpec {
Issuer : env . SupervisorUpstreamOIDC . Issuer ,
TLS : & idpv1alpha1 . TLSSpec {
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamOIDC . CABundle ) ) ,
} ,
AuthorizationConfig : idpv1alpha1 . OIDCAuthorizationConfig {
2022-01-19 21:20:49 +00:00
AdditionalScopes : additionalScopes ,
2021-07-09 03:29:15 +00:00
} ,
Claims : idpv1alpha1 . OIDCClaims {
Username : env . SupervisorUpstreamOIDC . UsernameClaim ,
Groups : env . SupervisorUpstreamOIDC . GroupsClaim ,
} ,
Client : idpv1alpha1 . OIDCClient {
SecretName : testlib . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
} ,
} , idpv1alpha1 . PhaseReady )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-07-09 03:29:15 +00:00
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-skip-listen" ,
"--oidc-ca-bundle" , testCABundlePath ,
"--oidc-session-cache" , sessionCachePath ,
} )
// Run "kubectl get namespaces" which should trigger a browser login via the plugin.
start := time . Now ( )
2022-03-22 17:17:45 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
2021-07-09 03:29:15 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
2022-02-15 21:45:04 +00:00
var kubectlStdoutPipe io . ReadCloser
if runtime . GOOS != "darwin" {
// For some unknown reason this breaks the pty library on some MacOS machines.
// The problem doesn't reproduce for everyone, so this is just a workaround.
kubectlStdoutPipe , err = kubectlCmd . StdoutPipe ( )
require . NoError ( t , err )
}
2021-07-09 03:29:15 +00:00
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
// Wait for the subprocess to print the login prompt.
t . Logf ( "waiting for CLI to output login URL and manual prompt" )
2021-07-29 22:49:16 +00:00
output := readFromFileUntilStringIsSeen ( t , ptyFile , "Optionally, paste your authorization code: " )
2021-07-09 03:29:15 +00:00
require . Contains ( t , output , "Log in by visiting this link:" )
2021-07-29 22:49:16 +00:00
require . Contains ( t , output , "Optionally, paste your authorization code: " )
2021-07-09 03:29:15 +00:00
// Find the line with the login URL.
var loginURL string
for _ , line := range strings . Split ( output , "\n" ) {
trimmed := strings . TrimSpace ( line )
if strings . HasPrefix ( trimmed , "https://" ) {
loginURL = trimmed
}
}
require . NotEmptyf ( t , loginURL , "didn't find login URL in output: %s" , output )
t . Logf ( "navigating to login page" )
require . NoError ( t , page . Navigate ( loginURL ) )
// Expect to be redirected to the upstream provider and log in.
2022-05-09 22:43:36 +00:00
browsertest . LoginToUpstreamOIDC ( t , page , env . SupervisorUpstreamOIDC )
2021-07-09 03:29:15 +00:00
// Expect to be redirected to the downstream callback which is serving the form_post HTML.
t . Logf ( "waiting for response page %s" , downstream . Spec . Issuer )
browsertest . WaitForURL ( t , page , regexp . MustCompile ( regexp . QuoteMeta ( downstream . Spec . Issuer ) ) )
// The response page should have failed to automatically post, and should now be showing the manual instructions.
authCode := formpostExpectManualState ( t , page )
// Enter the auth code in the waiting prompt, followed by a newline.
t . Logf ( "'manually' pasting authorization code %q to waiting prompt" , authCode )
_ , err = ptyFile . WriteString ( authCode + "\n" )
require . NoError ( t , err )
// Read all of the remaining output from the subprocess until EOF.
t . Logf ( "waiting for kubectl to output namespace list" )
2021-08-12 17:00:18 +00:00
// Read all output from the subprocess until EOF.
2021-07-09 03:29:15 +00:00
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlPtyOutputBytes , _ := io . ReadAll ( ptyFile )
2022-02-15 21:45:04 +00:00
if kubectlStdoutPipe != nil {
// On non-MacOS check that stdout of the CLI contains the expected output.
2022-08-24 21:45:55 +00:00
kubectlStdOutOutputBytes , _ := io . ReadAll ( kubectlStdoutPipe )
2022-02-15 21:45:04 +00:00
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlStdOutOutputBytes ) )
} else {
// On MacOS check that the pty (stdout+stderr+stdin) of the CLI contains the expected output.
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlPtyOutputBytes ) )
}
// Due to the GOOS check in the code above, on MacOS the pty will include stdout, and other platforms it will not.
// This warning message is supposed to be printed by the CLI on stderr.
require . Contains ( t , string ( kubectlPtyOutputBytes ) ,
"Access token from identity provider has lifetime of less than 3 hours. Expect frequent prompts to log in." )
2021-07-20 00:08:52 +00:00
2021-07-09 03:29:15 +00:00
t . Logf ( "first kubectl command took %s" , time . Since ( start ) . String ( ) )
2022-03-22 17:17:45 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2021-07-09 03:29:15 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
2021-08-12 17:00:18 +00:00
t . Run ( "with Supervisor OIDC upstream IDP and CLI password flow without web browser" , func ( t * testing . T ) {
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-08-12 17:00:18 +00:00
expectedUsername := env . SupervisorUpstreamOIDC . Username
expectedGroups := env . SupervisorUpstreamOIDC . ExpectedGroups
// 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 OIDC provider and wait for it to become ready.
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 ,
AllowPasswordGrant : true , // allow the CLI password flow for this OIDCIdentityProvider
} ,
Claims : idpv1alpha1 . OIDCClaims {
Username : env . SupervisorUpstreamOIDC . UsernameClaim ,
Groups : env . SupervisorUpstreamOIDC . GroupsClaim ,
} ,
Client : idpv1alpha1 . OIDCClient {
SecretName : testlib . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
} ,
} , idpv1alpha1 . PhaseReady )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-08-12 17:00:18 +00:00
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-skip-listen" ,
"--upstream-identity-provider-flow" , "cli_password" , // create a kubeconfig configured to use the cli_password flow
"--oidc-ca-bundle" , testCABundlePath ,
"--oidc-session-cache" , sessionCachePath ,
} )
// Run "kubectl get namespaces" which should trigger a browser-less CLI prompt login via the plugin.
start := time . Now ( )
2022-03-22 17:17:45 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
2021-08-12 17:00:18 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
// Wait for the subprocess to print the username prompt, then type the user's username.
readFromFileUntilStringIsSeen ( t , ptyFile , "Username: " )
_ , err = ptyFile . WriteString ( expectedUsername + "\n" )
require . NoError ( t , err )
// Wait for the subprocess to print the password prompt, then type the user's password.
readFromFileUntilStringIsSeen ( t , ptyFile , "Password: " )
_ , err = ptyFile . WriteString ( env . SupervisorUpstreamOIDC . Password + "\n" )
require . NoError ( t , err )
// Read all output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlOutputBytes , _ := io . ReadAll ( ptyFile )
2021-08-12 17:00:18 +00:00
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlOutputBytes ) )
t . Logf ( "first kubectl command took %s" , time . Since ( start ) . String ( ) )
2022-03-22 17:17:45 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2021-08-12 17:00:18 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
t . Run ( "with Supervisor OIDC upstream IDP and CLI password flow when OIDCIdentityProvider disallows it" , func ( t * testing . T ) {
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-08-12 17:00:18 +00:00
// Create upstream OIDC provider and wait for it to become ready.
oidcIdentityProvider := 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 ,
AllowPasswordGrant : false , // disallow the CLI password flow for this OIDCIdentityProvider!
} ,
Claims : idpv1alpha1 . OIDCClaims {
Username : env . SupervisorUpstreamOIDC . UsernameClaim ,
Groups : env . SupervisorUpstreamOIDC . GroupsClaim ,
} ,
Client : idpv1alpha1 . OIDCClient {
SecretName : testlib . CreateClientCredsSecret ( t , env . SupervisorUpstreamOIDC . ClientID , env . SupervisorUpstreamOIDC . ClientSecret ) . Name ,
} ,
} , idpv1alpha1 . PhaseReady )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-08-12 17:00:18 +00:00
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-skip-listen" ,
// Create a kubeconfig configured to use the cli_password flow. By specifying all
// available --upstream-identity-provider-* options the CLI should skip IDP discovery
// and use the provided values without validating them. "cli_password" will not show
// up in the list of available flows for this IDP in the discovery response.
"--upstream-identity-provider-name" , oidcIdentityProvider . Name ,
"--upstream-identity-provider-type" , "oidc" ,
"--upstream-identity-provider-flow" , "cli_password" ,
"--oidc-ca-bundle" , testCABundlePath ,
"--oidc-session-cache" , sessionCachePath ,
} )
2022-03-22 21:23:50 +00:00
// Run "kubectl get --raw /healthz" which should trigger a browser-less CLI prompt login via the plugin.
// Avoid using something like "kubectl get namespaces" for this test because we expect the auth to fail,
// and kubectl might call the credential exec plugin a second time to try to auth again if it needs to do API
// discovery, in which case this test would hang until the kubectl subprocess is killed because the process
// would be stuck waiting for input on the second username prompt. "kubectl get --raw /healthz" doesn't need
// to do API discovery, so we know it will only call the credential exec plugin once.
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "--raw" , "/healthz" , "--kubeconfig" , kubeconfigPath )
2021-08-12 17:00:18 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
// Wait for the subprocess to print the username prompt, then type the user's username.
readFromFileUntilStringIsSeen ( t , ptyFile , "Username: " )
_ , err = ptyFile . WriteString ( env . SupervisorUpstreamOIDC . Username + "\n" )
require . NoError ( t , err )
// Wait for the subprocess to print the password prompt, then type the user's password.
readFromFileUntilStringIsSeen ( t , ptyFile , "Password: " )
_ , err = ptyFile . WriteString ( env . SupervisorUpstreamOIDC . Password + "\n" )
require . NoError ( t , err )
// Read all output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlOutputBytes , _ := io . ReadAll ( ptyFile )
2021-08-12 17:00:18 +00:00
kubectlOutput := string ( kubectlOutputBytes )
// The output should look like an authentication failure, because the OIDCIdentityProvider disallows password grants.
t . Log ( "kubectl command output (expecting a login failed error):\n" , kubectlOutput )
require . Contains ( t , kubectlOutput ,
` Error: could not complete Pinniped login: login failed with code "access_denied": ` +
` The resource owner or authorization server denied the request. ` +
2021-08-16 22:40:34 +00:00
` Resource owner password credentials grant is not allowed for this upstream provider according to its configuration. ` ,
2021-08-12 17:00:18 +00:00
)
} )
2021-07-20 00:08:52 +00:00
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands
// by interacting with the CLI's username and password prompts.
t . Run ( "with Supervisor LDAP upstream IDP using username and password prompts" , func ( t * testing . T ) {
2022-06-08 19:57:00 +00:00
testlib . SkipTestWhenLDAPIsUnavailable ( t , env )
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-05-11 20:55:46 +00:00
expectedUsername := env . SupervisorUpstreamLDAP . TestUserMailAttributeValue
2021-05-28 20:27:11 +00:00
expectedGroups := env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs
2021-05-11 20:55:46 +00:00
2021-07-20 00:08:52 +00:00
setupClusterForEndToEndLDAPTest ( t , expectedUsername , env )
2021-05-11 20:55:46 +00:00
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-05-11 20:55:46 +00:00
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-session-cache" , sessionCachePath ,
} )
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
start := time . Now ( )
2022-03-22 17:17:45 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
2021-05-11 20:55:46 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
2020-12-15 18:26:54 +00:00
2021-05-11 20:55:46 +00:00
// Wait for the subprocess to print the username prompt, then type the user's username.
readFromFileUntilStringIsSeen ( t , ptyFile , "Username: " )
_ , err = ptyFile . WriteString ( expectedUsername + "\n" )
require . NoError ( t , err )
2020-12-15 18:26:54 +00:00
2021-05-11 20:55:46 +00:00
// Wait for the subprocess to print the password prompt, then type the user's password.
readFromFileUntilStringIsSeen ( t , ptyFile , "Password: " )
_ , err = ptyFile . WriteString ( env . SupervisorUpstreamLDAP . TestUserPassword + "\n" )
require . NoError ( t , err )
2021-08-12 17:00:18 +00:00
// Read all output from the subprocess until EOF.
2021-07-20 00:08:52 +00:00
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlOutputBytes , _ := io . ReadAll ( ptyFile )
2021-07-20 00:08:52 +00:00
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlOutputBytes ) )
t . Logf ( "first kubectl command took %s" , time . Since ( start ) . String ( ) )
2022-03-22 17:17:45 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2021-07-20 00:08:52 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands
// by passing username and password via environment variables, thus avoiding the CLI's username and password prompts.
t . Run ( "with Supervisor LDAP upstream IDP using PINNIPED_USERNAME and PINNIPED_PASSWORD env vars" , func ( t * testing . T ) {
2022-06-08 19:57:00 +00:00
testlib . SkipTestWhenLDAPIsUnavailable ( t , env )
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-07-20 00:08:52 +00:00
expectedUsername := env . SupervisorUpstreamLDAP . TestUserMailAttributeValue
expectedGroups := env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs
setupClusterForEndToEndLDAPTest ( t , expectedUsername , env )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-07-20 00:08:52 +00:00
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-session-cache" , sessionCachePath ,
} )
// Set up the username and password env vars to avoid the interactive prompts.
const usernameEnvVar = "PINNIPED_USERNAME"
originalUsername , hadOriginalUsername := os . LookupEnv ( usernameEnvVar )
t . Cleanup ( func ( ) {
if hadOriginalUsername {
require . NoError ( t , os . Setenv ( usernameEnvVar , originalUsername ) )
}
} )
require . NoError ( t , os . Setenv ( usernameEnvVar , expectedUsername ) )
2021-07-27 00:20:49 +00:00
const passwordEnvVar = "PINNIPED_PASSWORD" //nolint:gosec // this is not a credential
2021-07-20 00:08:52 +00:00
originalPassword , hadOriginalPassword := os . LookupEnv ( passwordEnvVar )
t . Cleanup ( func ( ) {
if hadOriginalPassword {
require . NoError ( t , os . Setenv ( passwordEnvVar , originalPassword ) )
}
} )
require . NoError ( t , os . Setenv ( passwordEnvVar , env . SupervisorUpstreamLDAP . TestUserPassword ) )
// Run "kubectl get namespaces" which should run an LDAP-style login without interactive prompts for username and password.
start := time . Now ( )
2022-03-22 17:17:45 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
2021-07-20 00:08:52 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
2021-08-12 17:00:18 +00:00
// Read all output from the subprocess until EOF.
2021-05-13 23:02:24 +00:00
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlOutputBytes , _ := io . ReadAll ( ptyFile )
2021-07-20 00:08:52 +00:00
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlOutputBytes ) )
2021-05-11 20:55:46 +00:00
t . Logf ( "first kubectl command took %s" , time . Since ( start ) . String ( ) )
2021-07-20 00:08:52 +00:00
// The next kubectl command should not require auth, so we should be able to run it without these env vars.
require . NoError ( t , os . Unsetenv ( usernameEnvVar ) )
require . NoError ( t , os . Unsetenv ( passwordEnvVar ) )
2022-03-22 17:17:45 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2021-05-11 20:55:46 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
2020-12-15 18:26:54 +00:00
} )
2021-08-26 23:18:05 +00:00
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands
// by interacting with the CLI's username and password prompts.
t . Run ( "with Supervisor ActiveDirectory upstream IDP using username and password prompts" , func ( t * testing . T ) {
2022-06-08 19:57:00 +00:00
testlib . SkipTestWhenActiveDirectoryIsUnavailable ( t , env )
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-08-26 23:18:05 +00:00
expectedUsername := env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue
expectedGroups := env . SupervisorUpstreamActiveDirectory . TestUserIndirectGroupsSAMAccountPlusDomainNames
setupClusterForEndToEndActiveDirectoryTest ( t , expectedUsername , env )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-08-26 23:18:05 +00:00
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-session-cache" , sessionCachePath ,
} )
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
start := time . Now ( )
2022-03-22 17:17:45 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
2021-08-26 23:18:05 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
// Wait for the subprocess to print the username prompt, then type the user's username.
readFromFileUntilStringIsSeen ( t , ptyFile , "Username: " )
_ , err = ptyFile . WriteString ( expectedUsername + "\n" )
require . NoError ( t , err )
// Wait for the subprocess to print the password prompt, then type the user's password.
readFromFileUntilStringIsSeen ( t , ptyFile , "Password: " )
_ , err = ptyFile . WriteString ( env . SupervisorUpstreamActiveDirectory . TestUserPassword + "\n" )
require . NoError ( t , err )
// Read all output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlOutputBytes , _ := io . ReadAll ( ptyFile )
2021-08-26 23:18:05 +00:00
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlOutputBytes ) )
t . Logf ( "first kubectl command took %s" , time . Since ( start ) . String ( ) )
2022-03-22 17:17:45 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2021-08-26 23:18:05 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
// Add an ActiveDirectory upstream IDP and try using it to authenticate during kubectl commands
// by passing username and password via environment variables, thus avoiding the CLI's username and password prompts.
t . Run ( "with Supervisor ActiveDirectory upstream IDP using PINNIPED_USERNAME and PINNIPED_PASSWORD env vars" , func ( t * testing . T ) {
2022-06-08 19:57:00 +00:00
testlib . SkipTestWhenActiveDirectoryIsUnavailable ( t , env )
2022-03-22 17:17:45 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2021-08-26 23:18:05 +00:00
expectedUsername := env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue
expectedGroups := env . SupervisorUpstreamActiveDirectory . TestUserIndirectGroupsSAMAccountPlusDomainNames
setupClusterForEndToEndActiveDirectoryTest ( t , expectedUsername , env )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2021-08-26 23:18:05 +00:00
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-session-cache" , sessionCachePath ,
} )
// Set up the username and password env vars to avoid the interactive prompts.
const usernameEnvVar = "PINNIPED_USERNAME"
originalUsername , hadOriginalUsername := os . LookupEnv ( usernameEnvVar )
t . Cleanup ( func ( ) {
if hadOriginalUsername {
require . NoError ( t , os . Setenv ( usernameEnvVar , originalUsername ) )
}
} )
require . NoError ( t , os . Setenv ( usernameEnvVar , expectedUsername ) )
const passwordEnvVar = "PINNIPED_PASSWORD" //nolint:gosec // this is not a credential
originalPassword , hadOriginalPassword := os . LookupEnv ( passwordEnvVar )
t . Cleanup ( func ( ) {
if hadOriginalPassword {
require . NoError ( t , os . Setenv ( passwordEnvVar , originalPassword ) )
}
} )
require . NoError ( t , os . Setenv ( passwordEnvVar , env . SupervisorUpstreamActiveDirectory . TestUserPassword ) )
// Run "kubectl get namespaces" which should run an LDAP-style login without interactive prompts for username and password.
start := time . Now ( )
2022-03-22 17:17:45 +00:00
kubectlCmd := exec . CommandContext ( testCtx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
2021-08-26 23:18:05 +00:00
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
ptyFile , err := pty . Start ( kubectlCmd )
require . NoError ( t , err )
// Read all output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
2022-08-24 21:45:55 +00:00
kubectlOutputBytes , _ := io . ReadAll ( ptyFile )
2021-08-26 23:18:05 +00:00
requireKubectlGetNamespaceOutput ( t , env , string ( kubectlOutputBytes ) )
t . Logf ( "first kubectl command took %s" , time . Since ( start ) . String ( ) )
// The next kubectl command should not require auth, so we should be able to run it without these env vars.
require . NoError ( t , os . Unsetenv ( usernameEnvVar ) )
require . NoError ( t , os . Unsetenv ( passwordEnvVar ) )
2022-03-22 17:17:45 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
2021-08-26 23:18:05 +00:00
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
2022-04-26 19:51:56 +00:00
2022-05-10 17:30:32 +00:00
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
t . Run ( "with Supervisor LDAP upstream IDP and browser flow with with form_post automatic authcode delivery to CLI" , func ( t * testing . T ) {
2022-06-08 19:57:00 +00:00
testlib . SkipTestWhenLDAPIsUnavailable ( t , env )
2022-04-26 19:51:56 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
2022-05-10 17:30:32 +00:00
tempDir := testutil . TempDir ( t ) // per-test tmp dir to avoid sharing files between tests
2022-04-26 19:51:56 +00:00
// Start a fresh browser driver because we don't want to share cookies between the various tests in this file.
page := browsertest . Open ( t )
expectedUsername := env . SupervisorUpstreamLDAP . TestUserMailAttributeValue
2022-05-05 15:49:58 +00:00
expectedGroups := env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs
2022-04-26 19:51:56 +00:00
setupClusterForEndToEndLDAPTest ( t , expectedUsername , env )
// Use a specific session cache for this test.
2022-05-10 17:30:32 +00:00
sessionCachePath := tempDir + "/test-sessions.yaml"
2022-04-26 19:51:56 +00:00
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 ,
"--upstream-identity-provider-flow" , "browser_authcode" ,
"--oidc-session-cache" , sessionCachePath ,
} )
// 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 ( ) ... )
2022-05-10 17:30:32 +00:00
// 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 , page )
2022-04-26 19:51:56 +00:00
2022-05-10 17:30:32 +00:00
// Confirm that we got to the Supervisor's login page, fill out the form, and submit the form.
browsertest . LoginToUpstreamLDAP ( t , page , downstream . Spec . Issuer ,
expectedUsername , env . SupervisorUpstreamLDAP . TestUserPassword )
2022-04-26 19:51:56 +00:00
2022-05-10 17:30:32 +00:00
formpostExpectSuccessState ( t , page )
2022-04-26 19:51:56 +00:00
2022-05-10 17:30:32 +00:00
requireKubectlGetNamespaceOutput ( t , env , waitForKubectlOutput ( t , kubectlOutputChan ) )
2022-04-26 19:51:56 +00:00
2022-05-10 17:30:32 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
2022-04-26 19:51:56 +00:00
2022-05-10 17:30:32 +00:00
// Add an Active Directory upstream IDP and try using it to authenticate during kubectl commands, using the browser flow.
t . Run ( "with Supervisor Active Directory upstream IDP and browser flow with with form_post automatic authcode delivery to CLI" , func ( t * testing . T ) {
2022-06-08 19:57:00 +00:00
testlib . SkipTestWhenActiveDirectoryIsUnavailable ( t , env )
2022-05-10 17:30:32 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
tempDir := testutil . TempDir ( t ) // 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.
page := browsertest . Open ( t )
expectedUsername := env . SupervisorUpstreamActiveDirectory . TestUserPrincipalNameValue
expectedGroups := env . SupervisorUpstreamActiveDirectory . TestUserIndirectGroupsSAMAccountPlusDomainNames
setupClusterForEndToEndActiveDirectoryTest ( t , expectedUsername , env )
// Use a specific session cache for this test.
sessionCachePath := tempDir + "/test-sessions.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 ,
"--upstream-identity-provider-flow" , "browser_authcode" ,
"--oidc-session-cache" , sessionCachePath ,
} )
// 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 , page )
// Confirm that we got to the Supervisor's login page, fill out the form, and submit the form.
2022-05-09 22:43:36 +00:00
browsertest . LoginToUpstreamLDAP ( t , page , downstream . Spec . Issuer ,
2022-05-10 17:30:32 +00:00
expectedUsername , env . SupervisorUpstreamActiveDirectory . TestUserPassword )
2022-05-05 15:49:58 +00:00
formpostExpectSuccessState ( t , page )
2022-05-10 17:30:32 +00:00
requireKubectlGetNamespaceOutput ( t , env , waitForKubectlOutput ( t , kubectlOutputChan ) )
2022-05-05 15:49:58 +00:00
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
2022-04-26 19:51:56 +00:00
} )
2022-06-02 18:27:54 +00:00
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands, using the env var to choose the browser flow.
t . Run ( "with Supervisor LDAP upstream IDP and browser flow selected by env var override with with form_post automatic authcode delivery to CLI" , func ( t * testing . T ) {
2022-06-08 19:57:00 +00:00
testlib . SkipTestWhenLDAPIsUnavailable ( t , env )
2022-06-02 18:27:54 +00:00
testCtx , cancel := context . WithTimeout ( context . Background ( ) , 5 * time . Minute )
t . Cleanup ( cancel )
tempDir := testutil . TempDir ( t ) // 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.
page := browsertest . Open ( t )
expectedUsername := env . SupervisorUpstreamLDAP . TestUserMailAttributeValue
expectedGroups := env . SupervisorUpstreamLDAP . TestUserDirectGroupsDNs
setupClusterForEndToEndLDAPTest ( t , expectedUsername , env )
// Use a specific session cache for this test.
sessionCachePath := tempDir + "/test-sessions.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 ,
"--upstream-identity-provider-flow" , "cli_password" , // put cli_password in the kubeconfig, so we can override it with the env var
"--oidc-session-cache" , sessionCachePath ,
} )
// Override the --upstream-identity-provider-flow flag from the kubeconfig using the env var.
require . NoError ( t , os . Setenv ( "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW" , "browser_authcode" ) )
t . Cleanup ( func ( ) {
require . NoError ( t , os . Unsetenv ( "PINNIPED_UPSTREAM_IDENTITY_PROVIDER_FLOW" ) )
} )
// 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 , page )
// Confirm that we got to the Supervisor's login page, fill out the form, and submit the form.
browsertest . LoginToUpstreamLDAP ( t , page , downstream . Spec . Issuer ,
expectedUsername , env . SupervisorUpstreamLDAP . TestUserPassword )
formpostExpectSuccessState ( t , page )
requireKubectlGetNamespaceOutput ( t , env , waitForKubectlOutput ( t , kubectlOutputChan ) )
requireUserCanUseKubectlWithoutAuthenticatingAgain ( testCtx , t , env ,
downstream ,
kubeconfigPath ,
sessionCachePath ,
pinnipedExe ,
expectedUsername ,
expectedGroups ,
)
} )
2021-05-11 20:55:46 +00:00
}
2020-12-15 18:26:54 +00:00
2022-05-10 17:30:32 +00:00
func startKubectlAndOpenAuthorizationURLInBrowser ( testCtx context . Context , t * testing . T , kubectlCmd * exec . Cmd , page * agouti . Page ) chan string {
// Wrap the stdout and stderr pipes with TeeReaders which will copy each incremental read to an
// in-memory buffer, so we can have the full output available to us at the end.
originalStderrPipe , err := kubectlCmd . StderrPipe ( )
require . NoError ( t , err )
originalStdoutPipe , err := kubectlCmd . StdoutPipe ( )
require . NoError ( t , err )
var stderrPipeBuf , stdoutPipeBuf bytes . Buffer
stderrPipe := io . TeeReader ( originalStderrPipe , & stderrPipeBuf )
stdoutPipe := io . TeeReader ( originalStdoutPipe , & stdoutPipeBuf )
t . Logf ( "starting kubectl subprocess" )
require . NoError ( t , kubectlCmd . Start ( ) )
t . Cleanup ( func ( ) {
// Consume readers so that the tee buffers will contain all the output so far.
_ , stdoutReadAllErr := readAllCtx ( testCtx , stdoutPipe )
_ , stderrReadAllErr := readAllCtx ( testCtx , stderrPipe )
// Note that Wait closes the stdout/stderr pipes, so we don't need to close them ourselves.
waitErr := kubectlCmd . Wait ( )
t . Logf ( "kubectl subprocess exited with code %d" , kubectlCmd . ProcessState . ExitCode ( ) )
// Upon failure, print the full output so far of the kubectl command.
var testAlreadyFailedErr error
if t . Failed ( ) {
testAlreadyFailedErr = errors . New ( "test failed prior to clean up function" )
}
cleanupErrs := utilerrors . NewAggregate ( [ ] error { waitErr , stdoutReadAllErr , stderrReadAllErr , testAlreadyFailedErr } )
if cleanupErrs != nil {
t . Logf ( "kubectl stdout was:\n----start of stdout\n%s\n----end of stdout" , stdoutPipeBuf . String ( ) )
t . Logf ( "kubectl stderr was:\n----start of stderr\n%s\n----end of stderr" , stderrPipeBuf . String ( ) )
}
require . NoErrorf ( t , cleanupErrs , "kubectl process did not exit cleanly and/or the test failed. " +
"Note: if kubectl's first call to the Pinniped CLI results in the Pinniped CLI returning an error, " +
"then kubectl may call the Pinniped CLI again, which may hang because it will wait for the user " +
"to finish the login. This test will kill the kubectl process after a timeout. In this case, the " +
" kubectl output printed above will include multiple prompts for the user to enter their authcode." ,
)
} )
// Start a background goroutine to read stderr from the CLI and parse out the login URL.
loginURLChan := make ( chan string , 1 )
spawnTestGoroutine ( testCtx , t , func ( ) error {
reader := bufio . NewReader ( testlib . NewLoggerReader ( t , "stderr" , stderrPipe ) )
scanner := bufio . NewScanner ( reader )
for scanner . Scan ( ) {
loginURL , err := url . Parse ( strings . TrimSpace ( scanner . Text ( ) ) )
if err == nil && loginURL . Scheme == "https" {
loginURLChan <- loginURL . String ( ) // this channel is buffered so this will not block
return nil
}
}
return fmt . Errorf ( "expected stderr to contain login URL" )
} )
// Start a background goroutine to read stdout from kubectl and return the result as a string.
kubectlOutputChan := make ( chan string , 1 )
spawnTestGoroutine ( testCtx , t , func ( ) error {
output , err := readAllCtx ( testCtx , stdoutPipe )
if err != nil {
return err
}
t . Logf ( "kubectl output:\n%s\n" , output )
kubectlOutputChan <- string ( output ) // this channel is buffered so this will not block
return nil
} )
// Wait for the CLI to print out the login URL and open the browser to it.
t . Logf ( "waiting for CLI to output login URL" )
var loginURL string
select {
case <- time . After ( 1 * time . Minute ) :
require . Fail ( t , "timed out waiting for login URL" )
case loginURL = <- loginURLChan :
}
t . Logf ( "navigating to login page: %q" , loginURL )
require . NoError ( t , page . Navigate ( loginURL ) )
return kubectlOutputChan
}
func waitForKubectlOutput ( t * testing . T , kubectlOutputChan chan string ) string {
t . Logf ( "waiting for kubectl output" )
var kubectlOutput string
select {
case <- time . After ( 1 * time . Minute ) :
require . Fail ( t , "timed out waiting for kubectl output" )
case kubectlOutput = <- kubectlOutputChan :
}
return kubectlOutput
}
2021-07-20 00:08:52 +00:00
func setupClusterForEndToEndLDAPTest ( t * testing . T , username string , env * testlib . TestEnv ) {
// 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 : username } ,
rbacv1 . RoleRef { Kind : "ClusterRole" , APIGroup : rbacv1 . GroupName , Name : "view" } ,
)
testlib . WaitForUserToHaveAccess ( t , username , [ ] string { } , & authorizationv1 . ResourceAttributes {
Verb : "get" ,
Group : "" ,
Version : "v1" ,
Resource : "namespaces" ,
} )
// Put the bind service account's info into a Secret.
bindSecret := testlib . CreateTestSecret ( t , env . SupervisorNamespace , "ldap-service-account" , corev1 . SecretTypeBasicAuth ,
map [ string ] string {
corev1 . BasicAuthUsernameKey : env . SupervisorUpstreamLDAP . BindUsername ,
corev1 . BasicAuthPasswordKey : env . SupervisorUpstreamLDAP . BindPassword ,
} ,
)
// Create upstream LDAP provider and wait for it to become ready.
testlib . CreateTestLDAPIdentityProvider ( t , idpv1alpha1 . LDAPIdentityProviderSpec {
Host : env . SupervisorUpstreamLDAP . Host ,
TLS : & idpv1alpha1 . TLSSpec {
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamLDAP . CABundle ) ) ,
} ,
Bind : idpv1alpha1 . LDAPIdentityProviderBind {
SecretName : bindSecret . 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 )
}
2021-08-26 23:18:05 +00:00
func setupClusterForEndToEndActiveDirectoryTest ( t * testing . T , username string , env * testlib . TestEnv ) {
// 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 : username } ,
rbacv1 . RoleRef { Kind : "ClusterRole" , APIGroup : rbacv1 . GroupName , Name : "view" } ,
)
testlib . WaitForUserToHaveAccess ( t , username , [ ] string { } , & authorizationv1 . ResourceAttributes {
Verb : "get" ,
Group : "" ,
Version : "v1" ,
Resource : "namespaces" ,
} )
// Put the bind service account's info into a Secret.
bindSecret := testlib . CreateTestSecret ( t , env . SupervisorNamespace , "ldap-service-account" , corev1 . SecretTypeBasicAuth ,
map [ string ] string {
corev1 . BasicAuthUsernameKey : env . SupervisorUpstreamActiveDirectory . BindUsername ,
corev1 . BasicAuthPasswordKey : env . SupervisorUpstreamActiveDirectory . BindPassword ,
} ,
)
// Create upstream LDAP provider and wait for it to become ready.
testlib . CreateTestActiveDirectoryIdentityProvider ( t , idpv1alpha1 . ActiveDirectoryIdentityProviderSpec {
Host : env . SupervisorUpstreamActiveDirectory . Host ,
TLS : & idpv1alpha1 . TLSSpec {
CertificateAuthorityData : base64 . StdEncoding . EncodeToString ( [ ] byte ( env . SupervisorUpstreamActiveDirectory . CABundle ) ) ,
} ,
Bind : idpv1alpha1 . ActiveDirectoryIdentityProviderBind {
SecretName : bindSecret . Name ,
} ,
} , idpv1alpha1 . ActiveDirectoryPhaseReady )
}
2021-07-09 03:29:15 +00:00
func readFromFileUntilStringIsSeen ( t * testing . T , f * os . File , until string ) string {
2021-05-11 20:55:46 +00:00
readFromFile := ""
2020-12-15 18:26:54 +00:00
2021-06-22 15:23:19 +00:00
testlib . RequireEventuallyWithoutError ( t , func ( ) ( bool , error ) {
2021-05-11 20:55:46 +00:00
someOutput , foundEOF := readAvailableOutput ( t , f )
readFromFile += someOutput
if strings . Contains ( readFromFile , until ) {
return true , nil // found it! finished.
}
if foundEOF {
2022-02-15 19:19:49 +00:00
return false , fmt . Errorf ( "reached EOF of subcommand's output without seeing expected string %q. Output read so far was:\n%s" , until , readFromFile )
2021-05-11 20:55:46 +00:00
}
return false , nil // keep waiting and reading
} , 1 * time . Minute , 1 * time . Second )
2021-07-09 03:29:15 +00:00
return readFromFile
2021-05-11 20:55:46 +00:00
}
2020-12-15 18:26:54 +00:00
2021-05-11 20:55:46 +00:00
func readAvailableOutput ( t * testing . T , r io . Reader ) ( string , bool ) {
buf := make ( [ ] byte , 1024 )
n , err := r . Read ( buf )
if err != nil {
if err == io . EOF {
return string ( buf [ : n ] ) , true
}
2021-05-12 18:24:00 +00:00
require . NoError ( t , err )
2020-12-15 18:26:54 +00:00
}
2021-05-11 20:55:46 +00:00
return string ( buf [ : n ] ) , false
}
2021-07-20 00:08:52 +00:00
func requireKubectlGetNamespaceOutput ( t * testing . T , env * testlib . TestEnv , kubectlOutput string ) {
t . Log ( "kubectl command output:\n" , kubectlOutput )
require . Greaterf ( t , len ( kubectlOutput ) , 0 , "expected to get some more output from the kubectl subcommand, but did not" )
// Should look generally like a list of namespaces, with one namespace listed per line in a table format.
require . Greaterf ( t , len ( strings . Split ( kubectlOutput , "\n" ) ) , 2 , "expected some namespaces to be returned, got %q" , kubectlOutput )
require . Contains ( t , kubectlOutput , fmt . Sprintf ( "\n%s " , env . ConciergeNamespace ) )
require . Contains ( t , kubectlOutput , fmt . Sprintf ( "\n%s " , env . SupervisorNamespace ) )
2021-07-28 17:40:07 +00:00
if len ( env . ToolsNamespace ) > 0 {
2021-07-20 00:08:52 +00:00
require . Contains ( t , kubectlOutput , fmt . Sprintf ( "\n%s " , env . ToolsNamespace ) )
}
}
2021-05-11 20:55:46 +00:00
func requireUserCanUseKubectlWithoutAuthenticatingAgain (
ctx context . Context ,
t * testing . T ,
2021-06-22 15:23:19 +00:00
env * testlib . TestEnv ,
2021-05-11 20:55:46 +00:00
downstream * configv1alpha1 . FederationDomain ,
kubeconfigPath string ,
sessionCachePath string ,
pinnipedExe string ,
expectedUsername string ,
expectedGroups [ ] string ,
) {
// Run kubectl, which should work without any prompting for authentication.
kubectlCmd := exec . CommandContext ( ctx , "kubectl" , "get" , "namespace" , "--kubeconfig" , kubeconfigPath )
kubectlCmd . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
startTime := time . Now ( )
kubectlOutput2 , err := kubectlCmd . CombinedOutput ( )
2020-12-15 18:26:54 +00:00
require . NoError ( t , err )
require . Greaterf ( t , len ( bytes . Split ( kubectlOutput2 , [ ] byte ( "\n" ) ) ) , 2 , "expected some namespaces to be returned again" )
2021-05-11 20:55:46 +00:00
t . Logf ( "second kubectl command took %s" , time . Since ( startTime ) . String ( ) )
2021-01-11 19:58:07 +00:00
2021-05-11 20:55:46 +00:00
// Probe our cache for the current ID token as a proxy for a whoami API.
2021-01-11 19:58:07 +00:00
cache := filesession . New ( sessionCachePath , filesession . WithErrorReporter ( func ( err error ) {
require . NoError ( t , err )
} ) )
downstreamScopes := [ ] string { coreosoidc . ScopeOfflineAccess , coreosoidc . ScopeOpenID , "pinniped:request-audience" }
sort . Strings ( downstreamScopes )
token := cache . GetToken ( oidcclient . SessionCacheKey {
Issuer : downstream . Spec . Issuer ,
ClientID : "pinniped-cli" ,
Scopes : downstreamScopes ,
RedirectURI : "http://localhost:0/callback" ,
} )
require . NotNil ( t , token )
2021-05-13 21:24:10 +00:00
requireGCAnnotationsOnSessionStorage ( ctx , t , env . SupervisorNamespace , startTime , token )
2021-01-11 19:58:07 +00:00
idTokenClaims := token . IDToken . Claims
2021-05-11 20:55:46 +00:00
require . Equal ( t , expectedUsername , idTokenClaims [ oidc . DownstreamUsernameClaim ] )
2021-01-15 02:46:56 +00:00
// The groups claim in the file ends up as an []interface{}, so adjust our expectation to match.
2021-05-11 20:55:46 +00:00
expectedGroupsAsEmptyInterfaces := make ( [ ] interface { } , 0 , len ( expectedGroups ) )
for _ , g := range expectedGroups {
expectedGroupsAsEmptyInterfaces = append ( expectedGroupsAsEmptyInterfaces , g )
2021-01-11 19:58:07 +00:00
}
2021-05-17 18:10:26 +00:00
require . ElementsMatch ( t , expectedGroupsAsEmptyInterfaces , idTokenClaims [ oidc . DownstreamGroupsClaim ] )
expectedGroupsPlusAuthenticated := append ( [ ] string { } , expectedGroups ... )
expectedGroupsPlusAuthenticated = append ( expectedGroupsPlusAuthenticated , "system:authenticated" )
2021-05-11 20:55:46 +00:00
2022-07-26 00:25:21 +00:00
// Confirm we are the right user according to Kube by calling the WhoAmIRequest API.
// Use --validate=false with this command because running this command against any cluster which has
// the ServerSideFieldValidation feature gate enabled causes this command to return an RBAC error
// complaining that this user does not have permission to list CRDs:
// error validating data: failed to check CRD: failed to list CRDs: customresourcedefinitions.apiextensions.k8s.io is forbidden:
// User "pinny" cannot list resource "customresourcedefinitions" in API group "apiextensions.k8s.io" at the cluster scope; if you choose to ignore these errors, turn validation off with --validate=false
// While it is true that the user cannot list CRDs, that fact seems unrelated to making a create call to the
// aggregated API endpoint, so this is a strange error, but it can be easily reproduced.
kubectlCmd3 := exec . CommandContext ( ctx , "kubectl" , "create" , "-f" , "-" , "-o" , "yaml" , "--kubeconfig" , kubeconfigPath , "--validate=false" )
2021-02-19 18:21:10 +00:00
kubectlCmd3 . Env = append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
2021-05-11 20:55:46 +00:00
kubectlCmd3 . Stdin = strings . NewReader ( here . Docf ( `
apiVersion : identity . concierge . % s / v1alpha1
kind : WhoAmIRequest
2021-05-17 18:10:26 +00:00
` , env . APIGroupSuffix ) )
2021-05-11 20:55:46 +00:00
2021-02-19 18:21:10 +00:00
kubectlOutput3 , err := kubectlCmd3 . CombinedOutput ( )
2022-07-26 00:25:21 +00:00
require . NoErrorf ( t , err ,
"expected no error but got error, combined stdout/stderr was:\n----start of output\n%s\n----end of output" , kubectlOutput3 )
2021-05-11 20:55:46 +00:00
2021-05-17 18:10:26 +00:00
whoAmI := deserializeWhoAmIRequest ( t , string ( kubectlOutput3 ) , env . APIGroupSuffix )
require . Equal ( t , expectedUsername , whoAmI . Status . KubernetesUserInfo . User . Username )
require . ElementsMatch ( t , expectedGroupsPlusAuthenticated , whoAmI . Status . KubernetesUserInfo . User . Groups )
2021-03-04 19:46:18 +00:00
// Validate that `pinniped whoami` returns the correct identity.
assertWhoami (
ctx ,
t ,
true ,
pinnipedExe ,
kubeconfigPath ,
2021-05-11 20:55:46 +00:00
expectedUsername ,
2021-05-11 18:09:37 +00:00
expectedGroupsPlusAuthenticated ,
2021-03-04 19:46:18 +00:00
)
2020-12-15 18:26:54 +00:00
}
2021-05-11 20:55:46 +00:00
2021-05-13 21:24:10 +00:00
func requireGCAnnotationsOnSessionStorage ( ctx context . Context , t * testing . T , supervisorNamespace string , startTime time . Time , token * oidctypes . Token ) {
2021-05-12 16:05:13 +00:00
// check that the access token is new (since it's just been refreshed) and has close to two minutes left.
2021-05-13 21:24:10 +00:00
testutil . RequireTimeInDelta ( t , startTime . Add ( 2 * time . Minute ) , token . AccessToken . Expiry . Time , 15 * time . Second )
2021-05-12 16:05:13 +00:00
2021-06-22 15:23:19 +00:00
kubeClient := testlib . NewKubernetesClientset ( t ) . CoreV1 ( )
2021-05-12 16:05:13 +00:00
// get the access token secret that matches the signature from the cache
accessTokenSignature := strings . Split ( token . AccessToken . Token , "." ) [ 1 ]
accessSecretName := getSecretNameFromSignature ( t , accessTokenSignature , "access-token" )
2021-05-13 21:24:10 +00:00
accessTokenSecret , err := kubeClient . Secrets ( supervisorNamespace ) . Get ( ctx , accessSecretName , metav1 . GetOptions { } )
2021-05-12 16:05:13 +00:00
require . NoError ( t , err )
// Check that the access token garbage-collect-after value is 9 hours from now
accessTokenGCTimeString := accessTokenSecret . Annotations [ "storage.pinniped.dev/garbage-collect-after" ]
accessTokenGCTime , err := time . Parse ( crud . SecretLifetimeAnnotationDateFormat , accessTokenGCTimeString )
require . NoError ( t , err )
require . True ( t , accessTokenGCTime . After ( time . Now ( ) . Add ( 9 * time . Hour ) ) )
// get the refresh token secret that matches the signature from the cache
refreshTokenSignature := strings . Split ( token . RefreshToken . Token , "." ) [ 1 ]
refreshSecretName := getSecretNameFromSignature ( t , refreshTokenSignature , "refresh-token" )
2021-05-13 21:24:10 +00:00
refreshTokenSecret , err := kubeClient . Secrets ( supervisorNamespace ) . Get ( ctx , refreshSecretName , metav1 . GetOptions { } )
2021-05-12 16:05:13 +00:00
require . NoError ( t , err )
// Check that the refresh token garbage-collect-after value is 9 hours
refreshTokenGCTimeString := refreshTokenSecret . Annotations [ "storage.pinniped.dev/garbage-collect-after" ]
refreshTokenGCTime , err := time . Parse ( crud . SecretLifetimeAnnotationDateFormat , refreshTokenGCTimeString )
require . NoError ( t , err )
require . True ( t , refreshTokenGCTime . After ( time . Now ( ) . Add ( 9 * time . Hour ) ) )
// the access token and the refresh token should be garbage collected at essentially the same time
testutil . RequireTimeInDelta ( t , accessTokenGCTime , refreshTokenGCTime , 1 * time . Minute )
2021-05-13 21:24:10 +00:00
}
2021-05-12 16:05:13 +00:00
2021-06-22 15:23:19 +00:00
func runPinnipedGetKubeconfig ( t * testing . T , env * testlib . TestEnv , pinnipedExe string , tempDir string , pinnipedCLICommand [ ] string ) string {
2021-05-11 20:55:46 +00:00
// Run "pinniped get kubeconfig" to get a kubeconfig YAML.
envVarsWithProxy := append ( os . Environ ( ) , env . ProxyEnv ( ) ... )
kubeconfigYAML , stderr := runPinnipedCLI ( t , envVarsWithProxy , pinnipedExe , pinnipedCLICommand ... )
t . Logf ( "stderr output from 'pinniped get kubeconfig':\n%s\n\n" , stderr )
t . Logf ( "test kubeconfig:\n%s\n\n" , kubeconfigYAML )
2021-06-22 15:23:19 +00:00
restConfig := testlib . NewRestConfigFromKubeconfig ( t , kubeconfigYAML )
2021-05-11 20:55:46 +00:00
require . NotNil ( t , restConfig . ExecProvider )
require . Equal ( t , [ ] string { "login" , "oidc" } , restConfig . ExecProvider . Args [ : 2 ] )
kubeconfigPath := filepath . Join ( tempDir , "kubeconfig.yaml" )
2022-08-24 21:45:55 +00:00
require . NoError ( t , os . WriteFile ( kubeconfigPath , [ ] byte ( kubeconfigYAML ) , 0600 ) )
2021-05-11 20:55:46 +00:00
return kubeconfigPath
}
2021-05-12 16:05:13 +00:00
func getSecretNameFromSignature ( t * testing . T , signature string , typeLabel string ) string {
t . Helper ( )
// try to decode base64 signatures to prevent double encoding of binary data
signatureBytes , err := base64 . RawURLEncoding . DecodeString ( signature )
require . NoError ( t , err )
// lower case base32 encoding insures that our secret name is valid per ValidateSecretName in k/k
var b32 = base32 . StdEncoding . WithPadding ( base32 . NoPadding )
signatureAsValidName := strings . ToLower ( b32 . EncodeToString ( signatureBytes ) )
return fmt . Sprintf ( "pinniped-storage-%s-%s" , typeLabel , signatureAsValidName )
}
2022-02-08 15:27:26 +00:00
func readAllCtx ( ctx context . Context , r io . Reader ) ( [ ] byte , error ) {
errCh := make ( chan error , 1 )
data := & atomic . Value { }
go func ( ) { // copied from io.ReadAll and modified to use the atomic.Value above
b := make ( [ ] byte , 0 , 512 )
data . Store ( string ( b ) ) // cast to string to make a copy of the byte slice
for {
if len ( b ) == cap ( b ) {
// Add more capacity (let append pick how much).
b = append ( b , 0 ) [ : len ( b ) ]
data . Store ( string ( b ) ) // cast to string to make a copy of the byte slice
}
n , err := r . Read ( b [ len ( b ) : cap ( b ) ] )
b = b [ : len ( b ) + n ]
data . Store ( string ( b ) ) // cast to string to make a copy of the byte slice
if err != nil {
if err == io . EOF {
err = nil
}
errCh <- err
return
}
}
} ( )
select {
case <- ctx . Done ( ) :
b , _ := data . Load ( ) . ( string )
return nil , fmt . Errorf ( "failed to complete read all: %w, data read so far:\n%q" , ctx . Err ( ) , b )
case err := <- errCh :
b , _ := data . Load ( ) . ( string )
if len ( b ) == 0 {
return nil , err
}
return [ ] byte ( b ) , err
}
}