Split test environment variables so there's a specific supervisor upstream client.

Prior to this we re-used the CLI testing client to test the authorize flow of the supervisor, but they really need to be separate upstream clients. For example, the supervisor client should be a non-public client with a client secret and a different callback endpoint.

Signed-off-by: Matt Moyer <moyerm@vmware.com>
This commit is contained in:
Matt Moyer 2020-11-19 15:05:31 -06:00
parent b8fb37b9f6
commit bc700d58ae
No known key found for this signature in database
GPG Key ID: EAE88AD172C5AE2D
6 changed files with 88 additions and 46 deletions

View File

@ -295,9 +295,16 @@ export PINNIPED_TEST_PROXY=http://127.0.0.1:12346
export PINNIPED_TEST_CLI_OIDC_ISSUER=https://dex.dex.svc.cluster.local/dex export PINNIPED_TEST_CLI_OIDC_ISSUER=https://dex.dex.svc.cluster.local/dex
export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}" export PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}"
export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli export PINNIPED_TEST_CLI_OIDC_CLIENT_ID=pinniped-cli
export PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT=48095 export PINNIPED_TEST_CLI_OIDC_CALLBACK_URL=http://127.0.0.1:48095/callback
export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com export PINNIPED_TEST_CLI_OIDC_USERNAME=pinny@example.com
export PINNIPED_TEST_CLI_OIDC_PASSWORD=password export PINNIPED_TEST_CLI_OIDC_PASSWORD=password
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER=https://dex.dex.svc.cluster.local/dex
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE="${test_ca_bundle_pem}"
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_ID=pinniped-supervisor
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_SECRET=pinniped-supervisor-secret
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CALLBACK_URL=https://127.0.0.1:12345/some/path/callback
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME=pinny@example.com
export PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD=password
read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true read -r -d '' PINNIPED_TEST_CLUSTER_CAPABILITY_YAML << PINNIPED_TEST_CLUSTER_CAPABILITY_YAML_EOF || true
${pinniped_cluster_capability_file_content} ${pinniped_cluster_capability_file_content}

View File

@ -24,6 +24,12 @@ staticClients:
redirectURIs: redirectURIs:
- #@ "http://127.0.0.1:" + str(data.values.ports.cli) + "/callback" - #@ "http://127.0.0.1:" + str(data.values.ports.cli) + "/callback"
- #@ "http://[::1]:" + str(data.values.ports.cli) + "/callback" - #@ "http://[::1]:" + str(data.values.ports.cli) + "/callback"
- id: pinniped-supervisor
name: 'Pinniped Supervisor'
secret: pinniped-supervisor-secret
redirectURIs:
- #@ "http://127.0.0.1:" + str(data.values.ports.cli) + "/callback"
- #@ "http://[::1]:" + str(data.values.ports.cli) + "/callback"
enablePasswordDB: true enablePasswordDB: true
staticPasswords: staticPasswords:
- username: "pinny" - username: "pinny"

View File

