mirror of
https://github.com/vmware-tanzu/pinniped.git
synced 2026-01-08 07:11:53 +00:00
Merge pull request #1851 from vmware-tanzu/ben/status/jwt-authenticator
Improve JWTAuthenticator Status
This commit is contained in:
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -174,6 +174,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
@@ -43,6 +43,10 @@ rules:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
|
||||
resources: [ jwtauthenticators, webhookauthenticators ]
|
||||
verbs: [ get, list, watch ]
|
||||
- apiGroups:
|
||||
- #@ pinnipedDevAPIGroupWithPrefix("authentication.concierge")
|
||||
resources: [ jwtauthenticators/status, webhookauthenticators/status ]
|
||||
verbs: [ get, list, watch, update ]
|
||||
---
|
||||
kind: ClusterRoleBinding
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
|
||||
13
generated/1.21/README.adoc
generated
13
generated/1.21/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -180,6 +180,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
13
generated/1.22/README.adoc
generated
13
generated/1.22/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -180,6 +180,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
13
generated/1.23/README.adoc
generated
13
generated/1.23/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -174,6 +174,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
13
generated/1.24/README.adoc
generated
13
generated/1.24/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -174,6 +174,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
13
generated/1.25/README.adoc
generated
13
generated/1.25/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -174,6 +174,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
13
generated/1.26/README.adoc
generated
13
generated/1.26/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -174,6 +174,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
13
generated/1.27/README.adoc
generated
13
generated/1.27/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -174,6 +174,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
13
generated/1.28/README.adoc
generated
13
generated/1.28/README.adoc
generated
@@ -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.
|
||||
|===
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -174,6 +174,14 @@ spec:
|
||||
x-kubernetes-list-map-keys:
|
||||
- type
|
||||
x-kubernetes-list-type: map
|
||||
phase:
|
||||
default: Pending
|
||||
description: Phase summarizes the overall status of the JWTAuthenticator.
|
||||
enum:
|
||||
- Pending
|
||||
- Ready
|
||||
- Error
|
||||
type: string
|
||||
type: object
|
||||
required:
|
||||
- spec
|
||||
|
||||
@@ -1,10 +1,23 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package v1alpha1
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
type JWTAuthenticatorPhase string
|
||||
|
||||
const (
|
||||
// JWTAuthenticatorPhasePending is the default phase for newly-created JWTAuthenticator resources.
|
||||
JWTAuthenticatorPhasePending JWTAuthenticatorPhase = "Pending"
|
||||
|
||||
// JWTAuthenticatorPhaseReady is the phase for an JWTAuthenticator resource in a healthy state.
|
||||
JWTAuthenticatorPhaseReady JWTAuthenticatorPhase = "Ready"
|
||||
|
||||
// JWTAuthenticatorPhaseError is the phase for an JWTAuthenticator in an unhealthy state.
|
||||
JWTAuthenticatorPhaseError JWTAuthenticatorPhase = "Error"
|
||||
)
|
||||
|
||||
// Status of a JWT authenticator.
|
||||
type JWTAuthenticatorStatus struct {
|
||||
// Represents the observations of the authenticator's current state.
|
||||
@@ -13,6 +26,10 @@ type JWTAuthenticatorStatus struct {
|
||||
// +listType=map
|
||||
// +listMapKey=type
|
||||
Conditions []metav1.Condition `json:"conditions,omitempty" patchStrategy:"merge" patchMergeKey:"type"`
|
||||
// Phase summarizes the overall status of the JWTAuthenticator.
|
||||
// +kubebuilder:default=Pending
|
||||
// +kubebuilder:validation:Enum=Pending;Ready;Error
|
||||
Phase JWTAuthenticatorPhase `json:"phase,omitempty"`
|
||||
}
|
||||
|
||||
// Spec for configuring a JWT authenticator.
|
||||
|
||||
@@ -7,38 +7,79 @@ package jwtcachefiller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"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"
|
||||
typeJWKSFetchValid = "JWKSFetchValid"
|
||||
typeAuthenticatorValid = "AuthenticatorValid"
|
||||
|
||||
reasonSuccess = "Success"
|
||||
reasonNotReady = "NotReady"
|
||||
reasonUnableToValidate = "UnableToValidate"
|
||||
reasonInvalidIssuerURL = "InvalidIssuerURL"
|
||||
reasonInvalidIssuerURLScheme = "InvalidIssuerURLScheme"
|
||||
reasonInvalidProviderJWKSURL = "InvalidProviderJWKSURL"
|
||||
reasonInvalidProviderJWKSURLScheme = "InvalidProviderJWKSURLScheme"
|
||||
reasonInvalidTLSConfiguration = "InvalidTLSConfiguration"
|
||||
reasonInvalidDiscoveryProbe = "InvalidDiscoveryProbe"
|
||||
reasonInvalidAuthenticator = "InvalidAuthenticator"
|
||||
reasonInvalidTokenSigningFailure = "InvalidTokenSigningFailure"
|
||||
reasonInvalidCouldNotFetchJWKS = "InvalidCouldNotFetchJWKS"
|
||||
|
||||
msgUnableToValidate = "unable to validate; see other conditions for details"
|
||||
|
||||
// These default values come from the way that the Supervisor issues and signs tokens. We make these
|
||||
// the defaults for a JWTAuthenticator so that they can easily integrate with the Supervisor.
|
||||
defaultUsernameClaim = oidcapi.IDTokenClaimUsername
|
||||
defaultGroupsClaim = oidcapi.IDTokenClaimGroups
|
||||
|
||||
minimalJWTToTriggerJWKSFetch = "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.e30."
|
||||
)
|
||||
|
||||
type providerJSON struct {
|
||||
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 +99,7 @@ type tokenAuthenticatorCloser interface {
|
||||
pinnipedauthenticator.Closer
|
||||
}
|
||||
|
||||
type jwtAuthenticator struct {
|
||||
type cachedJWTAuthenticator struct {
|
||||
tokenAuthenticatorCloser
|
||||
spec *auth1alpha1.JWTAuthenticatorSpec
|
||||
}
|
||||
@@ -66,16 +107,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 +131,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 +173,58 @@ 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.TLS, conditions)
|
||||
_, conditions, issuerOk := c.validateIssuer(specCopy.Issuer, conditions)
|
||||
okSoFar := tlsOk && issuerOk
|
||||
|
||||
client := phttp.Default(rootCAs)
|
||||
client.Timeout = 30 * time.Second // copied from Kube OIDC code
|
||||
coreOSCtx := coreosoidc.ClientContext(context.Background(), client)
|
||||
|
||||
pJSON, provider, conditions, providerErr := c.validateProviderDiscovery(coreOSCtx, specCopy.Issuer, conditions, okSoFar)
|
||||
errs = append(errs, providerErr)
|
||||
okSoFar = okSoFar && providerErr == nil
|
||||
|
||||
jwksURL, conditions, jwksErr := c.validateProviderJWKSURL(provider, pJSON, conditions, okSoFar)
|
||||
errs = append(errs, jwksErr)
|
||||
okSoFar = okSoFar && jwksErr == nil
|
||||
|
||||
keySet, conditions, jwksFetchErr := c.validateJWKSFetch(coreOSCtx, jwksURL, conditions, okSoFar)
|
||||
errs = append(errs, jwksFetchErr)
|
||||
okSoFar = okSoFar && jwksFetchErr == nil
|
||||
|
||||
// Make a deep copy of the spec so we aren't storing pointers to something that the informer cache
|
||||
// may mutate!
|
||||
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(
|
||||
client,
|
||||
obj.Spec.DeepCopy(),
|
||||
keySet,
|
||||
conditions,
|
||||
okSoFar)
|
||||
errs = append(errs, err)
|
||||
|
||||
if !conditionsutil.HadErrorCondition(conditions) {
|
||||
c.cache.Store(cacheKey, cachedAuthenticator)
|
||||
c.log.Info("added new jwt authenticator", "jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer)
|
||||
}
|
||||
|
||||
c.cache.Store(cacheKey, jwtAuthenticator)
|
||||
c.log.WithValues("jwtAuthenticator", klog.KObj(obj), "issuer", obj.Spec.Issuer).Info("added new jwt authenticator")
|
||||
return nil
|
||||
err = c.updateStatus(ctx.Context, obj, conditions)
|
||||
errs = append(errs, err)
|
||||
|
||||
// Sync loop errors:
|
||||
// - Should not be configuration errors. Config errors a user must correct belong on the .Status
|
||||
// object. The controller simply must wait for a user to correct before running again.
|
||||
// - Other errors, such as networking errors, etc. are the types of errors that should return here
|
||||
// and signal the controller to retry the sync loop. These may be corrected by machines.
|
||||
return errorsutil.NewAggregate(errs)
|
||||
}
|
||||
|
||||
func (c *controller) extractValueAsJWTAuthenticator(value authncache.Value) *jwtAuthenticator {
|
||||
jwtAuthenticator, ok := value.(*jwtAuthenticator)
|
||||
func (c *jwtCacheFillerController) extractValueAsJWTAuthenticator(value authncache.Value) *cachedJWTAuthenticator {
|
||||
jwtAuthenticator, ok := value.(*cachedJWTAuthenticator)
|
||||
if !ok {
|
||||
actualType := "<nil>"
|
||||
if t := reflect.TypeOf(value); t != nil {
|
||||
@@ -150,11 +236,240 @@ 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) validateTLS(tlsSpec *auth1alpha1.TLSSpec, conditions []*metav1.Condition) (*x509.CertPool, []*metav1.Condition, bool) {
|
||||
rootCAs, _, err := pinnipedauthenticator.CABundle(tlsSpec)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid TLS configuration: %w", err)
|
||||
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 := "successfully parsed specified CA bundle"
|
||||
if rootCAs == nil {
|
||||
msg = "no CA bundle specified"
|
||||
}
|
||||
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
|
||||
}
|
||||
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeIssuerURLValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "issuer is a valid URL",
|
||||
})
|
||||
return issuerURL, conditions, true
|
||||
}
|
||||
|
||||
func (c *jwtCacheFillerController) validateProviderDiscovery(ctx context.Context, issuer string, conditions []*metav1.Condition, prereqOk bool) (*providerJSON, *coreosoidc.Provider, []*metav1.Condition, error) {
|
||||
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, 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)
|
||||
}
|
||||
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSURLValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "jwks_uri is a valid URL",
|
||||
})
|
||||
return pJSON.JWKSURL, conditions, nil
|
||||
}
|
||||
|
||||
// validateJWKSFetch deliberately takes an unsigned JWT to trigger coreosoidc.NewRemoteKeySet to
|
||||
// indirectly fetch the JWKS. This lets us report a status about the endpoint, even though
|
||||
// we expect the verification checks to actually fail. This also pre-warms the cache of keys
|
||||
// in the remote keyset object.
|
||||
func (c *jwtCacheFillerController) validateJWKSFetch(ctx context.Context, jwksURL string, conditions []*metav1.Condition, prereqOk bool) (*coreosoidc.RemoteKeySet, []*metav1.Condition, error) {
|
||||
if !prereqOk {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msgUnableToValidate,
|
||||
})
|
||||
return nil, conditions, nil
|
||||
}
|
||||
keySet := coreosoidc.NewRemoteKeySet(ctx, jwksURL)
|
||||
|
||||
// keySet.verifySignature calls functions which may error in a couple of ways that
|
||||
// we will treat as success because we are really only concerned here that we could
|
||||
// fetch the keys at all.
|
||||
_, verifyWithKeySetErr := keySet.VerifySignature(ctx, minimalJWTToTriggerJWKSFetch)
|
||||
if verifyWithKeySetErr == nil {
|
||||
// No unit test.
|
||||
// Since we hard-coded this token we expect there to always be a verification error.
|
||||
// The purpose of this function is really to test if we can get the JWKS, not to actually validate a token.
|
||||
// Therefore, we should never hit this path, nevertheless, lets handle just in case something unexpected happens.
|
||||
errText := "jwks should not have verified unsigned jwt token"
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: errText,
|
||||
})
|
||||
return nil, conditions, errors.New(errText)
|
||||
}
|
||||
|
||||
verifyErrString := verifyWithKeySetErr.Error()
|
||||
// We need to fetch the keys. This is the main concern of this function.
|
||||
if strings.HasPrefix(verifyErrString, "fetching keys") {
|
||||
errText := "could not fetch keys"
|
||||
msg := fmt.Sprintf("%s: %s", errText, verifyErrString)
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonInvalidCouldNotFetchJWKS,
|
||||
Message: msg,
|
||||
})
|
||||
return nil, conditions, fmt.Errorf("%s: %w", errText, verifyWithKeySetErr)
|
||||
}
|
||||
// This error indicates success of this check. We only wanted to test if we could fetch, we aren't actually
|
||||
// testing for valid signature verification.
|
||||
if strings.Contains(verifyErrString, "failed to verify id token signature") {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "successfully fetched jwks",
|
||||
})
|
||||
return keySet, conditions, nil
|
||||
}
|
||||
|
||||
// No unit tests, currently no way to reach this code path.
|
||||
errText := "unexpected verification error while fetching jwks"
|
||||
msg := fmt.Sprintf("%s: %s", errText, verifyErrString)
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeJWKSFetchValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msg,
|
||||
})
|
||||
return nil, conditions, fmt.Errorf("%s: %w", errText, verifyWithKeySetErr)
|
||||
}
|
||||
|
||||
// newCachedJWTAuthenticator creates a jwt authenticator from the provided spec.
|
||||
func (c *jwtCacheFillerController) newCachedJWTAuthenticator(client *http.Client, spec *auth1alpha1.JWTAuthenticatorSpec, keySet *coreosoidc.RemoteKeySet, conditions []*metav1.Condition, prereqOk bool) (*cachedJWTAuthenticator, []*metav1.Condition, error) {
|
||||
if !prereqOk {
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeAuthenticatorValid,
|
||||
Status: metav1.ConditionUnknown,
|
||||
Reason: reasonUnableToValidate,
|
||||
Message: msgUnableToValidate,
|
||||
})
|
||||
return nil, conditions, nil
|
||||
}
|
||||
|
||||
usernameClaim := spec.Claims.Username
|
||||
@@ -166,33 +481,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 +498,76 @@ func newJWTAuthenticator(spec *auth1alpha1.JWTAuthenticatorSpec) (*jwtAuthentica
|
||||
},
|
||||
},
|
||||
},
|
||||
KeySet: coreosoidc.NewRemoteKeySet(ctx, providerJSON.JWKSURL),
|
||||
KeySet: keySet,
|
||||
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) updateStatus(
|
||||
ctx context.Context,
|
||||
original *auth1alpha1.JWTAuthenticator,
|
||||
conditions []*metav1.Condition,
|
||||
) error {
|
||||
updated := original.DeepCopy()
|
||||
|
||||
if conditionsutil.HadErrorCondition(conditions) {
|
||||
updated.Status.Phase = auth1alpha1.JWTAuthenticatorPhaseError
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeReady,
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: reasonNotReady,
|
||||
Message: "the JWTAuthenticator is not ready: see other conditions for details",
|
||||
})
|
||||
} else {
|
||||
updated.Status.Phase = auth1alpha1.JWTAuthenticatorPhaseReady
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeReady,
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: reasonSuccess,
|
||||
Message: "the JWTAuthenticator is ready",
|
||||
})
|
||||
}
|
||||
|
||||
_ = conditionsutil.MergeConfigConditions(
|
||||
conditions,
|
||||
original.Generation,
|
||||
&updated.Status.Conditions,
|
||||
plog.New().WithName(controllerName),
|
||||
metav1.NewTime(c.clock.Now()),
|
||||
)
|
||||
|
||||
if equality.Semantic.DeepEqual(original, updated) {
|
||||
return nil
|
||||
}
|
||||
_, err := c.client.AuthenticationV1alpha1().JWTAuthenticators().UpdateStatus(ctx, updated, metav1.UpdateOptions{})
|
||||
return err
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package conditionsutil
|
||||
@@ -119,3 +119,12 @@ func mergeConfigCondition(existing *[]metav1.Condition, new *metav1.Condition) b
|
||||
// Otherwise the entry is already up to date.
|
||||
return false
|
||||
}
|
||||
|
||||
func HadErrorCondition(conditions []*metav1.Condition) bool {
|
||||
for _, c := range conditions {
|
||||
if c.Status != metav1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package supervisorconfig
|
||||
@@ -211,7 +211,7 @@ func (c *federationDomainWatcherController) processAllFederationDomains(
|
||||
// made the FederationDomain's endpoints available.
|
||||
fdToConditionsMap[federationDomain] = conditions
|
||||
|
||||
if !hadErrorCondition(conditions) {
|
||||
if !conditionsutil.HadErrorCondition(conditions) {
|
||||
// Successfully validated the FederationDomain, so allow it to be loaded.
|
||||
federationDomainIssuers = append(federationDomainIssuers, federationDomainIssuer)
|
||||
}
|
||||
@@ -794,7 +794,7 @@ func (c *federationDomainWatcherController) updateStatus(
|
||||
) error {
|
||||
updated := federationDomain.DeepCopy()
|
||||
|
||||
if hadErrorCondition(conditions) {
|
||||
if conditionsutil.HadErrorCondition(conditions) {
|
||||
updated.Status.Phase = configv1alpha1.FederationDomainPhaseError
|
||||
conditions = append(conditions, &metav1.Condition{
|
||||
Type: typeReady,
|
||||
@@ -950,15 +950,6 @@ func newCrossFederationDomainConfigValidator(federationDomains []*configv1alpha1
|
||||
}
|
||||
}
|
||||
|
||||
func hadErrorCondition(conditions []*metav1.Condition) bool {
|
||||
for _, c := range conditions {
|
||||
if c.Status != metav1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func stringSetsEqual(a []string, b []string) bool {
|
||||
aSet := sets.New(a...)
|
||||
bSet := sets.New(b...)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package supervisorconfig
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"go.pinniped.dev/internal/here"
|
||||
"go.pinniped.dev/internal/idtransform"
|
||||
"go.pinniped.dev/internal/testutil"
|
||||
"go.pinniped.dev/internal/testutil/conditionstestutil"
|
||||
)
|
||||
|
||||
func TestFederationDomainWatcherControllerInformerFilters(t *testing.T) {
|
||||
@@ -490,29 +491,8 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
allHappyConditionsSuccess := func(issuer string, time metav1.Time, observedGeneration int64) []metav1.Condition {
|
||||
return sortConditionsByType([]metav1.Condition{
|
||||
return conditionstestutil.SortByType([]metav1.Condition{
|
||||
happyTransformationExamplesCondition(frozenMetav1Now, 123),
|
||||
happyTransformationExpressionsCondition(frozenMetav1Now, 123),
|
||||
happyKindCondition(frozenMetav1Now, 123),
|
||||
@@ -527,7 +507,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
}
|
||||
|
||||
allHappyConditionsLegacyConfigurationSuccess := func(issuer string, idpName string, time metav1.Time, observedGeneration int64) []metav1.Condition {
|
||||
return replaceConditions(
|
||||
return conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess(issuer, time, observedGeneration),
|
||||
[]metav1.Condition{
|
||||
happyIdentityProvidersFoundConditionLegacyConfigurationSuccess(idpName, time, observedGeneration),
|
||||
@@ -736,7 +716,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
wantStatusUpdates: []*configv1alpha1.FederationDomain{
|
||||
expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain,
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123),
|
||||
@@ -778,7 +758,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
wantStatusUpdates: []*configv1alpha1.FederationDomain{
|
||||
expectedFederationDomainStatusUpdate(invalidIssuerURLFederationDomain,
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, oidcIdentityProvider.Name, frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIssuerURLValidConditionCannotHaveQuery(frozenMetav1Now, 123),
|
||||
@@ -818,7 +798,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "duplicate1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte.cOm/a", oidcIdentityProvider.Name, frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
|
||||
@@ -830,7 +810,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "duplicate2", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate.com/a", oidcIdentityProvider.Name, frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
|
||||
@@ -891,7 +871,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "fd1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess("https://iSSueR-duPlicAte-adDress.cOm/path1", oidcIdentityProvider.Name, frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
|
||||
@@ -903,7 +883,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "fd2", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess("https://issuer-duplicate-address.com:1234/path2", oidcIdentityProvider.Name, frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadOneTLSSecretPerIssuerHostnameCondition(frozenMetav1Now, 123),
|
||||
@@ -915,7 +895,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "invalidIssuerURLFederationDomain", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess(invalidIssuerURL, oidcIdentityProvider.Name, frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
unknownIssuerIsUniqueCondition(frozenMetav1Now, 123),
|
||||
@@ -943,7 +923,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
wantStatusUpdates: []*configv1alpha1.FederationDomain{
|
||||
expectedFederationDomainStatusUpdate(federationDomain1,
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123),
|
||||
@@ -952,7 +932,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
),
|
||||
expectedFederationDomainStatusUpdate(federationDomain2,
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess(federationDomain2.Spec.Issuer, "", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIdentityProvidersFoundConditionLegacyConfigurationIdentityProviderNotFound(frozenMetav1Now, 123),
|
||||
@@ -973,7 +953,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
wantStatusUpdates: []*configv1alpha1.FederationDomain{
|
||||
expectedFederationDomainStatusUpdate(federationDomain1,
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsLegacyConfigurationSuccess(federationDomain1.Spec.Issuer, "", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIdentityProvidersFoundConditionIdentityProviderNotSpecified(3, frozenMetav1Now, 123),
|
||||
@@ -1025,7 +1005,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIdentityProvidersFoundConditionIdentityProvidersObjectRefsNotFound(here.Doc(
|
||||
@@ -1179,7 +1159,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadDisplayNamesUniqueCondition(`"duplicate1", "duplicate2"`, frozenMetav1Now, 123),
|
||||
@@ -1242,7 +1222,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadAPIGroupSuffixCondition(`"", "", "wrong.example.com"`, frozenMetav1Now, 123),
|
||||
@@ -1304,7 +1284,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadKindCondition(`"", "wrong"`, frozenMetav1Now, 123),
|
||||
@@ -1354,7 +1334,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadTransformationExpressionsCondition(here.Doc(
|
||||
@@ -1500,7 +1480,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadTransformationExamplesCondition(here.Doc(
|
||||
@@ -1599,7 +1579,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://issuer1.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadTransformationExamplesCondition(here.Doc(
|
||||
@@ -1746,7 +1726,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config1", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadAPIGroupSuffixCondition(`"this is wrong"`, frozenMetav1Now, 123),
|
||||
@@ -1806,7 +1786,7 @@ func TestTestFederationDomainWatcherControllerSync(t *testing.T) {
|
||||
ObjectMeta: metav1.ObjectMeta{Name: "config2", Namespace: namespace, Generation: 123},
|
||||
},
|
||||
configv1alpha1.FederationDomainPhaseError,
|
||||
replaceConditions(
|
||||
conditionstestutil.Replace(
|
||||
allHappyConditionsSuccess("https://not-unique.com", frozenMetav1Now, 123),
|
||||
[]metav1.Condition{
|
||||
sadIssuerIsUniqueCondition(frozenMetav1Now, 123),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2021-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package ldapupstreamwatcher implements a controller which watches LDAPIdentityProviders.
|
||||
@@ -209,9 +209,9 @@ func (c *ldapWatcherController) Sync(ctx controllerlib.Context) error {
|
||||
requeue := false
|
||||
validatedUpstreams := make([]upstreamprovider.UpstreamLDAPIdentityProviderI, 0, len(actualUpstreams))
|
||||
for _, upstream := range actualUpstreams {
|
||||
valid, requestedRequeue := c.validateUpstream(ctx.Context, upstream)
|
||||
if valid != nil {
|
||||
validatedUpstreams = append(validatedUpstreams, valid)
|
||||
validProvider, requestedRequeue := c.validateUpstream(ctx.Context, upstream)
|
||||
if validProvider != nil {
|
||||
validatedUpstreams = append(validatedUpstreams, validProvider)
|
||||
}
|
||||
if requestedRequeue {
|
||||
requeue = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Package controllermanager provides an entrypoint into running all of the controllers that run as
|
||||
@@ -244,8 +244,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,
|
||||
).
|
||||
|
||||
31
internal/testutil/conditionstestutil/conditionstestutil.go
Normal file
31
internal/testutil/conditionstestutil/conditionstestutil.go
Normal file
@@ -0,0 +1,31 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package conditionstestutil
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func SortByType(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
|
||||
}
|
||||
|
||||
func Replace(originals []metav1.Condition, replacements []metav1.Condition) []metav1.Condition {
|
||||
for _, sadReplaceCondition := range replacements {
|
||||
for origIndex, origCondition := range originals {
|
||||
if origCondition.Type == sadReplaceCondition.Type {
|
||||
originals[origIndex] = sadReplaceCondition
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
return originals
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
@@ -67,8 +67,15 @@ func TestSuccessfulCredentialRequest_Browser(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "jwt authenticator",
|
||||
authenticator: testlib.CreateTestJWTAuthenticatorForCLIUpstream,
|
||||
name: "jwt authenticator",
|
||||
authenticator: func(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
|
||||
authenticator := testlib.CreateTestJWTAuthenticatorForCLIUpstream(ctx, t)
|
||||
return corev1.TypedLocalObjectReference{
|
||||
APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
|
||||
Kind: "JWTAuthenticator",
|
||||
Name: authenticator.Name,
|
||||
}
|
||||
},
|
||||
token: func(t *testing.T) (string, string, []string) {
|
||||
pinnipedExe := testlib.PinnipedCLIPath(t)
|
||||
credOutput, _ := runPinnipedLoginOIDC(ctx, t, pinnipedExe)
|
||||
|
||||
398
test/integration/concierge_jwtauthenticator_status_test.go
Normal file
398
test/integration/concierge_jwtauthenticator_status_test.go
Normal file
@@ -0,0 +1,398 @@
|
||||
// Copyright 2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
"go.pinniped.dev/generated/latest/apis/concierge/authentication/v1alpha1"
|
||||
"go.pinniped.dev/test/testlib"
|
||||
)
|
||||
|
||||
func TestConciergeJWTAuthenticatorStatus_Parallel(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
run func(t *testing.T)
|
||||
}{
|
||||
{
|
||||
name: "valid spec with no errors and all good status conditions and phase will result in a jwt authenticator that is ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle))
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
Audience: "some-fake-audience",
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseReady)
|
||||
|
||||
testlib.WaitForJWTAuthenticatorStatusConditions(
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid spec with invalid CA in TLS config will result in a jwt authenticator that is not ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := "invalid base64-encoded data"
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
Audience: "some-fake-audience",
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseError)
|
||||
|
||||
testlib.WaitForJWTAuthenticatorStatusConditions(
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
replaceSomeConditions(
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0),
|
||||
[]metav1.Condition{
|
||||
{
|
||||
Type: "Ready",
|
||||
Status: "False",
|
||||
Reason: "NotReady",
|
||||
Message: "the JWTAuthenticator is not ready: see other conditions for details",
|
||||
}, {
|
||||
Type: "AuthenticatorValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "JWKSURLValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "JWKSFetchValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "DiscoveryURLValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "TLSConfigurationValid",
|
||||
Status: "False",
|
||||
Reason: "InvalidTLSConfiguration",
|
||||
Message: "invalid TLS configuration: illegal base64 data at input byte 7",
|
||||
},
|
||||
},
|
||||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid spec with valid CA in TLS config but does not match issuer server will result in a jwt authenticator that is not ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURVVENDQWptZ0F3SUJBZ0lWQUpzNStTbVRtaTJXeUI0bGJJRXBXaUs5a1RkUE1BMEdDU3FHU0liM0RRRUIKQ3dVQU1COHhDekFKQmdOVkJBWVRBbFZUTVJBd0RnWURWUVFLREFkUWFYWnZkR0ZzTUI0WERUSXdNRFV3TkRFMgpNamMxT0ZvWERUSTBNRFV3TlRFMk1qYzFPRm93SHpFTE1Ba0dBMVVFQmhNQ1ZWTXhFREFPQmdOVkJBb01CMUJwCmRtOTBZV3d3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRRERZWmZvWGR4Z2NXTEMKZEJtbHB5a0tBaG9JMlBuUWtsVFNXMno1cGcwaXJjOGFRL1E3MXZzMTRZYStmdWtFTGlvOTRZYWw4R01DdVFrbApMZ3AvUEE5N1VYelhQNDBpK25iNXcwRGpwWWd2dU9KQXJXMno2MFRnWE5NSFh3VHk4ME1SZEhpUFVWZ0VZd0JpCmtkNThzdEFVS1Y1MnBQTU1reTJjNy9BcFhJNmRXR2xjalUvaFBsNmtpRzZ5dEw2REtGYjJQRWV3MmdJM3pHZ2IKOFVVbnA1V05DZDd2WjNVY0ZHNXlsZEd3aGc3cnZ4U1ZLWi9WOEhCMGJmbjlxamlrSVcxWFM4dzdpUUNlQmdQMApYZWhKZmVITlZJaTJtZlczNlVQbWpMdnVKaGpqNDIrdFBQWndvdDkzdWtlcEgvbWpHcFJEVm9wamJyWGlpTUYrCkYxdnlPNGMxQWdNQkFBR2pnWU13Z1lBd0hRWURWUjBPQkJZRUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1IKTUI4R0ExVWRJd1FZTUJhQUZNTWJpSXFhdVkwajRVWWphWDl0bDJzby9LQ1JNQjBHQTFVZEpRUVdNQlFHQ0NzRwpBUVVGQndNQ0JnZ3JCZ0VGQlFjREFUQVBCZ05WSFJNQkFmOEVCVEFEQVFIL01BNEdBMVVkRHdFQi93UUVBd0lCCkJqQU5CZ2txaGtpRzl3MEJBUXNGQUFPQ0FRRUFYbEh4M2tIMDZwY2NDTDlEVE5qTnBCYnlVSytGd2R6T2IwWFYKcmpNaGtxdHVmdEpUUnR5T3hKZ0ZKNXhUR3pCdEtKamcrVU1pczBOV0t0VDBNWThVMU45U2c5SDl0RFpHRHBjVQpxMlVRU0Y4dXRQMVR3dnJIUzIrdzB2MUoxdHgrTEFiU0lmWmJCV0xXQ21EODUzRlVoWlFZekkvYXpFM28vd0p1CmlPUklMdUpNUk5vNlBXY3VLZmRFVkhaS1RTWnk3a25FcHNidGtsN3EwRE91eUFWdG9HVnlkb3VUR0FOdFhXK2YKczNUSTJjKzErZXg3L2RZOEJGQTFzNWFUOG5vZnU3T1RTTzdiS1kzSkRBUHZOeFQzKzVZUXJwNGR1Nmh0YUFMbAppOHNaRkhidmxpd2EzdlhxL3p1Y2JEaHEzQzBhZnAzV2ZwRGxwSlpvLy9QUUFKaTZLQT09Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K"
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.SupervisorUpstreamOIDC.Issuer,
|
||||
Audience: "some-fake-audience",
|
||||
// Some random generated cert
|
||||
// Issuer: C=US, O=Pivotal
|
||||
// No SAN provided
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseError)
|
||||
|
||||
testlib.WaitForJWTAuthenticatorStatusConditions(
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
replaceSomeConditions(
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0),
|
||||
[]metav1.Condition{
|
||||
{
|
||||
Type: "Ready",
|
||||
Status: "False",
|
||||
Reason: "NotReady",
|
||||
Message: "the JWTAuthenticator is not ready: see other conditions for details",
|
||||
}, {
|
||||
Type: "AuthenticatorValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "JWKSURLValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "JWKSFetchValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "DiscoveryURLValid",
|
||||
Status: "False",
|
||||
Reason: "InvalidDiscoveryProbe",
|
||||
Message: `could not perform oidc discovery on provider issuer: Get "` + env.SupervisorUpstreamOIDC.Issuer + `/.well-known/openid-configuration": tls: failed to verify certificate: x509: certificate signed by unknown authority`,
|
||||
}, {
|
||||
Type: "TLSConfigurationValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "successfully parsed specified CA bundle",
|
||||
},
|
||||
},
|
||||
))
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid with bad issuer will result in a jwt authenticator that is not ready",
|
||||
run: func(t *testing.T) {
|
||||
caBundleString := base64.StdEncoding.EncodeToString([]byte(env.SupervisorUpstreamOIDC.CABundle))
|
||||
fakeIssuerURL := "https://127.0.0.1:443/some-fake-issuer"
|
||||
jwtAuthenticator := testlib.CreateTestJWTAuthenticator(ctx, t, v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: fakeIssuerURL,
|
||||
Audience: "some-fake-audience",
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: caBundleString,
|
||||
},
|
||||
}, v1alpha1.JWTAuthenticatorPhaseError)
|
||||
|
||||
testlib.WaitForJWTAuthenticatorStatusConditions(
|
||||
ctx, t,
|
||||
jwtAuthenticator.Name,
|
||||
replaceSomeConditions(
|
||||
allSuccessfulJWTAuthenticatorConditions(len(caBundleString) != 0),
|
||||
[]metav1.Condition{
|
||||
{
|
||||
Type: "Ready",
|
||||
Status: "False",
|
||||
Reason: "NotReady",
|
||||
Message: "the JWTAuthenticator is not ready: see other conditions for details",
|
||||
}, {
|
||||
Type: "AuthenticatorValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "JWKSURLValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "JWKSFetchValid",
|
||||
Status: "Unknown",
|
||||
Reason: "UnableToValidate",
|
||||
Message: "unable to validate; see other conditions for details",
|
||||
}, {
|
||||
Type: "DiscoveryURLValid",
|
||||
Status: "False",
|
||||
Reason: "InvalidDiscoveryProbe",
|
||||
Message: fmt.Sprintf(`could not perform oidc discovery on provider issuer: Get "%s/.well-known/openid-configuration": dial tcp 127.0.0.1:443: connect: connection refused`, fakeIssuerURL),
|
||||
},
|
||||
},
|
||||
))
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tt.run(t)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConciergeJWTAuthenticatorCRDValidations_Parallel(t *testing.T) {
|
||||
env := testlib.IntegrationEnv(t)
|
||||
jwtAuthenticatorClient := testlib.NewConciergeClientset(t).AuthenticationV1alpha1().JWTAuthenticators()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Minute)
|
||||
t.Cleanup(cancel)
|
||||
|
||||
objectMeta := testlib.ObjectMetaWithRandomName(t, "jwt-authenticator")
|
||||
tests := []struct {
|
||||
name string
|
||||
jwtAuthenticator *v1alpha1.JWTAuthenticator
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "issuer can not be empty string",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: objectMeta,
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: "",
|
||||
Audience: "fake-audience",
|
||||
},
|
||||
},
|
||||
wantErr: `JWTAuthenticator.authentication.concierge.` + env.APIGroupSuffix + ` "` + objectMeta.Name + `" is invalid: ` +
|
||||
`spec.issuer: Invalid value: "": spec.issuer in body should be at least 1 chars long`,
|
||||
},
|
||||
{
|
||||
name: "audience can not be empty string",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: objectMeta,
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: "https://example.com",
|
||||
Audience: "",
|
||||
},
|
||||
},
|
||||
wantErr: `JWTAuthenticator.authentication.concierge.` + env.APIGroupSuffix + ` "` + objectMeta.Name + `" is invalid: ` +
|
||||
`spec.audience: Invalid value: "": spec.audience in body should be at least 1 chars long`,
|
||||
},
|
||||
{
|
||||
name: "issuer must be https",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: objectMeta,
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: "http://www.example.com",
|
||||
Audience: "foo",
|
||||
},
|
||||
},
|
||||
wantErr: `JWTAuthenticator.authentication.concierge.` + env.APIGroupSuffix + ` "` + objectMeta.Name + `" is invalid: ` +
|
||||
`spec.issuer: Invalid value: "http://www.example.com": spec.issuer in body should match '^https://'`,
|
||||
},
|
||||
{
|
||||
name: "minimum valid authenticator",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.CLIUpstreamOIDC.Issuer,
|
||||
Audience: "foo",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid authenticator can have empty claims block",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.CLIUpstreamOIDC.Issuer,
|
||||
Audience: "foo",
|
||||
Claims: v1alpha1.JWTTokenClaims{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid authenticator can have empty group claim and empty username claim",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.CLIUpstreamOIDC.Issuer,
|
||||
Audience: "foo",
|
||||
Claims: v1alpha1.JWTTokenClaims{
|
||||
Groups: "",
|
||||
Username: "",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid authenticator can have empty TLS block",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.CLIUpstreamOIDC.Issuer,
|
||||
Audience: "foo",
|
||||
Claims: v1alpha1.JWTTokenClaims{
|
||||
Groups: "",
|
||||
Username: "",
|
||||
},
|
||||
TLS: &v1alpha1.TLSSpec{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "valid authenticator can have empty TLS CertificateAuthorityData",
|
||||
jwtAuthenticator: &v1alpha1.JWTAuthenticator{
|
||||
ObjectMeta: testlib.ObjectMetaWithRandomName(t, "jwtauthenticator"),
|
||||
Spec: v1alpha1.JWTAuthenticatorSpec{
|
||||
Issuer: env.CLIUpstreamOIDC.Issuer,
|
||||
Audience: "foo",
|
||||
Claims: v1alpha1.JWTTokenClaims{
|
||||
Groups: "",
|
||||
Username: "",
|
||||
},
|
||||
TLS: &v1alpha1.TLSSpec{
|
||||
CertificateAuthorityData: "pretend-this-is-a-certificate",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
tt := test
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, createErr := jwtAuthenticatorClient.Create(ctx, tt.jwtAuthenticator, metav1.CreateOptions{})
|
||||
|
||||
t.Cleanup(func() {
|
||||
// delete if it exists
|
||||
delErr := jwtAuthenticatorClient.Delete(ctx, tt.jwtAuthenticator.Name, metav1.DeleteOptions{})
|
||||
if !errors.IsNotFound(delErr) {
|
||||
require.NoError(t, delErr)
|
||||
}
|
||||
})
|
||||
|
||||
if tt.wantErr != "" {
|
||||
wantErr := tt.wantErr
|
||||
require.EqualError(t, createErr, wantErr)
|
||||
} else {
|
||||
require.NoError(t, createErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func allSuccessfulJWTAuthenticatorConditions(caBundleExists bool) []metav1.Condition {
|
||||
tlsConfigValidMsg := "no CA bundle specified"
|
||||
if caBundleExists {
|
||||
tlsConfigValidMsg = "successfully parsed specified CA bundle"
|
||||
}
|
||||
return []metav1.Condition{{
|
||||
Type: "AuthenticatorValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "authenticator initialized",
|
||||
}, {
|
||||
Type: "DiscoveryURLValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "discovery performed successfully",
|
||||
}, {
|
||||
Type: "IssuerURLValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "issuer is a valid URL",
|
||||
}, {
|
||||
Type: "JWKSFetchValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "successfully fetched jwks",
|
||||
}, {
|
||||
Type: "JWKSURLValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "jwks_uri is a valid URL",
|
||||
}, {
|
||||
Type: "Ready",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: "the JWTAuthenticator is ready",
|
||||
}, {
|
||||
Type: "TLSConfigurationValid",
|
||||
Status: "True",
|
||||
Reason: "Success",
|
||||
Message: tlsConfigValidMsg,
|
||||
}}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
package integration
|
||||
|
||||
@@ -111,12 +111,13 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
)
|
||||
|
||||
// Create a JWTAuthenticator that will validate the tokens from the downstream issuer.
|
||||
// If the FederationDomain is not Ready, the JWTAuthenticator cannot be ready, either.
|
||||
clusterAudience := "test-cluster-" + testlib.RandHex(t, 8)
|
||||
authenticator := testlib.CreateTestJWTAuthenticator(topSetupCtx, t, authv1alpha.JWTAuthenticatorSpec{
|
||||
Issuer: federationDomain.Spec.Issuer,
|
||||
Audience: clusterAudience,
|
||||
TLS: &authv1alpha.TLSSpec{CertificateAuthorityData: testCABundleBase64},
|
||||
})
|
||||
}, authv1alpha.JWTAuthenticatorPhaseError)
|
||||
|
||||
// Add an OIDC upstream IDP and try using it to authenticate during kubectl commands.
|
||||
t.Run("with Supervisor OIDC upstream IDP and browser flow with with form_post automatic authcode delivery to CLI", func(t *testing.T) {
|
||||
@@ -161,6 +162,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -246,6 +248,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -333,6 +336,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -456,6 +460,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -586,6 +591,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -658,6 +664,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -733,6 +740,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -791,6 +799,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -853,6 +862,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -923,6 +933,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -981,6 +992,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -1053,6 +1065,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -1107,6 +1120,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -1161,6 +1175,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -1237,6 +1252,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
// Having one IDP should put the FederationDomain into a ready state.
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Create a ClusterRoleBinding to give our test user from the upstream read-only access to the cluster.
|
||||
testlib.CreateTestClusterRoleBinding(t,
|
||||
@@ -1270,6 +1286,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
// Having a second IDP should put the FederationDomain back into an error state until we tell it which one to use.
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseError)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Update the FederationDomain to use the two IDPs.
|
||||
federationDomainsClient := testlib.NewSupervisorClientset(t).ConfigV1alpha1().FederationDomains(env.SupervisorNamespace)
|
||||
@@ -1360,6 +1377,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
|
||||
// The FederationDomain should be valid after the above update.
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/test-sessions.yaml"
|
||||
@@ -1493,6 +1511,7 @@ func TestE2EFullIntegration_Browser(t *testing.T) {
|
||||
}, 20*time.Second, 250*time.Millisecond)
|
||||
// The FederationDomain should be valid after the above update.
|
||||
testlib.WaitForFederationDomainStatusPhase(testCtx, t, federationDomain.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(testCtx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Log out so we can try fresh logins again.
|
||||
require.NoError(t, os.Remove(credentialCachePath))
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package integration
|
||||
@@ -437,11 +437,14 @@ func TestGetAPIResourceList(t *testing.T) { //nolint:gocyclo // each t.Run is pr
|
||||
}
|
||||
}
|
||||
|
||||
// manually update this value whenever you add additional fields to an API resource and then run the generator
|
||||
totalExpectedAPIFields := 260
|
||||
|
||||
// Because we are parsing text from `kubectl explain` and because the format of that text can change
|
||||
// over time, make a rudimentary assertion that this test exercised the whole tree of all fields of all
|
||||
// Pinniped API resources. Without this, the test could accidentally skip parts of the tree if the
|
||||
// format has changed.
|
||||
require.Equal(t, 259, foundFieldNames,
|
||||
require.Equal(t, totalExpectedAPIFields, foundFieldNames,
|
||||
"Expected to find all known fields of all Pinniped API resources. "+
|
||||
"You may will need to update this expectation if you added new fields to the API types.",
|
||||
)
|
||||
|
||||
@@ -91,12 +91,13 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
)
|
||||
|
||||
// Create a JWTAuthenticator that will validate the tokens from the downstream issuer.
|
||||
// if the FederationDomain is not Ready, the JWTAuthenticator cannot be ready, either.
|
||||
clusterAudience := "test-cluster-" + testlib.RandHex(t, 8)
|
||||
authenticator := testlib.CreateTestJWTAuthenticator(ctx, t, authv1alpha.JWTAuthenticatorSpec{
|
||||
Issuer: downstream.Spec.Issuer,
|
||||
Audience: clusterAudience,
|
||||
TLS: &authv1alpha.TLSSpec{CertificateAuthorityData: testCABundleBase64},
|
||||
})
|
||||
}, authv1alpha.JWTAuthenticatorPhaseError)
|
||||
|
||||
const (
|
||||
yellowColor = "\u001b[33;1m"
|
||||
@@ -110,6 +111,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
|
||||
createdProvider := setupClusterForEndToEndLDAPTest(t, expectedUsername, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(ctx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml"
|
||||
@@ -257,6 +259,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
sAMAccountName := expectedUsername + "@" + env.SupervisorUpstreamActiveDirectory.Domain
|
||||
createdProvider := setupClusterForEndToEndActiveDirectoryTest(t, sAMAccountName, env)
|
||||
testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(ctx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml"
|
||||
@@ -418,6 +421,7 @@ func TestSupervisorWarnings_Browser(t *testing.T) {
|
||||
},
|
||||
}, idpv1alpha1.PhaseReady)
|
||||
testlib.WaitForFederationDomainStatusPhase(ctx, t, downstream.Name, configv1alpha1.FederationDomainPhaseReady)
|
||||
testlib.WaitForJWTAuthenticatorStatusPhase(ctx, t, authenticator.Name, authv1alpha.JWTAuthenticatorPhaseReady)
|
||||
|
||||
// Use a specific session cache for this test.
|
||||
sessionCachePath := tempDir + "/ldap-test-refresh-sessions.yaml"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2020-2023 the Pinniped contributors. All Rights Reserved.
|
||||
// Copyright 2020-2024 the Pinniped contributors. All Rights Reserved.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testlib
|
||||
@@ -204,13 +204,11 @@ func CreateTestWebhookAuthenticator(ctx context.Context, t *testing.T) corev1.Ty
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTestJWTAuthenticatorForCLIUpstream creates and returns a test JWTAuthenticator in
|
||||
// $PINNIPED_TEST_CONCIERGE_NAMESPACE, which will be automatically deleted at the end of the current
|
||||
// test's lifetime. It returns a corev1.TypedLocalObjectReference which describes the test JWT
|
||||
// authenticator within the test namespace.
|
||||
// CreateTestJWTAuthenticatorForCLIUpstream creates and returns a test JWTAuthenticator which will be automatically
|
||||
// deleted at the end of the current test's lifetime.
|
||||
//
|
||||
// CreateTestJWTAuthenticatorForCLIUpstream gets the OIDC issuer info from IntegrationEnv().CLIUpstreamOIDC.
|
||||
func CreateTestJWTAuthenticatorForCLIUpstream(ctx context.Context, t *testing.T) corev1.TypedLocalObjectReference {
|
||||
func CreateTestJWTAuthenticatorForCLIUpstream(ctx context.Context, t *testing.T) *auth1alpha1.JWTAuthenticator {
|
||||
t.Helper()
|
||||
testEnv := IntegrationEnv(t)
|
||||
spec := auth1alpha1.JWTAuthenticatorSpec{
|
||||
@@ -228,14 +226,17 @@ func CreateTestJWTAuthenticatorForCLIUpstream(ctx context.Context, t *testing.T)
|
||||
CertificateAuthorityData: base64.StdEncoding.EncodeToString([]byte(testEnv.CLIUpstreamOIDC.CABundle)),
|
||||
}
|
||||
}
|
||||
return CreateTestJWTAuthenticator(ctx, t, spec)
|
||||
authenticator := CreateTestJWTAuthenticator(ctx, t, spec, auth1alpha1.JWTAuthenticatorPhaseReady)
|
||||
return authenticator
|
||||
}
|
||||
|
||||
// CreateTestJWTAuthenticator creates and returns a test JWTAuthenticator in
|
||||
// $PINNIPED_TEST_CONCIERGE_NAMESPACE, which will be automatically deleted at the end of the current
|
||||
// test's lifetime. It returns a corev1.TypedLocalObjectReference which describes the test JWT
|
||||
// authenticator within the test namespace.
|
||||
func CreateTestJWTAuthenticator(ctx context.Context, t *testing.T, spec auth1alpha1.JWTAuthenticatorSpec) corev1.TypedLocalObjectReference {
|
||||
// CreateTestJWTAuthenticator creates and returns a test JWTAuthenticator which will be automatically deleted
|
||||
// at the end of the current test's lifetime.
|
||||
func CreateTestJWTAuthenticator(
|
||||
ctx context.Context,
|
||||
t *testing.T,
|
||||
spec auth1alpha1.JWTAuthenticatorSpec,
|
||||
expectedStatus auth1alpha1.JWTAuthenticatorPhase) *auth1alpha1.JWTAuthenticator {
|
||||
t.Helper()
|
||||
|
||||
client := NewConciergeClientset(t)
|
||||
@@ -260,11 +261,46 @@ func CreateTestJWTAuthenticator(ctx context.Context, t *testing.T, spec auth1alp
|
||||
require.NoErrorf(t, err, "could not cleanup test JWTAuthenticator %s", jwtAuthenticator.Name)
|
||||
})
|
||||
|
||||
return corev1.TypedLocalObjectReference{
|
||||
APIGroup: &auth1alpha1.SchemeGroupVersion.Group,
|
||||
Kind: "JWTAuthenticator",
|
||||
Name: jwtAuthenticator.Name,
|
||||
}
|
||||
WaitForJWTAuthenticatorStatusPhase(ctx, t, jwtAuthenticator.Name, expectedStatus)
|
||||
|
||||
return jwtAuthenticator
|
||||
}
|
||||
|
||||
func WaitForJWTAuthenticatorStatusPhase(ctx context.Context, t *testing.T, jwtAuthenticatorName string, expectPhase auth1alpha1.JWTAuthenticatorPhase) {
|
||||
t.Helper()
|
||||
jwtAuthenticatorClientSet := NewConciergeClientset(t).AuthenticationV1alpha1().JWTAuthenticators()
|
||||
|
||||
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
|
||||
jwtA, err := jwtAuthenticatorClientSet.Get(ctx, jwtAuthenticatorName, metav1.GetOptions{})
|
||||
requireEventually.NoError(err)
|
||||
requireEventually.Equalf(expectPhase, jwtA.Status.Phase, "actual status conditions were: %#v", jwtA.Status.Conditions)
|
||||
}, 60*time.Second, 1*time.Second, "expected the JWTAuthenticator to have status %q", expectPhase)
|
||||
}
|
||||
|
||||
func WaitForJWTAuthenticatorStatusConditions(ctx context.Context, t *testing.T, jwtAuthenticatorName string, expectConditions []metav1.Condition) {
|
||||
t.Helper()
|
||||
jwtAuthenticatorClient := NewConciergeClientset(t).AuthenticationV1alpha1().JWTAuthenticators()
|
||||
RequireEventuallyf(t, func(requireEventually *require.Assertions) {
|
||||
fd, err := jwtAuthenticatorClient.Get(ctx, jwtAuthenticatorName, metav1.GetOptions{})
|
||||
requireEventually.NoError(err)
|
||||
|
||||
requireEventually.Lenf(fd.Status.Conditions, len(expectConditions),
|
||||
"wanted status conditions: %#v", expectConditions)
|
||||
|
||||
for i, wantCond := range expectConditions {
|
||||
actualCond := fd.Status.Conditions[i]
|
||||
|
||||
// This is a cheat to avoid needing to make equality assertions on these fields.
|
||||
requireEventually.NotZero(actualCond.LastTransitionTime)
|
||||
wantCond.LastTransitionTime = actualCond.LastTransitionTime
|
||||
requireEventually.NotZero(actualCond.ObservedGeneration)
|
||||
wantCond.ObservedGeneration = actualCond.ObservedGeneration
|
||||
|
||||
requireEventually.Equalf(wantCond, actualCond,
|
||||
"wanted status conditions: %#v\nactual status conditions were: %#v\nnot equal at index %d",
|
||||
expectConditions, fd.Status.Conditions, i)
|
||||
}
|
||||
}, 60*time.Second, 1*time.Second, "wanted JWTAuthenticator conditions")
|
||||
}
|
||||
|
||||
// CreateTestFederationDomain creates and returns a test FederationDomain in the
|
||||
|
||||
Reference in New Issue
Block a user