Merge pull request #2491 from vmware/jwtauthenticator_new_features
CodeQL / Analyze (go) (push) Failing after 3m35s
CodeQL / Analyze (javascript) (push) Failing after 2m21s

add new features in JWTAuthenticator CRD
This commit is contained in:
Ryan Richard
2025-07-25 13:17:15 -07:00
committed by GitHub
75 changed files with 8994 additions and 765 deletions
+21
View File
@@ -209,6 +209,7 @@ func TestAuditLogsDuringLogin_Disruptive(t *testing.T) {
"personalInfo": map[string]any{
"username": "redacted",
"groups": []any{"redacted 2 values"},
"extras": map[string]any{"redacted": "redacted 1 keys"},
},
},
}, allConciergeTCRLogs)
@@ -342,6 +343,25 @@ func TestAuditLogsDuringLogin_Disruptive(t *testing.T) {
for _, log := range allConciergeTCRLogs {
require.NotEmpty(t, log["issuedClientCert"])
delete(log, "issuedClientCert")
// The value at the extras key "authentication.kubernetes.io/credential-id" will be a JWT ID,
// which is hard to predict, so just assert that it is there without worrying about its exact value.
require.Contains(t, log, "personalInfo")
personalInfo, ok := log["personalInfo"].(map[string]any)
require.True(t, ok)
require.NotNil(t, personalInfo["extras"])
extras, ok := personalInfo["extras"].(map[string]any)
require.True(t, ok)
require.Contains(t, extras, "authentication.kubernetes.io/credential-id")
require.Len(t, extras, 1) // should be the only key
id := extras["authentication.kubernetes.io/credential-id"]
idValues, ok := id.([]any)
require.True(t, ok)
require.Len(t, idValues, 1)
require.Regexp(t, "JTI=.+", idValues[0])
// Now that we have made assertions about all the expected extras,
// delete it so we can compare the rest using equals below.
delete(personalInfo, "extras")
}
// All values in the personalInfo map should not be redacted anymore.
@@ -357,6 +377,7 @@ func TestAuditLogsDuringLogin_Disruptive(t *testing.T) {
"personalInfo": map[string]any{
"username": expectedUsername,
"groups": expectedGroups,
// note: also has an "extras" key, which we deleted from the actual value above
},
},
}, allConciergeTCRLogs)
+2 -2
View File
@@ -222,7 +222,7 @@ func TestCLILoginOIDC_Browser(t *testing.T) {
cacheKey := oidcclient.SessionCacheKey{
Issuer: env.CLIUpstreamOIDC.Issuer,
ClientID: env.CLIUpstreamOIDC.ClientID,
Scopes: []string{"email", "offline_access", "openid", "profile"},
Scopes: []string{"email", "groups", "offline_access", "openid", "profile"}, // in alphabetical order
RedirectURI: strings.ReplaceAll(env.CLIUpstreamOIDC.CallbackURL, "127.0.0.1", "localhost"),
}
cached := cache.GetToken(cacheKey)
@@ -413,7 +413,7 @@ func oidcLoginCommand(ctx context.Context, t *testing.T, pinnipedExe string, ses
cmd := exec.CommandContext(ctx, pinnipedExe, "login", "oidc",
"--issuer", env.CLIUpstreamOIDC.Issuer,
"--client-id", env.CLIUpstreamOIDC.ClientID,
"--scopes", "offline_access,openid,email,profile",
"--scopes", "offline_access,openid,email,profile,groups",
"--listen-port", callbackURL.Port(),
"--session-cache", sessionCachePath,
"--credential-cache", t.TempDir()+"/credentials.yaml",
@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
@@ -6,7 +6,9 @@ package integration
import (
"context"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"fmt"
"testing"
"time"
@@ -23,12 +25,306 @@ import (
"go.pinniped.dev/test/testlib"
)
// TestCredentialRequest_Browser cannot run in parallel because runPinnipedLoginOIDC uses a fixed port
// for its localhost listener via --listen-port=env.CLIUpstreamOIDC.CallbackURL.Port() per oidcLoginCommand.
// Since ports are global to the process, tests using oidcLoginCommand must be run serially.
func TestCredentialRequest_Browser(t *testing.T) {
env := testlib.IntegrationEnv(t).WithCapability(testlib.ClusterSigningKeyIsAvailable)
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
t.Cleanup(cancel)
jwtAuthenticatorTypedLocalObjectReference := func(a *authenticationv1alpha1.JWTAuthenticator) corev1.TypedLocalObjectReference {
return corev1.TypedLocalObjectReference{
APIGroup: &authenticationv1alpha1.SchemeGroupVersion.Group,
Kind: "JWTAuthenticator",
Name: a.Name,
}
}
expectedExtras := func(t *testing.T, jwt string) []string {
// Dex tokens do not include a jti claim, so check if it exists.
claims := getJWTClaims(t, jwt)
_, ok := claims["jti"]
if !ok {
return []string{}
}
// Okta tokens contain a jti, so use it to make the expected value.
jti := getJWTClaimAsString(t, jwt, "jti")
require.NotEmpty(t, jti)
return []string{
// The Kubernetes jwtAuthenticator will automatically add this extra when there is a jti claim.
fmt.Sprintf("authentication.kubernetes.io/credential-id=JTI=%s", jti),
}
}
tests := []struct {
name string
authenticator func(context.Context, *testing.T) corev1.TypedLocalObjectReference
token func(t *testing.T) (tokenToSubmit string, wantUsername string, wantGroups []string, wantExtras []string)
}{
{
name: "webhook",
authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
return testlib.CreateTestWebhookAuthenticator(ctx, t, &env.TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady)
},
token: func(t *testing.T) (string, string, []string, []string) {
return env.TestUser.Token, env.TestUser.ExpectedUsername, env.TestUser.ExpectedGroups, []string{}
},
},
{
name: "minimal jwt authenticator",
authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
authenticator := testlib.CreateTestJWTAuthenticator(ctx, t, authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.CLIUpstreamOIDC.Issuer,
Audience: env.CLIUpstreamOIDC.ClientID,
Claims: authenticationv1alpha1.JWTTokenClaims{
Username: env.CLIUpstreamOIDC.UsernameClaim,
Groups: env.CLIUpstreamOIDC.GroupsClaim,
},
TLS: tlsSpecForCLIUpstreamOIDC(t),
}, authenticationv1alpha1.JWTAuthenticatorPhaseReady)
return jwtAuthenticatorTypedLocalObjectReference(authenticator)
},
token: func(t *testing.T) (string, string, []string, []string) {
pinnipedExe := testlib.PinnipedCLIPath(t)
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
token := credOutput.Status.Token
// Sanity check that the JWT contains the expected username claim.
username := getJWTClaimAsString(t, token, env.CLIUpstreamOIDC.UsernameClaim)
require.Equal(t, env.CLIUpstreamOIDC.Username, username)
// Sanity check that the JWT contains the expected groups claim.
// Dex doesn't return groups, so only check where we are expecting groups.
if len(env.CLIUpstreamOIDC.ExpectedGroups) > 0 {
groups := getJWTClaimAsStringSlice(t, token, env.CLIUpstreamOIDC.GroupsClaim)
t.Logf("found groups in JWT token: %#v", groups)
require.ElementsMatch(t, groups, env.CLIUpstreamOIDC.ExpectedGroups)
}
return token, env.CLIUpstreamOIDC.Username, env.CLIUpstreamOIDC.ExpectedGroups, expectedExtras(t, token)
},
},
{
name: "jwt authenticator with username and groups CEL expressions and additional extras and validation rules which allow auth",
authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
authenticator := testlib.CreateTestJWTAuthenticator(ctx, t, authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.CLIUpstreamOIDC.Issuer,
Audience: env.CLIUpstreamOIDC.ClientID,
Claims: authenticationv1alpha1.JWTTokenClaims{
UsernameExpression: "claims.sub",
GroupsExpression: `["group1", "group2"]`,
Extra: []authenticationv1alpha1.ExtraMapping{
{
Key: "example.com/sub",
ValueExpression: "claims.sub",
},
{
Key: "example.com/const",
ValueExpression: `"some-value"`,
},
},
},
ClaimValidationRules: []authenticationv1alpha1.ClaimValidationRule{
{
Claim: env.CLIUpstreamOIDC.UsernameClaim,
RequiredValue: env.CLIUpstreamOIDC.Username,
},
{
Expression: fmt.Sprintf("claims.%s == '%s'", env.CLIUpstreamOIDC.UsernameClaim, env.CLIUpstreamOIDC.Username),
Message: "only one specific user is allowed",
},
},
UserValidationRules: []authenticationv1alpha1.UserValidationRule{
{
Expression: "!user.username.startsWith('system:')",
Message: "username cannot used reserved system: prefix",
},
},
TLS: tlsSpecForCLIUpstreamOIDC(t),
}, authenticationv1alpha1.JWTAuthenticatorPhaseReady)
return jwtAuthenticatorTypedLocalObjectReference(authenticator)
},
token: func(t *testing.T) (string, string, []string, []string) {
pinnipedExe := testlib.PinnipedCLIPath(t)
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
token := credOutput.Status.Token
subject := getJWTClaimAsString(t, token, "sub")
require.NotEmpty(t, subject)
wantExtras := expectedExtras(t, token)
wantExtras = append(wantExtras, fmt.Sprintf("example.com/sub=%s", subject))
wantExtras = append(wantExtras, "example.com/const=some-value")
return token, subject, []string{"group1", "group2"}, wantExtras
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
authenticatorRefToSubmit := test.authenticator(ctx, t)
tokenToSubmit, wantUsername, wantGroups, wantExtras := test.token(t)
var response *loginv1alpha1.TokenCredentialRequest
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
var err error
response, err = testlib.CreateTokenCredentialRequest(ctx, t,
loginv1alpha1.TokenCredentialRequestSpec{Token: tokenToSubmit, Authenticator: authenticatorRefToSubmit},
)
requireEventually.NoError(err, "the request should never fail at the HTTP level")
requireEventually.NotNil(response)
requireEventually.NotNil(response.Status.Credential, "the response should contain a credential")
requireEventually.Emptyf(response.Status.Message, "value is: %q", safeDerefStringPtr(response.Status.Message))
requireEventually.NotNil(response.Status.Credential)
requireEventually.Empty(response.Spec)
requireEventually.Empty(response.Status.Credential.Token)
requireEventually.NotEmpty(response.Status.Credential.ClientCertificateData)
requireEventually.Equal(wantUsername, getCommonName(t, response.Status.Credential.ClientCertificateData))
requireEventually.ElementsMatch(wantGroups, getOrganizations(t, response.Status.Credential.ClientCertificateData))
requireEventually.ElementsMatch(wantExtras, getOrganizationalUnits(t, response.Status.Credential.ClientCertificateData))
requireEventually.NotEmpty(response.Status.Credential.ClientKeyData)
requireEventually.NotNil(response.Status.Credential.ExpirationTimestamp)
requireEventually.InDelta(5*time.Minute, time.Until(response.Status.Credential.ExpirationTimestamp.Time), float64(time.Minute))
}, 10*time.Second, 500*time.Millisecond)
// Create a client using the certificate from the CredentialRequest.
clientWithCertFromCredentialRequest := testlib.NewClientsetWithCertAndKey(
t,
response.Status.Credential.ClientCertificateData,
response.Status.Credential.ClientKeyData,
)
t.Run(
"access as user",
testlib.AccessAsUserTest(ctx, wantUsername, clientWithCertFromCredentialRequest),
)
for _, group := range wantGroups {
t.Run(
"access as group "+group,
testlib.AccessAsGroupTest(ctx, group, clientWithCertFromCredentialRequest),
)
}
})
}
}
// This test cannot run in parallel because runPinnipedLoginOIDC uses a fixed port
// for its localhost listener via --listen-port=env.CLIUpstreamOIDC.CallbackURL.Port() per oidcLoginCommand.
// Since ports are global to the process, tests using oidcLoginCommand must be run serially.
func TestCredentialRequest_JWTAuthenticatorRulesToDisallowLogin_Browser(t *testing.T) {
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
basicSpec := &authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.CLIUpstreamOIDC.Issuer,
Audience: env.CLIUpstreamOIDC.ClientID,
Claims: authenticationv1alpha1.JWTTokenClaims{
Username: env.CLIUpstreamOIDC.UsernameClaim,
Groups: env.CLIUpstreamOIDC.GroupsClaim,
},
TLS: tlsSpecForCLIUpstreamOIDC(t),
}
tests := []struct {
name string
authenticator func(context.Context, *testing.T) *authenticationv1alpha1.JWTAuthenticator
wantSuccessfulAuth bool
}{
{
// Sanity check to make sure that the basic JWTAuthenticator spec works before adding rules which should cause auth failure.
name: "JWTAuthenticator successful login",
authenticator: func(ctx context.Context, t *testing.T) *authenticationv1alpha1.JWTAuthenticator {
return testlib.CreateTestJWTAuthenticator(ctx, t, *basicSpec.DeepCopy(), authenticationv1alpha1.JWTAuthenticatorPhaseReady)
},
wantSuccessfulAuth: true,
},
{
name: "JWTAuthenticator ClaimValidationRules using CEL expression should be able to prevent login",
authenticator: func(ctx context.Context, t *testing.T) *authenticationv1alpha1.JWTAuthenticator {
spec := basicSpec.DeepCopy()
spec.ClaimValidationRules = []authenticationv1alpha1.ClaimValidationRule{
{
// This should cause the login to fail for this specific user.
Expression: fmt.Sprintf("claims.%s != '%s'", env.CLIUpstreamOIDC.UsernameClaim, env.CLIUpstreamOIDC.Username),
Message: "one specific user is disallowed",
},
}
return testlib.CreateTestJWTAuthenticator(ctx, t, *spec, authenticationv1alpha1.JWTAuthenticatorPhaseReady)
},
},
{
name: "JWTAuthenticator ClaimValidationRules using RequiredValue should be able to prevent login",
authenticator: func(ctx context.Context, t *testing.T) *authenticationv1alpha1.JWTAuthenticator {
spec := basicSpec.DeepCopy()
spec.ClaimValidationRules = []authenticationv1alpha1.ClaimValidationRule{
{
Claim: "sub",
RequiredValue: "this-will-never-be-the-sub-value",
},
}
return testlib.CreateTestJWTAuthenticator(ctx, t, *spec, authenticationv1alpha1.JWTAuthenticatorPhaseReady)
},
},
{
name: "JWTAuthenticator UserValidationRules CEL expressions should be able to prevent login",
authenticator: func(ctx context.Context, t *testing.T) *authenticationv1alpha1.JWTAuthenticator {
spec := basicSpec.DeepCopy()
spec.UserValidationRules = []authenticationv1alpha1.UserValidationRule{
{
Expression: "false",
Message: "nobody is allowed to auth",
},
}
return testlib.CreateTestJWTAuthenticator(ctx, t, *spec, authenticationv1alpha1.JWTAuthenticatorPhaseReady)
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
t.Cleanup(cancel)
authenticator := test.authenticator(ctx, t)
pinnipedExe := testlib.PinnipedCLIPath(t)
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
response, err := testlib.CreateTokenCredentialRequest(ctx, t,
loginv1alpha1.TokenCredentialRequestSpec{
Token: credOutput.Status.Token,
Authenticator: corev1.TypedLocalObjectReference{
APIGroup: &authenticationv1alpha1.SchemeGroupVersion.Group,
Kind: "JWTAuthenticator",
Name: authenticator.Name,
},
},
)
require.NoError(t, err, testlib.Sdump(err))
if test.wantSuccessfulAuth {
require.NotEmpty(t, response.Status.Credential)
require.Empty(t, response.Status.Message)
} else {
require.Nil(t, response.Status.Credential)
require.NotNil(t, response.Status.Message)
require.Equal(t, "authentication failed", *response.Status.Message)
}
})
}
}
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
func TestUnsuccessfulCredentialRequest_Parallel(t *testing.T) {
func TestCredentialRequest_ShouldFailWhenTheAuthenticatorDoesNotExist_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
t.Cleanup(cancel)
response, err := testlib.CreateTokenCredentialRequest(ctx, t,
loginv1alpha1.TokenCredentialRequestSpec{
@@ -46,110 +342,15 @@ func TestUnsuccessfulCredentialRequest_Parallel(t *testing.T) {
require.Equal(t, "authentication failed", *response.Status.Message)
}
// TestSuccessfulCredentialRequest_Browser cannot run in parallel because runPinnipedLoginOIDC uses a fixed port
// for its localhost listener via --listen-port=env.CLIUpstreamOIDC.CallbackURL.Port() per oidcLoginCommand.
// Since ports are global to the process, tests using oidcLoginCommand must be run serially.
func TestSuccessfulCredentialRequest_Browser(t *testing.T) {
env := testlib.IntegrationEnv(t).WithCapability(testlib.ClusterSigningKeyIsAvailable)
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Minute)
defer cancel()
tests := []struct {
name string
authenticator func(context.Context, *testing.T) corev1.TypedLocalObjectReference
token func(t *testing.T) (token string, username string, groups []string)
}{
{
name: "webhook",
authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
return testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady)
},
token: func(t *testing.T) (string, string, []string) {
return testlib.IntegrationEnv(t).TestUser.Token, env.TestUser.ExpectedUsername, env.TestUser.ExpectedGroups
},
},
{
name: "jwt authenticator",
authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
authenticator := testlib.CreateTestJWTAuthenticatorForCLIUpstream(ctx, t)
return corev1.TypedLocalObjectReference{
APIGroup: &authenticationv1alpha1.SchemeGroupVersion.Group,
Kind: "JWTAuthenticator",
Name: authenticator.Name,
}
},
token: func(t *testing.T) (string, string, []string) {
pinnipedExe := testlib.PinnipedCLIPath(t)
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
token := credOutput.Status.Token
// By default, the JWTAuthenticator expects the username to be in the "username" claim and the
// groups to be in the "groups" claim.
// However, we are configuring Pinniped in the `CreateTestJWTAuthenticatorForCLIUpstream` method above
// to read the username from the "sub" claim of the token instead.
username, groups := getJWTSubAndGroupsClaims(t, token)
return token, username, groups
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
authenticator := test.authenticator(ctx, t)
token, username, groups := test.token(t)
var response *loginv1alpha1.TokenCredentialRequest
testlib.RequireEventually(t, func(requireEventually *require.Assertions) {
var err error
response, err = testlib.CreateTokenCredentialRequest(ctx, t,
loginv1alpha1.TokenCredentialRequestSpec{Token: token, Authenticator: authenticator},
)
requireEventually.NoError(err, "the request should never fail at the HTTP level")
requireEventually.NotNil(response)
requireEventually.NotNil(response.Status.Credential, "the response should contain a credential")
requireEventually.Emptyf(response.Status.Message, "value is: %q", safeDerefStringPtr(response.Status.Message))
requireEventually.NotNil(response.Status.Credential)
requireEventually.Empty(response.Spec)
requireEventually.Empty(response.Status.Credential.Token)
requireEventually.NotEmpty(response.Status.Credential.ClientCertificateData)
requireEventually.Equal(username, getCommonName(t, response.Status.Credential.ClientCertificateData))
requireEventually.ElementsMatch(groups, getOrganizations(t, response.Status.Credential.ClientCertificateData))
requireEventually.NotEmpty(response.Status.Credential.ClientKeyData)
requireEventually.NotNil(response.Status.Credential.ExpirationTimestamp)
requireEventually.InDelta(5*time.Minute, time.Until(response.Status.Credential.ExpirationTimestamp.Time), float64(time.Minute))
}, 10*time.Second, 500*time.Millisecond)
// Create a client using the certificate from the CredentialRequest.
clientWithCertFromCredentialRequest := testlib.NewClientsetWithCertAndKey(
t,
response.Status.Credential.ClientCertificateData,
response.Status.Credential.ClientKeyData,
)
t.Run(
"access as user",
testlib.AccessAsUserTest(ctx, username, clientWithCertFromCredentialRequest),
)
for _, group := range groups {
t.Run(
"access as group "+group,
testlib.AccessAsGroupTest(ctx, group, clientWithCertFromCredentialRequest),
)
}
})
}
}
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthenticateTheUser_Parallel(t *testing.T) {
_ = testlib.IntegrationEnv(t).WithCapability(testlib.ClusterSigningKeyIsAvailable)
func TestCredentialRequest_ShouldFailWhenTheRequestIsValidButTheTokenDoesNotAuthenticateTheUser_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
// Create a testWebhook so we have a legitimate authenticator to pass to the
// TokenCredentialRequest API.
// Create a testWebhook so we have a legitimate authenticator to pass to the TokenCredentialRequest API.
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady)
t.Cleanup(cancel)
testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &env.TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady)
response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
loginv1alpha1.TokenCredentialRequestSpec{Token: "not a good token", Authenticator: testWebhook},
@@ -164,13 +365,13 @@ func TestFailedCredentialRequestWhenTheRequestIsValidButTheTokenDoesNotAuthentic
// TCRs are non-mutating and safe to run in parallel with serial tests, see main_test.go.
func TestCredentialRequest_ShouldFailWhenRequestDoesNotIncludeToken_Parallel(t *testing.T) {
_ = testlib.IntegrationEnv(t).WithCapability(testlib.ClusterSigningKeyIsAvailable)
env := testlib.IntegrationEnv(t).WithCapability(testlib.AnonymousAuthenticationSupported)
// Create a testWebhook so we have a legitimate authenticator to pass to the
// TokenCredentialRequest API.
// Create a testWebhook so we have a legitimate authenticator to pass to the TokenCredentialRequest API.
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &testlib.IntegrationEnv(t).TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady)
t.Cleanup(cancel)
testWebhook := testlib.CreateTestWebhookAuthenticator(ctx, t, &env.TestWebhook, authenticationv1alpha1.WebhookAuthenticatorPhaseReady)
response, err := testlib.CreateTokenCredentialRequest(context.Background(), t,
loginv1alpha1.TokenCredentialRequestSpec{Token: "", Authenticator: testWebhook},
@@ -210,6 +411,16 @@ func getOrganizations(t *testing.T, certPEM string) []string {
return cert.Subject.Organization
}
func getOrganizationalUnits(t *testing.T, certPEM string) []string {
t.Helper()
pemBlock, _ := pem.Decode([]byte(certPEM))
cert, err := x509.ParseCertificate(pemBlock.Bytes)
require.NoError(t, err)
return cert.Subject.OrganizationalUnit
}
func safeDerefStringPtr(s *string) string {
if s == nil {
return "<nil>"
@@ -217,18 +428,51 @@ func safeDerefStringPtr(s *string) string {
return *s
}
func getJWTSubAndGroupsClaims(t *testing.T, jwtToken string) (string, []string) {
func getJWTClaimAsString(t *testing.T, jwtToken string, claimName string) string {
t.Helper()
claims := getJWTClaims(t, jwtToken)
require.Contains(t, claims, claimName)
val := claims[claimName]
strVal, ok := val.(string)
require.Truef(t, ok, "expected value of claim %q to be a string, but it was: %#v", claimName, claims[claimName])
return strVal
}
func getJWTClaimAsStringSlice(t *testing.T, jwtToken string, claimName string) []string {
t.Helper()
claims := getJWTClaims(t, jwtToken)
require.Contains(t, claims, claimName)
val := claims[claimName]
anySliceVal, ok := val.([]any)
require.Truef(t, ok, "expected value of claim %q to be a []any, but it was: %#v", claimName, claims[claimName])
strSliceVal := make([]string, len(anySliceVal))
for i := range anySliceVal {
strSliceVal[i], ok = anySliceVal[i].(string)
require.Truef(t, ok, "expected every value of array at claim %q to be a string, but one element was: %#v", claimName, anySliceVal[i])
}
return strSliceVal
}
func getJWTClaims(t *testing.T, jwtToken string) map[string]any {
t.Helper()
token, err := josejwt.ParseSigned(jwtToken, []jose.SignatureAlgorithm{jose.ES256, jose.RS256})
require.NoError(t, err)
var claims struct {
Sub string `json:"sub"`
Groups []string `json:"groups"`
}
claims := map[string]any{}
err = token.UnsafeClaimsWithoutVerification(&claims)
require.NoError(t, err)
return claims.Sub, claims.Groups
return claims
}
func tlsSpecForCLIUpstreamOIDC(t *testing.T) *authenticationv1alpha1.TLSSpec {
env := testlib.IntegrationEnv(t)
// If the test upstream does not have a CA bundle specified, then don't configure it.
if env.CLIUpstreamOIDC.CABundle != "" {
return &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.CLIUpstreamOIDC.CABundle)),
}
}
return nil
}
@@ -1,4 +1,4 @@
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2024-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package integration
@@ -322,6 +322,308 @@ func TestConciergeJWTAuthenticatorStatus_Parallel(t *testing.T) {
},
),
},
{
name: "claims cannot use both username and usernameExpression",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
Claims: authenticationv1alpha1.JWTTokenClaims{
Username: "foo",
UsernameExpression: "bar",
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: `could not initialize jwt authenticator: claims.username: Invalid value: "": claim and expression can't both be set`,
},
},
),
},
{
name: "claims cannot use both groups and groupsExpression",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
Claims: authenticationv1alpha1.JWTTokenClaims{
Groups: "foo",
GroupsExpression: "bar",
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: `could not initialize jwt authenticator: claims.groups: Invalid value: "": claim and expression can't both be set`,
},
},
),
},
{
name: "username claim expression cannot use clams.email unless it also uses claims.email_verified elsewhere",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
Claims: authenticationv1alpha1.JWTTokenClaims{
UsernameExpression: "claims.email",
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: `could not initialize jwt authenticator: claims.usernameExpression: Invalid value: "claims.email": ` +
`claims.email_verified must be used in claims.usernameExpression or claims.extra[*].valueExpression or ` +
`claimValidationRules[*].expression when claims.email is used in claims.usernameExpression`,
},
},
),
},
{
name: "username claim expression cannot use invalid CEL expression",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
Claims: authenticationv1alpha1.JWTTokenClaims{
UsernameExpression: "this is not a valid CEL expression",
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: "could not initialize jwt authenticator: claims.usernameExpression: Invalid value: " +
"\"this is not a valid CEL expression\": compilation failed: ERROR: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>\n" +
" | this is not a valid CEL expression\n" +
" | .....^",
},
},
),
},
{
name: "groups claim expression cannot use invalid CEL expression",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
Claims: authenticationv1alpha1.JWTTokenClaims{
GroupsExpression: "this is not a valid CEL expression",
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: "could not initialize jwt authenticator: claims.groupsExpression: Invalid value: " +
"\"this is not a valid CEL expression\": compilation failed: ERROR: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>\n" +
" | this is not a valid CEL expression\n" +
" | .....^",
},
},
),
},
{
name: "extra keys cannot have equal sign and must be domain-prefixed path",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
Claims: authenticationv1alpha1.JWTTokenClaims{
Extra: []authenticationv1alpha1.ExtraMapping{
{
Key: "a=b",
ValueExpression: `"value"`,
},
},
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: `could not initialize jwt authenticator: [` +
`claims.extra[0].key: Invalid value: "a=b": must be a domain-prefixed path (such as "acme.io/foo"), ` +
`claims.extra[0].key: Invalid value: "a=b": Pinniped does not allow extra key names to contain equals sign]`,
},
},
),
},
{
name: "claimValidationRules claim and requiredValue are mutually exclusive with expression and message",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
ClaimValidationRules: []authenticationv1alpha1.ClaimValidationRule{
{
Claim: "foo",
RequiredValue: "bar",
Expression: "baz",
Message: "bat",
},
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: `could not initialize jwt authenticator: claimValidationRules[0]: Invalid value: "foo": claim and expression can't both be set`,
},
},
),
},
{
name: "claimValidationRules cannot use invalid CEL expressions",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
ClaimValidationRules: []authenticationv1alpha1.ClaimValidationRule{
{
Expression: "this is not a valid CEL expression",
},
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: "could not initialize jwt authenticator: claimValidationRules[0].expression: Invalid value: " +
"\"this is not a valid CEL expression\": compilation failed: ERROR: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>\n" +
" | this is not a valid CEL expression\n" +
" | .....^",
},
},
),
},
{
name: "userValidationRules must use valid CEL expressions",
spec: authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: env.SupervisorUpstreamOIDC.Issuer,
Audience: "some-fake-audience",
TLS: &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
},
UserValidationRules: []authenticationv1alpha1.UserValidationRule{
{
Expression: "this is not a valid CEL expression",
},
},
},
wantPhase: authenticationv1alpha1.JWTAuthenticatorPhaseError,
wantConditions: replaceSomeConditions(t,
allSuccessfulJWTAuthenticatorConditions(len(env.SupervisorUpstreamOIDC.CABundle) != 0),
[]metav1.Condition{
{
Type: "Ready",
Status: "False",
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}, {
Type: "AuthenticatorValid",
Status: "False",
Reason: "InvalidAuthenticator",
Message: "could not initialize jwt authenticator: userValidationRules[0].expression: Invalid value: " +
"\"this is not a valid CEL expression\": compilation failed: ERROR: <input>:1:6: Syntax error: mismatched input 'is' expecting <EOF>\n" +
" | this is not a valid CEL expression\n" +
" | .....^",
},
},
),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
@@ -479,40 +781,42 @@ func allSuccessfulJWTAuthenticatorConditions(caBundleExists bool) []metav1.Condi
if caBundleExists {
tlsConfigValidMsg = "spec.tls is valid: using configured CA bundle"
}
return []metav1.Condition{{
Type: "AuthenticatorValid",
Status: "True",
Reason: "Success",
Message: "authenticator initialized",
}, {
Type: "DiscoveryURLValid",
Status: "True",
Reason: "Success",
Message: "discovery performed successfully",
}, {
Type: "IssuerURLValid",
Status: "True",
Reason: "Success",
Message: "issuer is a valid URL",
}, {
Type: "JWKSFetchValid",
Status: "True",
Reason: "Success",
Message: "successfully fetched jwks",
}, {
Type: "JWKSURLValid",
Status: "True",
Reason: "Success",
Message: "jwks_uri is a valid URL",
}, {
Type: "Ready",
Status: "True",
Reason: "Success",
Message: "the JWTAuthenticator is ready",
}, {
Type: "TLSConfigurationValid",
Status: "True",
Reason: "Success",
Message: tlsConfigValidMsg,
}}
return []metav1.Condition{
{
Type: "AuthenticatorValid",
Status: "True",
Reason: "Success",
Message: "authenticator initialized",
}, {
Type: "DiscoveryURLValid",
Status: "True",
Reason: "Success",
Message: "discovery performed successfully",
}, {
Type: "IssuerURLValid",
Status: "True",
Reason: "Success",
Message: "issuer is a valid URL",
}, {
Type: "JWKSFetchValid",
Status: "True",
Reason: "Success",
Message: "successfully fetched jwks",
}, {
Type: "JWKSURLValid",
Status: "True",
Reason: "Success",
Message: "jwks_uri is a valid URL",
}, {
Type: "Ready",
Status: "True",
Reason: "Success",
Message: "the JWTAuthenticator is ready",
}, {
Type: "TLSConfigurationValid",
Status: "True",
Reason: "Success",
Message: tlsConfigValidMsg,
},
}
}
+3 -2
View File
@@ -451,8 +451,9 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr
}
}
// manually update this value whenever you add additional fields to an API resource and then run the generator
totalExpectedAPIFields := 310
// Manually update this value whenever you add additional fields to an API resource and then run the generator.
// This is to ensure that this test checked every field in our whole API surface area.
totalExpectedAPIFields := 323
// Because we are parsing text from `kubectl explain` and because the format of that text can change
// over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all
@@ -555,6 +555,7 @@ func TestSupervisorFederationDomainStatus_Disruptive(t *testing.T) {
}
}
//nolint:gocyclo // we have a lot of "if" conditions here, but it's okay
func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
env := testlib.IntegrationEnv(t)
fdClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace)
@@ -566,7 +567,10 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
// Certain non-CEL validation failures will prevent CEL validations from running,
// and the Kubernetes API server will return this error message for those cases.
const couldNotRunCELValidationsErrMessage = `<nil>: Invalid value: "null": some validation rules were not checked because the object was invalid; correct the existing errors to complete validation`
const couldNotRunCELValidationsErrMessage = "some validation rules were not checked because the object was invalid; correct the existing errors to complete validation"
const oldCouldNotRunCELValidationsErrMessage = `<nil>: Invalid value: "null": ` + couldNotRunCELValidationsErrMessage
// Starting in beta version of Kube 1.34, they removed the quotes around the null in this message.
const newCouldNotRunCELValidationsErrMessage = `<nil>: Invalid value: null: ` + couldNotRunCELValidationsErrMessage
tests := []struct {
name string
@@ -576,11 +580,14 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
// optionally override wantErr for one or more specific versions of Kube, due to changing validation error text
wantKube23OrOlderErrs []string
wantKube24Through31InclusiveErrs []string
wantKube32OrNewerErrs []string
wantKube32Through33InclusiveErrs []string
wantKube34OrNewerErrs []string
// These errors are appended to any other wanted errors when k8sAPIServerSupportsCEL is true
wantCELErrorsForKube25Through28Inclusive []string
wantCELErrorsForKube29Through33Inclusive []string
wantCELErrorsForKube29OrNewer []string
wantCELErrorsForKube34OrNewer []string
}{
{
name: "issuer cannot be empty",
@@ -644,6 +651,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
// For some unknown reason, Kubernetes versions 1.23 and older return errors *with indices* for this test case only.
wantKube23OrOlderErrs: []string{`spec.identityProviders[0].transforms.constants[1]: Duplicate value: map[string]interface {}{"name":"notUnique"}`},
wantErrs: []string{`spec.identityProviders[0].transforms.constants[1]: Duplicate value: map[string]interface {}{"name":"notUnique"}`},
wantKube34OrNewerErrs: []string{`spec.identityProviders[0].transforms.constants[1]: Duplicate value: {"name":"notUnique"}`},
},
{
name: "IDP transform constant names cannot be empty",
@@ -685,9 +693,10 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
},
wantKube23OrOlderErrs: []string{`spec.identityProviders.transforms.constants.name: Invalid value: "12345678901234567890123456789012345678901234567890123456789012345": spec.identityProviders.transforms.constants.name in body should be at most 64 chars long`},
wantKube24Through31InclusiveErrs: []string{`spec.identityProviders[0].transforms.constants[0].name: Too long: may not be longer than 64`},
wantKube32OrNewerErrs: []string{`spec.identityProviders[0].transforms.constants[0].name: Too long: may not be more than 64 bytes`},
wantCELErrorsForKube25Through28Inclusive: []string{couldNotRunCELValidationsErrMessage},
wantCELErrorsForKube29OrNewer: []string{couldNotRunCELValidationsErrMessage},
wantErrs: []string{`spec.identityProviders[0].transforms.constants[0].name: Too long: may not be more than 64 bytes`},
wantCELErrorsForKube25Through28Inclusive: []string{oldCouldNotRunCELValidationsErrMessage},
wantCELErrorsForKube29Through33Inclusive: []string{oldCouldNotRunCELValidationsErrMessage},
wantCELErrorsForKube34OrNewer: []string{newCouldNotRunCELValidationsErrMessage},
},
{
name: "IDP transform constant names must be a legal CEL variable name",
@@ -737,8 +746,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
},
},
},
wantErrs: []string{`spec.identityProviders[0].transforms.constants[0].type: Unsupported value: "this is invalid": supported values: "string", "stringList"`},
wantCELErrorsForKube29OrNewer: []string{couldNotRunCELValidationsErrMessage}, // this should not be checked on kind 1.25, 1.26, 1.27, 1.28
wantErrs: []string{`spec.identityProviders[0].transforms.constants[0].type: Unsupported value: "this is invalid": supported values: "string", "stringList"`},
wantCELErrorsForKube29Through33Inclusive: []string{oldCouldNotRunCELValidationsErrMessage},
wantCELErrorsForKube34OrNewer: []string{newCouldNotRunCELValidationsErrMessage},
},
{
name: "IDP transform expression types must be one of the allowed enum strings",
@@ -761,8 +771,9 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
},
},
},
wantErrs: []string{`spec.identityProviders[0].transforms.expressions[0].type: Unsupported value: "this is invalid": supported values: "policy/v1", "username/v1", "groups/v1"`},
wantCELErrorsForKube29OrNewer: []string{couldNotRunCELValidationsErrMessage}, // this should not be checked on kind 1.25, 1.26, 1.27, 1.28
wantErrs: []string{`spec.identityProviders[0].transforms.expressions[0].type: Unsupported value: "this is invalid": supported values: "policy/v1", "username/v1", "groups/v1"`},
wantCELErrorsForKube29Through33Inclusive: []string{oldCouldNotRunCELValidationsErrMessage},
wantCELErrorsForKube34OrNewer: []string{newCouldNotRunCELValidationsErrMessage},
},
{
name: "IDP transform expressions cannot be empty",
@@ -872,7 +883,7 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
}
})
if len(tt.wantErrs) > 0 && len(tt.wantKube23OrOlderErrs) > 0 && len(tt.wantKube24Through31InclusiveErrs) > 0 && len(tt.wantKube32OrNewerErrs) > 0 {
if len(tt.wantErrs) > 0 && len(tt.wantKube23OrOlderErrs) > 0 && len(tt.wantKube24Through31InclusiveErrs) > 0 && len(tt.wantKube32Through33InclusiveErrs) > 0 && len(tt.wantKube34OrNewerErrs) > 0 {
require.Fail(t, "test setup problem: wanted every possible kind of error, which would cause tt.wantErr to be unused")
}
@@ -900,16 +911,27 @@ func TestSupervisorFederationDomainCRDValidations_Parallel(t *testing.T) {
// Also allow overriding with an exact expected error for these Kube versions.
wantErr = tt.wantKube24Through31InclusiveErrs
}
if minor >= 32 && len(tt.wantKube32OrNewerErrs) > 0 {
if minor >= 32 && minor <= 33 && len(tt.wantKube32Through33InclusiveErrs) > 0 {
// Also allow overriding with an exact expected error for these Kube versions.
wantErr = tt.wantKube32OrNewerErrs
wantErr = tt.wantKube32Through33InclusiveErrs
}
if minor >= 34 && len(tt.wantKube34OrNewerErrs) > 0 {
// Also allow overriding with an exact expected error for these Kube versions.
wantErr = tt.wantKube34OrNewerErrs
}
if minor >= 25 && minor <= 28 && len(tt.wantCELErrorsForKube25Through28Inclusive) > 0 {
wantErr = append(wantErr, tt.wantCELErrorsForKube25Through28Inclusive...)
} else if minor >= 29 && len(tt.wantCELErrorsForKube29OrNewer) > 0 {
}
if minor >= 29 && minor <= 33 && len(tt.wantCELErrorsForKube29Through33Inclusive) > 0 {
wantErr = append(wantErr, tt.wantCELErrorsForKube29Through33Inclusive...)
}
if minor >= 29 && len(tt.wantCELErrorsForKube29OrNewer) > 0 {
wantErr = append(wantErr, tt.wantCELErrorsForKube29OrNewer...)
}
if minor >= 34 && len(tt.wantCELErrorsForKube34OrNewer) > 0 {
wantErr = append(wantErr, tt.wantCELErrorsForKube34OrNewer...)
}
// Did not want any error.
if len(wantErr) == 0 {
+1 -28
View File
@@ -1,4 +1,4 @@
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
// Copyright 2020-2025 the Pinniped contributors. All Rights Reserved.
// SPDX-License-Identifier: Apache-2.0
package testlib
@@ -6,7 +6,6 @@ package testlib
import (
"context"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
@@ -253,32 +252,6 @@ func WaitForWebhookAuthenticatorStatusConditions(ctx context.Context, t *testing
}, 60*time.Second, 1*time.Second, "wanted WebhookAuthenticator conditions")
}
// CreateTestJWTAuthenticatorForCLIUpstream creates and returns a test JWTAuthenticator which will be automatically
// deleted at the end of the current test's lifetime.
//
// CreateTestJWTAuthenticatorForCLIUpstream gets the OIDC issuer info from IntegrationEnv().CLIUpstreamOIDC.
func CreateTestJWTAuthenticatorForCLIUpstream(ctx context.Context, t *testing.T) *authenticationv1alpha1.JWTAuthenticator {
t.Helper()
testEnv := IntegrationEnv(t)
spec := authenticationv1alpha1.JWTAuthenticatorSpec{
Issuer: testEnv.CLIUpstreamOIDC.Issuer,
Audience: testEnv.CLIUpstreamOIDC.ClientID,
// The default UsernameClaim is "username" but the upstreams that we use for
// integration tests won't necessarily have that claim, so use "sub" here.
Claims: authenticationv1alpha1.JWTTokenClaims{Username: "sub"},
}
// If the test upstream does not have a CA bundle specified, then don't configure one in the
// JWTAuthenticator. Leaving TLSSpec set to nil will result in OIDC discovery using the OS's root
// CA store.
if testEnv.CLIUpstreamOIDC.CABundle != "" {
spec.TLS = &authenticationv1alpha1.TLSSpec{
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(testEnv.CLIUpstreamOIDC.CABundle)),
}
}
authenticator := CreateTestJWTAuthenticator(ctx, t, spec, authenticationv1alpha1.JWTAuthenticatorPhaseReady)
return authenticator
}
// CreateTestJWTAuthenticator creates and returns a test JWTAuthenticator which will be automatically deleted
// at the end of the current test's lifetime.
func CreateTestJWTAuthenticator(
+9 -6
View File
@@ -304,12 +304,15 @@ func loadEnvVars(t *testing.T, result *TestEnv) {
result.ShellContainerImage = needEnv(t, "PINNIPED_TEST_SHELL_CONTAINER_IMAGE")
result.CLIUpstreamOIDC = TestOIDCUpstream{
Issuer: needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER"),
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE")),
ClientID: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID"),
CallbackURL: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CALLBACK_URL"),
Username: needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME"),
Password: needEnv(t, "PINNIPED_TEST_CLI_OIDC_PASSWORD"),
Issuer: needEnv(t, "PINNIPED_TEST_CLI_OIDC_ISSUER"),
CABundle: base64Decoded(t, os.Getenv("PINNIPED_TEST_CLI_OIDC_ISSUER_CA_BUNDLE")),
UsernameClaim: os.Getenv("PINNIPED_TEST_CLI_OIDC_USERNAME_CLAIM"),
GroupsClaim: os.Getenv("PINNIPED_TEST_CLI_OIDC_GROUPS_CLAIM"),
ClientID: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CLIENT_ID"),
CallbackURL: needEnv(t, "PINNIPED_TEST_CLI_OIDC_CALLBACK_URL"),
Username: needEnv(t, "PINNIPED_TEST_CLI_OIDC_USERNAME"),
Password: needEnv(t, "PINNIPED_TEST_CLI_OIDC_PASSWORD"),
ExpectedGroups: filterEmpty(strings.Split(strings.ReplaceAll(os.Getenv("PINNIPED_TEST_CLI_OIDC_EXPECTED_GROUPS"), " ", ""), ",")),
}
result.SupervisorUpstreamOIDC = TestOIDCUpstream{