2021-01-19 18:50:22 +00:00
|
|
|
// Copyright 2020-2021 the Pinniped contributors. All Rights Reserved.
|
2020-09-16 14:19:51 +00:00
|
|
|
// SPDX-License-Identifier: Apache-2.0
|
2020-09-15 15:00:38 +00:00
|
|
|
package integration
|
|
|
|
|
|
|
|
import (
|
2020-10-13 15:41:53 +00:00
|
|
|
"bufio"
|
2020-10-14 17:28:08 +00:00
|
|
|
"bytes"
|
2020-09-15 15:00:38 +00:00
|
|
|
"context"
|
2020-10-13 15:41:53 +00:00
|
|
|
"encoding/json"
|
2020-10-14 17:28:08 +00:00
|
|
|
"errors"
|
|
|
|
"fmt"
|
2020-10-13 15:41:53 +00:00
|
|
|
"io"
|
2020-09-15 15:00:38 +00:00
|
|
|
"io/ioutil"
|
2020-11-19 21:05:31 +00:00
|
|
|
"net/url"
|
2020-09-15 15:00:38 +00:00
|
|
|
"os"
|
|
|
|
"os/exec"
|
|
|
|
"path/filepath"
|
2020-10-13 15:41:53 +00:00
|
|
|
"regexp"
|
|
|
|
"strings"
|
2020-09-15 15:00:38 +00:00
|
|
|
"testing"
|
|
|
|
"time"
|
|
|
|
|
2021-03-11 21:47:39 +00:00
|
|
|
"github.com/stretchr/testify/assert"
|
2020-09-15 15:00:38 +00:00
|
|
|
"github.com/stretchr/testify/require"
|
2020-10-14 17:28:08 +00:00
|
|
|
"golang.org/x/sync/errgroup"
|
2020-10-13 15:41:53 +00:00
|
|
|
"gopkg.in/square/go-jose.v2"
|
2021-03-04 19:46:18 +00:00
|
|
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
|
|
|
"k8s.io/apimachinery/pkg/runtime/serializer"
|
|
|
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
2020-10-13 15:41:53 +00:00
|
|
|
clientauthenticationv1beta1 "k8s.io/client-go/pkg/apis/clientauthentication/v1beta1"
|
2020-09-15 15:00:38 +00:00
|
|
|
|
2021-03-04 19:46:18 +00:00
|
|
|
identityapi "go.pinniped.dev/generated/latest/apis/concierge/identity"
|
|
|
|
identityv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/identity/v1alpha1"
|
|
|
|
loginapi "go.pinniped.dev/generated/latest/apis/concierge/login"
|
|
|
|
loginv1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/login/v1alpha1"
|
|
|
|
"go.pinniped.dev/internal/groupsuffix"
|
|
|
|
"go.pinniped.dev/internal/plog"
|
2020-11-24 19:38:28 +00:00
|
|
|
"go.pinniped.dev/internal/testutil"
|
2020-11-17 18:46:54 +00:00
|
|
|
"go.pinniped.dev/pkg/oidcclient"
|
|
|
|
"go.pinniped.dev/pkg/oidcclient/filesession"
|
2020-09-18 19:56:24 +00:00
|
|
|
"go.pinniped.dev/test/library"
|
2020-12-02 21:29:54 +00:00
|
|
|
"go.pinniped.dev/test/library/browsertest"
|
2020-09-15 15:00:38 +00:00
|
|
|
)
|
|
|
|
|
2020-12-15 00:42:02 +00:00
|
|
|
func TestCLIGetKubeconfigStaticToken(t *testing.T) {
|
2020-09-24 22:51:43 +00:00
|
|
|
env := library.IntegrationEnv(t).WithCapability(library.ClusterSigningKeyIsAvailable)
|
2020-09-15 15:00:38 +00:00
|
|
|
|
2020-09-22 00:55:04 +00:00
|
|
|
// Create a test webhook configuration to use with the CLI.
|
2021-03-05 01:25:43 +00:00
|
|
|
ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute)
|
2020-09-22 00:55:04 +00:00
|
|
|
defer cancelFunc()
|
|
|
|
|
2020-10-30 19:02:21 +00:00
|
|
|
authenticator := library.CreateTestWebhookAuthenticator(ctx, t)
|
2020-09-22 00:55:04 +00:00
|
|
|
|
2020-09-15 15:00:38 +00:00
|
|
|
// Build pinniped CLI.
|
2020-12-15 18:19:42 +00:00
|
|
|
pinnipedExe := library.PinnipedCLIPath(t)
|
2020-09-15 15:00:38 +00:00
|
|
|
|
2020-12-15 00:42:02 +00:00
|
|
|
for _, tt := range []struct {
|
2021-03-11 21:47:39 +00:00
|
|
|
name string
|
|
|
|
args []string
|
|
|
|
expectStderrContains []string
|
2020-12-15 00:42:02 +00:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
name: "newer command, but still using static parameters",
|
|
|
|
args: []string{
|
|
|
|
"get", "kubeconfig",
|
|
|
|
"--static-token", env.TestUser.Token,
|
2021-01-19 18:50:22 +00:00
|
|
|
"--concierge-api-group-suffix", env.APIGroupSuffix,
|
2020-12-15 00:42:02 +00:00
|
|
|
"--concierge-authenticator-type", "webhook",
|
|
|
|
"--concierge-authenticator-name", authenticator.Name,
|
|
|
|
},
|
2021-03-11 21:47:39 +00:00
|
|
|
expectStderrContains: []string{
|
|
|
|
"discovered CredentialIssuer",
|
|
|
|
"discovered Concierge endpoint",
|
|
|
|
"discovered Concierge certificate authority bundle",
|
|
|
|
"validated connection to the cluster",
|
|
|
|
},
|
2020-12-15 00:42:02 +00:00
|
|
|
},
|
|
|
|
} {
|
|
|
|
tt := tt
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
2021-03-06 00:14:45 +00:00
|
|
|
stdout, stderr := runPinnipedCLI(t, nil, pinnipedExe, tt.args...)
|
2021-03-11 21:47:39 +00:00
|
|
|
for _, s := range tt.expectStderrContains {
|
|
|
|
assert.Contains(t, stderr, s)
|
|
|
|
}
|
2020-12-15 00:42:02 +00:00
|
|
|
|
|
|
|
// Even the deprecated command should now generate a kubeconfig with the new "pinniped login static" command.
|
|
|
|
restConfig := library.NewRestConfigFromKubeconfig(t, stdout)
|
|
|
|
require.NotNil(t, restConfig.ExecProvider)
|
|
|
|
require.Equal(t, []string{"login", "static"}, restConfig.ExecProvider.Args[:2])
|
|
|
|
|
|
|
|
// In addition to the client-go based testing below, also try the kubeconfig
|
|
|
|
// with kubectl to validate that it works.
|
|
|
|
t.Run(
|
|
|
|
"access as user with kubectl",
|
2021-01-13 01:27:41 +00:00
|
|
|
library.AccessAsUserWithKubectlTest(stdout, env.TestUser.ExpectedUsername, env.ConciergeNamespace),
|
2020-12-15 00:42:02 +00:00
|
|
|
)
|
|
|
|
for _, group := range env.TestUser.ExpectedGroups {
|
|
|
|
group := group
|
|
|
|
t.Run(
|
|
|
|
"access as group "+group+" with kubectl",
|
2021-01-13 01:27:41 +00:00
|
|
|
library.AccessAsGroupWithKubectlTest(stdout, group, env.ConciergeNamespace),
|
2020-12-15 00:42:02 +00:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Create Kubernetes client with kubeconfig from pinniped CLI.
|
|
|
|
kubeClient := library.NewClientsetForKubeConfig(t, stdout)
|
|
|
|
|
|
|
|
// Validate that we can auth to the API via our user.
|
2021-01-13 01:27:41 +00:00
|
|
|
t.Run("access as user with client-go", library.AccessAsUserTest(ctx, env.TestUser.ExpectedUsername, kubeClient))
|
2020-12-15 00:42:02 +00:00
|
|
|
for _, group := range env.TestUser.ExpectedGroups {
|
|
|
|
group := group
|
2021-01-13 01:27:41 +00:00
|
|
|
t.Run("access as group "+group+" with client-go", library.AccessAsGroupTest(ctx, group, kubeClient))
|
2020-12-15 00:42:02 +00:00
|
|
|
}
|
2021-03-04 19:46:18 +00:00
|
|
|
|
|
|
|
// Validate that `pinniped whoami` returns the correct identity.
|
|
|
|
kubeconfigPath := filepath.Join(testutil.TempDir(t), "whoami-kubeconfig")
|
|
|
|
require.NoError(t, ioutil.WriteFile(kubeconfigPath, []byte(stdout), 0600))
|
|
|
|
assertWhoami(
|
|
|
|
ctx,
|
|
|
|
t,
|
|
|
|
false,
|
|
|
|
pinnipedExe,
|
|
|
|
kubeconfigPath,
|
|
|
|
env.TestUser.ExpectedUsername,
|
|
|
|
append(env.TestUser.ExpectedGroups, "system:authenticated"),
|
|
|
|
)
|
2020-12-15 00:42:02 +00:00
|
|
|
})
|
2020-09-21 18:40:11 +00:00
|
|
|
}
|
2020-09-15 15:00:38 +00:00
|
|
|
}
|
|
|
|
|
2021-03-06 00:14:45 +00:00
|
|
|
func runPinnipedCLI(t *testing.T, envVars []string, pinnipedExe string, args ...string) (string, string) {
|
2020-09-15 15:00:38 +00:00
|
|
|
t.Helper()
|
2020-12-15 00:42:02 +00:00
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd := exec.Command(pinnipedExe, args...)
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
cmd.Stderr = &stderr
|
2021-03-06 00:14:45 +00:00
|
|
|
cmd.Env = envVars
|
2020-12-15 00:42:02 +00:00
|
|
|
require.NoErrorf(t, cmd.Run(), "stderr:\n%s\n\nstdout:\n%s\n\n", stderr.String(), stdout.String())
|
|
|
|
return stdout.String(), stderr.String()
|
2020-09-15 15:00:38 +00:00
|
|
|
}
|
2020-10-13 15:41:53 +00:00
|
|
|
|
2021-03-04 19:46:18 +00:00
|
|
|
func assertWhoami(ctx context.Context, t *testing.T, useProxy bool, pinnipedExe, kubeconfigPath, wantUsername string, wantGroups []string) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
apiGroupSuffix := library.IntegrationEnv(t).APIGroupSuffix
|
|
|
|
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
|
|
cmd := exec.CommandContext(
|
|
|
|
ctx,
|
|
|
|
pinnipedExe,
|
|
|
|
"whoami",
|
|
|
|
"--kubeconfig",
|
|
|
|
kubeconfigPath,
|
|
|
|
"--output",
|
|
|
|
"yaml",
|
|
|
|
"--api-group-suffix",
|
|
|
|
apiGroupSuffix,
|
|
|
|
)
|
|
|
|
if useProxy {
|
|
|
|
cmd.Env = append(os.Environ(), library.IntegrationEnv(t).ProxyEnv()...)
|
|
|
|
}
|
|
|
|
cmd.Stdout = &stdout
|
|
|
|
cmd.Stderr = &stderr
|
|
|
|
require.NoErrorf(t, cmd.Run(), "stderr:\n%s\n\nstdout:\n%s\n\n", stderr.String(), stdout.String())
|
|
|
|
|
|
|
|
whoAmI := deserializeWhoAmIRequest(t, stdout.String(), apiGroupSuffix)
|
|
|
|
require.Equal(t, wantUsername, whoAmI.Status.KubernetesUserInfo.User.Username)
|
|
|
|
require.ElementsMatch(t, wantGroups, whoAmI.Status.KubernetesUserInfo.User.Groups)
|
|
|
|
}
|
|
|
|
|
|
|
|
func deserializeWhoAmIRequest(t *testing.T, data string, apiGroupSuffix string) *identityv1alpha1.WhoAmIRequest {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
scheme, _, _ := conciergeschemeNew(apiGroupSuffix)
|
|
|
|
codecs := serializer.NewCodecFactory(scheme)
|
|
|
|
respInfo, ok := runtime.SerializerInfoForMediaType(codecs.SupportedMediaTypes(), runtime.ContentTypeYAML)
|
|
|
|
require.True(t, ok)
|
|
|
|
|
|
|
|
obj, err := runtime.Decode(respInfo.Serializer, []byte(data))
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
return obj.(*identityv1alpha1.WhoAmIRequest)
|
|
|
|
}
|
|
|
|
|
2020-10-13 15:41:53 +00:00
|
|
|
func TestCLILoginOIDC(t *testing.T) {
|
2020-10-13 21:09:13 +00:00
|
|
|
env := library.IntegrationEnv(t)
|
2020-10-13 15:41:53 +00:00
|
|
|
|
2020-10-22 16:49:04 +00:00
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
|
2020-10-13 21:09:13 +00:00
|
|
|
defer cancel()
|
|
|
|
|
2020-10-13 15:41:53 +00:00
|
|
|
// Build pinniped CLI.
|
2020-12-15 18:19:42 +00:00
|
|
|
pinnipedExe := library.PinnipedCLIPath(t)
|
2020-10-13 15:41:53 +00:00
|
|
|
|
2020-12-08 01:39:51 +00:00
|
|
|
// Run "pinniped login oidc" to get an ExecCredential struct with an OIDC ID token.
|
2020-12-15 18:23:52 +00:00
|
|
|
credOutput, sessionCachePath := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
|
2020-12-08 01:39:51 +00:00
|
|
|
|
|
|
|
// Assert some properties of the ExecCredential.
|
|
|
|
t.Logf("validating ExecCredential")
|
|
|
|
require.NotNil(t, credOutput.Status)
|
|
|
|
require.Empty(t, credOutput.Status.ClientKeyData)
|
|
|
|
require.Empty(t, credOutput.Status.ClientCertificateData)
|
|
|
|
|
|
|
|
// There should be at least 1 minute of remaining expiration (probably more).
|
|
|
|
require.NotNil(t, credOutput.Status.ExpirationTimestamp)
|
|
|
|
ttl := time.Until(credOutput.Status.ExpirationTimestamp.Time)
|
|
|
|
require.Greater(t, ttl.Milliseconds(), (1 * time.Minute).Milliseconds())
|
|
|
|
|
|
|
|
// Assert some properties about the token, which should be a valid JWT.
|
|
|
|
require.NotEmpty(t, credOutput.Status.Token)
|
|
|
|
jws, err := jose.ParseSigned(credOutput.Status.Token)
|
|
|
|
require.NoError(t, err)
|
|
|
|
claims := map[string]interface{}{}
|
|
|
|
require.NoError(t, json.Unmarshal(jws.UnsafePayloadWithoutVerification(), &claims))
|
|
|
|
require.Equal(t, env.CLITestUpstream.Issuer, claims["iss"])
|
|
|
|
require.Equal(t, env.CLITestUpstream.ClientID, claims["aud"])
|
|
|
|
require.Equal(t, env.CLITestUpstream.Username, claims["email"])
|
|
|
|
require.NotEmpty(t, claims["nonce"])
|
|
|
|
|
|
|
|
// Run the CLI again with the same session cache and login parameters.
|
|
|
|
t.Logf("starting second CLI subprocess to test session caching")
|
|
|
|
cmd2Output, err := oidcLoginCommand(ctx, t, pinnipedExe, sessionCachePath).CombinedOutput()
|
|
|
|
require.NoError(t, err, string(cmd2Output))
|
|
|
|
|
|
|
|
// Expect the CLI to output the same ExecCredential in JSON format.
|
|
|
|
t.Logf("validating second ExecCredential")
|
|
|
|
var credOutput2 clientauthenticationv1beta1.ExecCredential
|
|
|
|
require.NoErrorf(t, json.Unmarshal(cmd2Output, &credOutput2),
|
|
|
|
"command returned something other than an ExecCredential:\n%s", string(cmd2Output))
|
|
|
|
require.Equal(t, credOutput, credOutput2)
|
|
|
|
|
|
|
|
// Overwrite the cache entry to remove the access and ID tokens.
|
|
|
|
t.Logf("overwriting cache to remove valid ID token")
|
|
|
|
cache := filesession.New(sessionCachePath)
|
|
|
|
cacheKey := oidcclient.SessionCacheKey{
|
|
|
|
Issuer: env.CLITestUpstream.Issuer,
|
|
|
|
ClientID: env.CLITestUpstream.ClientID,
|
|
|
|
Scopes: []string{"email", "offline_access", "openid", "profile"},
|
|
|
|
RedirectURI: strings.ReplaceAll(env.CLITestUpstream.CallbackURL, "127.0.0.1", "localhost"),
|
|
|
|
}
|
|
|
|
cached := cache.GetToken(cacheKey)
|
|
|
|
require.NotNil(t, cached)
|
|
|
|
require.NotNil(t, cached.RefreshToken)
|
|
|
|
require.NotEmpty(t, cached.RefreshToken.Token)
|
|
|
|
cached.IDToken = nil
|
|
|
|
cached.AccessToken = nil
|
|
|
|
cache.PutToken(cacheKey, cached)
|
|
|
|
|
|
|
|
// Run the CLI a third time with the same session cache and login parameters.
|
|
|
|
t.Logf("starting third CLI subprocess to test refresh flow")
|
|
|
|
cmd3Output, err := oidcLoginCommand(ctx, t, pinnipedExe, sessionCachePath).CombinedOutput()
|
|
|
|
require.NoError(t, err, string(cmd2Output))
|
|
|
|
|
|
|
|
// Expect the CLI to output a new ExecCredential in JSON format (different from the one returned the first two times).
|
|
|
|
t.Logf("validating third ExecCredential")
|
|
|
|
var credOutput3 clientauthenticationv1beta1.ExecCredential
|
|
|
|
require.NoErrorf(t, json.Unmarshal(cmd3Output, &credOutput3),
|
|
|
|
"command returned something other than an ExecCredential:\n%s", string(cmd2Output))
|
|
|
|
require.NotEqual(t, credOutput2.Status.Token, credOutput3.Status.Token)
|
|
|
|
}
|
|
|
|
|
2020-12-15 18:23:52 +00:00
|
|
|
func runPinnipedLoginOIDC(
|
2020-12-08 01:39:51 +00:00
|
|
|
ctx context.Context,
|
|
|
|
t *testing.T,
|
|
|
|
pinnipedExe string,
|
|
|
|
) (clientauthenticationv1beta1.ExecCredential, string) {
|
|
|
|
t.Helper()
|
|
|
|
|
|
|
|
env := library.IntegrationEnv(t)
|
|
|
|
|
2020-10-21 20:02:42 +00:00
|
|
|
// Make a temp directory to hold the session cache for this test.
|
2020-11-24 19:38:28 +00:00
|
|
|
sessionCachePath := testutil.TempDir(t) + "/sessions.yaml"
|
2020-10-21 20:02:42 +00:00
|
|
|
|
2020-12-08 01:39:51 +00:00
|
|
|
// Start the browser driver.
|
|
|
|
page := browsertest.Open(t)
|
|
|
|
|
|
|
|
// Start the CLI running the "login oidc [...]" command with stdout/stderr connected to pipes.
|
2020-10-22 22:35:06 +00:00
|
|
|
cmd := oidcLoginCommand(ctx, t, pinnipedExe, sessionCachePath)
|
2020-10-14 17:28:08 +00:00
|
|
|
stderr, err := cmd.StderrPipe()
|
|
|
|
require.NoError(t, err)
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
|
|
require.NoError(t, err)
|
2020-10-22 22:35:06 +00:00
|
|
|
t.Logf("starting CLI subprocess")
|
2020-10-14 17:28:08 +00:00
|
|
|
require.NoError(t, cmd.Start())
|
|
|
|
t.Cleanup(func() {
|
|
|
|
err := cmd.Wait()
|
|
|
|
t.Logf("CLI subprocess exited with code %d", cmd.ProcessState.ExitCode())
|
|
|
|
require.NoErrorf(t, err, "CLI process did not exit cleanly")
|
|
|
|
})
|
2020-10-13 15:41:53 +00:00
|
|
|
|
|
|
|
// Start a background goroutine to read stderr from the CLI and parse out the login URL.
|
|
|
|
loginURLChan := make(chan string)
|
2020-10-14 17:28:08 +00:00
|
|
|
spawnTestGoroutine(t, func() (err error) {
|
|
|
|
defer func() {
|
|
|
|
closeErr := stderr.Close()
|
|
|
|
if closeErr == nil || errors.Is(closeErr, os.ErrClosed) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
err = fmt.Errorf("stderr stream closed with error: %w", closeErr)
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
2020-10-22 15:30:51 +00:00
|
|
|
reader := bufio.NewReader(library.NewLoggerReader(t, "stderr", stderr))
|
2020-10-14 17:28:08 +00:00
|
|
|
line, err := reader.ReadString('\n')
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not read login URL line from stderr: %w", err)
|
|
|
|
}
|
2020-10-13 15:41:53 +00:00
|
|
|
const prompt = "Please log in: "
|
2020-10-14 17:28:08 +00:00
|
|
|
if !strings.HasPrefix(line, prompt) {
|
|
|
|
return fmt.Errorf("expected %q to have prefix %q", line, prompt)
|
|
|
|
}
|
2020-10-13 15:41:53 +00:00
|
|
|
loginURLChan <- strings.TrimPrefix(line, prompt)
|
2020-10-14 17:28:08 +00:00
|
|
|
return readAndExpectEmpty(reader)
|
|
|
|
})
|
2020-10-13 15:41:53 +00:00
|
|
|
|
|
|
|
// Start a background goroutine to read stdout from the CLI and parse out an ExecCredential.
|
|
|
|
credOutputChan := make(chan clientauthenticationv1beta1.ExecCredential)
|
2020-10-14 17:28:08 +00:00
|
|
|
spawnTestGoroutine(t, func() (err error) {
|
|
|
|
defer func() {
|
2020-12-15 18:24:28 +00:00
|
|
|
closeErr := stdout.Close()
|
2020-10-14 17:28:08 +00:00
|
|
|
if closeErr == nil || errors.Is(closeErr, os.ErrClosed) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if err == nil {
|
|
|
|
err = fmt.Errorf("stdout stream closed with error: %w", closeErr)
|
|
|
|
}
|
|
|
|
}()
|
2020-10-22 15:30:51 +00:00
|
|
|
reader := bufio.NewReader(library.NewLoggerReader(t, "stdout", stdout))
|
2020-10-13 15:41:53 +00:00
|
|
|
var out clientauthenticationv1beta1.ExecCredential
|
2020-10-14 17:28:08 +00:00
|
|
|
if err := json.NewDecoder(reader).Decode(&out); err != nil {
|
|
|
|
return fmt.Errorf("could not read ExecCredential from stdout: %w", err)
|
|
|
|
}
|
2020-10-13 15:41:53 +00:00
|
|
|
credOutputChan <- out
|
2020-10-14 17:28:08 +00:00
|
|
|
return readAndExpectEmpty(reader)
|
2020-10-13 21:09:13 +00:00
|
|
|
})
|
2020-10-13 15:41:53 +00:00
|
|
|
|
|
|
|
// 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")
|
|
|
|
require.NoError(t, page.Navigate(loginURL))
|
|
|
|
|
2020-12-02 21:29:54 +00:00
|
|
|
// Expect to be redirected to the upstream provider and log in.
|
|
|
|
browsertest.LoginToUpstream(t, page, env.CLITestUpstream)
|
2020-10-13 15:41:53 +00:00
|
|
|
|
2020-12-02 21:29:54 +00:00
|
|
|
// Expect to be redirected to the localhost callback.
|
|
|
|
t.Logf("waiting for redirect to callback")
|
2020-11-19 21:05:31 +00:00
|
|
|
callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLITestUpstream.CallbackURL) + `\?.+\z`)
|
2020-12-02 21:29:54 +00:00
|
|
|
browsertest.WaitForURL(t, page, callbackURLPattern)
|
2020-10-13 15:41:53 +00:00
|
|
|
|
|
|
|
// Wait for the "pre" element that gets rendered for a `text/plain` page, and
|
|
|
|
// assert that it contains the success message.
|
|
|
|
t.Logf("verifying success page")
|
2020-12-02 21:29:54 +00:00
|
|
|
browsertest.WaitForVisibleElements(t, page, "pre")
|
2020-10-13 15:41:53 +00:00
|
|
|
msg, err := page.First("pre").Text()
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, "you have been logged in and may now close this tab", msg)
|
|
|
|
|
|
|
|
// Expect the CLI to output an ExecCredential in JSON format.
|
|
|
|
t.Logf("waiting for CLI to output ExecCredential JSON")
|
|
|
|
var credOutput clientauthenticationv1beta1.ExecCredential
|
|
|
|
select {
|
|
|
|
case <-time.After(10 * time.Second):
|
|
|
|
require.Fail(t, "timed out waiting for exec credential output")
|
|
|
|
case credOutput = <-credOutputChan:
|
|
|
|
}
|
|
|
|
|
2020-12-08 01:39:51 +00:00
|
|
|
return credOutput, sessionCachePath
|
2020-10-13 15:41:53 +00:00
|
|
|
}
|
|
|
|
|
2020-10-14 17:28:08 +00:00
|
|
|
func readAndExpectEmpty(r io.Reader) (err error) {
|
|
|
|
var remainder bytes.Buffer
|
|
|
|
_, err = io.Copy(&remainder, r)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if r := remainder.String(); r != "" {
|
|
|
|
return fmt.Errorf("expected remainder to be empty, but got %q", r)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func spawnTestGoroutine(t *testing.T, f func() error) {
|
|
|
|
t.Helper()
|
|
|
|
var eg errgroup.Group
|
|
|
|
t.Cleanup(func() {
|
|
|
|
require.NoError(t, eg.Wait(), "background goroutine failed")
|
|
|
|
})
|
|
|
|
eg.Go(f)
|
|
|
|
}
|
2020-10-22 22:35:06 +00:00
|
|
|
|
|
|
|
func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, sessionCachePath string) *exec.Cmd {
|
|
|
|
env := library.IntegrationEnv(t)
|
2020-11-19 21:05:31 +00:00
|
|
|
callbackURL, err := url.Parse(env.CLITestUpstream.CallbackURL)
|
|
|
|
require.NoError(t, err)
|
2020-11-16 16:40:18 +00:00
|
|
|
cmd := exec.CommandContext(ctx, pinnipedExe, "login", "oidc",
|
2020-11-19 21:05:31 +00:00
|
|
|
"--issuer", env.CLITestUpstream.Issuer,
|
|
|
|
"--client-id", env.CLITestUpstream.ClientID,
|
2020-12-04 17:21:30 +00:00
|
|
|
"--scopes", "offline_access,openid,email,profile",
|
2020-11-19 21:05:31 +00:00
|
|
|
"--listen-port", callbackURL.Port(),
|
2020-10-22 22:35:06 +00:00
|
|
|
"--session-cache", sessionCachePath,
|
|
|
|
"--skip-browser",
|
|
|
|
)
|
2020-11-16 20:04:08 +00:00
|
|
|
|
|
|
|
// If there is a custom CA bundle, pass it via --ca-bundle and a temporary file.
|
2020-11-19 21:05:31 +00:00
|
|
|
if env.CLITestUpstream.CABundle != "" {
|
2020-11-24 19:38:28 +00:00
|
|
|
path := filepath.Join(testutil.TempDir(t), "test-ca.pem")
|
2020-11-19 21:05:31 +00:00
|
|
|
require.NoError(t, ioutil.WriteFile(path, []byte(env.CLITestUpstream.CABundle), 0600))
|
2020-11-16 20:04:08 +00:00
|
|
|
cmd.Args = append(cmd.Args, "--ca-bundle", path)
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there is a custom proxy, set it using standard environment variables.
|
2020-12-15 17:45:40 +00:00
|
|
|
cmd.Env = append(os.Environ(), env.ProxyEnv()...)
|
2020-11-16 16:40:18 +00:00
|
|
|
return cmd
|
2020-10-22 22:35:06 +00:00
|
|
|
}
|
2021-03-04 19:46:18 +00:00
|
|
|
|
|
|
|
// conciergeschemeNew is a temporary private function to stand in place for
|
|
|
|
// "go.pinniped.dev/internal/concierge/scheme".New until the later function is merged to main.
|
|
|
|
func conciergeschemeNew(apiGroupSuffix string) (_ *runtime.Scheme, login, identity schema.GroupVersion) {
|
|
|
|
// standard set up of the server side scheme
|
|
|
|
scheme := runtime.NewScheme()
|
|
|
|
|
|
|
|
// add the options to empty v1
|
|
|
|
metav1.AddToGroupVersion(scheme, metav1.Unversioned)
|
|
|
|
|
|
|
|
// nothing fancy is required if using the standard group suffix
|
|
|
|
if apiGroupSuffix == groupsuffix.PinnipedDefaultSuffix {
|
|
|
|
schemeBuilder := runtime.NewSchemeBuilder(
|
|
|
|
loginv1alpha1.AddToScheme,
|
|
|
|
loginapi.AddToScheme,
|
|
|
|
identityv1alpha1.AddToScheme,
|
|
|
|
identityapi.AddToScheme,
|
|
|
|
)
|
|
|
|
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
|
|
|
return scheme, loginv1alpha1.SchemeGroupVersion, identityv1alpha1.SchemeGroupVersion
|
|
|
|
}
|
|
|
|
|
|
|
|
loginConciergeGroupData, identityConciergeGroupData := groupsuffix.ConciergeAggregatedGroups(apiGroupSuffix)
|
|
|
|
|
|
|
|
addToSchemeAtNewGroup(scheme, loginv1alpha1.GroupName, loginConciergeGroupData.Group, loginv1alpha1.AddToScheme, loginapi.AddToScheme)
|
|
|
|
addToSchemeAtNewGroup(scheme, identityv1alpha1.GroupName, identityConciergeGroupData.Group, identityv1alpha1.AddToScheme, identityapi.AddToScheme)
|
|
|
|
|
|
|
|
// manually register conversions and defaulting into the correct scheme since we cannot directly call AddToScheme
|
|
|
|
schemeBuilder := runtime.NewSchemeBuilder(
|
|
|
|
loginv1alpha1.RegisterConversions,
|
|
|
|
loginv1alpha1.RegisterDefaults,
|
|
|
|
identityv1alpha1.RegisterConversions,
|
|
|
|
identityv1alpha1.RegisterDefaults,
|
|
|
|
)
|
|
|
|
utilruntime.Must(schemeBuilder.AddToScheme(scheme))
|
|
|
|
|
|
|
|
// we do not want to return errors from the scheme and instead would prefer to defer
|
|
|
|
// to the REST storage layer for consistency. The simplest way to do this is to force
|
|
|
|
// a cache miss from the authenticator cache. Kube API groups are validated via the
|
|
|
|
// IsDNS1123Subdomain func thus we can easily create a group that is guaranteed never
|
|
|
|
// to be in the authenticator cache. Add a timestamp just to be extra sure.
|
|
|
|
const authenticatorCacheMissPrefix = "_INVALID_API_GROUP_"
|
|
|
|
authenticatorCacheMiss := authenticatorCacheMissPrefix + time.Now().UTC().String()
|
|
|
|
|
|
|
|
// we do not have any defaulting functions for *loginv1alpha1.TokenCredentialRequest
|
|
|
|
// today, but we may have some in the future. Calling AddTypeDefaultingFunc overwrites
|
|
|
|
// any previously registered defaulting function. Thus to make sure that we catch
|
|
|
|
// a situation where we add a defaulting func, we attempt to call it here with a nil
|
|
|
|
// *loginv1alpha1.TokenCredentialRequest. This will do nothing when there is no
|
|
|
|
// defaulting func registered, but it will almost certainly panic if one is added.
|
|
|
|
scheme.Default((*loginv1alpha1.TokenCredentialRequest)(nil))
|
|
|
|
|
|
|
|
// on incoming requests, restore the authenticator API group to the standard group
|
|
|
|
// note that we are responsible for duplicating this logic for every external API version
|
|
|
|
scheme.AddTypeDefaultingFunc(&loginv1alpha1.TokenCredentialRequest{}, func(obj interface{}) {
|
|
|
|
credentialRequest := obj.(*loginv1alpha1.TokenCredentialRequest)
|
|
|
|
|
|
|
|
if credentialRequest.Spec.Authenticator.APIGroup == nil {
|
|
|
|
// force a cache miss because this is an invalid request
|
|
|
|
plog.Debug("invalid token credential request, nil group", "authenticator", credentialRequest.Spec.Authenticator)
|
|
|
|
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
restoredGroup, ok := groupsuffix.Unreplace(*credentialRequest.Spec.Authenticator.APIGroup, apiGroupSuffix)
|
|
|
|
if !ok {
|
|
|
|
// force a cache miss because this is an invalid request
|
|
|
|
plog.Debug("invalid token credential request, wrong group", "authenticator", credentialRequest.Spec.Authenticator)
|
|
|
|
credentialRequest.Spec.Authenticator.APIGroup = &authenticatorCacheMiss
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
credentialRequest.Spec.Authenticator.APIGroup = &restoredGroup
|
|
|
|
})
|
|
|
|
|
|
|
|
return scheme, schema.GroupVersion(loginConciergeGroupData), schema.GroupVersion(identityConciergeGroupData)
|
|
|
|
}
|
|
|
|
|
|
|
|
func addToSchemeAtNewGroup(scheme *runtime.Scheme, oldGroup, newGroup string, funcs ...func(*runtime.Scheme) error) {
|
|
|
|
// we need a temporary place to register our types to avoid double registering them
|
|
|
|
tmpScheme := runtime.NewScheme()
|
|
|
|
schemeBuilder := runtime.NewSchemeBuilder(funcs...)
|
|
|
|
utilruntime.Must(schemeBuilder.AddToScheme(tmpScheme))
|
|
|
|
|
|
|
|
for gvk := range tmpScheme.AllKnownTypes() {
|
|
|
|
if gvk.GroupVersion() == metav1.Unversioned {
|
|
|
|
continue // metav1.AddToGroupVersion registers types outside of our aggregated API group that we need to ignore
|
|
|
|
}
|
|
|
|
|
|
|
|
if gvk.Group != oldGroup {
|
|
|
|
panic(fmt.Errorf("tmp scheme has type not in the old aggregated API group %s: %s", oldGroup, gvk)) // programmer error
|
|
|
|
}
|
|
|
|
|
|
|
|
obj, err := tmpScheme.New(gvk)
|
|
|
|
if err != nil {
|
|
|
|
panic(err) // programmer error, scheme internal code is broken
|
|
|
|
}
|
|
|
|
newGVK := schema.GroupVersionKind{
|
|
|
|
Group: newGroup,
|
|
|
|
Version: gvk.Version,
|
|
|
|
Kind: gvk.Kind,
|
|
|
|
}
|
|
|
|
|
|
|
|
// register the existing type but with the new group in the correct scheme
|
|
|
|
scheme.AddKnownTypeWithName(newGVK, obj)
|
|
|
|
}
|
|
|
|
}
|