e2e test: PINNIPED_USERNAME/PINNIPED_PASSWORD env vars during LDAP login

This commit is contained in:
Ryan Richard 2021-07-19 17:08:52 -07:00
parent cac45fd999
commit deb699a84a

View File

@ -249,15 +249,16 @@ func TestE2EFullIntegration(t *testing.T) {
// It should now be in the "success" state.
formpostExpectSuccessState(t, page)
// Expect the CLI to output a list of namespaces in JSON format.
t.Logf("waiting for kubectl to output namespace list JSON")
// Expect the CLI to output a list of namespaces.
t.Logf("waiting for kubectl to output namespace list")
var kubectlOutput string
select {
case <-time.After(10 * time.Second):
require.Fail(t, "timed out waiting for kubectl output")
case kubectlOutput = <-kubectlOutputChan:
}
require.Greaterf(t, len(strings.Split(kubectlOutput, "\n")), 2, "expected some namespaces to be returned, got %q", kubectlOutput)
requireKubectlGetNamespaceOutput(t, env, kubectlOutput)
t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env,
@ -364,10 +365,11 @@ func TestE2EFullIntegration(t *testing.T) {
// Read all of the remaining output from the subprocess until EOF.
t.Logf("waiting for kubectl to output namespace list")
remainingOutput, _ := ioutil.ReadAll(ptyFile)
// Read all of the output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
require.Greaterf(t, len(remainingOutput), 0, "expected to get some more output from the kubectl subcommand, but did not")
require.Greaterf(t, len(strings.Split(string(remainingOutput), "\n")), 2, "expected some namespaces to be returned, got %q", string(remainingOutput))
kubectlOutputBytes, _ := ioutil.ReadAll(ptyFile)
requireKubectlGetNamespaceOutput(t, env, string(kubectlOutputBytes))
t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env,
@ -380,8 +382,9 @@ func TestE2EFullIntegration(t *testing.T) {
)
})
// Add an LDAP upstream IDP and try using it to authenticate during kubectl commands.
t.Run("with Supervisor LDAP upstream IDP", func(t *testing.T) {
// 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) {
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
t.Skip("LDAP integration test requires connectivity to an LDAP server")
}
@ -389,12 +392,130 @@ func TestE2EFullIntegration(t *testing.T) {
expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue
expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs
setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
// Use a specific session cache for this test.
sessionCachePath := tempDir + "/ldap-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-session-cache", sessionCachePath,
})
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
start := time.Now()
kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath)
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.SupervisorUpstreamLDAP.TestUserPassword + "\n")
require.NoError(t, err)
// Read all of the output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
kubectlOutputBytes, _ := ioutil.ReadAll(ptyFile)
requireKubectlGetNamespaceOutput(t, env, string(kubectlOutputBytes))
t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env,
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) {
if len(env.ToolsNamespace) == 0 && !env.HasCapability(testlib.CanReachInternetLDAPPorts) {
t.Skip("LDAP integration test requires connectivity to an LDAP server")
}
expectedUsername := env.SupervisorUpstreamLDAP.TestUserMailAttributeValue
expectedGroups := env.SupervisorUpstreamLDAP.TestUserDirectGroupsDNs
setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
// Use a specific session cache for this test.
sessionCachePath := tempDir + "/ldap-test-with-env-vars-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-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"
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()
kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath)
kubectlCmd.Env = append(os.Environ(), env.ProxyEnv()...)
ptyFile, err := pty.Start(kubectlCmd)
require.NoError(t, err)
// Read all of the output from the subprocess until EOF.
// Ignore any errors returned because there is always an error on linux.
kubectlOutputBytes, _ := ioutil.ReadAll(ptyFile)
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))
requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env,
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
})
}
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: expectedUsername},
rbacv1.Subject{Kind: rbacv1.UserKind, APIGroup: rbacv1.GroupName, Name: username},
rbacv1.RoleRef{Kind: "ClusterRole", APIGroup: rbacv1.GroupName, Name: "view"},
)
testlib.WaitForUserToHaveAccess(t, expectedUsername, []string{}, &authorizationv1.ResourceAttributes{
testlib.WaitForUserToHaveAccess(t, username, []string{}, &authorizationv1.ResourceAttributes{
Verb: "get",
Group: "",
Version: "v1",
@ -434,51 +555,6 @@ func TestE2EFullIntegration(t *testing.T) {
},
},
}, idpv1alpha1.LDAPPhaseReady)
// Use a specific session cache for this test.
sessionCachePath := tempDir + "/ldap-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-session-cache", sessionCachePath,
})
// Run "kubectl get namespaces" which should trigger an LDAP-style login CLI prompt via the plugin.
start := time.Now()
kubectlCmd := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath)
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.SupervisorUpstreamLDAP.TestUserPassword + "\n")
require.NoError(t, err)
// Read all of the remaining output from the subprocess until EOF.
remainingOutput, _ := ioutil.ReadAll(ptyFile)
// Ignore any errors returned because there is always an error on linux.
require.Greaterf(t, len(remainingOutput), 0, "expected to get some more output from the kubectl subcommand, but did not")
require.Greaterf(t, len(strings.Split(string(remainingOutput), "\n")), 2, "expected some namespaces to be returned, got %q", string(remainingOutput))
t.Logf("first kubectl command took %s", time.Since(start).String())
requireUserCanUseKubectlWithoutAuthenticatingAgain(ctx, t, env,
downstream,
kubeconfigPath,
sessionCachePath,
pinnipedExe,
expectedUsername,
expectedGroups,
)
})
}
func readFromFileUntilStringIsSeen(t *testing.T, f *os.File, until string) string {
@ -510,6 +586,19 @@ func readAvailableOutput(t *testing.T, r io.Reader) (string, bool) {
return string(buf[:n]), false
}
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))
if len(env.ToolsNamespace) == 0 {
require.Contains(t, kubectlOutput, fmt.Sprintf("\n%s ", env.ToolsNamespace))
}
}
func requireUserCanUseKubectlWithoutAuthenticatingAgain(
ctx context.Context,
t *testing.T,