@ -11,11 +11,11 @@ import (
"fmt" "fmt"
"io" "io"
"io/ioutil" "io/ioutil"
"net/url"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -118,7 +118,7 @@ type loginProviderPatterns struct {
func getLoginProvider(t *testing.T) *loginProviderPatterns { func getLoginProvider(t *testing.T) *loginProviderPatterns {
t.Helper() t.Helper()
issuer := library.IntegrationEnv(t).OIDCUpstream.Issuer issuer := library.IntegrationEnv(t).CLITestUpstream.Issuer
for _, p := range []loginProviderPatterns{ for _, p := range []loginProviderPatterns{
{ {
Name: "Okta", Name: "Okta",
@ -270,13 +270,13 @@ func TestCLILoginOIDC(t *testing.T) {
// Fill in the username and password and click "submit". // Fill in the username and password and click "submit".
t.Logf("logging into %s", loginProvider.Name) t.Logf("logging into %s", loginProvider.Name)
require.NoError(t, page.First(loginProvider.UsernameSelector).Fill(env.OIDCUpstream.Username)) require.NoError(t, page.First(loginProvider.UsernameSelector).Fill(env.CLITestUpstream.Username))
require.NoError(t, page.First(loginProvider.PasswordSelector).Fill(env.OIDCUpstream.Password)) require.NoError(t, page.First(loginProvider.PasswordSelector).Fill(env.CLITestUpstream.Password))
require.NoError(t, page.First(loginProvider.LoginButtonSelector).Click()) require.NoError(t, page.First(loginProvider.LoginButtonSelector).Click())
// Wait for the login to happen and us be redirected back to a localhost callback. // Wait for the login to happen and us be redirected back to a localhost callback.
t.Logf("waiting for redirect to localhost callback") t.Logf("waiting for redirect to localhost callback")
callbackURLPattern := regexp.MustCompile(`\Ahttp://127.0.0.1:` + strconv.Itoa(env.OIDCUpstream.LocalhostPort) + `/.+\z`) callbackURLPattern := regexp.MustCompile(`\A` + regexp.QuoteMeta(env.CLITestUpstream.CallbackURL) + `\?.+\z`)
waitForURL(t, page, callbackURLPattern) waitForURL(t, page, callbackURLPattern)
// Wait for the "pre" element that gets rendered for a `text/plain` page, and // Wait for the "pre" element that gets rendered for a `text/plain` page, and
@ -313,9 +313,9 @@ func TestCLILoginOIDC(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
claims := map[string]interface{}{} claims := map[string]interface{}{}
require.NoError(t, json.Unmarshal(jws.UnsafePayloadWithoutVerification(), &claims)) require.NoError(t, json.Unmarshal(jws.UnsafePayloadWithoutVerification(), &claims))
require.Equal(t, env.OIDCUpstream.Issuer, claims["iss"]) require.Equal(t, env.CLITestUpstream.Issuer, claims["iss"])
require.Equal(t, env.OIDCUpstream.ClientID, claims["aud"]) require.Equal(t, env.CLITestUpstream.ClientID, claims["aud"])
require.Equal(t, env.OIDCUpstream.Username, claims["email"]) require.Equal(t, env.CLITestUpstream.Username, claims["email"])
require.NotEmpty(t, claims["nonce"]) require.NotEmpty(t, claims["nonce"])
// Run the CLI again with the same session cache and login parameters. // Run the CLI again with the same session cache and login parameters.
@ -334,10 +334,10 @@ func TestCLILoginOIDC(t *testing.T) {
t.Logf("overwriting cache to remove valid ID token") t.Logf("overwriting cache to remove valid ID token")
cache := filesession.New(sessionCachePath) cache := filesession.New(sessionCachePath)
cacheKey := oidcclient.SessionCacheKey{ cacheKey := oidcclient.SessionCacheKey{
Issuer: env.OIDCUpstream.Issuer, Issuer: env.CLITestUpstream.Issuer,
ClientID: env.OIDCUpstream.ClientID, ClientID: env.CLITestUpstream.ClientID,
Scopes: []string{"email", "offline_access", "openid", "profile"}, Scopes: []string{"email", "offline_access", "openid", "profile"},
RedirectURI: fmt.Sprintf("http://localhost:%d/callback", env.OIDCUpstream.LocalhostPort), RedirectURI: strings.ReplaceAll(env.CLITestUpstream.CallbackURL, "127.0.0.1", "localhost"),
} }
cached := cache.GetToken(cacheKey) cached := cache.GetToken(cacheKey)
require.NotNil(t, cached) require.NotNil(t, cached)
@ -378,10 +378,24 @@ func waitForVisibleElements(t *testing.T, page *agouti.Page, selectors ...string
} }
func waitForURL(t *testing.T, page *agouti.Page, pat *regexp.Regexp) { func waitForURL(t *testing.T, page *agouti.Page, pat *regexp.Regexp) {
require.Eventually(t, func() bool { var lastURL string
url, err := page.URL() require.Eventuallyf(t,
return err == nil && pat.MatchString(url) func() bool {
}, 10*time.Second, 100*time.Millisecond) url, err := page.URL()
if err == nil && pat.MatchString(url) {
return true
}
if url != lastURL {
t.Logf("saw URL %s", url)
lastURL = url
}
return false
},
10*time.Second,
100*time.Millisecond,
"expected to browse to %s, but never got there",
pat,
)
} }
func readAndExpectEmpty(r io.Reader) (err error) { func readAndExpectEmpty(r io.Reader) (err error) {
@ -407,18 +421,20 @@ func spawnTestGoroutine(t *testing.T, f func() error) {
func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, sessionCachePath string) *exec.Cmd { func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, sessionCachePath string) *exec.Cmd {
env := library.IntegrationEnv(t) env := library.IntegrationEnv(t)
callbackURL, err := url.Parse(env.CLITestUpstream.CallbackURL)
require.NoError(t, err)
cmd := exec.CommandContext(ctx, pinnipedExe, "login", "oidc", cmd := exec.CommandContext(ctx, pinnipedExe, "login", "oidc",
"--issuer", env.OIDCUpstream.Issuer, "--issuer", env.CLITestUpstream.Issuer,
"--client-id", env.OIDCUpstream.ClientID, "--client-id", env.CLITestUpstream.ClientID,
"--listen-port", strconv.Itoa(env.OIDCUpstream.LocalhostPort), "--listen-port", callbackURL.Port(),
"--session-cache", sessionCachePath, "--session-cache", sessionCachePath,
"--skip-browser", "--skip-browser",
) )
// If there is a custom CA bundle, pass it via --ca-bundle and a temporary file. // If there is a custom CA bundle, pass it via --ca-bundle and a temporary file.
if env.OIDCUpstream.CABundle != "" { if env.CLITestUpstream.CABundle != "" {
path := filepath.Join(t.TempDir(), "test-ca.pem") path := filepath.Join(t.TempDir(), "test-ca.pem")
require.NoError(t, ioutil.WriteFile(path, []byte(env.OIDCUpstream.CABundle), 0600)) require.NoError(t, ioutil.WriteFile(path, []byte(env.CLITestUpstream.CABundle), 0600))
cmd.Args = append(cmd.Args, "--ca-bundle", path) cmd.Args = append(cmd.Args, "--ca-bundle", path)
} }

View File

@ -68,15 +68,13 @@ func TestSupervisorLogin(t *testing.T) {
require.Equal(t, http.StatusUnprocessableEntity, rsp.StatusCode) require.Equal(t, http.StatusUnprocessableEntity, rsp.StatusCode)
// Create upstream OIDC provider. // Create upstream OIDC provider.
testClientID := "test-client-id"
testClientSecret := "test-client-secret"
spec := idpv1alpha1.UpstreamOIDCProviderSpec{ spec := idpv1alpha1.UpstreamOIDCProviderSpec{
Issuer: env.OIDCUpstream.Issuer, Issuer: env.SupervisorTestUpstream.Issuer,
TLS: &idpv1alpha1.TLSSpec{ TLS: &idpv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.OIDCUpstream.CABundle)), CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)),
}, },
Client: idpv1alpha1.OIDCClient{ Client: idpv1alpha1.OIDCClient{
SecretName: makeTestClientCredsSecret(t, testClientID, testClientSecret).Name, SecretName: makeTestClientCredsSecret(t, env.SupervisorTestUpstream.ClientID, env.SupervisorTestUpstream.ClientSecret).Name,
}, },
} }
upstream := makeTestUpstream(t, spec, idpv1alpha1.PhaseReady) upstream := makeTestUpstream(t, spec, idpv1alpha1.PhaseReady)
@ -94,7 +92,7 @@ func TestSupervisorLogin(t *testing.T) {
ctx, ctx,
t, t,
upstream.Spec.Issuer, upstream.Spec.Issuer,
testClientID, env.SupervisorTestUpstream.ClientID,
upstreamRedirectURI, upstreamRedirectURI,
rsp.Header.Get("Location"), rsp.Header.Get("Location"),
) )
@ -147,9 +145,9 @@ func requireValidRedirectLocation(
return url.Parse(env.Proxy) return url.Parse(env.Proxy)
} }
} }
if env.OIDCUpstream.CABundle != "" { if env.SupervisorTestUpstream.CABundle != "" {
transport.TLSClientConfig = &tls.Config{RootCAs: x509.NewCertPool()} transport.TLSClientConfig = &tls.Config{RootCAs: x509.NewCertPool()}
transport.TLSClientConfig.RootCAs.AppendCertsFromPEM([]byte(env.OIDCUpstream.CABundle)) transport.TLSClientConfig.RootCAs.AppendCertsFromPEM([]byte(env.SupervisorTestUpstream.CABundle))
} }
ctx = oidc.ClientContext(ctx, &http.Client{Transport: &transport}) ctx = oidc.ClientContext(ctx, &http.Client{Transport: &transport})

View File

@ -48,9 +48,9 @@ func TestSupervisorUpstreamOIDCDiscovery(t *testing.T) {
t.Run("valid", func(t *testing.T) { t.Run("valid", func(t *testing.T) {
t.Parallel() t.Parallel()
spec := v1alpha1.UpstreamOIDCProviderSpec{ spec := v1alpha1.UpstreamOIDCProviderSpec{
Issuer: env.OIDCUpstream.Issuer, Issuer: env.SupervisorTestUpstream.Issuer,
TLS: &v1alpha1.TLSSpec{ TLS: &v1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.OIDCUpstream.CABundle)), CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorTestUpstream.CABundle)),
}, },
AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{ AuthorizationConfig: v1alpha1.OIDCAuthorizationConfig{
AdditionalScopes: []string{"email", "profile"}, AdditionalScopes: []string{"email", "profile"},

View File

@ -6,7 +6,6 @@ package library
import ( import (
"io/ioutil" "io/ioutil"
"os" "os"
"strconv"
"strings" "strings"
"testing" "testing"
@ -46,14 +45,18 @@ type TestEnv struct {
ExpectedGroups []string `json:"expectedGroups"` ExpectedGroups []string `json:"expectedGroups"`
} `json:"testUser"` } `json:"testUser"`
OIDCUpstream struct { CLITestUpstream TestOIDCUpstream `json:"cliOIDCUpstream"`
Issuer string `json:"issuer"` SupervisorTestUpstream TestOIDCUpstream `json:"supervisorOIDCUpstream"`
CABundle string `json:"caBundle" ` }
ClientID string `json:"clientID"`
LocalhostPort int `json:"localhostPort"` type TestOIDCUpstream struct {
Username string `json:"username"` Issuer string `json:"issuer"`
Password string `json:"password"` CABundle string `json:"caBundle" `
} `json:"oidcUpstream"` ClientID string `json:"clientID"`
ClientSecret string `json:"clientSecret"`
CallbackURL string `json:"callback"`
Username string `json:"username"`
Password string `json:"password"`
} }
// IntegrationEnv gets the integration test environment from OS environment variables. This // IntegrationEnv gets the integration test environment from OS environment variables. This
@ -130,12 +133,24 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
require.NotEmpty(t, result.SupervisorCustomLabels, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS cannot be empty") require.NotEmpty(t, result.SupervisorCustomLabels, "PINNIPED_TEST_SUPERVISOR_CUSTOM_LABELS cannot be empty")
result.Proxy = os.Getenv("PINNIPED_TEST_PROXY") result.Proxy = os.Getenv("PINNIPED_TEST_PROXY")
result.OIDCUpstream.Issuer = needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER") result.CLITestUpstream = TestOIDCUpstream{
result.OIDCUpstream.CABundle = os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE") Issuer: needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER"),
result.OIDCUpstream.ClientID = needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID") CABundle: os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE"),
result.OIDCUpstream.LocalhostPort, _ = strconv.Atoi(needEnv(t, "PINNIPED_TEST_CLI_OIDC_LOCALHOST_PORT")) ClientID: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID"),
result.OIDCUpstream.Username = needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME") CallbackURL: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CALLBACK_URL"),
result.OIDCUpstream.Password = needEnv(t, "PINNIPED_TEST_CLI_OIDC_PASSWORD") Username: needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME"),
Password: needEnv(t, "PINNIPED_TEST_CLI_OIDC_PASSWORD"),
}
result.SupervisorTestUpstream = TestOIDCUpstream{
Issuer: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER"),
CABundle: os.Getenv("PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_ISSUER_CA_BUNDLE"),
ClientID: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_ID"),
ClientSecret: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CLIENT_SECRET"),
CallbackURL: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_CALLBACK_URL"),
Username: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_USERNAME"),
Password: needEnv(t, "PINNIPED_TEST_SUPERVISOR_UPSTREAM_OIDC_PASSWORD"),
}
} }
func (e *TestEnv) HasCapability(cap Capability) bool { func (e *TestEnv) HasCapability(cap Capability) bool {