Add .Status to JWTAuthenticator with Conditions,Phase

- "Ready" condition & supporting conditions
- Legacy "Phase" for convenience
- Refactor newCachedJWTAuthenticator() func
  to improve ability to provide additional conditions
- Update JWTAuthenticator.Status type
- Update RBAC for SA to get/watch/update JWTAuthenticator.Status
- Update logger to plog, add tests for logs & statuses
- update Sync() to reduce enqueue when error is config/user managed, perhaps remove validateJWKSResponse()
This commit is contained in:
Benjamin A. Petersen
2024-02-27 15:45:32 -08:00
committed by Ryan Richard
parent f18d7310a8
commit a4447fa606
31 changed files with 1323 additions and 124 deletions
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -174,6 +174,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+4
View File
@@ -43,6 +43,10 @@ rules:
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
resources: [ jwtauthenticators, webhookauthenticators ]
verbs: [ get, list, watch ]
- apiGroups:
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
resources: [ jwtauthenticators/status, webhookauthenticators/status ]
verbs: [ get, list, watch, update ]
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.21/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-21-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -180,6 +180,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.22/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-22-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -180,6 +180,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.23/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-23-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -174,6 +174,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.24/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-24-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -174,6 +174,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-25-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -174,6 +174,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.26/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-26-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -174,6 +174,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.27/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-27-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -174,6 +174,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
+13
View File
@@ -46,6 +46,18 @@ JWTAuthenticator describes the configuration of a JWT authenticator.
[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase"]
==== JWTAuthenticatorPhase (string)
.Appears In:
****
- xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-jwtauthenticatorstatus[$$JWTAuthenticatorStatus$$]
****
[id="{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-jwtauthenticatorspec"]
==== JWTAuthenticatorSpec
@@ -80,6 +92,7 @@ Status of a JWT authenticator.
|===
| Field | Description
| *`conditions`* __link:https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.28/#condition-v1-meta[$$Condition$$] array__ | Represents the observations of the authenticator's current state.
| *`phase`* __xref:{anchor_prefix}-go-pinniped-dev-generated-1-28-apis-concierge-authentication-v1alpha1-jwtauthenticatorphase[$$JWTAuthenticatorPhase$$]__ | Phase summarizes the overall status of the JWTAuthenticator.
|===
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -174,6 +174,14 @@ spec:
x-kubernetes-list-map-keys:
- type
x-kubernetes-list-type: map
phase:
default: Pending
description: Phase summarizes the overall status of the JWTAuthenticator.
enum:
- Pending
- Ready
- Error
type: string
type: object
required:
- spec
@@ -5,6 +5,19 @@ package v1alpha1
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
type JWTAuthenticatorPhase string
const (
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
)
// Status of a JWT authenticator.
type JWTAuthenticatorStatus struct {
// Represents the observations of the authenticator's current state.
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
// +listType=map
// +listMapKey=type
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
// Phase summarizes the overall status of the JWTAuthenticator.
// +kubebuilder:default=Pending
// +kubebuilder:validation:Enum=Pending;Ready;Error
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
}
// Spec for configuring a JWT authenticator.
@@ -7,38 +7,74 @@ package jwtcachefiller
import (
"context"
"crypto/x509"
"fmt"
"net/http"
"net/url"
"reflect"
"time"
coreosoidc "github.com/coreos/go-oidc/v3/oidc"
"github.com/go-jose/go-jose/v3"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/equality"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
errorsutil "k8s.io/apimachinery/pkg/util/errors"
"k8s.io/apiserver/pkg/apis/apiserver"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/plugin/pkg/authenticator/token/oidc"
"k8s.io/klog/v2"
"k8s.io/utils/clock"
"k8s.io/utils/ptr"
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
oidcapi "go.pinniped.dev/generated/latest/apis/supervisor/oidc"
conciergeclientset "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned"
authinformers "go.pinniped.dev/generated/latest/client/concierge/informers/externalversions/authentication/v1alpha1"
pinnipedcontroller "go.pinniped.dev/internal/controller"
pinnipedauthenticator "go.pinniped.dev/internal/controller/authenticator"
"go.pinniped.dev/internal/controller/authenticator/authncache"
"go.pinniped.dev/internal/controller/conditionsutil"
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/net/phttp"
"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"
typeReady = "Ready"
typeTLSConfigurationValid = "TLSConfigurationValid"
typeIssuerURLValid = "IssuerURLValid"
typeDiscoveryValid = "DiscoveryURLValid"
typeJWKSURLValid = "JWKSURLValid"
typeJWKSURLValidResponse = "JWKSURLValidResponse"
typeAuthenticatorValid = "AuthenticatorValid"
reasonSuccess = "Success"
reasonNotReady = "NotReady"
reasonUnableToValidate = "UnableToValidate"
reasonInvalidIssuerURL = "InvalidIssuerURL"
reasonInvalidIssuerURLScheme = "InvalidIssuerURLScheme"
reasonInvalidProviderJWKSURL = "InvalidProviderJWKSURL"
reasonInvalidProviderJWKSURLScheme = "InvalidProviderJWKSURLScheme"
reasonInvalidProviderJWKResponse = "InvalidProviderJWKResponse"
reasonInvalidTLSConfiguration = "InvalidTLSConfiguration"
reasonInvalidDiscoveryProbe = "InvalidDiscoveryProbe"
reasonInvalidAuthenticator = "InvalidAuthenticator"
msgUnableToValidate = "unable to validate; other issues present"
defaultUsernameClaim = oidcapi.IDTokenClaimUsername
defaultGroupsClaim = oidcapi.IDTokenClaimGroups
)
type providerJSON struct {
JWKSURL string `json:"jwks_uri"`
}
// defaultSupportedSigningAlgos returns the default signing algos that this JWTAuthenticator
// supports (i.e., if none are supplied by the user).
func defaultSupportedSigningAlgos() []string {
@@ -58,7 +94,7 @@ type tokenAuthenticatorCloser interface {
pinnipedauthenticator.Closer
}
type jwtAuthenticator struct {
type cachedJWTAuthenticator struct {
tokenAuthenticatorCloser
spec *auth1alpha1.JWTAuthenticatorSpec
}
@@ -66,16 +102,20 @@ type jwtAuthenticator struct {
// New instantiates a new controllerlib.Controller which will populate the provided authncache.Cache.
func New(
cache *authncache.Cache,
client conciergeclientset.Interface,
jwtAuthenticators authinformers.JWTAuthenticatorInformer,
log logr.Logger,
clock clock.Clock,
log plog.Logger,
) controllerlib.Controller {
return controllerlib.New(
controllerlib.Config{
Name: "jwtcachefiller-controller",
Syncer: &controller{
Name: controllerName,
Syncer: &jwtCacheFillerController{
cache: cache,
client: client,
jwtAuthenticators: jwtAuthenticators,
log: log.WithName("jwtcachefiller-controller"),
clock: clock,
log: log.WithName(controllerName),
},
},
controllerlib.WithInformer(
@@ -86,16 +126,19 @@ func New(
)
}
type controller struct {
type jwtCacheFillerController struct {
cache *authncache.Cache
jwtAuthenticators authinformers.JWTAuthenticatorInformer
log logr.Logger
client conciergeclientset.Interface
clock clock.Clock
log plog.Logger
}
// Sync implements controllerlib.Syncer.
func (c *controller) Sync(ctx controllerlib.Context) error {
func (c *jwtCacheFillerController) Sync(ctx controllerlib.Context) error {
obj, err := c.jwtAuthenticators.Lister().Get(ctx.Key.Name)
if err != nil && errors.IsNotFound(err) {
if err != nil && apierrors.IsNotFound(err) {
c.log.Info("Sync() found that the JWTAuthenticator does not exist yet or was deleted")
return nil
}
@@ -125,20 +168,46 @@ func (c *controller) Sync(ctx controllerlib.Context) error {
}
}
conditions := make([]*metav1.Condition, 0)
specCopy := obj.Spec.DeepCopy()
var errs []error
rootCAs, conditions, tlsOk := c.validateTLS(specCopy, conditions)
_, conditions, issuerOk := c.validateIssuer(specCopy.Issuer, conditions)
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, conditions, tlsOk && issuerOk)
errs = append(errs, providerErr)
jwksURL, conditions, jwksErr := c.validateProviderJWKSURL(provider, pJSON, conditions, tlsOk && issuerOk && providerErr == nil)
errs = append(errs, jwksErr)
// Make a deep copy of the spec so we aren't storing pointers to something that the informer cache
// may mutate!
jwtAuthenticator, err := newJWTAuthenticator(obj.Spec.DeepCopy())
if err != nil {
return fmt.Errorf("failed to build jwt authenticator: %w", err)
// may mutate! We don't store status as status is derived from spec.
cachedAuthenticator, conditions, err := c.newCachedJWTAuthenticator(coreOSCtx, client, obj.Spec.DeepCopy(), jwksURL, conditions, tlsOk && issuerOk && providerErr == nil && jwksErr == nil)
errs = append(errs, err)
if !hadErrorCondition(conditions) {
c.cache.Store(cacheKey, cachedAuthenticator) // the in-memory cache must need the extra functionality
c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer).Info("added new jwt authenticator")
}
c.cache.Store(cacheKey, jwtAuthenticator)
c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer).Info("added new jwt authenticator")
return nil
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
// 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
// and signal the controller to retry the sync loop. These may be corrected by machines.
return errorsutil.NewAggregate(errs)
}
func (c *controller) extractValueAsJWTAuthenticator(value authncache.Value) *jwtAuthenticator {
jwtAuthenticator, ok := value.(*jwtAuthenticator)
func (c *jwtCacheFillerController) extractValueAsJWTAuthenticator(value authncache.Value) *cachedJWTAuthenticator {
jwtAuthenticator, ok := value.(*cachedJWTAuthenticator)
if !ok {
actualType := "<nil>"
if t := reflect.TypeOf(value); t != nil {
@@ -150,11 +219,55 @@ func (c *controller) extractValueAsJWTAuthenticator(value authncache.Value) *jwt
return jwtAuthenticator
}
// newJWTAuthenticator creates a jwt authenticator from the provided spec.
func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthenticator, error) {
rootCAs, _, err := pinnipedauthenticator.CABundle(spec.TLS)
func (c *jwtCacheFillerController) updateStatus(
ctx context.Context,
original *auth1alpha1.JWTAuthenticator,
conditions []*metav1.Condition,
) error {
updated := original.DeepCopy()
if 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{})
if err != nil {
return nil, fmt.Errorf("invalid TLS configuration: %w", err)
c.log.Info(fmt.Sprintf("ERROR: %v", err))
}
return err
}
// newCachedJWTAuthenticator creates a jwt authenticator from the provided spec.
func (c *jwtCacheFillerController) newCachedJWTAuthenticator(ctx context.Context, client *http.Client, spec *auth1alpha1.JWTAuthenticatorSpec, jwksURL string, 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
@@ -166,33 +279,6 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica
groupsClaim = defaultGroupsClaim
}
// copied from Kube OIDC code
issuerURL, err := url.Parse(spec.Issuer)
if err != nil {
return nil, err
}
if issuerURL.Scheme != "https" {
return nil, fmt.Errorf("issuer (%q) has invalid scheme (%q), require 'https'", spec.Issuer, issuerURL.Scheme)
}
client := phttp.Default(rootCAs)
client.Timeout = 30 * time.Second // copied from Kube OIDC code
ctx := coreosoidc.ClientContext(context.Background(), client)
provider, err := coreosoidc.NewProvider(ctx, spec.Issuer)
if err != nil {
return nil, fmt.Errorf("could not initialize provider: %w", err)
}
providerJSON := &struct {
JWKSURL string `json:"jwks_uri"`
}{}
if err := provider.Claims(providerJSON); err != nil {
return nil, fmt.Errorf("could not get provider jwks_uri: %w", err) // should be impossible because coreosoidc.NewProvider validates this
}
if len(providerJSON.JWKSURL) == 0 {
return nil, fmt.Errorf("issuer %q does not have jwks_uri set", spec.Issuer)
}
oidcAuthenticator, err := oidc.New(oidc.Options{
JWTAuthenticator: apiserver.JWTAuthenticator{
Issuer: apiserver.Issuer{
@@ -210,16 +296,196 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica
},
},
},
KeySet: coreosoidc.NewRemoteKeySet(ctx, providerJSON.JWKSURL),
KeySet: coreosoidc.NewRemoteKeySet(ctx, jwksURL),
SupportedSigningAlgs: defaultSupportedSigningAlgos(),
Client: client,
})
if err != nil {
return nil, fmt.Errorf("could not initialize authenticator: %w", err)
// 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)
}
return &jwtAuthenticator{
msg := "authenticator initialized"
conditions = append(conditions, &metav1.Condition{
Type: typeAuthenticatorValid,
Status: metav1.ConditionTrue,
Reason: reasonSuccess,
Message: msg,
})
return &cachedJWTAuthenticator{
tokenAuthenticatorCloser: oidcAuthenticator,
spec: spec,
}, nil
}, conditions, nil
}
func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context, spec *auth1alpha1.JWTAuthenticatorSpec, conditions []*metav1.Condition, prereqOk bool) (*providerJSON, *coreosoidc.Provider, []*metav1.Condition, error) {
if !prereqOk {
conditions = append(conditions, &metav1.Condition{
Type: typeDiscoveryValid,
Status: metav1.ConditionUnknown,
Reason: reasonUnableToValidate,
Message: msgUnableToValidate,
})
return nil, nil, conditions, nil
}
provider, err := coreosoidc.NewProvider(ctx, spec.Issuer)
pJSON := &providerJSON{}
if err != nil {
errText := "could not perform oidc discovery on provider issuer"
msg := fmt.Sprintf("%s: %s", errText, err.Error())
conditions = append(conditions, &metav1.Condition{
Type: typeDiscoveryValid,
Status: metav1.ConditionFalse,
Reason: reasonInvalidDiscoveryProbe,
Message: msg,
})
// resync err, may be machine or other types of non-config error
return nil, nil, conditions, fmt.Errorf("%s: %w", errText, err)
}
msg := "discovery performed successfully"
conditions = append(conditions, &metav1.Condition{
Type: typeDiscoveryValid,
Status: metav1.ConditionTrue,
Reason: reasonSuccess,
Message: msg,
})
return pJSON, provider, conditions, nil
}
func (c *jwtCacheFillerController) validateProviderJWKSURL(provider *coreosoidc.Provider, pJSON *providerJSON, conditions []*metav1.Condition, prereqOk bool) (string, []*metav1.Condition, error) {
if provider == nil || pJSON == nil || !prereqOk {
conditions = append(conditions, &metav1.Condition{
Type: typeJWKSURLValid,
Status: metav1.ConditionUnknown,
Reason: reasonUnableToValidate,
Message: msgUnableToValidate,
})
return "", conditions, nil
}
// should be impossible because coreosoidc.NewProvider validates this, thus we can't write a test to get in this state (currently)
if err := provider.Claims(pJSON); err != nil {
errText := "could not get provider jwks_uri"
msg := fmt.Sprintf("%s: %s", errText, err.Error())
conditions = append(conditions, &metav1.Condition{
Type: typeJWKSURLValid,
Status: metav1.ConditionFalse,
Reason: reasonInvalidProviderJWKSURL,
Message: msg,
})
// resync err, the user may not be able to fix this via config, it may be the server may be misbehaving.
return pJSON.JWKSURL, conditions, fmt.Errorf("%s: %w", errText, err)
}
parsedJWKSURL, err := url.Parse(pJSON.JWKSURL)
if err != nil {
errText := "could not parse provider jwks_uri"
msg := fmt.Sprintf("%s: %s", errText, err.Error())
conditions = append(conditions, &metav1.Condition{
Type: typeJWKSURLValid,
Status: metav1.ConditionFalse,
Reason: reasonInvalidProviderJWKSURL,
Message: msg,
})
// resync err, the user may not be able to fix this via config, it may be the server may be misbehaving.
return pJSON.JWKSURL, conditions, fmt.Errorf("%s: %w", errText, err)
}
// spec asserts https is required. https://openid.net/specs/openid-connect-discovery-1_0.html
if parsedJWKSURL.Scheme != "https" {
msg := fmt.Sprintf("jwks_uri %s has invalid scheme, require 'https'", pJSON.JWKSURL)
conditions = append(conditions, &metav1.Condition{
Type: typeJWKSURLValid,
Status: metav1.ConditionFalse,
Reason: reasonInvalidProviderJWKSURLScheme,
Message: msg,
})
return pJSON.JWKSURL, conditions, fmt.Errorf("%s", msg)
}
msg := fmt.Sprintf("jwks_uri (%s) is a valid URL", parsedJWKSURL)
conditions = append(conditions, &metav1.Condition{
Type: typeJWKSURLValid,
Status: metav1.ConditionTrue,
Reason: reasonSuccess,
Message: msg,
})
return pJSON.JWKSURL, conditions, nil
}
func (c *jwtCacheFillerController) validateTLS(spec *auth1alpha1.JWTAuthenticatorSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
rootCAs, _, err := pinnipedauthenticator.CABundle(spec.TLS)
if err != nil {
msg := fmt.Sprintf("%s: %s", "invalid TLS configuration", err.Error())
conditions = append(conditions, &metav1.Condition{
Type: typeTLSConfigurationValid,
Status: metav1.ConditionFalse,
Reason: reasonInvalidTLSConfiguration,
Message: msg,
})
return rootCAs, conditions, false
}
msg := "valid TLS configuration"
conditions = append(conditions, &metav1.Condition{
Type: typeTLSConfigurationValid,
Status: metav1.ConditionTrue,
Reason: reasonSuccess,
Message: msg,
})
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
}
msg := fmt.Sprintf("spec.issuer (%s) is a valid URL", issuer)
conditions = append(conditions, &metav1.Condition{
Type: typeIssuerURLValid,
Status: metav1.ConditionTrue,
Reason: reasonSuccess,
Message: msg,
})
return issuerURL, conditions, true
}
func hadErrorCondition(conditions []*metav1.Condition) bool {
for _, c := range conditions {
if c.Status != metav1.ConditionTrue {
return true
}
}
return false
}
@@ -4,6 +4,7 @@
package jwtcachefiller
import (
"bytes"
"context"
"crypto/ecdsa"
"crypto/elliptic"
@@ -15,6 +16,8 @@ import (
"encoding/pem"
"fmt"
"net/http"
"net/url"
"sort"
"strings"
"testing"
"time"
@@ -28,6 +31,7 @@ import (
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
clocktesting "k8s.io/utils/clock/testing"
auth1alpha1 "go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
pinnipedfake "go.pinniped.dev/generated/latest/client/concierge/clientset/versioned/fake"
@@ -36,8 +40,8 @@ import (
"go.pinniped.dev/internal/controllerlib"
"go.pinniped.dev/internal/crypto/ptls"
"go.pinniped.dev/internal/mocks/mocktokenauthenticatorcloser"
"go.pinniped.dev/internal/plog"
"go.pinniped.dev/internal/testutil"
"go.pinniped.dev/internal/testutil/testlogger"
"go.pinniped.dev/internal/testutil/tlsserver"
)
@@ -61,18 +65,18 @@ func TestController(t *testing.T) {
customGroupsClaim := "my-custom-groups-claim"
distributedGroups := []string{"some-distributed-group-1", "some-distributed-group-2"}
mux := http.NewServeMux()
server := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
goodMux := http.NewServeMux()
goodOIDCIssuerServer := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tlsserver.AssertTLS(t, r, ptls.Default)
mux.ServeHTTP(w, r)
goodMux.ServeHTTP(w, r)
}), tlsserver.RecordTLSHello)
mux.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
goodMux.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, server.URL, server.URL+"/jwks.json")
_, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, goodOIDCIssuerServer.URL, goodOIDCIssuerServer.URL+"/jwks.json")
require.NoError(t, err)
}))
mux.Handle("/jwks.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
goodMux.Handle("/jwks.json", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ecJWK := jose.JSONWebKey{
Key: goodECSigningKey,
KeyID: goodECSigningKeyID,
@@ -90,19 +94,20 @@ func TestController(t *testing.T) {
}
require.NoError(t, json.NewEncoder(w).Encode(jwks))
}))
// Claims without the subject, to be used distributed claims tests.
// OIDC 1.0 section 5.6.2:
// A sub (subject) Claim SHOULD NOT be returned from the Claims Provider unless its value
// is an identifier for the End-User at the Claims Provider (and not for the OpenID Provider or another party);
// this typically means that a sub Claim SHOULD NOT be provided.
claimsWithoutSubject := jwt.Claims{
Issuer: server.URL,
Issuer: goodOIDCIssuerServer.URL,
Audience: []string{goodAudience},
Expiry: jwt.NewNumericDate(time.Now().Add(time.Hour)),
NotBefore: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now().Add(-time.Hour)),
}
mux.Handle("/claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
goodMux.Handle("/claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Unfortunately we have to set this up pretty early in the test because we can't redeclare
// mux.Handle. This means that we can't return a different groups claim per test; we have to
// return both and predecide which groups are returned.
@@ -123,7 +128,7 @@ func TestController(t *testing.T) {
_, err = w.Write([]byte(distributedClaimsJwt))
require.NoError(t, err)
}))
mux.Handle("/wrong_claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
goodMux.Handle("/wrong_claim_source", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Unfortunately we have to set this up pretty early in the test because we can't redeclare
// mux.Handle. This means that we can't return a different groups claim per test; we have to
// return both and predecide which groups are returned.
@@ -144,17 +149,48 @@ func TestController(t *testing.T) {
require.NoError(t, err)
}))
goodIssuer := server.URL
badMuxInvalidJWKSURI := http.NewServeMux()
badOIDCIssuerServerInvalidJWKSURI := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tlsserver.AssertTLS(t, r, ptls.Default)
badMuxInvalidJWKSURI.ServeHTTP(w, r)
}), tlsserver.RecordTLSHello)
badMuxInvalidJWKSURI.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURI.URL, "https://.café .com/café/café/café/coffee/jwks.json")
require.NoError(t, err)
}))
badMuxInvalidJWKSURIScheme := http.NewServeMux()
badOIDCIssuerServerInvalidJWKSURIScheme := tlsserver.TLSTestServer(t, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
tlsserver.AssertTLS(t, r, ptls.Default)
badMuxInvalidJWKSURIScheme.ServeHTTP(w, r)
}), tlsserver.RecordTLSHello)
badMuxInvalidJWKSURIScheme.Handle("/.well-known/openid-configuration", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, err := fmt.Fprintf(w, `{"issuer": "%s", "jwks_uri": "%s"}`, badOIDCIssuerServerInvalidJWKSURIScheme.URL, "http://.café.com/café/café/café/coffee/jwks.json")
require.NoError(t, err)
}))
goodIssuer := goodOIDCIssuerServer.URL
badIssuerInvalidJWKSURI := badOIDCIssuerServerInvalidJWKSURI.URL
badIssuerInvalidJWKSURIScheme := badOIDCIssuerServerInvalidJWKSURIScheme.URL
someOtherIssuer := "https://some-other-issuer.com" // placeholder only for tests that don't get far enough to make requests
someOtherLocalhostIssuer := "https://127.0.0.1:443/some-other-issuer"
someOtherLocalhostIssuerParsed, err := url.Parse(someOtherLocalhostIssuer)
require.NoError(t, err)
frozenNow := time.Date(2020, time.September, 23, 7, 42, 0, 0, time.Local)
frozenMetav1Now := metav1.NewTime(frozenNow)
someJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(server.TLS),
TLS: tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
}
someJWTAuthenticatorSpecWithUsernameClaim := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(server.TLS),
TLS: tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
Claims: auth1alpha1.JWTTokenClaims{
Username: "my-custom-username-claim",
},
@@ -162,48 +198,338 @@ func TestController(t *testing.T) {
someJWTAuthenticatorSpecWithGroupsClaim := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(server.TLS),
TLS: tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
Claims: auth1alpha1.JWTTokenClaims{
Groups: customGroupsClaim,
},
}
otherJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: "https://some-other-issuer.com",
Issuer: someOtherIssuer,
Audience: goodAudience,
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"},
// Some random generated cert
// Issuer: C=US, O=Pivotal
// No SAN provided
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"},
}
missingTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: goodIssuer,
Audience: goodAudience,
}
invalidTLSJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: "https://some-other-issuer.com",
Issuer: someOtherIssuer,
Audience: goodAudience,
TLS: &auth1alpha1.TLSSpec{CertificateAuthorityData: "invalid base64-encoded data"},
}
invalidIssuerJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: "https://.café .com/café/café/café/coffee",
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
}
invalidIssuerSchemeJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: "http://.café.com/café/café/café/coffee",
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(goodOIDCIssuerServer.TLS),
}
validIssuerURLButDoesNotExistJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: someOtherLocalhostIssuer,
Audience: goodAudience,
}
badIssuerJWKSURIJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: badIssuerInvalidJWKSURI,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURI.TLS),
}
badIssuerJWKSURISchemeJWTAuthenticatorSpec := &auth1alpha1.JWTAuthenticatorSpec{
Issuer: badIssuerInvalidJWKSURIScheme,
Audience: goodAudience,
TLS: tlsSpecFromTLSConfig(badOIDCIssuerServerInvalidJWKSURIScheme.TLS),
}
happyReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "Ready",
Status: "True",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "Success",
Message: "the JWTAuthenticator is ready",
}
}
sadReadyCondition := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "Ready",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "NotReady",
Message: "the JWTAuthenticator is not ready: see other conditions for details",
}
}
happyTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "TLSConfigurationValid",
Status: "True",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "Success",
Message: "valid TLS configuration",
}
}
sadTLSConfigurationValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "TLSConfigurationValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidTLSConfiguration",
Message: "invalid TLS configuration: illegal base64 data at input byte 7",
}
}
happyIssuerURLValid := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "IssuerURLValid",
Status: "True",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "Success",
Message: fmt.Sprintf("spec.issuer (%s) is a valid URL", issuer),
}
}
sadIssuerURLValidInvalid := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "IssuerURLValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidIssuerURL",
Message: fmt.Sprintf(`spec.issuer URL is invalid: parse "%s": invalid character " " in host name`, issuer),
}
}
sadIssuerURLValidInvalidScheme := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "IssuerURLValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidIssuerURLScheme",
Message: fmt.Sprintf("spec.issuer %s has invalid scheme, require 'https'", issuer),
}
}
happyAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "AuthenticatorValid",
Status: "True",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "Success",
Message: "authenticator initialized",
}
}
unknownAuthenticatorValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "AuthenticatorValid",
Status: "Unknown",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "UnableToValidate",
Message: "unable to validate; other issues present",
}
}
// NOTE: we can't reach this error the way our code is written.
// We check many things and fail early, resulting in an "Unknown" Authenticator status.
// The only possible fail for the Authenticator itself would require us to allow more
// configuration for users. See comments in the jwtauthenticator.go newCachedJWTAuthenticator()
// func itself for more information.
// sadAuthenticatorValid := func() metav1.Condition {}
happyDiscoveryURLValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "DiscoveryURLValid",
Status: "True",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "Success",
Message: "discovery performed successfully",
}
}
unknownDiscoveryURLValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "DiscoveryURLValid",
Status: "Unknown",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "UnableToValidate",
Message: "unable to validate; other issues present",
}
}
sadDiscoveryURLValidx509 := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "DiscoveryURLValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidDiscoveryProbe",
Message: fmt.Sprintf(`could not perform oidc discovery on provider issuer: Get "%s/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`, issuer),
}
}
sadDiscoveryURLValidConnectionRefused := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
parsed, err := url.Parse(issuer)
require.NoError(t, err)
return metav1.Condition{
Type: "DiscoveryURLValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidDiscoveryProbe",
Message: fmt.Sprintf(`could not perform oidc discovery on provider issuer: Get "%s/.well-known/openid-configuration": dial tcp %s: connect: connection refused`, issuer, parsed.Host),
}
}
happyJWKSURLValid := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
parsed, err := url.Parse(issuer)
require.NoError(t, err)
return metav1.Condition{
Type: "JWKSURLValid",
Status: "True",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "Success",
Message: fmt.Sprintf("jwks_uri (https://%s/jwks.json) is a valid URL", parsed.Host),
}
}
unknownJWKSURLValid := func(time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "JWKSURLValid",
Status: "Unknown",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "UnableToValidate",
Message: "unable to validate; other issues present",
}
}
sadJWKSURLValidParseURI := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "JWKSURLValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidProviderJWKSURL",
Message: `could not parse provider jwks_uri: parse "` + issuer + `": invalid character " " in host name`,
}
}
sadJWKSURLValidScheme := func(issuer string, time metav1.Time, observedGeneration int64) metav1.Condition {
return metav1.Condition{
Type: "JWKSURLValid",
Status: "False",
ObservedGeneration: observedGeneration,
LastTransitionTime: time,
Reason: "InvalidProviderJWKSURLScheme",
Message: `jwks_uri ` + issuer + ` has invalid scheme, require 'https'`,
}
}
// happyJWKSURLValidResponse := func(time metav1.Time, observedGeneration int64) metav1.Condition {
// return metav1.Condition{
// Type: "JWKSURLValidResponse",
// Status: "True",
// ObservedGeneration: observedGeneration,
// LastTransitionTime: time,
// Reason: "Success",
// Message: "jwks_uri responds to requests",
// }
// }
// sadJWKSURLValidResponse := func() metav1.Condition {}
// TODO: extract to a helper since copied from FederationDomain
sortConditionsByType := func(c []metav1.Condition) []metav1.Condition {
cp := make([]metav1.Condition, len(c))
copy(cp, c)
sort.SliceStable(cp, func(i, j int) bool {
return cp[i].Type < cp[j].Type
})
return cp
}
replaceConditions := func(conditions []metav1.Condition, sadConditions []metav1.Condition) []metav1.Condition {
for _, sadReplaceCondition := range sadConditions {
for origIndex, origCondition := range conditions {
if origCondition.Type == sadReplaceCondition.Type {
conditions[origIndex] = sadReplaceCondition
break
}
}
}
return conditions
}
// condition collections
allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []metav1.Condition {
return sortConditionsByType([]metav1.Condition{
happyAuthenticatorValid(time, observedGeneration),
happyDiscoveryURLValid(time, observedGeneration),
happyIssuerURLValid(issuer, time, observedGeneration),
happyJWKSURLValid(issuer, time, observedGeneration),
// happyJWKSURLValidResponse(time, observedGeneration),
happyReadyCondition(time, observedGeneration),
happyTLSConfigurationValid(time, observedGeneration),
})
}
// TODO(BEN): might need a full JWTAuthenticatorStatus with Phase, otherwise remove this.
// someJWTAuthenticatorStatus := auth1alpha1.JWTAuthenticatorStatus{
// Conditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
// Phase: auth1alpha1.JWTAuthenticatorPhaseReady,
// }
tests := []struct {
name string
cache func(*testing.T, *authncache.Cache, bool)
syncKey controllerlib.Key
jwtAuthenticators []runtime.Object
wantClose bool
wantErr testutil.RequireErrorStringFunc
wantLogs []string
name string
cache func(*testing.T, *authncache.Cache, bool)
// syncKey is used behind the scenes to simulate the pre-existence of jwtAuthenticator(s).
// if not provided, then the test assumes the jwtAuthenticator(s) are new Authenticator(s).
syncKey controllerlib.Key
jwtAuthenticators []runtime.Object
wantClose bool
// Only errors that are non-config related errors are returned from the sync loop.
// Errors such as url.Parse of the issuer are not returned as they imply a user error.
// Since these errors trigger a resync, we are careful only to return an error when
// 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
wantCacheEntries int
wantUsernameClaim string
wantGroupsClaim string
runTestsOnResultingAuthenticator bool
}{
{
name: "not found",
name: "404: jwt authenticator not found will abort sync loop and not attempt to write status",
syncKey: controllerlib.Key{Name: "test-name"},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="Sync() found that the JWTAuthenticator does not exist yet or was deleted"`,
// timestamp and caller will not be empty, but we aren't concerned with the values. only testing the interesting bits.
wantLogs: []map[string]any{
{
"level": "info",
"timestamp": frozenNow.String(),
"logger": "jwtcachefiller-controller",
"message": "Sync() found that the JWTAuthenticator does not exist yet or was deleted",
},
},
},
// {
// TODO:
// lines ~ 146
// should we test this? can we simulate a server error without rewriting the whole test?
// prob with a if(tt.serverErr) then controller := New( withBadClients ) or something....
// name: "non-404 `failed to get JWTAuthenticator` for other API server reasons",
// },
{
name: "valid jwt authenticator with CA",
name: "valid jwt authenticator with CA will complete sync loop successfully with success conditions and ready phase",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
@@ -213,14 +539,22 @@ func TestController(t *testing.T) {
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantLogs: []map[string]any{{
"level": "info",
"logger": "jwtcachefiller-controller",
"message": "added new jwt authenticator",
"issuer": goodIssuer,
"jwtAuthenticator": map[string]interface{}{
"name": "test-name",
},
}},
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
wantStatusPhase: "Ready",
wantCacheEntries: 1,
runTestsOnResultingAuthenticator: true,
},
{
name: "valid jwt authenticator with custom username claim",
name: "valid jwt authenticator with custom username claim will complete sync loop successfully with success conditions and ready phase",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
@@ -230,15 +564,23 @@ func TestController(t *testing.T) {
Spec: *someJWTAuthenticatorSpecWithUsernameClaim,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantLogs: []map[string]any{{
"level": "info",
"logger": "jwtcachefiller-controller",
"message": "added new jwt authenticator",
"issuer": goodIssuer,
"jwtAuthenticator": map[string]interface{}{
"name": "test-name",
},
}},
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
wantStatusPhase: "Ready",
wantCacheEntries: 1,
wantUsernameClaim: someJWTAuthenticatorSpecWithUsernameClaim.Claims.Username,
runTestsOnResultingAuthenticator: true,
},
{
name: "valid jwt authenticator with custom groups claim",
name: "valid jwt authenticator with custom groups claim will complete sync loop successfully with success conditions and ready phase",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
@@ -248,15 +590,23 @@ func TestController(t *testing.T) {
Spec: *someJWTAuthenticatorSpecWithGroupsClaim,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantLogs: []map[string]any{{
"level": "info",
"logger": "jwtcachefiller-controller",
"message": "added new jwt authenticator",
"issuer": goodIssuer,
"jwtAuthenticator": map[string]interface{}{
"name": "test-name",
},
}},
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
wantStatusPhase: "Ready",
wantCacheEntries: 1,
wantGroupsClaim: someJWTAuthenticatorSpecWithGroupsClaim.Claims.Groups,
runTestsOnResultingAuthenticator: true,
},
{
name: "updating jwt authenticator with new fields closes previous instance",
name: "updating jwt authenticator with new fields closes previous instance and will complete sync loop successfully with success conditions and ready phase",
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
cache.Store(
authncache.Key{
@@ -277,14 +627,22 @@ func TestController(t *testing.T) {
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantLogs: []map[string]any{{
"level": "info",
"logger": "jwtcachefiller-controller",
"message": "added new jwt authenticator",
"issuer": goodIssuer,
"jwtAuthenticator": map[string]interface{}{
"name": "test-name",
},
}},
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
wantStatusPhase: "Ready",
wantCacheEntries: 1,
runTestsOnResultingAuthenticator: true,
},
{
name: "updating jwt authenticator with the same value does nothing",
name: "updating jwt authenticator with the same value does not trigger sync loop and will not update conditions or phase",
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
cache.Store(
authncache.Key{
@@ -305,14 +663,24 @@ func TestController(t *testing.T) {
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="actual jwt authenticator and desired jwt authenticator are the same" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantLogs: []map[string]any{{
"level": "info",
"logger": "jwtcachefiller-controller",
"message": "actual jwt authenticator and desired jwt authenticator are the same",
"issuer": goodIssuer,
"jwtAuthenticator": map[string]interface{}{
"name": "test-name",
},
}},
// We do not cache status, so no status or phase is expected on an updated jwt authenticator
// if the cached spec matches, the sync loop will exit before updating status
// wantStatusConditions: nil
// wantStatusPhase: "Ready",
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",
name: "updating jwt authenticator when cache value is wrong type will complete sync loop successfully with success conditions and ready phase",
cache: func(t *testing.T, cache *authncache.Cache, wantClose bool) {
cache.Store(
authncache.Key{
@@ -332,15 +700,27 @@ func TestController(t *testing.T) {
Spec: *someJWTAuthenticatorSpec,
},
},
wantLogs: []string{
`jwtcachefiller-controller "level"=0 "msg"="wrong JWT authenticator type in cache" "actualType"="struct { authenticator.Token }"`,
`jwtcachefiller-controller "level"=0 "msg"="added new jwt authenticator" "issuer"="` + goodIssuer + `" "jwtAuthenticator"={"name":"test-name"}`,
},
wantLogs: []map[string]any{{
"level": "info",
"logger": "jwtcachefiller-controller",
"message": "wrong JWT authenticator type in cache",
"actualType": "struct { authenticator.Token }",
}, {
"level": "info",
"logger": "jwtcachefiller-controller",
"message": "added new jwt authenticator",
"issuer": goodIssuer,
"jwtAuthenticator": map[string]interface{}{
"name": "test-name",
},
}},
wantStatusConditions: allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
wantStatusPhase: "Ready",
wantCacheEntries: 1,
runTestsOnResultingAuthenticator: true,
},
{
name: "valid jwt authenticator without CA",
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",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
@@ -350,10 +730,23 @@ func TestController(t *testing.T) {
Spec: *missingTLSJWTAuthenticatorSpec,
},
},
wantErr: testutil.WantX509UntrustedCertErrorString(`failed to build jwt authenticator: could not initialize provider: Get "`+goodIssuer+`/.well-known/openid-configuration": %s`, "Acme Co"),
// no explicit logs, this is an issue of config, the user must provide TLS config for the
// custom cert provided for this server.
wantStatusConditions: replaceConditions(
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
[]metav1.Condition{
sadReadyCondition(frozenMetav1Now, 0),
sadDiscoveryURLValidx509(goodIssuer, frozenMetav1Now, 0),
unknownAuthenticatorValid(frozenMetav1Now, 0),
unknownJWKSURLValid(frozenMetav1Now, 0),
},
),
wantStatusPhase: "Error",
wantSyncLoopErr: testutil.WantX509UntrustedCertErrorString(`could not perform oidc discovery on provider issuer: Get "`+goodIssuer+`/.well-known/openid-configuration": %s`, "Acme Co"),
wantCacheEntries: 0,
},
{
name: "invalid jwt authenticator CA",
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",
syncKey: controllerlib.Key{Name: "test-name"},
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
@@ -363,8 +756,143 @@ func TestController(t *testing.T) {
Spec: *invalidTLSJWTAuthenticatorSpec,
},
},
wantErr: testutil.WantExactErrorString("failed to build jwt authenticator: invalid TLS configuration: illegal base64 data at input byte 7"),
// no explicit logs, this is an issue of config, the user must provide TLS config that
// isn't incorrect due to mistyping or other issues
wantStatusConditions: replaceConditions(
allHappyConditionsSuccess(someOtherIssuer, frozenMetav1Now, 0),
[]metav1.Condition{
sadReadyCondition(frozenMetav1Now, 0),
sadTLSConfigurationValid(frozenMetav1Now, 0),
unknownDiscoveryURLValid(frozenMetav1Now, 0),
unknownAuthenticatorValid(frozenMetav1Now, 0),
unknownJWKSURLValid(frozenMetav1Now, 0),
},
),
wantStatusPhase: "Error",
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",
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *invalidIssuerJWTAuthenticatorSpec,
},
},
syncKey: controllerlib.Key{Name: "test-name"},
wantStatusConditions: replaceConditions(
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
[]metav1.Condition{
sadReadyCondition(frozenMetav1Now, 0),
sadIssuerURLValidInvalid("https://.café .com/café/café/café/coffee", frozenMetav1Now, 0),
unknownDiscoveryURLValid(frozenMetav1Now, 0),
unknownAuthenticatorValid(frozenMetav1Now, 0),
unknownJWKSURLValid(frozenMetav1Now, 0),
},
),
wantStatusPhase: "Error",
}, {
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",
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *invalidIssuerSchemeJWTAuthenticatorSpec,
},
},
syncKey: controllerlib.Key{Name: "test-name"},
wantStatusConditions: replaceConditions(
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
[]metav1.Condition{
sadReadyCondition(frozenMetav1Now, 0),
sadIssuerURLValidInvalidScheme("http://.café.com/café/café/café/coffee", frozenMetav1Now, 0),
unknownDiscoveryURLValid(frozenMetav1Now, 0),
unknownAuthenticatorValid(frozenMetav1Now, 0),
unknownJWKSURLValid(frozenMetav1Now, 0),
},
),
wantStatusPhase: "Error",
}, {
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",
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *validIssuerURLButDoesNotExistJWTAuthenticatorSpec,
},
},
syncKey: controllerlib.Key{Name: "test-name"},
wantStatusConditions: replaceConditions(
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
[]metav1.Condition{
happyIssuerURLValid(someOtherLocalhostIssuer, frozenMetav1Now, 0),
sadReadyCondition(frozenMetav1Now, 0),
sadDiscoveryURLValidConnectionRefused(someOtherLocalhostIssuer, frozenMetav1Now, 0),
unknownAuthenticatorValid(frozenMetav1Now, 0),
unknownJWKSURLValid(frozenMetav1Now, 0),
},
),
wantStatusPhase: "Error",
wantSyncLoopErr: testutil.WantExactErrorString(`could not perform oidc discovery on provider issuer: Get "` + someOtherLocalhostIssuer + `/.well-known/openid-configuration": dial tcp ` + someOtherLocalhostIssuerParsed.Host + `: connect: connection refused`),
},
// cannot be tested currently the way the coreos lib works.
// the constructor requires an issuer in the payload and validates the issuer matches the actual issuer,
// 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",
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *badIssuerJWKSURIJWTAuthenticatorSpec,
},
},
syncKey: controllerlib.Key{Name: "test-name"},
wantStatusConditions: replaceConditions(
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
[]metav1.Condition{
happyIssuerURLValid(badIssuerInvalidJWKSURI, frozenMetav1Now, 0),
sadReadyCondition(frozenMetav1Now, 0),
unknownAuthenticatorValid(frozenMetav1Now, 0),
sadJWKSURLValidParseURI("https://.café .com/café/café/café/coffee/jwks.json", frozenMetav1Now, 0),
},
),
wantStatusPhase: "Error",
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",
jwtAuthenticators: []runtime.Object{
&auth1alpha1.JWTAuthenticator{
ObjectMeta: metav1.ObjectMeta{
Name: "test-name",
},
Spec: *badIssuerJWKSURISchemeJWTAuthenticatorSpec,
},
},
syncKey: controllerlib.Key{Name: "test-name"},
wantStatusConditions: replaceConditions(
allHappyConditionsSuccess(goodIssuer, frozenMetav1Now, 0),
[]metav1.Condition{
happyIssuerURLValid(badIssuerInvalidJWKSURIScheme, frozenMetav1Now, 0),
sadReadyCondition(frozenMetav1Now, 0),
unknownAuthenticatorValid(frozenMetav1Now, 0),
sadJWKSURLValidScheme("http://.café.com/café/café/café/coffee/jwks.json", frozenMetav1Now, 0),
},
),
wantStatusPhase: "Error",
wantSyncLoopErr: testutil.WantExactErrorString("jwks_uri http://.café.com/café/café/café/coffee/jwks.json has invalid scheme, require 'https'"),
},
// cannot be tested the way we are invoking oidc.New as we don't provide enough configuration
// knobs to actually invoke the code in a broken way. We always give a good client, good keys, and
// good signing algos. In the future if we allow any of these to be configured we may have opportunity
// to test for errors.
// {name: "newCachedJWTAuthenticator: could not initialize oidc authenticator..." },
// TODO: are happy, unknown, sad covered in all the above? or do we need more?
}
for _, tt := range tests {
@@ -372,32 +900,75 @@ func TestController(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
fakeClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
informers := pinnipedinformers.NewSharedInformerFactory(fakeClient, 0)
pinnipedAPIClient := pinnipedfake.NewSimpleClientset(tt.jwtAuthenticators...)
pinnipedInformers := pinnipedinformers.NewSharedInformerFactory(pinnipedAPIClient, 0)
cache := authncache.New()
testLog := testlogger.NewLegacy(t) //nolint:staticcheck // old test with lots of log statements
var log bytes.Buffer
logger := plog.TestLogger(t, &log)
if tt.cache != nil {
tt.cache(t, cache, tt.wantClose)
}
controller := New(cache, informers.Authentication().V1alpha1().JWTAuthenticators(), testLog.Logger)
controller := New(
cache,
pinnipedAPIClient,
pinnipedInformers.Authentication().V1alpha1().JWTAuthenticators(),
clocktesting.NewFakeClock(frozenNow),
logger)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
informers.Start(ctx.Done())
pinnipedInformers.Start(ctx.Done())
controllerlib.TestRunSynchronously(t, controller)
syncCtx := controllerlib.Context{Context: ctx, Key: tt.syncKey}
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantErr != nil {
testutil.RequireErrorStringFromErr(t, err, tt.wantErr)
if err := controllerlib.TestSync(t, controller, syncCtx); tt.wantSyncLoopErr != nil {
testutil.RequireErrorStringFromErr(t, err, tt.wantSyncLoopErr)
} else {
require.NoError(t, err)
}
require.Equal(t, tt.wantLogs, testLog.Lines())
require.Equal(t, tt.wantCacheEntries, len(cache.Keys()))
actualLogLines := logLines(log.String())
require.Equal(t, len(actualLogLines), len(tt.wantLogs), "log line count should be correct")
for logLineNum, logLine := range actualLogLines {
require.NotNil(t, tt.wantLogs[logLineNum], "expected log line should never be empty")
var lineStruct map[string]any
err := json.Unmarshal([]byte(logLine), &lineStruct)
if err != nil {
t.Error(err)
}
require.Equal(t, lineStruct["level"], tt.wantLogs[logLineNum]["level"], fmt.Sprintf("log line (%d) log level should be correct", logLineNum))
// require.Equal(t, lineStruct["timestamp"], tt.wantLogs[logLineNum]["timestamp"], fmt.Sprintf("log line (%d) timestamp should be correct", logLineNum))
require.Equal(t, lineStruct["logger"], tt.wantLogs[logLineNum]["logger"], fmt.Sprintf("log line (%d) logger should be correct", logLineNum))
require.NotEmpty(t, lineStruct["caller"], fmt.Sprintf("log line (%d) caller should not be empty", logLineNum))
require.Equal(t, lineStruct["message"], tt.wantLogs[logLineNum]["message"], fmt.Sprintf("log line (%d) message should be correct", logLineNum))
if lineStruct["issuer"] != nil {
require.Equal(t, lineStruct["issuer"], tt.wantLogs[logLineNum]["issuer"], fmt.Sprintf("log line (%d) issuer should be correct", logLineNum))
}
if lineStruct["jwtAuthenticator"] != nil {
require.Equal(t, lineStruct["jwtAuthenticator"], tt.wantLogs[logLineNum]["jwtAuthenticator"], fmt.Sprintf("log line (%d) jwtAuthenticator should be correct", logLineNum))
}
if lineStruct["actualType"] != nil {
require.Equal(t, lineStruct["actualType"], tt.wantLogs[logLineNum]["actualType"], fmt.Sprintf("log line (%d) actualType should be correct", logLineNum))
}
}
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, "jwt authenticator status.phase should be correct")
}
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()))
if !tt.runTestsOnResultingAuthenticator {
return // end of test unless we wanted to run tests on the resulting authenticator from the cache
@@ -413,7 +984,7 @@ func TestController(t *testing.T) {
require.NotNil(t, cachedAuthenticator)
// Schedule it to be closed at the end of the test.
t.Cleanup(cachedAuthenticator.(*jwtAuthenticator).Close)
t.Cleanup(cachedAuthenticator.(*cachedJWTAuthenticator).Close)
const (
goodSubject = "some-subject"
@@ -772,8 +1343,16 @@ func newCacheValue(t *testing.T, spec auth1alpha1.JWTAuthenticatorSpec, wantClos
}
tokenAuthenticatorCloser.EXPECT().Close().Times(wantCloses)
return &jwtAuthenticator{
return &cachedJWTAuthenticator{
tokenAuthenticatorCloser: tokenAuthenticatorCloser,
spec: &spec,
}
}
func logLines(logs string) []string {
if len(logs) == 0 {
return nil
}
return strings.Split(strings.TrimSpace(logs), "\n")
}
@@ -236,6 +236,8 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol
WithController(
webhookcachefiller.New(
c.AuthenticatorCache,
// TODO (BEN): add the client here for next story
// client.PinnipedConcierge.AuthenticationV1alpha1().WebhookAuthenticators(),
informers.pinniped.Authentication().V1alpha1().WebhookAuthenticators(),
plog.Logr(), //nolint:staticcheck // old controller with lots of log statements
),
@@ -244,8 +246,10 @@ func PrepareControllers(c *Config) (controllerinit.RunnerBuilder, error) { //nol
WithController(
jwtcachefiller.New(
c.AuthenticatorCache,
client.PinnipedConcierge,
informers.pinniped.Authentication().V1alpha1().JWTAuthenticators(),
plog.Logr(), //nolint:staticcheck // old controller with lots of log statements
clock.RealClock{},
plog.New(),
),
singletonWorker,
).