diff --git a/test/integration/e2e_test.go b/test/integration/e2e_test.go index b70a5053..f624b01e 100644 --- a/test/integration/e2e_test.go +++ b/test/integration/e2e_test.go @@ -6,6 +6,7 @@ import ( "bufio" "bytes" "context" + "encoding/base32" "encoding/base64" "errors" "fmt" @@ -25,11 +26,13 @@ import ( authorizationv1 "k8s.io/api/authorization/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 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" "go.pinniped.dev/internal/certauthority" + "go.pinniped.dev/internal/crud" "go.pinniped.dev/internal/oidc" "go.pinniped.dev/internal/testutil" "go.pinniped.dev/pkg/oidcclient" @@ -42,7 +45,7 @@ import ( func TestE2EFullIntegration(t *testing.T) { env := library.IntegrationEnv(t) - ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Minute) + ctx, cancelFunc := context.WithTimeout(context.Background(), 15*time.Minute) defer cancelFunc() // Build pinniped CLI. @@ -261,6 +264,8 @@ func TestE2EFullIntegration(t *testing.T) { t.Logf("first kubectl command took %s", time.Since(start).String()) // Run kubectl again, which should work with no browser interaction. + t.Logf("Waiting 6 minutes to allow access token to expire") + time.Sleep(6 * time.Minute) kubectlCmd2 := exec.CommandContext(ctx, "kubectl", "get", "namespace", "--kubeconfig", kubeconfigPath) kubectlCmd2.Env = append(os.Environ(), env.ProxyEnv()...) start = time.Now() @@ -284,6 +289,38 @@ func TestE2EFullIntegration(t *testing.T) { }) require.NotNil(t, token) + // check that the access token is new (since it's just been refreshed) and has close to two minutes left. + testutil.RequireTimeInDelta(t, start.Add(2*time.Minute), token.AccessToken.Expiry.Time, 15*time.Second) + + kubeClient := library.NewKubernetesClientset(t).CoreV1() + + // 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") + accessTokenSecret, err := kubeClient.Secrets(env.SupervisorNamespace).Get(ctx, accessSecretName, metav1.GetOptions{}) + 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") + refreshTokenSecret, err := kubeClient.Secrets(env.SupervisorNamespace).Get(ctx, refreshSecretName, metav1.GetOptions{}) + 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) + idTokenClaims := token.IDToken.Claims require.Equal(t, env.SupervisorTestUpstream.Username, idTokenClaims[oidc.DownstreamUsernameClaim]) @@ -338,3 +375,14 @@ status: append(env.SupervisorTestUpstream.ExpectedGroups, "system:authenticated"), ) } + +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) +}