mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2025-12-23 06:15:47 +00:00
Update jwtauthenticator unit tests to check actions
- Add test to verify timestamps are particularly updated - Improve diff output in tests for actions - Make jwtauthenticator status tests parallel - Update copyright headers in multiple files
This commit is contained in:
committed by
Ryan Richard
parent
868ff9ed2b
commit
e8482ab9e9
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
@@ -8,6 +8,7 @@ package jwtcachefiller
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
@@ -17,7 +18,6 @@ import (
|
||||
|
||||
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/ory/fosite/token/jwt"
|
||||
"k8s.io/apimachinery/pkg/api/equality"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -42,8 +42,6 @@ import (
|
||||
"go.pinniped.dev/internal/plog"
|
||||
)
|
||||
|
||||
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
||||
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
||||
const (
|
||||
controllerName = "jwtcachefiller-controller"
|
||||
|
||||
@@ -65,13 +63,17 @@ const (
|
||||
reasonInvalidTLSConfiguration = "InvalidTLSConfiguration"
|
||||
reasonInvalidDiscoveryProbe = "InvalidDiscoveryProbe"
|
||||
reasonInvalidAuthenticator = "InvalidAuthenticator"
|
||||
reasonInvalidTokenSigning = "InvalidTokenSigning"
|
||||
reasonInvalidTokenSigningFailure = "InvalidTokenSigningFailure"
|
||||
reasonInvalidCouldNotFetchJWKS = "InvalidCouldNotFetchJWKS"
|
||||
|
||||
msgUnableToValidate = "unable to validate; see other conditions for details"
|
||||
|
||||
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
||||
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
||||
defaultUsernameClaim = oidcapi.IDTokenClaimUsername
|
||||
defaultGroupsClaim = oidcapi.IDTokenClaimGroups
|
||||
|
||||
minimalJWTToTriggerJWKSFetch = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.e30."
|
||||
)
|
||||
|
||||
type providerJSON struct {
|
||||
@@ -177,19 +179,23 @@ func (c *jwtCacheFillerController) Sync(ctx controllerlib.Context) error {
|
||||
|
||||
rootCAs, conditions, tlsOk := c.validateTLS(specCopy.TLS, conditions)
|
||||
_, conditions, issuerOk := c.validateIssuer(specCopy.Issuer, conditions)
|
||||
okSoFar := tlsOk && issuerOk
|
||||
|
||||
client := phttp.Default(rootCAs)
|
||||
client.Timeout = 30 * time.Second // copied from Kube OIDC code
|
||||
coreOSCtx := coreosoidc.ClientContext(context.Background(), client)
|
||||
|
||||
pJSON, provider, conditions, providerErr := c.validateProviderDiscovery(coreOSCtx, specCopy.Issuer, conditions, tlsOk && issuerOk)
|
||||
pJSON, provider, conditions, providerErr := c.validateProviderDiscovery(coreOSCtx, specCopy.Issuer, conditions, okSoFar)
|
||||
errs = append(errs, providerErr)
|
||||
okSoFar = okSoFar && providerErr == nil
|
||||
|
||||
jwksURL, conditions, jwksErr := c.validateProviderJWKSURL(provider, pJSON, conditions, tlsOk && issuerOk && providerErr == nil)
|
||||
jwksURL, conditions, jwksErr := c.validateProviderJWKSURL(provider, pJSON, conditions, okSoFar)
|
||||
errs = append(errs, jwksErr)
|
||||
okSoFar = okSoFar && jwksErr == nil
|
||||
|
||||
keySet, conditions, jwksFetchErr := c.validateJWKSFetch(coreOSCtx, jwksURL, conditions, tlsOk && issuerOk && providerErr == nil && jwksErr == nil)
|
||||
keySet, conditions, jwksFetchErr := c.validateJWKSFetch(coreOSCtx, jwksURL, conditions, okSoFar)
|
||||
errs = append(errs, jwksFetchErr)
|
||||
okSoFar = okSoFar && jwksFetchErr == nil
|
||||
|
||||
// Make a deep copy of the spec so we aren't storing pointers to something that the informer cache
|
||||
// may mutate! We don't store status as status is derived from spec.
|
||||
@@ -198,7 +204,7 @@ func (c *jwtCacheFillerController) Sync(ctx controllerlib.Context) error {
|
||||
obj.Spec.DeepCopy(),
|
||||
keySet,
|
||||
conditions,
|
||||
tlsOk && issuerOk && providerErr == nil && jwksErr == nil && jwksFetchErr == nil)
|
||||
okSoFar)
|
||||
errs = append(errs, err)
|
||||
|
||||
if !conditionsutil.HadErrorCondition(conditions) {
|
||||
@@ -209,10 +215,10 @@ func (c *jwtCacheFillerController) Sync(ctx controllerlib.Context) error {
|
||||
err = c.updateStatus(ctx.Context, obj, conditions)
|
||||
errs = append(errs, err)
|
||||
|
||||
// sync loop errors:
|
||||
// - should not be configuration errors. config errors a user must correct belong on the .Status
|
||||
// Sync loop errors:
|
||||
// - Should not be configuration errors. Config errors a user must correct belong on the .Status
|
||||
// object. The controller simply must wait for a user to correct before running again.
|
||||
// - other errors, such as networking errors, etc. are the types of errors that should return here
|
||||
// - Other errors, such as networking errors, etc. are the types of errors that should return here
|
||||
// and signal the controller to retry the sync loop. These may be corrected by machines.
|
||||
return errorsutil.NewAggregate(errs)
|
||||
}
|
||||
@@ -230,174 +236,63 @@ func (c *jwtCacheFillerController) extractValueAsJWTAuthenticator(value authncac
|
||||
return jwtAuthenticator
|
||||
}
|
||||
|
||||
func (c *jwtCacheFillerController) updateStatus(
|
||||
ctx context.Context,
|
||||
original *auth1alpha1.JWTAuthenticator,
|
||||
conditions []*metav1.Condition,
|
||||
) error {
|
||||
updated := original.DeepCopy()
|
||||
|
||||
if conditionsutil.HadErrorCondition(conditions) {
|
||||
updated.Status.Phase = auth1alpha1.JWTAuthenticatorPhaseError
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeReady,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonNotReady,
|
||||
Message: "the JWTAuthenticator is not ready: see other conditions for details",
|
||||
})
|
||||
} else {
|
||||
updated.Status.Phase = auth1alpha1.JWTAuthenticatorPhaseReady
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeReady,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "the JWTAuthenticator is ready",
|
||||
})
|
||||
}
|
||||
|
||||
_ = conditionsutil.MergeConfigConditions(
|
||||
conditions,
|
||||
original.Generation,
|
||||
&updated.Status.Conditions,
|
||||
plog.New().WithName(controllerName),
|
||||
metav1.NewTime(c.clock.Now()),
|
||||
)
|
||||
|
||||
if equality.Semantic.DeepEqual(original, updated) {
|
||||
return nil
|
||||
}
|
||||
_, err := c.client.AuthenticationV1alpha1().JWTAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *jwtCacheFillerController) validateJWKSFetch(ctx context.Context, jwksURL string, conditions []*metav1.Condition, prereqOk bool) (*coreosoidc.RemoteKeySet, []*metav1.Condition, error) {
|
||||
if !prereqOk {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msgUnableToValidate,
|
||||
})
|
||||
return nil, conditions, nil
|
||||
}
|
||||
keySet := coreosoidc.NewRemoteKeySet(ctx, jwksURL)
|
||||
testJWTToken := jwt.NewWithClaims(jwt.SigningMethodNone, jwt.MapClaims{
|
||||
"aud": "fake-audience-for-verification-probe",
|
||||
})
|
||||
rawToken, signErr := testJWTToken.SignedString(jwt.UnsafeAllowNoneSignatureType)
|
||||
// no unit tests for this block.
|
||||
// this is not configurable, there is no way to change the token we are using
|
||||
// for testing, so we simply shouldn't hit this block.
|
||||
if signErr != nil {
|
||||
errText := "could not sign tokens"
|
||||
msg := fmt.Sprintf("%s: %s", errText, signErr.Error())
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidTokenSigning,
|
||||
Message: msg,
|
||||
})
|
||||
return keySet, conditions, fmt.Errorf("%s: %w", errText, signErr)
|
||||
}
|
||||
_, verifyWithKeySetErr := keySet.VerifySignature(ctx, rawToken)
|
||||
verifyErrString := verifyWithKeySetErr.Error()
|
||||
// we need to fetch the keys. this is the main concern of this function
|
||||
if strings.Contains(verifyErrString, "fetching keys") {
|
||||
errText := "could not fetch keys"
|
||||
msg := fmt.Sprintf("%s: %s", errText, verifyErrString)
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidCouldNotFetchJWKS,
|
||||
Message: msg,
|
||||
})
|
||||
return keySet, conditions, fmt.Errorf("%s: %w", errText, verifyWithKeySetErr)
|
||||
}
|
||||
// this error indicates success of this check. we only wanted to test if we could fetch, we aren't actually
|
||||
// testing for valid signature verification.
|
||||
if strings.Contains(verifyErrString, "failed to verify id token signature") {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "successfully fetched jwks",
|
||||
})
|
||||
return keySet, conditions, nil
|
||||
}
|
||||
// any other errors we will ignore and treat this as a success.
|
||||
return keySet, conditions, nil
|
||||
}
|
||||
|
||||
// newCachedJWTAuthenticator creates a jwt authenticator from the provided spec.
|
||||
func (c *jwtCacheFillerController) newCachedJWTAuthenticator(client *http.Client, spec *auth1alpha1.JWTAuthenticatorSpec, keySet *coreosoidc.RemoteKeySet, conditions []*metav1.Condition, prereqOk bool) (*cachedJWTAuthenticator, []*metav1.Condition, error) {
|
||||
if !prereqOk {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeAuthenticatorValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msgUnableToValidate,
|
||||
})
|
||||
return nil, conditions, nil
|
||||
}
|
||||
|
||||
usernameClaim := spec.Claims.Username
|
||||
if usernameClaim == "" {
|
||||
usernameClaim = defaultUsernameClaim
|
||||
}
|
||||
groupsClaim := spec.Claims.Groups
|
||||
if groupsClaim == "" {
|
||||
groupsClaim = defaultGroupsClaim
|
||||
}
|
||||
|
||||
oidcAuthenticator, err := oidc.New(oidc.Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: spec.Issuer,
|
||||
Audiences: []string{spec.Audience},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: usernameClaim,
|
||||
Prefix: ptr.To(""),
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: groupsClaim,
|
||||
Prefix: ptr.To(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
KeySet: keySet,
|
||||
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
|
||||
Client: client,
|
||||
})
|
||||
func (c *jwtCacheFillerController) validateTLS(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
|
||||
rootCAs, _, err := pinnipedauthenticator.CABundle(tlsSpec)
|
||||
if err != nil {
|
||||
// no unit test for this failure.
|
||||
// it seems that our production code doesn't provide config knobs that would allow
|
||||
// incorrect configuration of oidc.New(). We validate inputs before we get to this point
|
||||
// and exit early if there are problems. In the future, if we allow more configuration,
|
||||
// such as supported signing algorithm config, we may be able to test this.
|
||||
errText := "could not initialize oidc authenticator"
|
||||
msg := fmt.Sprintf("%s: %s", errText, err.Error())
|
||||
msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error())
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeAuthenticatorValid,
|
||||
Type: typeTLSConfigurationValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidAuthenticator,
|
||||
Reason: reasonInvalidTLSConfiguration,
|
||||
Message: msg,
|
||||
})
|
||||
// resync err, lots of possible issues that may or may not be machine related
|
||||
return nil, conditions, fmt.Errorf("%s: %w", errText, err)
|
||||
return rootCAs, conditions, false
|
||||
}
|
||||
|
||||
msg := "successfully parsed specified CA bundle"
|
||||
if rootCAs == nil {
|
||||
msg = "no CA bundle specified"
|
||||
}
|
||||
msg := "authenticator initialized"
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeAuthenticatorValid,
|
||||
Type: typeTLSConfigurationValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: msg,
|
||||
})
|
||||
return &cachedJWTAuthenticator{
|
||||
tokenAuthenticatorCloser: oidcAuthenticator,
|
||||
spec: spec,
|
||||
}, conditions, nil
|
||||
return rootCAs, conditions, true
|
||||
}
|
||||
|
||||
func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*metav1.Condition) (*url.URL, []*metav1.Condition, bool) {
|
||||
issuerURL, err := url.Parse(issuer)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("%s: %s", "spec.issuer URL is invalid", err.Error())
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeIssuerURLValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidIssuerURL,
|
||||
Message: msg,
|
||||
})
|
||||
return nil, conditions, false
|
||||
}
|
||||
|
||||
if issuerURL.Scheme != "https" {
|
||||
msg := fmt.Sprintf("spec.issuer %s has invalid scheme, require 'https'", issuer)
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeIssuerURLValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidIssuerURLScheme,
|
||||
Message: msg,
|
||||
})
|
||||
return nil, conditions, false
|
||||
}
|
||||
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeIssuerURLValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "issuer is a valid URL",
|
||||
})
|
||||
return issuerURL, conditions, true
|
||||
}
|
||||
|
||||
func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context, issuer string, conditions []*metav1.Condition, prereqOk bool) (*providerJSON, *coreosoidc.Provider, []*metav1.Condition, error) {
|
||||
@@ -493,61 +388,186 @@ func (c *jwtCacheFillerController) validateProviderJWKSURL(provider *coreosoidc.
|
||||
return pJSON.JWKSURL, conditions, nil
|
||||
}
|
||||
|
||||
func (c *jwtCacheFillerController) validateTLS(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
|
||||
rootCAs, _, err := pinnipedauthenticator.CABundle(tlsSpec)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error())
|
||||
// validateJWKSFetch deliberately takes an unsigned JWT to trigger coreosoidc.NewRemoteKeySet to
|
||||
// indirectly fetch the JWKS. This lets us report a status about the endpoint, even though
|
||||
// we expect the verification checks to actually fail. This also pre-warms the cache of keys
|
||||
// in the remote keyset object.
|
||||
func (c *jwtCacheFillerController) validateJWKSFetch(ctx context.Context, jwksURL string, conditions []*metav1.Condition, prereqOk bool) (*coreosoidc.RemoteKeySet, []*metav1.Condition, error) {
|
||||
if !prereqOk {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeTLSConfigurationValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidTLSConfiguration,
|
||||
Message: msg,
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msgUnableToValidate,
|
||||
})
|
||||
return rootCAs, conditions, false
|
||||
return nil, conditions, nil
|
||||
}
|
||||
keySet := coreosoidc.NewRemoteKeySet(ctx, jwksURL)
|
||||
|
||||
// keySet.verifySignature calls functions which may error in a couple of ways that
|
||||
// we will treat as success because we are really only concerned here that we could
|
||||
// fetch the keys at all.
|
||||
_, verifyWithKeySetErr := keySet.VerifySignature(ctx, minimalJWTToTriggerJWKSFetch)
|
||||
if verifyWithKeySetErr == nil {
|
||||
// No unit test.
|
||||
// Since we hard-coded this token we expect there to always be a verification error.
|
||||
// The purpose of this function is really to test if we can get the JWKS, not to actually validate a token.
|
||||
// Therefore, we should never hit this path, nevertheless, lets handle just in case something unexpected happens.
|
||||
errText := "jwks should not have verified unsigned jwt token"
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: errText,
|
||||
})
|
||||
return nil, conditions, errors.New(errText)
|
||||
}
|
||||
|
||||
msg := "successfully parsed specified CA bundle"
|
||||
if rootCAs == nil {
|
||||
msg = "no CA bundle specified"
|
||||
}
|
||||
verifyErrString := verifyWithKeySetErr.Error()
|
||||
// We need to fetch the keys. This is the main concern of this function.
|
||||
if strings.HasPrefix(verifyErrString, "fetching keys") {
|
||||
errText := "could not fetch keys"
|
||||
msg := fmt.Sprintf("%s: %s", errText, verifyErrString)
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeTLSConfigurationValid,
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidCouldNotFetchJWKS,
|
||||
Message: msg,
|
||||
})
|
||||
return nil, conditions, fmt.Errorf("%s: %w", errText, verifyWithKeySetErr)
|
||||
}
|
||||
// This error indicates success of this check. We only wanted to test if we could fetch, we aren't actually
|
||||
// testing for valid signature verification.
|
||||
if strings.Contains(verifyErrString, "failed to verify id token signature") {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "successfully fetched jwks",
|
||||
})
|
||||
return keySet, conditions, nil
|
||||
}
|
||||
|
||||
// No unit tests, currently no way to reach this code path.
|
||||
errText := "unexpected verification error while fetching jwks"
|
||||
msg := fmt.Sprintf("%s: %s", errText, verifyErrString)
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msg,
|
||||
})
|
||||
return nil, conditions, fmt.Errorf("%s: %w", errText, verifyWithKeySetErr)
|
||||
}
|
||||
|
||||
// newCachedJWTAuthenticator creates a jwt authenticator from the provided spec.
|
||||
func (c *jwtCacheFillerController) newCachedJWTAuthenticator(client *http.Client, spec *auth1alpha1.JWTAuthenticatorSpec, keySet *coreosoidc.RemoteKeySet, conditions []*metav1.Condition, prereqOk bool) (*cachedJWTAuthenticator, []*metav1.Condition, error) {
|
||||
if !prereqOk {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeAuthenticatorValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msgUnableToValidate,
|
||||
})
|
||||
return nil, conditions, nil
|
||||
}
|
||||
|
||||
usernameClaim := spec.Claims.Username
|
||||
if usernameClaim == "" {
|
||||
usernameClaim = defaultUsernameClaim
|
||||
}
|
||||
groupsClaim := spec.Claims.Groups
|
||||
if groupsClaim == "" {
|
||||
groupsClaim = defaultGroupsClaim
|
||||
}
|
||||
|
||||
oidcAuthenticator, err := oidc.New(oidc.Options{
|
||||
JWTAuthenticator: apiserver.JWTAuthenticator{
|
||||
Issuer: apiserver.Issuer{
|
||||
URL: spec.Issuer,
|
||||
Audiences: []string{spec.Audience},
|
||||
},
|
||||
ClaimMappings: apiserver.ClaimMappings{
|
||||
Username: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: usernameClaim,
|
||||
Prefix: ptr.To(""),
|
||||
},
|
||||
Groups: apiserver.PrefixedClaimOrExpression{
|
||||
Claim: groupsClaim,
|
||||
Prefix: ptr.To(""),
|
||||
},
|
||||
},
|
||||
},
|
||||
KeySet: keySet,
|
||||
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
|
||||
Client: client,
|
||||
})
|
||||
if err != nil {
|
||||
// no unit test for this failure.
|
||||
// it seems that our production code doesn't provide config knobs that would allow
|
||||
// incorrect configuration of oidc.New(). We validate inputs before we get to this point
|
||||
// and exit early if there are problems. In the future, if we allow more configuration,
|
||||
// such as supported signing algorithm config, we may be able to test this.
|
||||
errText := "could not initialize oidc authenticator"
|
||||
msg := fmt.Sprintf("%s: %s", errText, err.Error())
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeAuthenticatorValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidAuthenticator,
|
||||
Message: msg,
|
||||
})
|
||||
// resync err, lots of possible issues that may or may not be machine related
|
||||
return nil, conditions, fmt.Errorf("%s: %w", errText, err)
|
||||
}
|
||||
msg := "authenticator initialized"
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeAuthenticatorValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: msg,
|
||||
})
|
||||
return rootCAs, conditions, true
|
||||
return &cachedJWTAuthenticator{
|
||||
tokenAuthenticatorCloser: oidcAuthenticator,
|
||||
spec: spec,
|
||||
}, conditions, nil
|
||||
}
|
||||
|
||||
func (c *jwtCacheFillerController) validateIssuer(issuer string, conditions []*metav1.Condition) (*url.URL, []*metav1.Condition, bool) {
|
||||
issuerURL, err := url.Parse(issuer)
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("%s: %s", "spec.issuer URL is invalid", err.Error())
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeIssuerURLValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidIssuerURL,
|
||||
Message: msg,
|
||||
})
|
||||
return nil, conditions, false
|
||||
}
|
||||
func (c *jwtCacheFillerController) updateStatus(
|
||||
ctx context.Context,
|
||||
original *auth1alpha1.JWTAuthenticator,
|
||||
conditions []*metav1.Condition,
|
||||
) error {
|
||||
updated := original.DeepCopy()
|
||||
|
||||
if issuerURL.Scheme != "https" {
|
||||
msg := fmt.Sprintf("spec.issuer %s has invalid scheme, require 'https'", issuer)
|
||||
if conditionsutil.HadErrorCondition(conditions) {
|
||||
updated.Status.Phase = auth1alpha1.JWTAuthenticatorPhaseError
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeIssuerURLValid,
|
||||
Type: typeReady,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidIssuerURLScheme,
|
||||
Message: msg,
|
||||
Reason: reasonNotReady,
|
||||
Message: "the JWTAuthenticator is not ready: see other conditions for details",
|
||||
})
|
||||
return nil, conditions, false
|
||||
}
|
||||
|
||||
} else {
|
||||
updated.Status.Phase = auth1alpha1.JWTAuthenticatorPhaseReady
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeIssuerURLValid,
|
||||
Type: typeReady,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "issuer is a valid URL",
|
||||
Message: "the JWTAuthenticator is ready",
|
||||
})
|
||||
return issuerURL, conditions, true
|
||||
}
|
||||
|
||||
_ = conditionsutil.MergeConfigConditions(
|
||||
conditions,
|
||||
original.Generation,
|
||||
&updated.Status.Conditions,
|
||||
plog.New().WithName(controllerName),
|
||||
metav1.NewTime(c.clock.Now()),
|
||||
)
|
||||
|
||||
if equality.Semantic.DeepEqual(original, updated) {
|
||||
return nil
|
||||
}
|
||||
_, err := c.client.AuthenticationV1alpha1().JWTAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -24,9 +24,13 @@ import (
|
||||
"github.com/go-jose/go-jose/v3"
|
||||
"github.com/go-jose/go-jose/v3/jwt"
|
||||
"github.com/golang/mock/gomock"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
fositejwt "github.com/ory/fosite/token/jwt"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
@@ -46,6 +50,13 @@ import (
|
||||
"go.pinniped.dev/internal/testutil/tlsserver"
|
||||
)
|
||||
|
||||
func TestMinimalJWTToTriggerJWKSFetch(t *testing.T) {
|
||||
tinyJWT := fositejwt.NewWithClaims(fositejwt.SigningMethodNone, fositejwt.MapClaims{})
|
||||
tinyJWTStr, err := tinyJWT.SignedString(fositejwt.UnsafeAllowNoneSignatureType)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, tinyJWTStr, minimalJWTToTriggerJWKSFetch)
|
||||
}
|
||||
|
||||
func TestController(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -192,6 +203,9 @@ func TestController(t *testing.T) {
|
||||
frozenMetav1Now := metav1.NewTime(nowDoesntMatter)
|
||||
frozenClock := clocktesting.NewFakeClock(nowDoesntMatter)
|
||||
|
||||
timeInThePast := time.Date(1111, time.January, 1, 1, 1, 1, 111111, time.Local)
|
||||
frozenTimeInThePast := metav1.NewTime(timeInThePast)
|
||||
|
||||
someJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: goodIssuer,
|
||||
Audience: goodAudience,
|
||||
@@ -497,7 +511,16 @@ func TestController(t *testing.T) {
|
||||
happyTLSConfigurationValidCAParsed(someTime, observedGeneration),
|
||||
})
|
||||
}
|
||||
|
||||
jwtAuthenticatorsGVR := schema.GroupVersionResource{
|
||||
Group: "authentication.concierge.pinniped.dev",
|
||||
Version: "v1alpha1",
|
||||
Resource: "jwtauthenticators",
|
||||
}
|
||||
jwtAUthenticatorGVK := schema.GroupVersionKind{
|
||||
Group: "authentication.concierge.pinniped.dev",
|
||||
Version: "v1alpha1",
|
||||
Kind: "JWTAuthenticator",
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
cache func(*testing.T, *authncache.Cache, bool)
|
||||
@@ -512,15 +535,14 @@ func TestController(t *testing.T) {
|
||||
// something can be automatically corrected on a retry (ie an error that might be networking).
|
||||
wantSyncLoopErr testutil.RequireErrorStringFunc
|
||||
wantLogs []map[string]any
|
||||
wantStatusConditions []metav1.Condition
|
||||
wantStatusPhase auth1alpha1.JWTAuthenticatorPhase
|
||||
wantActions func() []coretesting.Action
|
||||
wantCacheEntries int
|
||||
wantUsernameClaim string
|
||||
wantGroupsClaim string
|
||||
runTestsOnResultingAuthenticator bool
|
||||
}{
|
||||
{
|
||||
name: "404: jwt authenticator not found will abort sync loop and not attempt to write status",
|
||||
name: "404: JWTAuthenticator not found will abort sync loop, no status conditions.",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
wantLogs: []map[string]any{
|
||||
{
|
||||
@@ -530,12 +552,15 @@ func TestController(t *testing.T) {
|
||||
"message": "Sync() found that the JWTAuthenticator does not exist yet or was deleted",
|
||||
},
|
||||
},
|
||||
wantActions: func() []coretesting.Action {
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
}
|
||||
},
|
||||
},
|
||||
// Existing code that was never tested. We would likely have to create a server with bad clients to
|
||||
// simulate this.
|
||||
// { name: "non-404 `failed to get JWTAuthenticator` for other API server reasons" }
|
||||
{
|
||||
name: "valid jwt authenticator sync loop with no change will preserve existing status",
|
||||
name: "Sync: valid and unchanged JWTAuthenticator: loop will preserve existing status conditions",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
@@ -559,12 +584,78 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
}, {
|
||||
name: "Sync: changed JWTAuthenticator: loop will update timestamps only on relevant statuses",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
// sad and unknwn will update with new statuses and timestamps
|
||||
sadReadyCondition(frozenTimeInThePast, 0),
|
||||
sadDiscoveryURLValidx509(goodIssuer, frozenTimeInThePast, 0),
|
||||
unknownAuthenticatorValid(frozenTimeInThePast, 0),
|
||||
unknownJWKSURLValid(frozenTimeInThePast, 0),
|
||||
unknownJWKSFetch(frozenTimeInThePast, 0),
|
||||
// this one will remain unchanged as it was good to begin with
|
||||
happyTLSConfigurationValidCAParsed(frozenTimeInThePast, 0),
|
||||
},
|
||||
),
|
||||
Phase: "Error",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantLogs: []map[string]any{{
|
||||
"level": "info",
|
||||
"timestamp": "2099-08-08T13:57:36.123456Z",
|
||||
"logger": "jwtcachefiller-controller",
|
||||
"message": "added new jwt authenticator",
|
||||
"issuer": goodIssuer,
|
||||
"jwtAuthenticator": map[string]interface{}{
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
// this timestamp should not have updated, it didn't change.
|
||||
happyTLSConfigurationValidCAParsed(frozenTimeInThePast, 0),
|
||||
},
|
||||
),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
},
|
||||
{
|
||||
name: "valid jwt authenticator with CA will complete sync loop successfully with success conditions and ready phase",
|
||||
name: "Sync: valid JWTAuthenticator with CA: loop will complete successfully and update status conditions.",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
@@ -584,13 +675,29 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
runTestsOnResultingAuthenticator: true,
|
||||
},
|
||||
{
|
||||
name: "valid jwt authenticator with custom username claim will complete sync loop successfully with success conditions and ready phase",
|
||||
name: "Sync: JWTAuthenticator with custom username claim: loop will complete successfully and update status conditions.",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
@@ -610,14 +717,30 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpecWithUsernameClaim,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
wantUsernameClaim: someJWTAuthenticatorSpecWithUsernameClaim.Claims.Username,
|
||||
runTestsOnResultingAuthenticator: true,
|
||||
},
|
||||
{
|
||||
name: "valid jwt authenticator with custom groups claim will complete sync loop successfully with success conditions and ready phase",
|
||||
name: "Sync: JWTAuthenticator with custom groups claim: loop will complete successfully and update status conditions.",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
@@ -637,14 +760,30 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpecWithGroupsClaim,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
wantGroupsClaim: someJWTAuthenticatorSpecWithGroupsClaim.Claims.Groups,
|
||||
runTestsOnResultingAuthenticator: true,
|
||||
},
|
||||
{
|
||||
name: "updating jwt authenticator with new fields closes previous instance and will complete sync loop successfully with success conditions and ready phase",
|
||||
name: "Sync: JWTAuthenticator with new fields: loop will close previous instance of JWTAuthenticator and complete successfully and update status conditions.",
|
||||
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
||||
cache.Store(
|
||||
authncache.Key{
|
||||
@@ -675,13 +814,29 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
runTestsOnResultingAuthenticator: true,
|
||||
},
|
||||
{
|
||||
name: "updating jwt authenticator with the same value does not trigger sync loop and will not update conditions or phase",
|
||||
name: "Sync: JWTAuthenticator with no change: loop will abort early and not update status conditions.",
|
||||
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
||||
cache.Store(
|
||||
authncache.Key{
|
||||
@@ -712,11 +867,17 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantActions: func() []coretesting.Action {
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
runTestsOnResultingAuthenticator: false, // skip the tests because the authenticator left in the cache is the mock version that was added above
|
||||
},
|
||||
{
|
||||
name: "updating jwt authenticator when cache value is wrong type will complete sync loop successfully with success conditions and ready phase",
|
||||
name: "Sync: JWTAuthenticator update when cached authenticator is different type: loop will complete successfully and update status conditions.",
|
||||
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
|
||||
cache.Store(
|
||||
authncache.Key{
|
||||
@@ -752,13 +913,29 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
runTestsOnResultingAuthenticator: true,
|
||||
},
|
||||
{
|
||||
name: "TLS: valid jwt authenticator without CA will fail to cache the authenticator and will fail with failed and unknown conditions and Error phase and will enqueue a resync in case of machine error",
|
||||
name: "Sync: valid JWTAuthenticator without CA: loop will fail to cache the authenticator, will write failed and unknown status conditions, and will enqueue resync",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
@@ -768,9 +945,14 @@ func TestController(t *testing.T) {
|
||||
Spec: *missingTLSJWTAuthenticatorSpec,
|
||||
},
|
||||
},
|
||||
// no explicit logs, this is an issue of config, the user must provide TLS config for the
|
||||
// custom cert provided for this server.
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *missingTLSJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
sadReadyCondition(frozenMetav1Now, 0),
|
||||
@@ -781,12 +963,23 @@ func TestController(t *testing.T) {
|
||||
happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
// no explicit logs, this is an issue of config, the user must provide TLS config for the
|
||||
// custom cert provided for this server.
|
||||
wantSyncLoopErr: testutil.WantX509UntrustedCertErrorString(`could not perform oidc discovery on provider issuer: Get "`+goodIssuer+`/.well-known/openid-configuration": %s`, "Acme Co"),
|
||||
wantCacheEntries: 0,
|
||||
},
|
||||
{
|
||||
name: "validateTLS: invalid jwt authenticator CA will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
|
||||
name: "validateTLS: JWTAuthenticator with invalid CA: loop will fail, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
@@ -796,7 +989,14 @@ func TestController(t *testing.T) {
|
||||
Spec: *invalidTLSJWTAuthenticatorSpec,
|
||||
},
|
||||
},
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *invalidTLSJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(someOtherIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
sadReadyCondition(frozenMetav1Now, 0),
|
||||
@@ -807,10 +1007,19 @@ func TestController(t *testing.T) {
|
||||
unknownJWKSFetch(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 0,
|
||||
}, {
|
||||
name: "validateIssuer: parsing error (spec.issuer URL is invalid) will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
|
||||
name: "validateIssuer: parsing error (spec.issuer URL is invalid): loop will fail sync, will write failed and unknown status conditions, but will not enqueue a resync due to user config error",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -820,7 +1029,14 @@ func TestController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *invalidIssuerJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
sadReadyCondition(frozenMetav1Now, 0),
|
||||
@@ -831,9 +1047,18 @@ func TestController(t *testing.T) {
|
||||
unknownJWKSFetch(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
name: "validateIssuer: parsing error (spec.issuer URL has invalid scheme, requires https) will fail sync loop and will report failed and unknown conditions and Error phase, but will not enqueue a resync due to user config error",
|
||||
name: "validateIssuer: parsing error (spec.issuer URL has invalid scheme, requires https): loop will fail sync, will write failed and unknown conditions, but will not enqueue a resync due to user config error",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -843,7 +1068,14 @@ func TestController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *invalidIssuerSchemeJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
sadReadyCondition(frozenMetav1Now, 0),
|
||||
@@ -854,9 +1086,18 @@ func TestController(t *testing.T) {
|
||||
unknownJWKSFetch(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
}, {
|
||||
name: "validateProviderDiscovery: could not perform oidc discovery on provider issuer will fail sync loop and will report failed and unknown conditions and Error phase and will enqueue new sync",
|
||||
name: "validateProviderDiscovery: could not perform oidc discovery on provider issuer: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -866,7 +1107,14 @@ func TestController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *validIssuerURLButDoesNotExistJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
happyIssuerURLValid(frozenMetav1Now, 0),
|
||||
@@ -878,7 +1126,16 @@ func TestController(t *testing.T) {
|
||||
happyTLSConfigurationValidNoCA(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantSyncLoopErr: testutil.WantExactErrorString(`could not perform oidc discovery on provider issuer: Get "` + goodIssuer + `/foo/bar/baz/shizzle/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`),
|
||||
},
|
||||
// cannot be tested currently the way the coreos lib works.
|
||||
@@ -886,7 +1143,7 @@ func TestController(t *testing.T) {
|
||||
// which ensures the .Claims() parsing cannot fail (in the current impl)
|
||||
// { name: "validateProviderJWKSURL: could not get provider jwks_uri... ",},
|
||||
{
|
||||
name: "validateProviderJWKSURL: could not parse provider jwks_uri will fail sync loop and will report failed and unknown conditions and Error phase and will enqueue new sync",
|
||||
name: "validateProviderJWKSURL: could not parse provider jwks_uri: loop will fail sync, will write failed and unknown conditions, and will enqueue new sync",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -896,7 +1153,14 @@ func TestController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *badIssuerJWKSURIJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
happyIssuerURLValid(frozenMetav1Now, 0),
|
||||
@@ -906,10 +1170,19 @@ func TestController(t *testing.T) {
|
||||
unknownJWKSFetch(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantSyncLoopErr: testutil.WantExactErrorString(`could not parse provider jwks_uri: parse "https://.café .com/café/café/café/coffee/jwks.json": invalid character " " in host name`),
|
||||
}, {
|
||||
name: "validateProviderJWKSURL: invalid scheme, requires 'https' will fail sync loop and will report failed and unknown conditions and Error phase and will enqueue new sync",
|
||||
name: "validateProviderJWKSURL: invalid scheme, requires 'https': loop will fail sync, will write failed and unknown conditions, and will enqueue new sync",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -919,7 +1192,14 @@ func TestController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *badIssuerJWKSURISchemeJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
happyIssuerURLValid(frozenMetav1Now, 0),
|
||||
@@ -929,13 +1209,22 @@ func TestController(t *testing.T) {
|
||||
unknownJWKSFetch(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantSyncLoopErr: testutil.WantExactErrorString("jwks_uri http://.café.com/café/café/café/coffee/jwks.json has invalid scheme, require 'https'"),
|
||||
},
|
||||
// since this is a hard-coded token we can't do any meaningful testing for this case (and should also never have an error)
|
||||
// {name: "validateJWKSFetch: could not sign tokens"},
|
||||
{
|
||||
name: "validateJWKSFetch: could not fetch keys.... this should be a resync err",
|
||||
name: "validateJWKSFetch: could not fetch keys: loop will fail sync, will write failed and unknown status conditions, and will enqueue a resync",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -945,7 +1234,14 @@ func TestController(t *testing.T) {
|
||||
},
|
||||
},
|
||||
syncKey: controllerlib.Key{Name: "test-name"},
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *jwksFetchShouldFailJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
happyIssuerURLValid(frozenMetav1Now, 0),
|
||||
@@ -954,11 +1250,20 @@ func TestController(t *testing.T) {
|
||||
sadJWKSFetch(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "Error",
|
||||
Phase: "Error",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantSyncLoopErr: testutil.WantExactErrorString("could not fetch keys: fetching keys oidc: get keys failed: 404 Not Found 404 page not found\n"),
|
||||
},
|
||||
{
|
||||
name: "updateStatus: when updateStatus() is called with matching original and updated conditions, it will not update",
|
||||
name: "updateStatus: called with matching original and updated conditions: will not make request to update conditions",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -982,12 +1287,16 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
},
|
||||
{
|
||||
name: "updateStatus: when updateStatus() is called with different original and updated conditions, it will update the conditions",
|
||||
name: "updateStatus: called with different original and updated conditions: will make request to update conditions",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -1016,12 +1325,28 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
wantStatusPhase: "Ready",
|
||||
wantActions: func() []coretesting.Action {
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantCacheEntries: 1,
|
||||
},
|
||||
{
|
||||
name: "updateStatus: when updateStatus() fails an error will enqueue a new sync",
|
||||
name: "updateStatus: when update request fails: error will enqueue a resync",
|
||||
jwtAuthenticators: []runtime.Object{
|
||||
&auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
@@ -1049,6 +1374,27 @@ func TestController(t *testing.T) {
|
||||
},
|
||||
)
|
||||
},
|
||||
wantActions: func() []coretesting.Action {
|
||||
// This captures that there was an attempt to update to Ready, allHappyConditions,
|
||||
// but the wantSyncLoopErr indicates that there is a failure, so the JWTAuthenticator
|
||||
// remains with a bad phase and at least 1 sad condition
|
||||
updateStatusAction := coretesting.NewUpdateAction(jwtAuthenticatorsGVR, "", &auth1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "test-name",
|
||||
},
|
||||
Spec: *someJWTAuthenticatorSpec,
|
||||
Status: auth1alpha1.JWTAuthenticatorStatus{
|
||||
Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
Phase: "Ready",
|
||||
},
|
||||
})
|
||||
updateStatusAction.Subresource = "status"
|
||||
return []coretesting.Action{
|
||||
coretesting.NewListAction(jwtAuthenticatorsGVR, jwtAUthenticatorGVK, "", metav1.ListOptions{}),
|
||||
coretesting.NewWatchAction(jwtAuthenticatorsGVR, "", metav1.ListOptions{}),
|
||||
updateStatusAction,
|
||||
}
|
||||
},
|
||||
wantLogs: []map[string]any{{
|
||||
"level": "info",
|
||||
"timestamp": "2099-08-08T13:57:36.123456Z",
|
||||
@@ -1059,14 +1405,6 @@ func TestController(t *testing.T) {
|
||||
"name": "test-name",
|
||||
},
|
||||
}},
|
||||
// conditions and phase match previous state since update failed
|
||||
wantStatusConditions: conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
|
||||
[]metav1.Condition{
|
||||
sadReadyCondition(frozenMetav1Now, 0),
|
||||
},
|
||||
),
|
||||
wantStatusPhase: "SomethingThatWontUpdate",
|
||||
wantSyncLoopErr: testutil.WantExactErrorString("some update error"),
|
||||
wantCacheEntries: 1,
|
||||
},
|
||||
@@ -1143,14 +1481,11 @@ func TestController(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if tt.jwtAuthenticators != nil {
|
||||
var jwtAuthSubject *auth1alpha1.JWTAuthenticator
|
||||
getCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
jwtAuthSubject, getErr := pinnipedAPIClient.AuthenticationV1alpha1().JWTAuthenticators().Get(getCtx, "test-name", metav1.GetOptions{})
|
||||
require.NoError(t, getErr)
|
||||
require.Equal(t, tt.wantStatusConditions, jwtAuthSubject.Status.Conditions, "status.conditions must be correct")
|
||||
require.Equal(t, tt.wantStatusPhase, jwtAuthSubject.Status.Phase, "status.phase should be correct")
|
||||
if !assert.ElementsMatch(t, tt.wantActions(), pinnipedAPIClient.Actions()) {
|
||||
// cmp.Diff is superior to require.ElementsMatch in terms of readability here.
|
||||
// require.ElementsMatch will handle pointers better than require.Equal, but
|
||||
// the timestamps are still incredibly verbose.
|
||||
require.Fail(t, cmp.Diff(tt.wantActions(), pinnipedAPIClient.Actions()), "actions should be exactly the expected number of actions and also contain the correct resources")
|
||||
}
|
||||
|
||||
require.Equal(t, tt.wantCacheEntries, len(cache.Keys()), fmt.Sprintf("expected cache entries is incorrect. wanted:%d, got: %d, keys: %v", tt.wantCacheEntries, len(cache.Keys()), cache.Keys()))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package ldapupstreamwatcher implements a controller which watches LDAPIdentityProviders.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
@@ -18,8 +18,7 @@ import (
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
// Never run this test in parallel since deleting all federation domains is disruptive, see main_test.go.
|
||||
func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
func TestConciergeJWTAuthenticatorStatus_Parallel(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
@@ -31,28 +30,30 @@ func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
{
|
||||
name: "valid spec with no errors and all good status conditions and phase will result in a jwt authenticator that is ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle))
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
Audience: "some-fake-audience",
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseReady)
|
||||
|
||||
testlib.WaitForJWTAuthenticatorStatusConditions(
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
allSuccessfulJWTAuthenticatorConditions())
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid spec with invalid CA in TLS config will result in a jwt authenticator that is not ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := "invalid base64-encoded data"
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
Audience: "some-fake-audience",
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: "invalid base64-encoded data",
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseError)
|
||||
|
||||
@@ -60,7 +61,7 @@ func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
replaceSomeConditions(
|
||||
allSuccessfulJWTAuthenticatorConditions(),
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0),
|
||||
[]metav1.Condition{
|
||||
{
|
||||
Type: "Ready",
|
||||
@@ -100,6 +101,7 @@ func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
{
|
||||
name: "valid spec with valid CA in TLS config but does not match issuer server will result in a jwt authenticator that is not ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
Audience: "some-fake-audience",
|
||||
@@ -107,7 +109,7 @@ func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
// Issuer: C=US, O=Pivotal
|
||||
// No SAN provided
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K",
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseError)
|
||||
|
||||
@@ -115,7 +117,7 @@ func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
replaceSomeConditions(
|
||||
allSuccessfulJWTAuthenticatorConditions(),
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0),
|
||||
[]metav1.Condition{
|
||||
{
|
||||
Type: "Ready",
|
||||
@@ -155,12 +157,13 @@ func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
{
|
||||
name: "invalid with bad issuer will result in a jwt authenticator that is not ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle))
|
||||
fakeIssuerURL := "https://127.0.0.1:443/some-fake-issuer"
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: fakeIssuerURL,
|
||||
Audience: "some-fake-audience",
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle)),
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseError)
|
||||
|
||||
@@ -168,7 +171,7 @@ func TestConciergeJWTAuthenticatorStatus_Disruptive(t *testing.T) {
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
replaceSomeConditions(
|
||||
allSuccessfulJWTAuthenticatorConditions(),
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0),
|
||||
[]metav1.Condition{
|
||||
{
|
||||
Type: "Ready",
|
||||
@@ -347,12 +350,15 @@ func TestConciergeJWTAuthenticatorCRDValidations_Parallel(t *testing.T) {
|
||||
} else {
|
||||
require.NoError(t, createErr)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func allSuccessfulJWTAuthenticatorConditions() []metav1.Condition {
|
||||
func allSuccessfulJWTAuthenticatorConditions(caBundleExists bool) []metav1.Condition {
|
||||
tlsConfigValidMsg := "no CA bundle specified"
|
||||
if caBundleExists {
|
||||
tlsConfigValidMsg = "successfully parsed specified CA bundle"
|
||||
}
|
||||
return []metav1.Condition{{
|
||||
Type: "AuthenticatorValid",
|
||||
Status: "True",
|
||||
@@ -387,6 +393,6 @@ func allSuccessfulJWTAuthenticatorConditions() []metav1.Condition {
|
||||
Type: "TLSConfigurationValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "successfully parsed specified CA bundle",
|
||||
Message: tlsConfigValidMsg,
|
||||
}}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
Reference in New Issue
Block a user