diff --git a/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl b/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl index b0bf988b3..da1cf354e 100644 --- a/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl +++ b/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go.tmpl @@ -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. diff --git a/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index a7981552c..b464cfa2b 100644 --- a/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/deploy/concierge/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/deploy/concierge/rbac.yaml b/deploy/concierge/rbac.yaml index eade914a4..ad5541261 100644 --- a/deploy/concierge/rbac.yaml +++ b/deploy/concierge/rbac.yaml @@ -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 diff --git a/generated/1.21/README.adoc b/generated/1.21/README.adoc index 2447ed582..34dca9ce2 100644 --- a/generated/1.21/README.adoc +++ b/generated/1.21/README.adoc @@ -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. |=== diff --git a/generated/1.21/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.21/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.21/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.21/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.21/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.21/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index 2f8daff74..cbfd8175f 100644 --- a/generated/1.21/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.21/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/1.22/README.adoc b/generated/1.22/README.adoc index 199aa0720..f8943df40 100644 --- a/generated/1.22/README.adoc +++ b/generated/1.22/README.adoc @@ -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. |=== diff --git a/generated/1.22/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.22/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.22/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.22/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.22/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.22/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index 2f8daff74..cbfd8175f 100644 --- a/generated/1.22/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.22/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/1.23/README.adoc b/generated/1.23/README.adoc index 6d62a3194..8b0a61615 100644 --- a/generated/1.23/README.adoc +++ b/generated/1.23/README.adoc @@ -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. |=== diff --git a/generated/1.23/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.23/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.23/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.23/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.23/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.23/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index a7981552c..b464cfa2b 100644 --- a/generated/1.23/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.23/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/1.24/README.adoc b/generated/1.24/README.adoc index f4c67a3cb..89c550839 100644 --- a/generated/1.24/README.adoc +++ b/generated/1.24/README.adoc @@ -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. |=== diff --git a/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.24/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index a7981552c..b464cfa2b 100644 --- a/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.24/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/1.25/README.adoc b/generated/1.25/README.adoc index 2afd6da76..0f44a7538 100644 --- a/generated/1.25/README.adoc +++ b/generated/1.25/README.adoc @@ -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. |=== diff --git a/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.25/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index a7981552c..b464cfa2b 100644 --- a/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.25/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/1.26/README.adoc b/generated/1.26/README.adoc index 56b12fcdc..406eac404 100644 --- a/generated/1.26/README.adoc +++ b/generated/1.26/README.adoc @@ -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. |=== diff --git a/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.26/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index a7981552c..b464cfa2b 100644 --- a/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.26/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/1.27/README.adoc b/generated/1.27/README.adoc index f10fa085a..9bafbbbff 100644 --- a/generated/1.27/README.adoc +++ b/generated/1.27/README.adoc @@ -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. |=== diff --git a/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.27/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index a7981552c..b464cfa2b 100644 --- a/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.27/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/1.28/README.adoc b/generated/1.28/README.adoc index 096594c85..0aedac991 100644 --- a/generated/1.28/README.adoc +++ b/generated/1.28/README.adoc @@ -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. |=== diff --git a/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/1.28/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml b/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml index a7981552c..b464cfa2b 100644 --- a/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml +++ b/generated/1.28/crds/authentication.concierge.pinniped.dev_jwtauthenticators.yaml @@ -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 diff --git a/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go b/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go index b0bf988b3..da1cf354e 100644 --- a/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go +++ b/generated/latest/apis/concierge/authentication/v1alpha1/types_jwtauthenticator.go @@ -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. diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go index 6b829235a..38b44fc10 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller.go @@ -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 := "" 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 } diff --git a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go index cd95bfdb4..7b0a01ffe 100644 --- a/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go +++ b/internal/controller/authenticator/jwtcachefiller/jwtcachefiller_test.go @@ -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") +} diff --git a/internal/controllermanager/prepare_controllers.go b/internal/controllermanager/prepare_controllers.go index 29acfe143..7b00d0748 100644 --- a/internal/controllermanager/prepare_controllers.go +++ b/internal/controllermanager/prepare_controllers.go @@ -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, ).